Overview
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.
Features
- Lazy image loading
- ES6 transpiler
- JavaScript source maps
- Sass CSS preprocessor
- PostCSS Autoprefixer
- CSSnano
- Webpack 3
Source Code: https://github.com/jimfrenette/uiCookbook/tree/master/photogrid
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 will be used later by npm 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
, _photogrid.scss
and _lazyimage.scss
. The leading underscore in the filename denotes that the file is a Sass partial and therefore will only be processed if imported.
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 the lazyimage module in the app entry point.
index.js
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";
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',
css: './sass/style.scss',
},
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 || {}
}
},
{
loader: 'postcss-loader'
},
{
loader: 'sass-loader'
}
]
})
},
{
test: /\.js$/,
use: 'babel-loader'
}
]
},
plugins: [
new ExtractTextPlugin('style.css'),
],
devtool: '#eval-source-map'
}
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 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 browserlist 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"
],
...
}
Create a PostCSS configuration module in the project root for requiring the autoprefixer plugin.
postcss.config.js
module.exports = {
plugins: [
require('autoprefixer')
]
}
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-es2015
Create a .babelrc configuration file in the root of the project.
.babelrc
{
"presets": [
[
"es2015",
{
"modules": false
}
]
]
}
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
})
])
}
- 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.
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.
Lightbox
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({}, 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);
});
}
$('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');
});
window.onclick = function(evt) {
if (evt.target.dataset.lightbox == lightbox) {
$('[data-lightbox="' + lightbox + '"]').removeClass('is-open');
}
}
$(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
.lightbox {
max-height: 0;
position: fixed;
z-index: 1;
left: 0;
top: 0;
width: 100%;
height: 100%;
overflow: auto;
background-color: rgb(0,0,0);
background-color: rgba(0,0,0,0.75);
> div {
position: relative;
background-color: rgb(0,0,0);
margin: 15% auto;
padding: 20px;
color: #fff;
.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%;
}
}
.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);
}
}
Import the lightbox partial, slick and slick-theme into the style Sass file.
style.scss
...
@import "lightbox";
@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 || {}
}
},
{
loader: 'postcss-loader'
},
{
loader: 'resolve-url-loader'
},
{
loader: 'sass-loader?sourceMap'
}
]
})
},
{
test: /\.js$/,
use: 'babel-loader'
},
{
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.