JavaScript
Article

Creating an Image Gallery with the Flickr API — Style and Logic

By Aurelio De Rosa

In the first part of this mini-series on how to develop a simple gallery using the Flickr API, we discussed the requirements of the project, the markup needed to structure the HTML page, and two out of five CSS modules.

In this second and final part, we’ll cover the remaining CSS modules and the JavaScript code that powers the project. Without further ado, let’s get started.

The Styles (continued)

In the previous article, we discussed the module of the helper classes and the layout modules. Next in the list is the gallery module.

The gallery module defines the styles of the gallery and its components. It’s made of simple declarations, and I’ll highlight a few points of interest.

The first point is that the element with a class of gallery, which acts as a container for the photo shown in its natural size, is given a fixed height of 500px. Then, the img element inside it – used to show the selected image – is constrained by setting its max-height and max-width property to 100%. By doing so, we ensure that the image doesn’t overflow the container.

The second point is that we define how the style of the arrows changes when users hover over or focus on them. Styling for the focus event is important because it enhances the accessibility of an element for those users navigating the website via the keyboard (for example, by hitting the TAB key).

In case you consider yourself a beginner with CSS, you might also want to study how the buttons are made circular and how the arrows are drawn.

The full code of this module is presented below:

.gallery
{
   position: relative;
   height: 500px;
   border: 1px solid #FFFFFF;
}

.gallery img
{
   display: block;
   margin: 0 auto;
   max-width: 100%;
   max-height: 100%;
}

.gallery__arrow
{
   position: absolute;
   top: 50%;
   display: block;
   width: 60px;
   height: 60px;
   border: none;
   border-radius: 50%;
   background-color: #000000;
   opacity: 0.7;
   cursor: pointer;
}

.gallery__arrow:hover,
.gallery__arrow:focus
{
   opacity: 1;
}

.gallery__arrow:before,
.gallery__arrow:after
{
   content: '';
   position: absolute;
   width: 10px;
   height: 40%;
   background-color: #FFFFFF;
}

.gallery__arrow:before
{
   bottom: 12px;
}

.gallery__arrow:after
{
   top: 12px;
}

.gallery__arrow:hover:before,
.gallery__arrow:focus:before,
.gallery__arrow:hover:after,
.gallery__arrow:focus:after
{
   background-color: #FCB712;
}

.gallery__arrow--left
{
   left: 0.5em;
}

.gallery__arrow--left:before
{
   transform: rotate(-40deg);
   left: 35%;
}

.gallery__arrow--left:after
{
   transform: rotate(40deg);
   left: 35%;
}

.gallery__arrow--right
{
   right: 0.5em;
}

.gallery__arrow--right:before
{
   transform: rotate(40deg);
   right: 35%;
}

.gallery__arrow--right:after
{
   transform: rotate(-40deg);
   right: 35%;
}

The Thumbnails Module

The thumbnails module doesn’t contain anything too fancy. It forces the thumbnails to be five in a row by setting the width property to 19%, a margin-right of 1%, and the display property to inline-block. The other point worth a mention is that there is an effect that takes place when a thumbnail is hovered or focused to enhance the accessibility, as discussed in the previous section.

The full code for this module is as follows:

.thumbnails__list,
.thumbnails__pager
{
   margin: 0;
   padding: 0;
   list-style-type: none;
}

.thumbnails__list li
{
   display: inline-block;
   width: 19%;
   margin-top: 1%;
   margin-right: 1%;
}

.thumbnail
{
   width: 100%;
}

.thumbnail:hover,
.thumbnail:focus
{
   border: 1px solid #FCB720;
   opacity: 0.7;
}

.thumbnails__pager
{
   text-align: right;
   margin: 0.5em 0;
}

.thumbnails__pager li
{
   display: inline;
}

.thumbnails__pager a
{
   margin: 0 0.2em;
   color: #FFFFFF;
   text-decoration: none;
}

.thumbnails__pager a.current,
.thumbnails__pager a:hover,
.thumbnails__pager a:focus
{
   color: #FCB720;
   text-decoration: underline;
}

The Homepage Module

The last module is the homepage module. Here is where we style elements of the project that don’t fit any of the other modules and that are specific to the homepage. When dealing with real-world projects, you’ll often find yourself styling elements that have a certain look only on a given page, and in such cases it makes sense to create a specific CSS file just for that page.

The full code of the homepage.css file is presented below:

.form-search
{
   margin: 0.5em 0;
   text-align: right;
}

.form-search #query
{
   padding: 0.2em;
}

.form-search input
{
   color: #000000;
}

.thumbnails
{
   border-bottom: 3px solid #FFFFFF;
}

.copyright
{
   margin-top: 0.5em;
   margin-bottom: 0.5em;
   text-align: right;
}

With this last module, we’ve completed the overview of the CSS files used to style the project, so it’s now time to discuss the business logic.

The Business Logic

The business logic of the project is also organized into small modules, with one file – main.js – acting as the glue between the markup and the JavaScript modules. This file is where we’ll define the event handlers for the buttons of the gallery, what happens when a user clicks one of the links in the pager, and what to do when the user searches for some given text.

Before we examine the peculiarities of each module, I want to highlight a few interesting techniques I’ve used. The first is that each module is defined using an IIFE (Immediately-Invoked Function Expression), allowing us to create private variables and methods and to avoid polluting the global scope. The second is that, in each module, I’ve employed strict mode, which enforces more restrictive rules for how the JavaScript code is executed. For example, it eliminates some JavaScript silent errors by changing them to throw errors. Finally, each file implements the module pattern.

With these points in mind, let’s take a look at the modules defined.

The Utility Module

The first module we’ll discuss is the utility module. It contains methods that are of general interest and that our JavaScript modules will use. It defines only two methods: extend and buildUrl.

The extend method is a simplified version of its namesake in jQuery, and it’s used to merge the properties of two or more objects into one (the first parameter). In case you’re not a JavaScript ninja, you may want to learn how I enabled this method to accept an arbitrary number of objects by using arguments. arguments is an Array-like object corresponding to the arguments passed to a function.

The buildUrl method is used to create a valid URL containing a query string, starting from a URL and an object of names and values to use in the query string.

The code of the utility module is defined as follows:

(function(document, window) {
   'use strict';

   function buildUrl(url, parameters){
      var queryString = '';

      for(var key in parameters) {
         if (parameters.hasOwnProperty(key)) {
            queryString += encodeURIComponent(key) + '=' + encodeURIComponent(parameters[key]) + '&';
         }
      }

      if (queryString.lastIndexOf('&') === queryString.length - 1){
         queryString = queryString.substring(0, queryString.length - 1);
      }

      return url + '?' + queryString;
   }

   function extend(object) {
      for(var i = 1; i < arguments.length; i++) {
          for(var key in arguments[i]) {
             if (arguments[i].hasOwnProperty(key)) {
                object[key] = arguments[i][key];
             }
          }
      }

      return object;
   }

   window.Utility = {
      buildUrl: buildUrl,
      extend: extend
   };
})(document, window);

The gallery module defines a Gallery object exposed in the global scope. Its constructor accepts two parameters: the list of the photos (i.e. an array containing the URLs of the photos) belonging to the gallery, and the DOM element that will show the image in its natural size. This object defines the features of our gallery, such as the ability to show the previous (showPrevious method) or next (showNext method) image, or to create the list of thumbnails (createThumbnailsGallery method).

This module demonstrates an interesting technique for solving a common closure problem that occurs when dealing with loops and event handlers. I’ve discussed this problem and its solution in my article 5 More JavaScript Interview Exercises (points 1 and 2). Here the function defined outside the loop is clickHandler().

Now that you’re aware of the tricks employed in this module, you’re ready to read its complete source:

(function(document, window) {
   'use strict';

   function Gallery(photos, container) {
      this.currentIndex = 0;
      this.photos = photos;
      this.container = container;

      this.showPhoto(this.currentIndex);
   }

   Gallery.prototype.showPhoto = function(index) {
      if (index >= 0 && index < this.photos.length) {
         this.currentIndex = index;
         this.container.src = Flickr.buildPhotoLargeUrl(this.photos[this.currentIndex]);
      }
   };

   Gallery.prototype.showPrevious = function() {
      if (this.currentIndex > 0) {
         this.currentIndex--;
      }

      this.showPhoto(this.currentIndex);
   };

   Gallery.prototype.showNext = function() {
      if (this.currentIndex < this.photos.length - 1) {
         this.currentIndex++;
      }

      this.showPhoto(this.currentIndex);
   };

   Gallery.prototype.createThumbnailsGallery = function(container) {
      function clickHandler(index, gallery) {
         return function (event) {
            event.preventDefault();

            gallery.showPhoto(index);
         };
      }

      container.textContent = '';
      var image, link, listItem;
      for (var i = 0; i < this.photos.length; i++) {
         image = document.createElement('img');
         image.src = Flickr.buildThumbnailUrl(this.photos[i]);
         image.className = 'thumbnail';
         image.alt = this.photos[i].title;
         image.title = this.photos[i].title;

         link = document.createElement('a');
         link.href = image.src;
         link.addEventListener('click', clickHandler(i, this));
         link.appendChild(image);

         listItem = document.createElement('li');
         listItem.appendChild(link);

         container.appendChild(listItem);
      }
   };

   window.Gallery = Gallery;
})(document, window);

The Flickr Module

In a sense, the Flickr module is the core of our application, because it defines the code that uses the Flickr API. Unlike the other modules we’ve covered so far, you might want to extend this module to provide more features. For example, you could extend it to search photos based on the username of a user or based on the location of the photos. For this reason, instead of just exposing the Flickr object in the global scope, I’ll use the Utility.extend() method, as shown below:

window.Flickr = Utility.extend(window.Flickr || {}, {
   /* methods of this module defined here */
});

The Utility.extend() method is used in another part of this module and specifically in the first statement of the searchText() method. In this case it’s used to merge the parameters passed by the caller of the searchText() method with private information of the module that the caller shouldn’t know (and which is thus kept private), such as the API method to call (flickr.photos.search).

This module needs an API key to communicate with the Flickr API. I can’t share my API key with the world, so you need to insert your own as the value of the variable apiKey to have a completely working project. If you don’t provide such a key, all your requests to Flickr will fail.

With this last point in mind, here is the full code of this module:

(function(document, window) {
   'use strict';

   var apiKey = 'YOUR-API-KEY-HERE';
   var apiURL = 'https://api.flickr.com/services/rest/';

   function searchText(parameters) {
      var requestParameters = Utility.extend(parameters, {
         method: 'flickr.photos.search',
         api_key: apiKey,
         format: 'json'
      });

      var script = document.createElement('script');
      script.src = Utility.buildUrl(apiURL, requestParameters);
      document.head.appendChild(script);
      document.head.removeChild(script);
   }

   function buildThumbnailUrl(photo) {
      return 'https://farm' + photo.farm + '.staticflickr.com/' + photo.server +
      '/' + photo.id + '_' + photo.secret + '_q.jpg';
   }

   function buildPhotoUrl(photo) {
      return 'https://farm' + photo.farm + '.staticflickr.com/' + photo.server +
             '/' + photo.id + '_' + photo.secret + '.jpg';
   }

   function buildPhotoLargeUrl(photo) {
      return 'https://farm' + photo.farm + '.staticflickr.com/' + photo.server +
      '/' + photo.id + '_' + photo.secret + '_b.jpg';
   }

   window.Flickr = Utility.extend(window.Flickr || {}, {
      buildThumbnailUrl: buildThumbnailUrl,
      buildPhotoUrl: buildPhotoUrl,
      buildPhotoLargeUrl: buildPhotoLargeUrl,
      searchText: searchText
   });
})(document, window);

Tying It All Together: the Main Module

Now that we’ve discussed all the modules of our project, we need to tie them with the HTML elements of the page so that when, for example, the right arrow is clicked the service will display the next photo in the list. This is the role of the code contained in the main.js file. There are two parts of the code that I’d like to discuss: the pager and the arrows.

The pager shows up to six pages plus the special “buttons” (actually they are all a elements) to go to the first and last page, and to move to the previous and next page. When one of the elements of the pager is clicked, the service must show the thumbnails that belong to that page. For example, if the user clicks on page 3 (and remember, each page contains 15 thumbnails), the service should show to the user the photos that belong to this page, from the 31st to the 45th photos found (if any). To perform this action, we could add a listener to every link of the pager plus the special buttons, but this would be a waste of memory. We can do this much more efficiently by employing a technique called event delegation. So, instead of adding a listener for every child of the pager, we’ll add only one listener to the pager itself. Then, based on the element on which the click event was fired, we’ll perform the expected action. (If you’re unfamiliar with this topic, you can read the article How JavaScript Event Delegation Works by David Walsh.)

The second point I want to mention is that instead of adding an event listener on the two arrows just for the click event only, I added a listener for the keydown event too. By doing so, I can determine if the user has pressed a key of the keyboard while the focus was on the arrow. Then, if the key pressed was the ENTER key, I execute the same action the user would expect if the click event was triggered instead. This simple approach enables us to improve the accessibility of the service for those users navigating a website via the keyboard.

Both these interesting parts can be found in the function called init(), which is shown below together with the full code of the main module:

(function(document, window) {
   'use strict';

   var gallery;
   var lastSearch = 'London';

   function searchPhotos(text, page) {
      if (text.length === 0) {
         alert('Error: the field is required');
      }
      page = page > 0 ? page : 1;

      Flickr.searchText({
         text: text,
         per_page: 15,
         jsoncallback: 'Website.Homepage.showPhotos',
         page: page
      });
   }

   function createPager(element, parameters) {
      var pagesToShow = 5;
      var url = '/search/' + parameters.query + '/';
      element.textContent = '';

      var previousLinks = {
         '<<': 1,
         '<': (parameters.currentPage - 1 || parameters.currentPage)
      };

      for (var key in previousLinks) {
         link = document.createElement('a');
         link.href = url + previousLinks[key];
         link.innerHTML = '<span class="js-page-number visually-hidden">' + previousLinks[key] + '</span>' + key;
         var listItem = document.createElement('li');
         listItem.appendChild(link);
         element.appendChild(listItem);
      }

      // Avoid showing less than 6 pages in the pager because the user reaches the end
      var pagesDifference = parameters.pagesNumber - parameters.currentPage;
      var startIndex = parameters.currentPage;
      if (pagesDifference < pagesToShow) {
         startIndex = parameters.currentPage - (pagesToShow - pagesDifference - 1) || 1;
      }
      var link;
      for(var i = startIndex; i < parameters.currentPage + pagesToShow && i <= parameters.pagesNumber; i++) {
         link = document.createElement('a');
         link.href = url + i;
         link.innerHTML = '<span class="js-page-number">' + i + '</span>';
         if (i === parameters.currentPage) {
            link.className += ' current';
         }
         listItem = document.createElement('li');
         listItem.appendChild(link);
         element.appendChild(listItem);
      }

      var nextLinks = {
         '>': (parameters.currentPage === parameters.pagesNumber ? parameters.pagesNumber : parameters.currentPage + 1),
         '>>': parameters.pagesNumber
      };

      for (key in nextLinks) {
         link = document.createElement('a');
         link.href = url + nextLinks[key];
         link.innerHTML = '<span class="js-page-number visually-hidden">' + nextLinks[key] + '</span>' + key;
         var listItem = document.createElement('li');
         listItem.appendChild(link);
         element.appendChild(listItem);
      }
   }

   function showPhotos(data) {
      createPager(
         document.getElementsByClassName('js-thumbnails__pager')[0], {
            query: lastSearch,
            currentPage: data.photos.page,
            pagesNumber: data.photos.pages
         }
      );

      gallery = new Gallery(data.photos.photo, document.getElementsByClassName('js-gallery__image')[0]);
      gallery.createThumbnailsGallery(document.getElementsByClassName('js-thumbnails__list')[0]);
   }

   function init() {
      document.getElementsByClassName('js-form-search')[0].addEventListener('submit', function(event) {
         event.preventDefault();

         lastSearch = document.getElementById('query').value;
         if (lastSearch.length > 0) {
            searchPhotos(lastSearch, 1);
         }
      });

      var leftArrow = document.getElementsByClassName('js-gallery__arrow--left')[0];
      leftArrow.addEventListener('click', function() {
         gallery.showPrevious.bind(gallery)();
      });
      leftArrow.addEventListener('keydown', function(event) {
         if (event.which === 13) {
            gallery.showPrevious.bind(gallery)();
         }
      });

      var rightArrow = document.getElementsByClassName('js-gallery__arrow--right')[0];
      rightArrow.addEventListener('click', function() {
         gallery.showNext.bind(gallery)();
      });
      rightArrow.addEventListener('keydown', function(event) {
         if (event.which === 13) {
            gallery.showNext.bind(gallery)()();
         }
      });

      document.getElementsByClassName('js-thumbnails__pager')[0].addEventListener('click', function(event) {
         event.preventDefault();
         var page;
         var currentLink = this.getElementsByClassName('current')[0];
         if (event.target.nodeName === 'SPAN') {
            page = event.target.textContent;
         } else if (event.target.nodeName === 'A') {
            page = event.target.getElementsByClassName('js-page-number')[0].textContent;
         }

         // Avoid reloading the same page
         if (page && page !== currentLink.getElementsByClassName('js-page-number')[0].textContent) {
            searchPhotos(lastSearch, page);
         }
      });

      // Kickstart the page
      searchPhotos(lastSearch, 1);
   }

   window.Website = Utility.extend(window.Website || {}, {
      Homepage: {
         init: init,
         showPhotos: showPhotos
      }
   });
})(document, window);

Website.Homepage.init();

With the code of this last file we’ve finally completed our project.

Conclusion

In this two-part article, I’ve guided you through the creation of a simple service that draws on an external API. By making use of the Flickr API, we’ve allowed a user to generate a gallery of Flickr photos by searching their titles and descriptions. I hope you enjoyed it and that you’ve learned some new and interesting techniques or approaches.

The source code of the project is accessible on my GitHub account in the repository named Flickr gallery demo.

More:
Comments
adonald

Thank you for the article, it is very informative and has inspired me to implement something very similar.

You mention in the article that you do not want to share your Flickr API key with the world, but is this not exactly what you will be doing by using it in a JavaScript program? Surely this is freely available to anyone who visits your site, since the JavaScript needs to be downloaded and executed by the browser?

Would it be better to have all the business logic implemented server side rather than client side? (forgive me if I have missed something)

Mittineague

I usually interpret something like this

to mean that the uset needs to alter that part. i.e. similar to
$database ..... 'USER' ... 'PASSWORD'

Not as clear as something like

/* Configuration Variables */
var API_Key = ""; // Enter your API Key here
....

But common enough practice

Recommended
Sponsors
Because We Like You
Free Ebooks!

Grab SitePoint's top 10 web dev and design ebooks, completely free!

Get the latest in JavaScript, once a week, for free.