Improve this Doc

In this step, you will add a clickable phone image swapper to the phone details page.

Component Controller


app/phone-detail/phone-detail.component.js:

...
controller: ['$http', '$routeParams',
  function PhoneDetailController($http, $routeParams) {
    var self = this;

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

    $http.get('phones/' + $routeParams.phoneId + '.json').then(function(response) {
      self.phone = response.data;
      self.setImage(self.phone.images[0]);
    });
  }
]
...

In the phoneDetail component's controller, we created the mainImageUrl model property and set its default value to the first phone image URL.

We also created a setImage() method (to be used as event handler), that will change the value of mainImageUrl.

Component Template


app/phone-detail/phone-detail.template.html:

<img ng-src="{{$ctrl.mainImageUrl}}" class="phone" />
...
<ul class="phone-thumbs">
  <li ng-repeat="img in $ctrl.phone.images">
    <img ng-src="{{img}}" ng-click="$ctrl.setImage(img)" />
  </li>
</ul>
...

We bound the ngSrc directive of the large image to the $ctrl.mainImageUrl property.

We also registered an ngClick handler with thumbnail images. When a user clicks on one of the thumbnail images, the handler will use the $ctrl.setImage() method callback to change the value of the $ctrl.mainImageUrl property to the URL of the clicked thumbnail image.

Testing

To verify this new feature, we added two E2E tests. One verifies that mainImageUrl is set to the first phone image URL by default. The second test clicks on several thumbnail images and verifies that the main image URL changes accordingly.


e2e-tests/scenarios.js:

...

describe('View: Phone detail', function() {

  ...

  it('should display the first phone image as the main phone image', function() {
    var mainImage = element(by.css('img.phone'));

    expect(mainImage.getAttribute('src')).toMatch(/img\/phones\/nexus-s.0.jpg/);
  });

  it('should swap the main image when clicking on a thumbnail image', function() {
    var mainImage = element(by.css('img.phone'));
    var thumbnails = element.all(by.css('.phone-thumbs img'));

    thumbnails.get(2).click();
    expect(mainImage.getAttribute('src')).toMatch(/img\/phones\/nexus-s.2.jpg/);

    thumbnails.get(0).click();
    expect(mainImage.getAttribute('src')).toMatch(/img\/phones\/nexus-s.0.jpg/);
  });

});

...

You can now rerun the tests with npm run protractor.

We also have to refactor one of our unit tests, because of the addition of the mainImageUrl model property to the controller. As previously, we will use a mocked response.


app/phone-detail/phone-detail.component.spec.js:

...

describe('controller', function() {
  var $httpBackend, ctrl
  var xyzPhoneData = {
    name: 'phone xyz',
    images: ['image/url1.png', 'image/url2.png']
  };

  beforeEach(inject(function($componentController, _$httpBackend_, _$routeParams_) {
    $httpBackend = _$httpBackend_;
    $httpBackend.expectGET('phones/xyz.json').respond(xyzPhoneData);

    ...
  }));

  it('should fetch phone details', function() {
    expect(ctrl.phone).toBeUndefined();

    $httpBackend.flush();
    expect(ctrl.phone).toEqual(xyzPhoneData);
  });

});

...

Our unit tests should now be passing again.

Experiments

Summary

With the phone image swapper in place, we are ready for step 13 to learn an even better way to fetch data.