Super-Easy Angular Image Gallery

This entry is part 9 of 9 in the series: AngularJS Learning Series

Check it out!

Learning Angular has been a fantastic experience so far. I’ve been consistently surprised at the results I can get from a screenful of code. This week’s Image Gallery experiment was no different. Before I get into the particulars, let’s take a look at the finished product:

[iframe src=”http://embed.plnkr.co/MQwcpjMhq3DMTQrcRdy1″ height=”565″ width=”675″]

If you check out the source app.js on that example you’ll notice that the JavaScript adds up to about 25 lines of code, half of which are dedicated to loading and setting the data source. Yes, there’s some CSS3 transition coolness thrown in, but even that is minimal.

So How Does It Work?

The gallery is made up of three sections: the main image, the caption, and the thumbnails. If you compare the main image section and the thumbnail section you’ll see that they’re very similar. The main difference being the size of the image how many of the image you can see:

<div class="mainimageshell">
    <div class="viewwindow">
        <ul id="fullscroller" class="fullsizelist" ng-style="listposition" >
            <li ng-repeat="image in galleryData">
                <img id="fullsize" class="large" ng-src="{{image.image}}" />
            </li>
        </ul>
    </div>
</div>

<div class="captionshell">
    <p class="caption">{{selected.desc}}</p>
</div>

<div class="thumbsshell">
    <div class="thumbswrapper">
        <ul>
            <li ng-repeat="image in galleryData">
                <div class="thumbwrapper">
                    <a ng-href="" ng-click="scrollTo(image,$index)">
                        <img class="thumbnail" ng-src="{{image.image}}" />
                    </a>
                </div>
            </li>
        </ul>
    </div>
</div>

If you were to comment out the overflow line of the viewwindow class you would see all of the images lined up side by side, just like the thumbnails.

When iterating over the data using ng-repeat, $index will reflect the zero-based index of each item. Since we’re looping over the same data, the index for both the large image and the thumbnail will be the same. This allows me to multiply the index by the width of the images to arrive at the new positioning for the large image list. Once I specify the new positioning, my CSS handles the transition, something like this:

ul.fullsizelist{
    position:absolute;
    top:0;
    left:0;
    transition:  left .8s ease;
}

Thanks to Angular, notifying the DOM element of its new position is ultra-simple. If you take a look at the fullscroller UL inside the template you’ll see that it contains the Angular ng-style directive and that it’s bound to the listposition property. Listposition is updated in the scrollTo function every time a thumbnail is clicked and any change is immediately reflected in the view:

// Scroll to appropriate position based on image index and width
$scope.scrollTo = function(image,ind) {
    $scope.listposition = {left:(IMAGE_WIDTH * ind * -1) + "px"};
    $scope.selected = image;
}

In addition to modifying the viewwindow position, the scrollTo function also assigns the currently selected item to $scope.selected. If you look at the template you’ll see that caption is rendering “{{selected.desc}}”. When $scope.selected is changed, the caption will automatically change with it.

Full Disclosure

My original idea was to fade out the old image and fade in the new image when a thumbnail was clicked. This worked great on Chrome, but Firefox would flash the image on screen once loaded and wouldn’t transition in properly. It also required transition end listeners, which at the moment are not standard across browsers. That meant separate listeners for each browser engine.

The whole thing was becoming a hassle and the code was getting unruly. That’s when I made the decision to switch to a slider and it worked out great. Point being, don’t be afraid to experiment or refactor. Oftentimes the best coding decisions are born out of a change in approach.

Angular Intro to Modules, Services, Factories, and Filtering

This entry is part 5 of 9 in the series: AngularJS Learning Series

The Assignment

This week’s self-imposed Angular assignment was to create a small app that would dynamically filter a list of data using an input field as search criteria. I chose to base the app on my son’s homework assignment and use United States states and capitals.

In my previous examples I used Controllers for all functions and assignments. For simple non-critical applications the configuration works well, but for larger real-world applications a more abstract approach makes sense. Enter Angular Modules.

The Basics

Modules initialize Angular applications and wire together its services, filters, directives, etc. This makes the code portable, easier to debug, and easier to unit test.

To keep things simple, I only used a service in this example. The service is a gofer, responsible for carrying out any requests made by the controller. The controller sets scope properties and behaviors and handles calls from the template.

Here’s a basic module, service, and controller in Angular:

angular.module('myApp.service',[]);
angular.module('myApp',['myApp.service']);
var StatesController = function($scope) {}

On the first line is a service named ‘myApp.service’. Note that the dot is not necessary, nor is using the app name as a prefix. I could have named it ‘gofer’ if I wanted. The important part is that I let the main application module know the name, which brings me to line 2, the main module.

The main module names the app and gives it a a comma delimited reference to services, filters, etc. that can be referenced. If I defined a filter named ‘myApp.filter’ my module definition would have been: “angular.module(‘myApp’,[‘myApp.service’,’myApp.filter’]);”. The module name, “myApp”, will be used in my ngApp directive in the template.

Line 3 is the shell of a controller definition which will be directly accessible by the template. While it wouldn’t do anything at this point, we could hook those three lines up to a template and assign myApp to the ngApp directive without error.

Let’s Get Some Data!

As with most applications, we’ll need access to data. My data source is a JSON file that looks like this:

[
    {"name":"Alabama", "capital":"Montgomery","abbreviation":"AL"},
    {"name":"Alaska", "capital":"Juneau","abbreviation":"AK"},
    {"name":"Arizona", "capital":"Phoenix","abbreviation":"AZ"},
    {"name":"Arkansas", "capital":"Little Rock","abbreviation":"AR"},
    ...
]

I need a way to create a resource object out of that data so I can interact with it. Angular’s ngResource module provides exactly what I need in its $resource service.

NOTE: The ngResource module is NOT in the base angular.js file. To use it you must load angular-resource.js in addition to angular.js. If you get an error in your debugging console that says “Uncaught Error: No module: ngResource”, chances are you forgot to load angular-resource.js. You do use a developer tools, right?

To retrieve the resource I utilized a method used to create objects in Angular called a factory. When I put the factory method inside my service, and then provide my Controller with a reference to it, I can use it in my controller to set a scoped property, like this:

angular.module('myApp.service',['ngResource']).
    factory('States', function($resource){
        return $resource('states.json', {},{
            get: {method: 'GET', isArray:true}
        });
    });

angular.module('myApp',['myApp.service']);

var StatesController = function($scope,States) {
    $scope.stateData = States.get();
};

So, what changed? First, I gave my service a reference to Angular’s ngResource module. That will give me the ability to use the $resource service for my factory allowing me to interact with my data source, “states.json”. Notice that the factory has a name, “States” which I provide to my Controller. From there I can access actions inside the factory, such as “get”, “save”, and “delete”.

Now, if I create a template referencing the controller I can display the data using my stateData property. Check out the source code here:
[iframe src=”http://embed.plnkr.co/zXGpgCkUl8cF5rzbIWYP” width=”100%” height=”300″]

Make Something Angulary Happen

We now have a working module, a service, a factory, a controller, and data. Time to do something slick with it. Building off the html above, Inside my StatesController DIV I’m going to delete the current {{stateData}} dump and replace it with something a little easier to read:

<ul class="states">
   <li>
      <div class="resultwrapper">
         <span class="title">{{state.name}}</span><br>
         <span class="bold">Abbreviation:</span> {{state.abbreviation}}<br>
         <span class="bold">Capital:</span> {{state.capital}}
      </div>
   </li>
</ul>

Using ng-repeat and an unordered list, my state information now displays in a style-able list. Unfortunately, a couple of my states are not in alphabetical order. This can be fixed easily by telling the repeater to order it using a built-in filter module named orderBy after my iterator:

<ul class="states">
   <li ng-repeat="state in stateData | filter:criteria | orderBy:'name'">
       <div class="resultwrapper">
          <span class="title">{{state.name}}</span><br>
          <span class="bold">Abbreviation:</span> {{state.abbreviation}}<br>
          <span class="bold">Capital:</span> {{state.capital}}
       </div>
   </li>
</ul>

Note that ‘name’ is a string, not a reference. I could have also defined a property for it in the controller and used it with the orderBy.

Now that they’re in alphabetical order I want to be able to search them. This was done by adding a simple input and assigning it an ng-model of ‘critera’, then giving that to the ng-repeat filter. Angular uses the value entered into the criteria field as a filter for the stateData array. Throw in a style sheet and you have a pretty nifty widget using a very minimal amount of code.

The Result

[iframe src=”http://embed.plnkr.co/BixOgwEu66Hzco6rZJqt” width=”100%” height=”500″]