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 and XML? No Problem!

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

The Finished Product

JSON is a native object to AngularJS so working solely with JSON examples is easy. In the real world, however, you’ll probably need to deal with other types of data sources, like XML. Before I get too wordy, let’s start with what you’re really here for…the example:

[iframe src=”http://embed.plnkr.co/Hpx8GPHt0IqKADj8K1Qc” width=”100%” height=”365px”]

The Explanation

While the finished product looks like the result of 15 minutes of coding, I have to admit that it took me significantly longer to figure out. If you search the Angular Developer Guide or API for “XML” you’re going to come up woefully absent of useful results. Googling won’t get you much further. As frustration began to set in, I started thinking about what made loading an XML file more difficult than loading a JSON file. The answer is: not much!

Bridging the Language Gap

Angular likes JSON, and can you blame it? Angular is a JavaScript toolset and JSON is JavaScript Object Notation, a native brethren. XML, on the other hand, is tag-based markup…a foreign language to Angular. What I needed was a way to make Angular understand what XML was trying to communicate.

The key to overcoming any language gap is to have a translator. If there were a way to translate XML to JSON before handing it to the application then I could eliminate the language gap, and the problem.

A quick search for xml converters turned up multiple results. I chose Abdulla Abdurakhmano’s X2JS library because it was the first one I came across, it was lightweight, and it was pretty easy to use.

The code required to perform the translation is short and simple. Provide the library with XML, tell it which method to use, and receive JSON in return:

function(xml) {
 var x2js = new X2JS();
 var json = x2js.xml_str2json( xml );
 return json;
}

Where to Stick It

Now that I had a solution to the language gap I needed to decide where to implement it. Angular’s $http service conveniently applies transformation functions for both requests and responses. These functions apply transformations to data when it is sent and received, and they can be overridden.

Using the X2JS conversion code as a transformResponse function it was simple to get the $http service to read in XML and return JSON to the application, effectively creating the translator I needed. The results look something like this:

angular.module('myApp.service',[]).
    factory('DataSource', ['$http',function($http){
       return {
           get: function(callback){
                $http.get(
                    'data.xml',
                    {transformResponse:function(data) {
                    	// convert the data to JSON and provide
                    	// it to the success function below
						var x2js = new X2JS();
						var json = x2js.xml_str2json( data );
						return json;
						}
					}
                ).
                success(function(data, status) {
					// send the converted data back
					// to the callback function
                    callback(data);
                })
           }
       }
    }]);
    
angular.module('myApp',['myApp.service']);

var AppController = function($scope,DataSource) {
    
    //This is the callback function
    setData = function(data) {
        $scope.dataSet = data;
    }
        
    DataSource.get(setData);
    
}

That’s really all there is to it! The example above will retrieve an xml file named “data.xml”, convert it to JSON, and assign the results to $scope.dataSet.

The Caveat

XML comes in all shapes and sizes. Some use attributes, others don’t, and many use a combination of both. Some XML files use a proper header, others are just fragments. Your application will likely require tweaking to get the data into the shape that your application requires. Always read the docs, read the comments, and experiment.

The example at the beginning of this post is a great starting point. Fork it and experiment with different libraries or XML formats.

Keep on Coding!