@ngdoc overview @name Tutorial: 10 - Event Handlers @description In this step, you will add a clickable phone image swapper to the phone details page. The phone details view displays one large image of the current phone and several smaller thumbnail images. It would be great if we could replace the large image with any of the thumbnails just by clicking on the desired thumbnail image. Let's have a look at how we can do this with angular. The most important changes are listed below. You can see the full diff on {@link https://github.com/angular/angular-phonecat/compare/step-9...step-10 GitHub}: ## Controller __`app/js/controllers.js`:__
...
function PhoneDetailCtrl($xhr) {
  var self = this;

  $xhr('GET', 'phones/' + self.params.phoneId + '.json', function(code, response) {
    self.phone = response;
    self.mainImageUrl = response.images[0];
  });

  self.setImage = function(imageUrl) {
    self.mainImageUrl = imageUrl;
  }
}

//PhoneDetailCtrl.$inject = ['$xhr'];
In the `PhoneDetailCtrl` controller, we created the `mainImageUrl` model property and set its default value to the first phone image url. We also created a `setImage` controller method to change the value of `mainImageUrl`. ## Template __`app/partials/phone-detail.html`:__


...


...
We bound the `ng:src` attribute of the large image to the `mainImageUrl` property. We also registered an {@link api/angular.directive.ng:click `ng:click`} handler with thumbnail images. When a user clicks on one of the thumbnail images, the handler will use the `setImage` controller method to change the value of the `mainImageUrl` property to the url of the thumbnail image. ## Test To verify this new feature, we added two end-to-end tests. One verifies that the main image is set to the first phone image by default. The second test clicks on several thumbnail images and verifies that the main image changed appropriately. __`test/e2e/scenarios.js`:__
...
  describe('Phone detail view', function() {

    beforeEach(function() {
      browser().navigateTo('../../app/index.html#/phones/nexus-s');
    });


    it('should display the first phone image as the main phone image', function() {
       expect(element('img.phone').attr('src')).toBe('img/phones/nexus-s.0.jpg');
    });


    it('should swap main image if a thumbnail image is clicked on', function() {
      element('.phone-thumbs li:nth-child(3) img').click();
      expect(element('img.phone').attr('src')).toBe('img/phones/nexus-s.2.jpg');

      element('.phone-thumbs li:nth-child(1) img').click();
      expect(element('img.phone').attr('src')).toBe('img/phones/nexus-s.0.jpg');
    });
  });
});
You can now refresh the browser tab with the end-to-end test runner to see the tests run, or you can see them running on {@link http://angular.github.com/angular-phonecat/step-8/test/e2e/runner.html angular's server}. # Experiments * Let's add a new controller method to `PhoneCatCtrl`: this.hello = function(name) { alert('Hello ' + (name || 'world') + '!'); } and add: to the `index.html` template. The controller methods are inherited between controllers/scopes, so you can use the same snippet in the `phone-list.html` template as well. * Move the `hello` method from `PhoneCatCtrl` to `PhoneListCtrl` and you'll see that the button declared in `index.html` will stop working, while the one declared in the `phone-list.html` template remains operational. # Summary With the phone image swapper in place, we're ready for {@link step_11 step 11} (the last step!) to learn an even better way to fetch data. d='n13' href='#n13'>13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187
"""
Generic views that provide commonly needed behaviour.
"""

from rest_framework import views, mixins
from rest_framework.settings import api_settings
from django.views.generic.detail import SingleObjectMixin
from django.views.generic.list import MultipleObjectMixin


### Base classes for the generic views ###

class GenericAPIView(views.APIView):
    """
    Base class for all other generic views.
    """
    serializer_class = None
    model_serializer_class = api_settings.DEFAULT_MODEL_SERIALIZER_CLASS

    def get_serializer_context(self):
        """
        Extra context provided to the serializer class.
        """
        return {
            'request': self.request,
            'format': self.format_kwarg,
            'view': self
        }

    def get_serializer_class(self):
        """
        Return the class to use for the serializer.
        Use `self.serializer_class`, falling back to constructing a
        model serializer class from `self.model_serializer_class`
        """
        serializer_class = self.serializer_class

        if serializer_class is None:
            class DefaultSerializer(self.model_serializer_class):
                class Meta:
                    model = self.model
            serializer_class = DefaultSerializer

        return serializer_class

    def get_serializer(self, data=None, files=None, instance=None):
        # TODO: add support for files
        # TODO: add support for seperate serializer/deserializer
        serializer_class = self.get_serializer_class()
        context = self.get_serializer_context()
        return serializer_class(instance, data=data, context=context)


class MultipleObjectAPIView(MultipleObjectMixin, GenericAPIView):
    """
    Base class for generic views onto a queryset.
    """

    pagination_serializer_class = api_settings.DEFAULT_PAGINATION_SERIALIZER_CLASS
    paginate_by = api_settings.PAGINATE_BY

    def get_pagination_serializer_class(self):
        """
        Return the class to use for the pagination serializer.
        """
        class SerializerClass(self.pagination_serializer_class):
            class Meta:
                object_serializer_class = self.get_serializer_class()

        return SerializerClass

    def get_pagination_serializer(self, page=None):
        pagination_serializer_class = self.get_pagination_serializer_class()
        context = self.get_serializer_context()
        return pagination_serializer_class(instance=page, context=context)


class SingleObjectAPIView(SingleObjectMixin, GenericAPIView):
    """
    Base class for generic views onto a model instance.
    """
    pk_url_kwarg = 'pk'  # Not provided in Django 1.3
    slug_url_kwarg = 'slug'  # Not provided in Django 1.3

    def get_object(self):
        """
        Override default to add support for object-level permissions.
        """
        obj = super(SingleObjectAPIView, self).get_object()
        if not self.has_permission(self.request, obj):
            self.permission_denied(self.request)
        return obj


### Concrete view classes that provide method handlers ###
### by composing the mixin classes with a base view.   ###


class CreateAPIView(mixins.CreateModelMixin,
                    GenericAPIView):

    """
    Concrete view for creating a model instance.
    """
    def post(self, request, *args, **kwargs):
        return self.create(request, *args, **kwargs)


class ListAPIView(mixins.ListModelMixin,
                  MultipleObjectAPIView):
    """
    Concrete view for listing a queryset.
    """
    def get(self, request, *args, **kwargs):
        return self.list(request, *args, **kwargs)


class RetrieveAPIView(mixins.RetrieveModelMixin,
                      SingleObjectAPIView):
    """
    Concrete view for retrieving a model instance.
    """
    def get(self, request, *args, **kwargs):
        return self.retrieve(request, *args, **kwargs)


class DestroyAPIView(mixins.DestroyModelMixin,
                     SingleObjectAPIView):

    """
    Concrete view for deleting a model instance.
    """
    def delete(self, request, *args, **kwargs):
        return self.destroy(request, *args, **kwargs)


class UpdateAPIView(mixins.UpdateModelMixin,
                    SingleObjectAPIView):

    """
    Concrete view for updating a model instance.
    """
    def put(self, request, *args, **kwargs):
        return self.update(request, *args, **kwargs)


class ListCreateAPIView(mixins.ListModelMixin,
                        mixins.CreateModelMixin,
                        MultipleObjectAPIView):
    """
    Concrete view for listing a queryset or creating a model instance.
    """
    def get(self, request, *args, **kwargs):
        return self.list(request, *args, **kwargs)

    def post(self, request, *args, **kwargs):
        return self.create(request, *args, **kwargs)


class RetrieveDestroyAPIView(mixins.RetrieveModelMixin,
                             mixins.DestroyModelMixin,
                             SingleObjectAPIView):
    """
    Concrete view for retrieving or deleting a model instance.
    """
    def get(self, request, *args, **kwargs):
        return self.retrieve(request, *args, **kwargs)

    def delete(self, request, *args, **kwargs):
        return self.destroy(request, *args, **kwargs)


class RetrieveUpdateDestroyAPIView(mixins.RetrieveModelMixin,
                                   mixins.UpdateModelMixin,
                                   mixins.DestroyModelMixin,
                                   SingleObjectAPIView):
    """
    Concrete view for retrieving, updating or deleting a model instance.
    """
    def get(self, request, *args, **kwargs):
        return self.retrieve(request, *args, **kwargs)

    def put(self, request, *args, **kwargs):
        return self.update(request, *args, **kwargs)

    def delete(self, request, *args, **kwargs):
        return self.destroy(request, *args, **kwargs)