2
\$\begingroup\$

This is my first attempt at a jQuery plugin and really doing anything with jQuery which isn't simple DOM manipulation or initialising another plugin!

I'm really keen to learn where I have gone wrong and improve on the code.

The code for the plugin is:

(function($) {

    $.fn.unsplash = function(options, cbOnImageSelect) {
        var defaults = {
            apiRootURL: "https://api.unsplash.com", // root for API, should not need to be overridden                                                               
            searchTerm: '', // the search term the results will be relating to
            imagesToReturn: 6, // number of rows to output per page of results
            page: 1, // page of search results to display, will be 1 to start
            randomImagesToShow: 3, // number of images to display at the outset, 0 for none
            apiClientId: '6066a5f8e2e83faef343bdca81bd128aebccec5d601e8e27501b0f6375423477', // can be overridden for a client id per account
            orientation: 'landscape', // orientation of images to query for [landscape | portrait | squarish]
            source: '', // where the plugin is being called from
            cbOnImageSelect: $.noop
        };

        images = [];
        totalImageCount = 0;
        selectedImage = {};

        var settings = $.extend({}, defaults, options);

        //return to avoid breaking chaining
        return this.each(function() {
            //clear any content in the unsplash search
            $('#unsplashSearch').empty();
            //add a search form
            $('#unsplashSearch').append('<div class="in_searchBox"><input type="text" name="us_searchTerm" id="us_searchTerm" value="" maxlength="50"><button type="submit" name="us_SearchBtn" id="us_SearchBtn"><span>Search</span></button></div>');
            //bind the search handler event to the button in the form
            $("#us_SearchBtn").bind("click", function(e) {
                e.preventDefault();
                //reset page back to 1 to avoid a new search starting on the wrong page.
                settings.page = 1;
                //set the settings search term to the new value
                settings.searchTerm = $('#us_searchTerm').val();
                searchAndPopulate(getSearchURL(settings.searchTerm, 1), settings.apiClientId);
            });
            $('#unsplashSearch').append('<ul id="imageList" class="horizontalList"></ul>');
            $('#unsplashSearch').append('<div id="us_Paging" style="text-align: center;"></div>');
            if (settings.randomImagesToShow > 0) {
                //build up the request url based on whether the search term is null or not...
                searchAndPopulate((!settings.searchTerm) ? getRandomURL(settings.randomImagesToShow) : getSearchURL(settings.searchTerm, 1), settings.apiClientId);
            } else {
                $('#us_Paging').hide();
            }

            //private functions
            function callUnsplash(url) {
                return $.ajax({
                    url: url,
                    dataType: 'json',
                    method: 'GET',
                    beforeSend: function(xhr) {
                        xhr.setRequestHeader('Authorization', 'Client-ID ' + settings.apiClientId);
                        xhr.setRequestHeader('Accept-Version', 'v1');
                    },
                    success: function(json) {
                        totalImageCount = (json.hasOwnProperty('total')) ? json.total : settings.imagesToReturn;
                        images = (json.hasOwnProperty('results')) ? json.results : json;
                    },
                    error: function(xhr, status, error) {
                        alert("Sorry, something went wrong (" + error + " " + xhr.status + " " + xhr.statusText + ")");
                    }
                });
            }

            function searchAndPopulate(url, key) {
                var callUnsplashPromise = callUnsplash(url, key);
                callUnsplashPromise.done(populateImages);
            }

            function populateImages() {
                if (images.total == 0) {
                } else {
                    //clear any existing images in the list
                    $('#imageList').empty();
                    $.each(images, function(index, image) {
                        var liContent = `<li><a href="#" class="splashImage" data-photoid="${image.id}"><div class="in_max200"><img src='${image.urls.thumb}' class='in_singlepic in_max200'></a></li>`;
                        $('#imageList').append(liContent);
                    });
                    //bind the callback function to the splashImage so it is called when clicked. if no callback is provided, then the default will be performed
                    $(".splashImage").bind("click", function(event) {
                        event.preventDefault();
                        if (typeof settings.cbOnImageSelect == 'function') {
                            var photoId = $(this).data('photoid');
                            var url = getImageURL(photoId);
                            var getImagePromise = callUnsplash(url);
                            getImagePromise.done(function(i) {
                                settings.cbOnImageSelect.call(this, i);
                            });
                        }
                    });
                    if (totalImageCount <= settings.imagesToReturn) {
                        $('#us_Paging').hide();
                    } else {
                        $('#us_Paging').empty().show();
                        if (totalImageCount > settings.imagesToReturn) {
                            var noOfPages = Math.ceil(totalImageCount / settings.imagesToReturn);
                            var x = pagination(settings.page, noOfPages);
                            for (var p = 0; p < x.length; p++) {
                                var linkText = ((x[p] !== '...') && (x[p] !== settings.page)) ? '<a href="##" class="us_changePage" data-page="' + x[p] + '">' + x[p] + '</a>&nbsp;&nbsp;' : x[p] + '&nbsp;&nbsp;';
                                $('#us_Paging').append(linkText);
                            }
                            //add the click event to the buttons with the class us_changePage
                            $(".us_changePage").bind("click", function(event) {
                                event.preventDefault();
                                var nextPage = $(this).data('page');
                                settings.page = nextPage;
                                var nextURL = getSearchURL(settings.searchTerm, nextPage);
                                searchAndPopulate(nextURL, settings.apiClientId);
                            });
                        }
                    }
                }
            }

            function getSearchURL(searchTerm, pageToShow) {
                return settings.apiRootURL + '/search/photos?page=' + pageToShow + '&per_page=' + settings.imagesToReturn + '&orientation=' + settings.orientation + '&query=' + searchTerm;
            }

            function getRandomURL(numberOfImages) {
                return settings.apiRootURL + '/photos/random?count=' + numberOfImages + '&orientation=' + settings.orientation;
            }

            function getImageURL(photo_id) {
                return settings.apiRootURL + '/photos/' + photo_id;
            }

            function pagination(c, m) {
                var current = c,
                    last = m,
                    delta = 2,
                    left = current - delta,
                    right = current + delta + 1,
                    range = [],
                    rangeWithDots = [],
                    l;
                for (var i = 1; i <= last; i++) {
                    if (i == 1 || i == last || i >= left && i < right) {
                        range.push(i);
                    }
                }
                for (var i of range) {
                    if (l) {
                        if (i - l === 2) {
                            rangeWithDots.push(l + 1);
                        } else if (i - l !== 1) {
                            rangeWithDots.push('...');
                        }
                    }
                    rangeWithDots.push(i);
                    l = i;
                }
                return rangeWithDots;
            }
        });
    };
})(jQuery);

A link to a demo on JSFiddle: click here

Questions I have:

Is there a better way of maintaining the returned images JSON between the Ajax request and populating the page i.e.

images = [];

Have I bound the click event handlers correctly in the plugin; i.e.

$("#us_SearchBtn").bind("click", function(e) {

Also if you have any suggestions on how to run a different callback depending on which "Choose background" link is clicked, I'd be really grateful.

\$\endgroup\$
1
\$\begingroup\$

Questions I have:

Is there a better way of maintaining the returned images JSON between the Ajax request and populating the page i.e.

images = [];

images is a value that can be removed from the global scope. The value is defined when the jQuery promise object returns a value

images variable is not necessary

Define a function to handle errors

  function handleError(error) {
    alert(error.message)
  }

return a value from callUnsplash

  function callUnsplash(url) {
    return $.ajax({
      url: url,
      dataType: 'json',
      method: 'GET',
      beforeSend: function(xhr) {
        xhr.setRequestHeader('Authorization', 'Client-ID ' + settings.apiClientId);
        xhr.setRequestHeader('Accept-Version', 'v1');
      },
      success: function(json) {
        totalImageCount = (json.hasOwnProperty('total')) ? json.total : settings.imagesToReturn;
        // `return` value here, which will be `images` parameter at `populateImages`
        return json.hasOwnProperty('results') ? json.results : json;
      },
      error: function(xhr, status, error) {
        // throw error
        throw new Error("Sorry, something went wrong (" + error + " " + xhr.status + " " + xhr.statusText + ")");
      }
    });
  }

images is value returned from callUnsplash(). $.map() can be substituted for $.each() for ability to return an HTML string or jQuery object when passed as parameter to .append(). Concatenate HTML strings where possible instead of calling .append() more than once chained to more than one jQuery() call. Include missing closing </div> where HTML is appended to #imageList unexpected results.

  function populateImages(images) {
    if (images.total == 0) {} else {
      //clear any existing images in the list
      $('#imageList').empty()
      .append($.map(images, function(image, index) {
        return `<li><a href="#" class="splashImage" data-photoid="${image.id}"><div class="in_max200"><img src='${image.urls.thumb}' class='in_singlepic in_max200'></div></a></li>`
      }));
      //bind the callback function to the splashImage so it is called when clicked. if no callback is provided, then the default will be performed
      $(".splashImage").on("click", function(event) {
        event.preventDefault();
        if (typeof settings.cbOnImageSelect == 'function') {
          var photoId = $(this).data('photoid');
          var url = getImageURL(photoId);
          // use `.then()`, handle errors
          callUnsplash(url).then(function(i) {
            settings.cbOnImageSelect.call(this, i);
          }, handleError);
        }
      });
      if (totalImageCount <= settings.imagesToReturn) {
        $('#us_Paging').hide();
      } else {
        $('#us_Paging').empty().show();
        if (totalImageCount > settings.imagesToReturn) {
          var noOfPages = Math.ceil(totalImageCount / settings.imagesToReturn);
          var x = pagination(settings.page, noOfPages);
          for (var p = 0; p < x.length; p++) {
            var linkText = ((x[p] !== '...') && (x[p] !== settings.page)) ? '<a href="##" class="us_changePage" data-page="' + x[p] + '">' + x[p] + '</a>&nbsp;&nbsp;' : x[p] + '&nbsp;&nbsp;';
            $('#us_Paging').append(linkText);
          }
          //add the click event to the buttons with the class us_changePage
          $(".us_changePage").on("click", function(event) {
            event.preventDefault();
            var nextPage = $(this).data('page');
            settings.page = nextPage;
            var nextURL = getSearchURL(settings.searchTerm, nextPage);
            searchAndPopulate(nextURL, settings.apiClientId);
          });
        }
      }
    }
  }

Use .then() instead of .done(). Handle errors

  function searchAndPopulate(url, key) {
    callUnsplash(url, key)
    .then(populateImages, handleError);
  }

Have I bound the click event handlers correctly in the plugin; i.e.

$("#us_SearchBtn").bind("click", function(e) {

Substitute .on() for .bind() which is deprecated

Also if you have any suggestions on how to run a different callback depending on which "Choose background" link is clicked, I'd be really grateful.

Not certain what is meant by "a different callback". If a different handle for the click event is meant logic can be included in the function passed to .on() where the data-photoid="${image.id}" of the clicked element can be evaluated to perform different tasks based on the image.id value. For example

if ($(this).data().photoid === "abc") {
  // do stuff
}

(function($) {
  /*
   *  unsplash
   * 
   *  jQuery plugin to manage interaction with unsplash API
   * 
   *
   */
  $.fn.unsplash = function(options, cbOnImageSelect) {
    // defaults for the plugin, client ID can be provided on a init by init basis so that each client can have their own CLIENTID and therefore won't count towards
    // a common limit
    var defaults = {
      apiRootURL: "https://api.unsplash.com", // root for API, should not need to be overridden                                                               
      searchTerm: '', // the search term the results will be relating to
      imagesToReturn: 6, // number of rows to output per page of results
      page: 1, // page of search results to display, will be 1 to start
      randomImagesToShow: 3, // number of images to display at the outset, 0 for none
      apiClientId: '9371fe014143706ce089532bc0149f75e5672f1a07acdd1599d42461c73e0707', // can be overridden for a client id per account
      orientation: 'landscape', // orientation of images to query for [landscape | portrait | squarish]
      source: '', // where the plugin is being called from
      cbOnImageSelect: $.noop
    };

    // images = [];
    var totalImageCount = 0;
    var selectedImage = {};

    function handleError(error) {
      alert(error.message)
    }

    var settings = $.extend({}, defaults, options);

    //return to avoid breaking chaining
    return this.each(function() {
      //clear any content in the unsplash search
      $('#unsplashSearch').empty()
        //add a search form
        .append('<div class="in_searchBox"><input type="text" name="us_searchTerm" id="us_searchTerm" value="" maxlength="50"><button type="submit" name="us_SearchBtn" id="us_SearchBtn"><span>Search</span></button></div>');
      //bind the search handler event to the button in the form
      $("#us_SearchBtn").on("click", function(e) {
        e.preventDefault();
        //reset page back to 1 to avoid a new search starting on the wrong page.
        settings.page = 1;
        //set the settings search term to the new value
        settings.searchTerm = $('#us_searchTerm').val();
        searchAndPopulate(getSearchURL(settings.searchTerm, 1), settings.apiClientId);
      });
      $('#unsplashSearch').append('<ul id="imageList" class="horizontalList"></ul>' +
        '<div id="us_Paging" style="text-align: center;"></div>');
      if (settings.randomImagesToShow > 0) {
        //build up the request url based on whether the search term is null or not...
        searchAndPopulate((!settings.searchTerm) ? getRandomURL(settings.randomImagesToShow) : getSearchURL(settings.searchTerm, 1), settings.apiClientId);
      } else {
        $('#us_Paging').hide();
      }

      //private functions
      function callUnsplash(url) {
        return $.ajax({
          url: url,
          dataType: 'json',
          method: 'GET',
          beforeSend: function(xhr) {
            xhr.setRequestHeader('Authorization', 'Client-ID ' + settings.apiClientId);
            xhr.setRequestHeader('Accept-Version', 'v1');
          },
          success: function(json) {
            totalImageCount = (json.hasOwnProperty('total')) ? json.total : settings.imagesToReturn;
            return json.hasOwnProperty('results') ? json.results : json;
          },
          error: function(xhr, status, error) {
            throw new Error("Sorry, something went wrong (" + error + " " + xhr.status + " " + xhr.statusText + ")");
          }
        });
      }

      function searchAndPopulate(url, key) {
        callUnsplash(url, key)
          .then(populateImages, handleError);
      }

      function populateImages(images) {
        if (images.total == 0) {} else {
          //clear any existing images in the list
          $('#imageList').empty()
            .append($.map(images, function(image, index) {
              return `<li><a href="#" class="splashImage" data-photoid="${image.id}"><div class="in_max200"><img src='${image.urls.thumb}' class='in_singlepic in_max200'></div></a></li>`;
            }));
          //bind the callback function to the splashImage so it is called when clicked. if no callback is provided, then the default will be performed
          $(".splashImage").on("click", function(event) {
            event.preventDefault();
            if (typeof settings.cbOnImageSelect == 'function') {
              var photoId = $(this).data('photoid');
              var url = getImageURL(photoId);
              callUnsplash(url).then(function(i) {
                settings.cbOnImageSelect.call(this, i);
              }, handleError);
            }
          });
          if (totalImageCount <= settings.imagesToReturn) {
            $('#us_Paging').hide();
          } else {
            $('#us_Paging').empty().show();
            if (totalImageCount > settings.imagesToReturn) {
              var noOfPages = Math.ceil(totalImageCount / settings.imagesToReturn);
              var x = pagination(settings.page, noOfPages);
              for (var p = 0; p < x.length; p++) {
                var linkText = ((x[p] !== '...') && (x[p] !== settings.page)) ? '<a href="##" class="us_changePage" data-page="' + x[p] + '">' + x[p] + '</a>&nbsp;&nbsp;' : x[p] + '&nbsp;&nbsp;';
                $('#us_Paging').append(linkText);
              }
              //add the click event to the buttons with the class us_changePage
              $(".us_changePage").on("click", function(event) {
                event.preventDefault();
                var nextPage = $(this).data('page');
                settings.page = nextPage;
                var nextURL = getSearchURL(settings.searchTerm, nextPage);
                searchAndPopulate(nextURL, settings.apiClientId);
              });
            }
          }
        }
      }

      function getSearchURL(searchTerm, pageToShow) {
        return settings.apiRootURL + '/search/photos?page=' + pageToShow + '&per_page=' + settings.imagesToReturn + '&orientation=' + settings.orientation + '&query=' + searchTerm;
      }

      function getRandomURL(numberOfImages) {
        return settings.apiRootURL + '/photos/random?count=' + numberOfImages + '&orientation=' + settings.orientation;
      }

      function getImageURL(photo_id) {
        return settings.apiRootURL + '/photos/' + photo_id;
      }

      function pagination(c, m) {
        var current = c,
          last = m,
          delta = 2,
          left = current - delta,
          right = current + delta + 1,
          range = [],
          rangeWithDots = [],
          l;
        for (var i = 1; i <= last; i++) {
          if (i == 1 || i == last || i >= left && i < right) {
            range.push(i);
          }
        }
        for (var i of range) {
          if (l) {
            if (i - l === 2) {
              rangeWithDots.push(l + 1);
            } else if (i - l !== 1) {
              rangeWithDots.push('...');
            }
          }
          rangeWithDots.push(i);
          l = i;
        }
        return rangeWithDots;
      }
    });
  };
})(jQuery);

jsfiddle

\$\endgroup\$
  • \$\begingroup\$ Thanks for the comments; when I say a different callback, I mean if you clicked on choose image on widget 1 and selecting an image it would run a different callback from clicking on choose image on widget 3 and selecting an image - even if all the chooseImage links were initialised with a single unsplash() call. So I guess putting the callback as a data attribute on the choose image link and using eval(). Is that bad practice and if so, is there a better way? thanks \$\endgroup\$ – j4ffa Jan 7 at 12:22
  • \$\begingroup\$ @j4ffa By callback are you referring to the function set at cbOnImageSelect? eval() is not necessary. \$\endgroup\$ – guest271314 Jan 7 at 16:27

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.