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

Share this article

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.

How can I get started with Flickr API?

To get started with Flickr API, you first need to create an account on Flickr. Once you have an account, you can apply for an API key. This key is unique to your application and is used to authenticate your requests. After obtaining the key, you can start making API calls to fetch data from Flickr. You can use various programming languages like JavaScript, Python, or PHP to make these calls.

What is the purpose of the Flickr API?

The Flickr API is a powerful tool that allows developers to access and manipulate the vast amount of data stored on Flickr. This includes photos, albums, comments, tags, and more. Developers can use this API to create custom applications or websites that interact with Flickr, such as creating an image gallery or a photo search engine.

How can I use the Flickr API to create an image gallery?

To create an image gallery using the Flickr API, you need to make API calls to fetch the images you want to display. You can specify the parameters of your request to get specific images. Once you have the images, you can use HTML and CSS to create a gallery layout and display the images on your website.

What are the limitations of the Flickr API?

While the Flickr API is powerful, it does have some limitations. For example, there are rate limits on the number of API calls you can make in a certain period. Also, not all data on Flickr is accessible through the API. Some data may be private or restricted due to copyright issues.

Can I use the Flickr API for commercial purposes?

Yes, you can use the Flickr API for commercial purposes, but there are certain rules and restrictions you need to follow. For example, you must not use the API to create a similar or competing service to Flickr. Also, you must respect the rights of the content owners.

How can I handle errors when using the Flickr API?

The Flickr API returns error codes and messages when something goes wrong with your request. You can use these codes and messages to identify the problem and fix it. It’s important to handle these errors properly in your code to ensure your application runs smoothly.

Can I use the Flickr API with other APIs?

Yes, you can use the Flickr API in conjunction with other APIs to create more complex applications. For example, you could use the Google Maps API to display the location where a photo was taken.

How can I improve the performance of my Flickr API requests?

There are several ways to improve the performance of your Flickr API requests. For example, you can limit the amount of data returned by each request, or cache the results of API calls to reduce the number of requests.

How can I secure my Flickr API key?

It’s important to keep your Flickr API key secure to prevent unauthorized access to your Flickr account. You should never share your key publicly or include it in client-side code. Instead, store it securely on your server and use it to authenticate your API requests.

Can I use the Flickr API to upload photos?

Yes, you can use the Flickr API to upload photos. However, this requires additional permissions and a different API endpoint. You also need to handle file uploads in your code, which can be complex.

Aurelio De RosaAurelio De Rosa
View Author

I'm a (full-stack) web and app developer with more than 5 years' experience programming for the web using HTML, CSS, Sass, JavaScript, and PHP. I'm an expert of JavaScript and HTML5 APIs but my interests include web security, accessibility, performance, and SEO. I'm also a regular writer for several networks, speaker, and author of the books jQuery in Action, third edition and Instant jQuery Selectors.

APIsFlickr
Share this article
Read Next
Get the freshest news and resources for developers, designers and digital creators in your inbox each week