Slick Lazy Load Photo Grid Using Webpack 3

How to layout and lazy load images in a flexible grid similar to how facebook displays them in a post. Selected images open a lightbox for previewing within a carousel. Image alt text is converted into a caption below the image. YouTube Video

Features

  • Lazy image loading
  • ES6 transpiler
  • JavaScript source maps
  • Sass CSS preprocessor
  • PostCSS Autoprefixer
  • CSSnano
  • Webpack 3

uiCookbook Photogrid
Demo

Source Code

Getting Started

If you don’t already have Node.js installed, then that needs to be the first order of business. Head on over to Node.js and get that taken care of.

Navigate to the project root in your CLI, such as Terminal, Cygwin or PowerShell.

Enter npm init to interactively create a package.json file. Accepting the default options is okay for now. The metadata in this file can be updated later as needed.

npm init

A package.json file should now exist in the root of the project. This file will be used later with the npm command when installing modules and running scripts.

Create an index.html file in the root of the project. Add an unordered list of images to the body with the .photogrid style class. For lazy image loading, instead of using a src attribute, put the image path in a data attribute named data-src. Also include the dist/style.css link in the document head and dist/app.js link before the closing body tag.

index.html
  <!DOCTYPE html>
  <html lang="en">
  <head>
      <meta charset="UTF-8">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <meta http-equiv="X-UA-Compatible" content="ie=edge">
      <title>Document</title>
      <link rel="stylesheet" media="all" href="dist/style.css">
  </head>
  <body>
      <ul class="photogrid">
          <li>
              <img data-src="https://images.unsplash.com/reserve/unsplash_528b27288f41f_1.JPG?auto=format&fit=crop&w=2700&q=80&ixid=dW5zcGxhc2guY29tOzs7Ozs%3D" alt="Sea breeze and splashing waves. A photo by @dankapeter on Unsplash"/>
          </li>
          <li>
              <img data-src="https://images.unsplash.com/photo-1494633114655-819eb91fde40?auto=format&fit=crop&w=2550&q=80&ixid=dW5zcGxhc2guY29tOzs7Ozs%3D" alt="Above it All. A photo by @anthonyintraversato on Unsplash" />
          </li>
          <li>
              <img data-src="https://images.unsplash.com/photo-1511125357779-27038c647d9d?auto=format&fit=crop&w=2551&q=80&ixid=dW5zcGxhc2guY29tOzs7Ozs%3D" alt="Found this beauty while being lost in the streets of Cancún, Mexico. A photo by @odiin on Unsplash" />
          </li>
          <li>
              <img data-src="https://images.unsplash.com/photo-1483919283443-8db97e2bcd81?auto=format&fit=crop&w=2550&q=80&ixid=dW5zcGxhc2guY29tOzs7Ozs%3D" alt="Touring NYC. A photo by @freddymarschall on Unsplash" />
          </li>
          <li>
              <img data-src="https://images.unsplash.com/photo-1487357298028-b07e960d15a9?auto=format&fit=crop&w=2550&q=80&ixid=dW5zcGxhc2guY29tOzs7Ozs%3D" alt="Wind turbines, Greece. A photo by @jeisblack on Unsplash" />
          </li>
      </ul>
      <script async src="dist/app.js"></script>
  </body>
  </html>

Using Emmet, which is built into VS Code, you can create the index.html content by entering an exclamation mark on the first line then select the tab key.

Lazy Image Loading

This is accomplished using David Walsh’s Simple Image Lazy Load and Fade method.

Create a folder named src. In that folder, create both a js folder and a sass folder.

Within the js folder, create an index.js app entry point file and a lazyimage.js module file.

Within the sass folder, create a style.scss file that will be used as the entry point for Sass processing. Add three Sass partials, _base.scss, _lazyimage.scss and _photogrid.scss. The leading underscore in the filename denotes that the file is a Sass partial and therefore will only be processed if imported.

  • photogrid
    • index.html
    • package.json
    • src
      • js
        • index.js
        • lazyimage.jsc
      • sass
        • style.scss
        • _base.scss
        • _lazyimage.scss
        • _photogrid.scss

In the lazyimage.js module, export this Lazyimage ES6 class.

lazyimage.js
export default class Lazyimage {
  constructor(options) {

    this.init();
  }

  init() {

    [].forEach.call(document.querySelectorAll('img[data-src]'), function(img) {
      img.setAttribute('src', img.getAttribute('data-src'));
      img.onload = function() {
        img.removeAttribute('data-src');
      };
    });
  }
}

Import both the entry point Sass, style.scss and the lazyimage module into the index.js app entry point file.

index.js
import style from '../sass/style.scss'
import Lazyimage from './lazyimage'

new Lazyimage();

Add this Sass to the base partial for the unordered list and image element default style.

_base.scss
ul {
  padding: 0;
}

img {
  border-style: none;
  height: auto;
  max-width: 100%;
}

Add this Sass to the photogrid partial to apply the photogrid styling with flexbox to the list of images.

_photogrid.scss
ul.photogrid {
  margin: 0.5vw 0.5vw 0.5vw -0.5vw;
  font-size: 0;
  flex-flow: row wrap;
  display: flex;

  li {
    flex: auto;
    width: 200px;
    margin: 0.5vw;
  }
}

Add this Sass to the lazyimage partial to fade the image in when the image has loaded and the data-src attribute has been removed.

_lazyimage.scss
img {
  opacity: 1;
  transition: opacity 0.3s;
}

img[data-src] {
  opacity: 0;
}

Import the three partials into the style Sass file.

style.scss
@import "base";
@import "photogrid";
@import "lazyimage";

The next section covers adding Webpack and the build configurations, installing dependencies, running the first build and verifying the application output.


Webpack

Install Webpack. Version 3.10 is the latest as of this writing.

npm install --save-dev webpack

Create a webpack.config.js file in the root of the project. The CSS for this application is built using the process documented in my post last month, Webpack 3 Sass cssnano Autoprefixer Workflow. You are encouraged to read it for more information.

webpack.config.js
const path = require('path')
const webpack = require('webpack')
const ExtractTextPlugin = require('extract-text-webpack-plugin')

module.exports = {
context: path.resolve(__dirname, './src'),
  entry: {
    app: './js/index.js'
  },
  output: {
    path: path.resolve(__dirname, './dist'),
    publicPath: '/dist/',
    filename: '[name].js'
  },
  module: {
    rules: [
    {
      test: /\.(css|scss)$/,
      use: ExtractTextPlugin.extract({
        fallback: 'style-loader',
        use: [
          {
            loader: 'css-loader',
            options: {
              minimize: true || {/* CSSNano Options */}
            }
          },
          {
            loader: 'postcss-loader'
          },
          {
            loader: 'sass-loader'
          }
        ]
      })
    },
    {
      test: /\.js$/,
      use: 'babel-loader'
    }
    ]
  },
  plugins: [
    new ExtractTextPlugin('style.css'),
  ],
  devtool: '#eval-source-map'
}

Extract Text Plugin

Install the extract-text-webpack-plugin. This plugin is for extracting the css from the bundle into a style.css file.

npm install --save-dev extract-text-webpack-plugin

Autoprefixer

Autoprefixer evaluates the CSS and adds or removes vendor prefixes such as -webkit and -moz using caniuse.com data.

Install the autoprefixer PostCSS plugin.

npm install --save-dev autoprefixer

As devDependencies are installed using the --save-dev option, the package.json file will be updated so the node modules can be re-installed using npm install.

In the package.json file, add a browserslist configuration that lists minimum browser support for autoprefixer.

package.json
{
  "name": "photogrid",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "browserslist": [
    "> 2%",
    "last 2 versions",
    "ie > 9"
  ],

  ...
}

Note that the ellipsis ... in the code snippet above is not a part of the actual code and is there only to denote code that is being skipped and not applicable to the example. Keep this in mind when you encounter an ellipsis in the remaining snippets. To view the entire file, examine the source code.

Create a PostCSS configuration module in the project root for requiring the autoprefixer plugin.

postcss.config.js
module.exports = {
  plugins: [
    require('autoprefixer')
  ]
}

Babel

Babel is a JavaScript transpiler that converts the projects ES6 into ES5 JavaScript. Install Babel, loader and preset.

npm install --save-dev babel-core babel-loader babel-preset-env

Create a .babelrc configuration file in the root of the project.

.babelrc
{
  "presets": ["env"]
}

SASS and CSS Loaders

Install these loaders to handle the Sass and CSS for the extract-text-webpack-plugin.

SASS Loader compiles Sass to CSS, also requires node-sass.

npm install --save-dev sass-loader node-sass

PostCSS Loader processes CSS with PostCSS.

npm install --save-dev postcss-loader

CSS Loader resolves import at-rules and url functions in the CSS.

npm install --save-dev css-loader

Style Loader inlines <style></style> in the DOM.

npm install --save-dev style-loader

First Build

Install cross-dev to make the development and production NODE_ENV var easier to setup and use when running NPM scripts on various platforms, such as Windows and OS X.

npm install --save-dev cross-env

In the package.json file, define dev and build commands for npm-run-script. These are used to execute the development or production webpack bundle process.

package.json
...

  ],
  "scripts": {
    "dev": "cross-env NODE_ENV=development webpack --watch --progress --colors",
    "build": "cross-env NODE_ENV=production webpack --progress --hide-modules",
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  ...
}

At the end of the webpack configuration JavaScript after the module.export, add this code for the respective dev and build npm scripts using the NODE_ENV settings.

webpack.config.js
...

if (process.env.NODE_ENV === 'production') {
  module.exports.devtool = '#source-map'
  module.exports.plugins = (module.exports.plugins || []).concat([
    new webpack.DefinePlugin({
      'process.env': {
        NODE_ENV: '"production"'
      }
    }),
    new webpack.optimize.UglifyJsPlugin({
      sourceMap: true,
      compress: {
        warnings: false
      }
    }),
    new webpack.LoaderOptionsPlugin({
      minimize: true
    })
  ])
}

Using the npm-run-scripts alias npm run, execute the build.

npm run build

You should now have a new dist folder in the root of the project where the css and js are output. Load the webpage in a browser to see if the photo grid renders as expected.

The next section covers adding jQuery, the lightbox and slider.


Create a new file named lightbox.js in the src/js folder with jQuery and slick-carousel import statements at the top.

lightbox.js
import $ from 'jquery'
import 'slick-carousel'

Install both jQuery and Slick using npm with the --save option. This will list them as dependencies in the package.json file.

npm install --save jquery slick-carousel

Note that multiple packages can be installed with a single npm install command. For even less typing, use the npm install alias npm i. More info available in the npm-install documentation.

Add the Lightbox class to the lightbox.js module.

lightbox.js
...

export default class Lightbox {
    constructor(options) {

        this.settings = $.extend({/* defaults */}, options);

        this.init();
    }

    init() {

        let source = $(this.settings.source);

        if (source.length) {

            source.each((index, el) => {

                this.create(index, el);
            });
        }
    }

    create(index, el) {

        let lightbox = this.settings.name + '__' + index,
            opener = $(el).find(this.settings.opener);

        $('body').append('<div data-lightbox="' + lightbox + '" class="lightbox"><div></div></div>');

        if (this.settings.type === 'slider') {

            $('div[data-lightbox="' + lightbox + '"] > div')
                .append('<div class="lightbox-slider"></div>');

            var slider = $('div[data-lightbox="' + lightbox + '"] .lightbox-slider');

            slider.slick({
                dots: true
            });

            opener.each((index, el) => {
                this.popSlider(lightbox, slider, el);
            });
        }

        // close button
        $('div[data-lightbox="' + lightbox + '"] > div')
            .prepend('<a class="lightbox-close" href="javascript:void(0)">+</a>');

        $('.lightbox-close').on( 'click', function() {
            $('[data-lightbox="' + lightbox + '"]').removeClass('is-open');
        });

        //close on outside click
        window.onclick = function(evt) {
            if (evt.target.dataset.lightbox == lightbox) {
                $('[data-lightbox="' + lightbox + '"]').removeClass('is-open');
            }
        }

        // close on escape
        $(document).keyup(function(evt) {
            if (evt.which === 27) {
                $('[data-lightbox="' + lightbox + '"]').removeClass('is-open');
            }
        });

    }

    popSlider(lightbox, slider, el) {

        let img = $(el).find('img'),
            src = img.prop('src'),
            slide = document.createElement('div'),
            slideImg = document.createElement('img');

        slideImg.src = src;

        slide.appendChild(slideImg);

        if (img.attr('alt')) {
            let caption = document.createElement('p'),
                captionText = document.createTextNode(img.attr('alt'));

            caption.appendChild(captionText);
            slide.appendChild(caption);
        }

        slider.slick('slickAdd', slide);

        img.wrap('<a href="' + src + '"></a>').on( 'click', function(evt) {

            evt.preventDefault();

            $('[data-lightbox="' + lightbox + '"]').addClass('is-open');

            let index = $(this).closest(el).index();

            slider.slick('slickGoTo', index);
        });
    }
}

In the app entry point, import the lightbox module and set the options to target photogrid.

index.js
...

import Lightbox from './lightbox'

new Lightbox({
    name: 'lightbox',
    source: '.photogrid',
    opener: 'li',
    type: 'slider'
});

In the src/sass folder, create a new Sass partial named _lightbox.scss.

_lightbox.scss
/* overlay (background) */
.lightbox {
  max-height: 0; /* instead of `display: none` so slider can init properly. */

  position: fixed; /* stay in place */
  z-index: 1;
  left: 0;
  top: 0;
  width: 100%;
  height: 100%;
  overflow: auto; /* enable scroll if needed */
  background-color: rgb(0,0,0); /* fallback color */
  background-color: rgba(0,0,0,0.80); /* black w/ opacity */

  > div {
    position: relative;
    background-color: rgb(0,0,0);
    padding: 20px;
    color: #fff;
    //width: 100%; /* could be more or less, depending on screen size */
    width: 90vw;
    margin: 5vw auto;

    .slick-prev,
    .slick-next {
      z-index: 10;
    }
    .slick-prev {
      left: -20px;
    }
    .slick-next {
      right: -20px;
    }
    .slick-dots {
      li button:before {
        color: #fff;
      }
    }
  }

  &.is-open {
    max-height: 100%; /* unhide */
  }

  @media only screen and (max-width: 600px) {
    background-color: rgba(0,0,0,0.95); /* black w/ opacity */

    > div {
      padding: 40px 0 20px 0;
      width: 100vw;
      margin: 0 auto;

      .slick-slide p {
        padding: 0 15px;
      }

      .slick-next, .slick-prev {
        display: none;
      }
    }
  }

  @media only screen and (min-width: 1025px) {
    > div {
      max-width: 1024px;
    }
  }
}

/* close button */
.lightbox-close {
  position: absolute;
  top: 2px;
  right: 2px;
  text-align: center;
  line-height: 20px;
  font-size: 20px;
  font-weight: bold;
  color: rgba(255,255,255,0.75);
  width: 20px;
  height: 20px;
  transform: rotate(45deg);
  text-decoration: none;
  z-index: 10;

  &:hover {
    color: rgb(255,255,255);
  }

  @media only screen and (max-width: 600px) {
    top: 4px;
    right: 4px;
    line-height: 32px;
    font-size: 32px;
    width: 32px;
    height: 32px;
  }
}

Import the lightbox partial, slick and slick-theme into the style Sass file.

style.scss
...

@import "lightbox";

/* node_modules */
@import "~slick-carousel/slick/slick.scss";
@import "~slick-carousel/slick/slick-theme.scss";

Build

Before building the app and css again, the webpack configuration needs to be updated for font url loading in the slick-theme. Here is the updated module configuration.

webpack.config.js
...

  module: {
    rules: [
    {
      test: /\.(css|scss)$/,
      use: ExtractTextPlugin.extract({
        fallback: 'style-loader',
        use: [
          {
            loader: 'css-loader',
            options: {
              minimize: true || {/* CSSNano Options */}
            }
          },
          {
            loader: 'postcss-loader'
          },
          {
            /* for ~slick-carousel/slick/slick-theme.scss */
            loader: 'resolve-url-loader'
          },
          {
            /* for resolve-url-loader:
                source maps must be enabled on any preceding loader */
            loader: 'sass-loader?sourceMap'
          }
        ]
      })
    },
    {
      test: /\.js$/,
      use: 'babel-loader'
    },
    { /* for ~slick-carousel/slick/slick-theme.scss */
      test: /\.(eot|woff|woff2|ttf|svg|png|jpg|gif)$/,
      loader: 'url-loader?limit=30000&name=[name]-[hash].[ext]'
    }
    ]
  }

...

Install both url-loader and resolve-url-loader for webpack to handle the relative paths in the slick-theme.

npm install --save-dev url-loader resolve-url-loader

Run the build.

npm run build

For development, use npm run dev to watch for changes and build incrementally when changes are saved.

That’s it, refresh the browser and select a photo to open the photo preview lightbox and slider.


Part 1 of 2 in the Slick Carousel series.

Slick Carousel Responsive slidesToShow Recipe

comments powered by Disqus