How To Create a WordPress Shortcode for flickr Albums

How to create a WordPress custom shortcode to display photos from flickr. This post documents using the Slick Lazy Load Photo Grid along with the flickr API to render photo albums in WordPress Posts wherever the shortcode is entered. Webpack 4, autoprefixer, and babel are included for building the JS and CSS.

Requirements

  • flickr API key. From their API page, select the Create an App link for more information on how to request an API key.
  • Node.js with NPM

Shortcode

Creating a custom shortcode is done by adding PHP code to the theme functions.php file. I prefer keeping my custon WordPress functions in separate files rather than lumping them all in the existing functions.php file. To do this, create a functions folder and add a new slickflickr.php file to it.

Add the following php code to the slickflickr.php file to register the custom shortcode_handler function. Be sure to replace the example $api_key value with your flickr API key.

slickflickr.php
<?php
/**
 * Slick Flickr Album Shortcode
 *
 * @param array [photoset_id
 * example
 * [slickflickrshortcode photoset_id=72157685387149525]
 *
 * @return string Unordered list of images for lazy loading slick photo gallery
 *
 * url format reference:
 * https://www.flickr.com/services/api/misc.urls.html
 */
function slickflickr_shortcode_handler($args) {

    $xml = slickflickr_get_photoset($args['photoset_id']);

    ob_start();
  ?>
  <ul class="photogrid">
  <?php
    foreach($xml->photoset[0]->photo as $photo) {
        echo '<li><img data-src="https://farm' . $photo['farm'] . '.staticflickr.com/' . $photo['server'] . '/' . $photo['id'] . '_' . $photo['secret'] . '_b.jpg" alt="' . $photo['title'] . '" /></li>';
    }
    ?>
    </ul>
    <?php
    return ob_get_clean();
}
add_shortcode( 'slickflickrshortcode',  'slickflickr_shortcode_handler' );

function slickflickr_curl($url) {
    $ch = curl_init($url);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_HEADER, 0);
    $data = curl_exec($ch); // response
    curl_close($ch);
    return $data;
}

function slickflickr_get_photoset($photoset_id) {
    $results = null;
    $api_key = '1234a567bc89d101ef1ghij121314klm';
    $url = 'https://api.flickr.com/services/rest/?method=flickr.photosets.getPhotos&api_key=' . $api_key . '&photoset_id=' . $photoset_id . '&format=rest';
    $xml = new \SimpleXmlElement(slickflickr_curl($url));
    return $xml;
}
Reference: WordPress Shortcode API

At or near the bottom of the functions.php, add this require statement to include the custom shortcode handler in the theme.

functions.php
...

/**
* Slick flickr Shortcode Handler
*/
require get_parent_theme_file_path( '/functions/slickflickr.php' );
  • Note that the ellipsis ... in the code snippets throught this tutorial are not a part of the actual code and is there only to denote code that is being skipped and not applicable to the example.

Test the custom shortcode by entering the shortcode text in a post. For example, [slickflickrshortcode photoset_id=72157685387149525]. When you view-source for that post, you should see an unordered list with img elements. The flickr album image URL’s are added to the img element data-src attribute for lazy-loading. The styling and image lazy-loading is handled by CSS and JavaScript code that we will add to the WordPress theme in a subsequent section.


Theme

For the front end, we need to bring the UI build process for the JavaScript and CSS into a WordPress theme. In this tutorial, I’m going to use a copy of the TwentySeventeen Theme that comes with WordPress along with my TwentySeventeen Theme Sass. If you want a completely blank slate instead, you could use the _s theme I’ve written about here.

Download or clone the TwentySeventeen Theme Sass into the mytwentyseventeen folder. If you have an existing theme, download the zip file from github instead, extract, and copy the src folder into your theme.

cd wp-content/themes

git clone https://github.com/jimfrenette/twentyseventeen-sass.git mytwentyseventeen

Copy all of the twentyseventeen theme folders and files into the new mytwentyseventeen theme.

cd wp-content/themes

cp -r twentyseventeen/ mytwentyseventeen/

On Windows? I have written about some Unix like command line interfaces for it including WSL ubuntu zsh nvm etc., and Cygwin Oh My ZSH Recipe.

UI Build

Since we’re not using Gulp, delete gulpfile.js and package.json.

Create these files in the the new theme. We will edit them later. For example,

cd wp-content/themes/mytwentyseventeen

# create the .babelrc file
touch .babelrc

# create a new package.json file using npm
npm init

# create the postcss config
touch postcss.config.js

NPM Packages

Install NPM packages to transpile the Javascript and Sass using Webpack 4.

cd wp-content/themes/mytwentyseventeen

npm i webpack webpack-cli webpack-merge --save-dev

npm i autoprefixer --save-dev

npm i babel-loader @babel/core @babel/preset-env --save-dev

npm i css-loader style-loader --save-dev

npm i mini-css-extract-plugin --save-dev

npm i node-sass sass-loader --save-dev

npm i optimize-css-assets-webpack-plugin --save-dev

npm i postcss-loader --save-dev

npm i resolve-url-loader url-loader --save-dev

npm i uglifyjs-webpack-plugin --save-dev

Webpack 4 Configuration

Create a webpack folder with three config.js files.

cd wp-content/themes/mytwentyseventeen

mkdir webpack

cd webpack

touch base.config.js

touch dev.config.js

touch prod.config.js

The base.config.js file contains a configuration that is common to both dev and prod.

base.config.js
const path = require('path');
const MiniCssExtractPlugin = require("mini-css-extract-plugin");

module.exports = {
  context: path.resolve(__dirname, '../src'),
  entry: {
    app: './js/index.js'
  },
  output: {
    path: path.resolve(__dirname, '../dist')
  },
  plugins: [
    new MiniCssExtractPlugin({
      filename: "../style.css",
    })
  ],
  externals: {
    // require("jquery") is external and available
    // on the global var jQuery
    "jquery": "jQuery"
  },
  module: {
    rules: [
      {
        test: /\.(css|scss)$/,
        use: [
          MiniCssExtractPlugin.loader,
          'css-loader',
          {
            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$/,
        exclude: /node_modules/,
        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]'
      }
    ]
  }
}

The dev.config.js file contains only configuration settings specifically for development. The configuration settings in the base.config.js file are combined with this dev configuration by webpack-merge.

dev.config.js
const merge = require('webpack-merge');
const baseConfig = require('./base.config.js');

module.exports = merge(baseConfig, {
  devtool: 'eval-source-map',
});

The prod.config.js file contains only configuration settings specifically for the production build. The configuration settings in the base.config.js file are also combined with this configuration by webpack-merge.

prod.config.js
const merge = require('webpack-merge');
const baseConfig = require('./base.config.js');
const OptimizeCSSAssetsPlugin = require("optimize-css-assets-webpack-plugin");
const UglifyJsPlugin = require("uglifyjs-webpack-plugin");

module.exports = merge(baseConfig, {
  optimization: {
    minimizer: [
      new OptimizeCSSAssetsPlugin({})
    ]
  },
  plugins: [
    new UglifyJsPlugin({
      uglifyOptions: { output: { comments: false } }
    })
  ]
});

Add the presets property to the .babelrc congfiguration file.

.babelrc
{
  "presets": ["@babel/preset-env"]
}

Add this plugins property to require the autoprefixer postcss plugin to the postcss.config.js configuration file.

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

Add a browserslist property to the package.json file to inform our transpilers which browsers to support. Here, we’re targeting browsers greater than 2% according to global usage statistics and Internet Explorer versions greater than 10.

package.json
{
  "name": "mytwentyseventeen",
  "version": "1.0.0",
  "description": "WordPress Twenty Seventeen Theme",
  "main": "index.js",
  "browserslist": [
    "> 2%",
    "last 2 versions",
    "ie > 10"
  ],

  ...
}

In the src folder, create a js folder containing an index.js entrypoint that imports the Sass.

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

npm-run-script

In the package.json file, add dev and prod properties to the scripts object for running webpack.

package.json
  ...

  "scripts": {
    "dev": "webpack --mode=development --watch --progress --colors --config webpack/dev.config.js",
    "build": "webpack --mode=production --progress --hide-modules --config webpack/prod.config.js",
    "test": "echo \"Error: no test specified\" && exit 1"
  },

  ...
}

Lazy Load Album Images

This technique defers downloading of the images making the page load faster.

Inside the src/js, create a lazyimage.js module file.

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');
          };
      });
  }

}

To import the Lazyimage class from the module, add this code to the src/js/index.js file.

index.js
...

import Lazyimage from './lazyimage'

new Lazyimage();

Inside the src/sass folder, create a _photogrid.scss and a _lazyimage.scss file. The Sass partial filenames have leading underscore to indicate they only get processed when imported.

_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;
  }
}
_lazyimage.scss
img {
  opacity: 1;
  transition: opacity 0.3s;
}

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

Update src/sass/style.scss to import the Sass partials.

style.scss
...

@import "photogrid";
@import "lazyimage";

Run the Build

One sec, we need to link the dist/app.js that is going to be built in the theme footer. Add a script element before the closing body tag.

footer.php
...

<script async src="/wp-content/themes/mytwentyseventeen/dist/app.js"></script>

</body>
</html>

Before moving onto the lightbox and Slick slider Sass and JavaScript code, run the build to verify that everything is styled and working properly.

npm run build

If all looks good after clearing the cache, run the development build to watch for changes and build incrementally when the remaining lightbox and slider code is added.

npm run dev

Slick Slider Lightbox

Install Slick

npm i slick-carousel --save

Inside the src/js, create a lightbox.js module file.

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

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);
    });
  }
}

Update the src/js/index.js to import the Lightbox class from the module.

index.js

import style from '../sass/style.scss'
import Lazyimage from './lazyimage'
import Lightbox from './lightbox'

new Lazyimage();

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

Inside the src/sass folder, create a _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: 9;
  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;
  }
}

Update src/sass/style.scss adding the lightbox and slick-carousel imports.

style.scss
...

@import "photogrid";
@import "lazyimage";
@import "lightbox";

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

For a complete listing of the source files that have been created or updated, I advise you to examine the source code repository on GitHub.

Source Code

CSS Flexbox with Sidebars Toggle

This post documents how to create a fully responsive flexbox content layout that contains a pure CSS open/close toggle for the left and right sidebars. The CSS is built from Sass using Webpack with Autoprefixer. For development, a Webpack Dev Server is included.

Features

  • Pure CSS Sidebar toggle
  • Sass CSS preprocessor
  • PostCSS Autoprefixer
  • CSSnano
  • Webpack 4
  • Webpack Dev Server

responsive sidebars in flexbox

Getting Started

Create an index.html file in the root of the project with the following HTML. This markup creates a a layout with header, content and footer containers. The content container includes aside containers for the sidebars and checkbox inputs to toggle the sidebars open or closed.

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/app.css">
</head>
<body>
    <div class="site">
        <header class="header">Header</header>
        <div class="content">
            <article class="main">
                <p>Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo.</p>
            </article>
            <input type="checkbox" id="sidebar-1-toggle">
            <label for="sidebar-1-toggle"></label>
            <aside class="sidebar sidebar-1">
                <div>Sidebar 1</div>
            </aside>
            <input type="checkbox" id="sidebar-2-toggle">
            <label for="sidebar-2-toggle"></label>
            <aside class="sidebar sidebar-2">
                <div>Sidebar 2</div>
            </aside>
        </div>
        <footer class="footer">Footer</footer>
    </div>
</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.

Create a src/sass folder in the project. And in the sass folder create a style.scss file with the following css.

style.scss
body {
  min-width: 400px;
}

.site {
  max-width: 1440px;
  margin: 0 auto;
  position: relative;
  font-weight: bold;
  text-align: center;
}

.content {
  display: flex;
}

.header {
  background: tomato;
  padding: 10px;
}

.footer {
  background: lightgreen;
  padding: 10px;
}

.main {
  flex: 1;
  min-width: 0;
  text-align: left;
  background: deepskyblue;
  padding: 10px;
}

.sidebar > div {
  padding: 10px;
}

.sidebar-1 {
  background: gold;
}

.sidebar-2 {
  background: hotpink;
}

@media all and (max-width: 800px) {
  .content {
    flex-wrap: wrap;
  }
  .main {
    flex: 1 1 100%;
  }
}

@media all and (min-width: 801px) {
  .sidebar-1 { order: 1; }
  .main    { order: 2; }
  .sidebar-2 { order: 3; }
}

Webpack

Webpack is going to be used to process the Sass as its saved into CSS and serve the HTML locally.

  • Pre-requisite – the Webpack module bundler example here requires Node.js.

Use your favorite Node.js bash command line to setup the project. Navigate to the project root and enter npm init. Accepting the defaults is fine.

npm init

Now we’re ready to install Webpack and other devDependencies into our project. In case you’re wondering, the projects node_modules folder is where all of these packages get installed. For more information, visit npmjs.com and/or watch this introduction to npm video.

Install Webpack and its Command Line Interface.

npm install -D webpack webpack-cli

Create a config folder for the webpack configuration files.

In this config folder, there will be three webpack configuration files, base.config.js, dev.config.js and prod.config.js.

First, create the config/base.config.js which holds the configuration common to both dev and production.

base.config.js
const path = require('path');
const MiniCssExtractPlugin = require("mini-css-extract-plugin");

module.exports = {
  context: path.resolve(__dirname, '../src'),
  entry: {
    app: './js/index.js'
  },
  plugins: [
    new MiniCssExtractPlugin
  ],
  module: {
    rules: [
      {
        test: /\.(css|scss)$/,
        use: [
          MiniCssExtractPlugin.loader,
          'css-loader',
          {
            loader: 'postcss-loader' },
          {
            loader: 'sass-loader' }
        ]
      },
      {
        test: /\.js$/,
        exclude: /node_modules/,
        use: 'babel-loader'
      }
    ]
  }
}

The mini-css-extract-plugin extracts the css from the bundle and outputs the css file after the loaders process the sass.

Install the plugin and loaders for the base configuration.

npm install -D mini-css-extract-plugin

npm install -D babel-loader @babel/core @babel/preset-env

npm install -D autoprefixer style-loader css-loader postcss-loader

npm install -D node-sass sass-loader

Next, create the config/dev.config.js.

dev.config.js
const baseConfig = require('./base.config.js');
const merge = require('webpack-merge');
const writeFilePlugin = require('write-file-webpack-plugin');

module.exports = merge(baseConfig, {
  devtool: 'eval-source-map',
  plugins: [
    new writeFilePlugin()
  ]
});

Install the webpack-dev-server, write-file-webpack-plugin and webpack-merge.

npm install -D webpack-dev-server write-file-webpack-plugin

npm install -D webpack-merge

The write-file-webpack-plugin, like the mini-css-extract-plugin, outputs the css file for the in-memory webpack dev server. However, this plugin does not do anything unless it is used with the webpack-dev-server.

We are using webpack-merge to combine our separate webpack configuration modules.

We need a small configuration file for postcss. In the project root, create a file named postcss.config.js.

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

Create the config/prod.config.js.

prod.config.js
const baseConfig = require('./base.config.js');
const merge = require('webpack-merge');
const OptimizeCSSAssetsPlugin = require("optimize-css-assets-webpack-plugin");
const UglifyJsPlugin = require("uglifyjs-webpack-plugin");

module.exports = merge(baseConfig, {
  optimization: {
      minimizer: [
        new OptimizeCSSAssetsPlugin({})
      ]
  },
  plugins: [
    new UglifyJsPlugin({
      uglifyOptions: { output: { comments: false } }
    })
  ]
});

Install the optimize-css-assets-webpack-plugin and uglifyjs-webpack-plugin to make our client side assets as small as possible for production.

npm install -D optimize-css-assets-webpack-plugin

npm install -D uglifyjs-webpack-plugin

Supported Browsers

Add a browserslist property to the package.json file to define which vender prefixes to with the generated CSS. In this example, we target browsers greater than 2% according to global usage statistics and Internet Explorer versions greater than 9. [more info]

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

  ...
}

npm-run-script

Add these webpack commands to the scripts property. These npm script commands are what we will run to kickoff the webpack dev server and watch or prod webpack build.

package.json
  ...

  "browserslist": [
    "> 2%",
    "ie > 9"
  ],
  "scripts": {
    "dev": "webpack-dev-server --mode=development --watch --progress --colors --config config/dev.config.js --open",
    "build": "webpack --mode=production --progress --hide-modules --config config/prod.config.js",
    "test": "echo \"Error: no test specified\" && exit 1"
  },

  ...
}

Entrypoint JavaScript

Create a src/js/index.js javascript entrypoint file to import the style Sass for the webpack bundle.

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

Sidebar Toggle Sass

Create these three Sass partials for the sidebar toggle, src/sass/_toggle.scss, src/sass/_default-open.scss and src/sass/_default-closed.scss.

This is the _toggle.scss partial imported by the style.scss file.

_toggle.scss
/**
 * Sidebar toggle
 */

// uncomment for default closed sidebars
// @import "default-closed";
// uncomment for default open sidebars
@import "default-open";

/**
 * checkboxes open/close the sidebars
 * #sidebar-1-toggle and #sidebar-2-toggle
 */
 #sidebar-1-toggle, #sidebar-2-toggle {
  display: none;
}

label,
label {
  position: absolute;
  top: 0;
  display: block;
  padding: .25em 10px;
  font-size: 1.5em;
  cursor: pointer;
}

label {
  left: 0;
}

label {
  right: 0;
}

@media all and (max-width: 800px) {
  label,
  label {
    display: none;
  }
  .sidebar-1,
  .sidebar-2 {
    display: block;
    width: 50%;
  }
}

This is the _default-open.scss partial imported by the _toggle.scss.

_default-open.scss
/**
 * Sidebars open by default
 */

.sidebar {
  width: 20%;
}

label,
label {
  &:after {
    content: '\2013';
  }
}

#sidebar-1-toggle:checked ~ label,
#sidebar-2-toggle:checked ~ label {
  &:after {
    content: '+';
  }
}

#sidebar-1-toggle:checked ~ .sidebar-1,
#sidebar-2-toggle:checked ~ .sidebar-2 {
  display: none;
  width: 0;
}

This is the _default-closed.scss partial imported by the _toggle.scss file when uncommented.

_default-closed.scss
/**
 * Sidebars closed by default
 */

.sidebar {
  display: none;
}

label,
label {
  &:after {
    content: '+';
  }
}

#sidebar-1-toggle:checked ~ label,
#sidebar-2-toggle:checked ~ label {
  &:after {
    content: '\2013';
  }
}

#sidebar-1-toggle:checked ~ .sidebar-1,
#sidebar-2-toggle:checked ~ .sidebar-2 {
  display: block;
  width: 20%;
}

Webpack Dev Server

For development, use npm run dev to launch the dev server, open the browser, watch for changes and build incrementally when changes are saved.

npm run dev

Now test the development server and file watch build. Edit the _toggle.scss file and comment @import "default-open";, uncomment @import "default-closed";. Save the file, check the terminal output and refresh the browser.

For production, cancel the dev task Ctrl + C and run the build.

npm run build 
Source Code

Webpack 3 Sass cssnano Autoprefixer Workflow II

This follow up to the basic Webpack 3 Sass preprocessor workflow I wrote about here includes how to handle processing of relative URLs in the Sass for things like fonts and images. Ken Wheeler’s popular Slick carousel contains these asset types in its Sass, so let’s use that for this exercise.

Getting Started

Prerequisite: Node.js with npm.

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 json file is used to manage the project dependencies, set configuration options for supported modules and run npm scripts.

Create an index.html file in the project root with an unordered list of placeholder images. Include the dist/style.css link in the document head and dist/app.js link before the closing body tag. Add a class attribute with the value slider to the unordered list element. This sets it apart for selecting with our script and style code: <ul class="slider">

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="slider">
        <li>
            <h3>Slide 1</h3>
            <img src="https://placehold.it/800x600" />
        </li>
        <li>
            <h3>Slide 2</h3>
            <img src="https://placehold.it/800x600" />
        </li>
        <li>
            <h3>Slide 3</h3>
            <img src="https://placehold.it/800x600" />
        </li>
        <li>
            <h3>Slide 4</h3>
            <img src="https://placehold.it/800x600" />
        </li>
        <li>
            <h3>Slide 5</h3>
            <img src="https://placehold.it/800x600" />
        </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.

Slick

Install jQuery and Slick carousel using npm.

npm install --save jquery slick-carousel

In the src/js folder, create an index.js JavaScript file to import the Sass entry file and the jQuery and Slick carousel modules.

index.js
import style from '../sass/main.scss'
import $ from 'jquery'
import 'slick-carousel'

$('ul.slider').slick({
    dots: true
});

Before we can build this JavaScript a Webpack configuration and npm script will need to be defined.

Webpack & Babel

Install Webpack

npm install --save-dev webpack

Create a webpack.config.js file in the project root to transpile our ES6 JavaScript into ES5 for the browser using babel.

webpack.config.js
const path = require('path')
const webpack = require('webpack')

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: /\.js$/,
      exclude: /node_modules/,
      use: 'babel-loader'
    }
    ]
  },
  devtool: '#eval-source-map'
}

Install Babel

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

Create a .babelrc configuration file in the project root.

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

Here is how the project is structured so far.

  • project
    • .babelrc
    • index.html
    • package.json
    • webpack.config.js
    • src
      • js
        • index.js

Build

Install cross-dev for a consistent NODE_ENV variable when running NPM scripts on Windows and OS X.

npm install --save-dev cross-env

Define dev and build commands for npm-run-script in package.json.

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 bottom of the webpack configuration, add 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 to execute the build.

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

You should now have a new dist folder in the root of the project where the Webpack bundle is output.

The next page covers:

  • installing Webpack loaders to process Sass
  • installing Extract Text Plugin to output a style.css file
  • installing Autoprefixer PostCSS plugin
  • creating and importing Sass files
  • re-building and verifying the application output

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.

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 page covers adding Webpack and the build configurations, installing dependencies, running the first build and verifying the application output.


Google Maps API with Webpack

Google Map application that uses a draggable location marker to set address, longitude and latitude geocode inputs. This post covers some of the Webpack tools for both a local development environment and production build of the app constructed of a few JavaScript modules and css.

For application details, I encourage you to read my Google Maps API with Browserify post from a year ago. The following overview highlights building the refactored code with Webpack. This refactor brings the application more up to date with ES6 standards. Other changes include the removal of jQuery as a dependency. All of the source code is available for download and browsing at GitHub.

NPM

Here is the updated package.json for installing webpack, loaders and other dependencies. This file also defines scripts to start and run commands.

package.json
{
  "name": "gmap-webpack",
  "version": "0.0.1",
  "description": "location inputs populated by google maps api, built with webpack",
  "main": "",
  "scripts": {
    "start": "cross-env NODE_ENV=development webpack-dev-server --progress --inline --open",
    "build": "cross-env NODE_ENV=production webpack"
  },
  "devDependencies": {
    "babel-core": "^6.0.0",
    "babel-loader": "^6.0.0",
    "babel-preset-es2015": "^6.0.0",
    "cross-env": "^3.2.3",
    "css-loader": "^0.25.0",
    "style-loader": "^0.13.2",
    "webpack": "^2.2.1",
    "webpack-dev-server": "^2.4.1"
  }
}

The npm start command is using the cross-env plugin to set the Node environment variable properly for the platform. The webpack-dev-server then bundles the modules and launches a static web server inline for live reloading.

The npm run build command uses cross-env to set the environment flag. Then webpack bundles the modules according to the webpack.config below optimized with a source-map for production.

webpack.config
const path = require('path')
const webpack = require('webpack')

module.exports = {
    context: path.resolve(__dirname, './src'),
    entry: {
        app: './js/index.js',
    },
    output: {
        path: path.resolve(__dirname, './dist'),
        publicPath: '/dist/',
        filename: 'bundle.js'
    },
    devtool: '#eval-source-map',
    module: {
        rules: [{
            test: /\.js$/,
            loader: 'babel-loader',
            exclude: /node_modules/
        },
        {
            test: /\.css$/,
            use: ['style-loader','css-loader']
        }]
    }
}

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
    })
  ])
}

In the webpack.config file, the output.publicPath property is used by the webpack-dev-server to determine where the bundles should be served from.

The devtool property controls if and how source maps are generated. eval-source-map is for faster rebuild when in development. In production mode, devtool is set to use the appropriate source-map style instead.

To test drive the app and experiment with the dev server and live reloading, follow these steps assuming you have Node.js installed.

  1. Download and extract the source code or use git to clone the uiCookbook repository from https://github.com/jimfrenette/uiCookbook
  2. Navigate to the uiCookbook/geocode/gmap-webpack folder in your CLI, such as terminal or Cygwin.
  3. Run npm i or npm install
  4. Run npm start to bring up the dev server, bundle the modules and load the app in the browser.

Source Code

A Vue.js version of the Google Maps applicaton is available in the geocode/gmap-vue directory. The vue-cli webpack-simple scaffold was used to generate the Vue.js 2 project template.

Laravel User Authentication with Ajax Validation

This post documents how to add Ajax form validation to Laravel User Authentication that is generated by the Artisan console.

Requirements

An instance of Laravel 5.2 setup for local development is required. Head over to the Laravel 5.2 docs to get setup as needed. For my setup on Windows 10, I am using XAMPP 5.6.12 which meets all of the requirements. Note that your document root will need to be the laravel/public directory. Here is the virtual hosts configuration I am using for Laravel.

httpd-vhosts.conf
<VirtualHost *:8080>
    DocumentRoot "C:/xampp/htdocs/laravel/public"
    ServerName laravel.dev
    ServerAlias www.laravel.dev
    SetEnv APPLICATION_ENV development
    <Directory "C:/xampp/htdocs/laravel">
        Options Indexes MultiViews FollowSymLinks
        AllowOverride All
        Order allow,deny
        Allow from all
    </Directory>
</VirtualHost>

More info on setting up XAMPP for Windows

New Laravel Site

Using Composer, install Laravel 5.2 into htdocs/laravel

cd c:/xampp/htdocs

composer create-project laravel/laravel laravel "5.2.*"
Generate Authentication Scaffolding
cd laravel

php artisan make:auth

Now the Laravel site has routes and views to allow a user to register, login, logout, and reset their password. A HomeController has also been added to allow authenticated users to enter its view.

User Model

I prefer to organize models within their own directory. Therfore, let’s create a folder named Models in the app directory and move the User model into it.

mkdir app/Models

mv app/User.php app/Models

Edit app/Models/User.php, update the App namespace to App\Models.

User.php
<?php

namespace App\Models;

use Illuminate\Foundation\Auth\User as Authenticatable;

class User extends Authenticatable
{
    ...

  • Throughout this tutorial you will encounter an ellipsis … in the code examples. These are not a part of the code and are there only to denote code that is being skipped and not applicable to the example. To view the entire file, examine the source code.

Edit app/Http/Controllers/Auth/AuthController.php, update use App\User to use App\Models\User.

AuthController.php
<?php

namespace App\Http\Controllers\Auth;

use App\Models\User;
use Validator;
use App\Http\Controllers\Controller;
use Illuminate\Foundation\Auth\ThrottlesLogins;
use Illuminate\Foundation\Auth\AuthenticatesAndRegistersUsers;

class AuthController extends Controller
{
    ...

Edit, config/auth.php, update the authentication drivers user provider.

'users' => [
    'driver' => 'eloquent',
    'model' => App\User::class,
],
REPLACE
'model' => App\User::class,
WITH
'model' => App\Models\User::class,

Database

Edit the .env configuration file to access the database. I have already created a database named laravel on the MySQL Server that is part of XAMPP.

.env
DB_DATABASE=laravel
DB_USERNAME=root
DB_PASSWORD=

The settings above are the only ones I needed to update for my setup, I do not have a password set for MySQL Server and will just use the root account for local development. Now run php artisan migrate to create the tables for users.

Run Migration
php artisan migrate
Migration table created successfully.
Migrated: 2014_10_12_000000_create_users_table
Migrated: 2014_10_12_100000_create_password_resets_table

Validation Rules

The validation rules are currently located in the AuthController, I would like to be able to share them with the new controller we will create later for ajax validation. Since these rules apply to the User, and that model is already being used by the AuthController, it seeems to be the best place for them.

Edit app/Models/User.php, adding the public function rules() to the class.

User.php
<?php

namespace App\Models;

use Illuminate\Foundation\Auth\User as Authenticatable;

class User extends Authenticatable
{
    /**
     * Get the validation rules that apply to the user.
     *
     * @return array
     */
    public function rules()
    {
        return [
            'name' => 'required|max:255',
            'email' => 'required|email|max:255|unique:users',
            'password' => 'required|min:6|confirmed',
        ];
    }

    ...

Edit app/Http/Controllers/Auth/AuthController.php. Locate the validator function and replace the array of rules being passed to the Validator make method with a call to the newly created rules method in our User model.

REPLACE
return Validator::make($data, [
    'name' => 'required|max:255',
    'email' => 'required|email|max:255|unique:users',
    'password' => 'required|min:6|confirmed',
]);
WITH
return Validator::make($data, (new User)->rules());

Before going further, now would be a good time to test registering a new user, login, logout etc.. to make sure everyting works. All we have done so far aside from the code that artisan generated is move the User model and move the validation rules from the AuthController into it.

The next page covers using the api middleware in routes, creating the Validation\UserController for handling the XHR request and finally updating the templates and adding the JavaScript for Ajax.


Design Patterns

Design patterns are reusable solutions to commonly occurring problems in software design.
Addy Osmani

Constructor Pattern

All methods and properties of the object are added in the constructor.

function Track(artist, title, year) {
    this.artist = artist;
    this.title = title;
    this.year = year;

    this.trackInfo = function() {
        return this.title + ", by " + this.artist + ", Year released: " + this.year;
    };
}

var theFixer = new Track("Pearl Jam", "The Fixer", 2009);
var lazaretto =  new Track("Jack White", "Lazaretto", 2014);

console.log(theFixer.trackInfo()); // The Fixer, by Pearl Jam, Year released: 2009

Constructor Pattern with Prototype

function Track(artist, title, year) {
    this.artist = artist;
    this.title = title;
    this.year = year;
}

Track.prototype.trackInfo = function() {
    return this.title + ", by " + this.artist + ", Year released: " + this.year;
};

var theFixer = new Track("Pearl Jam", "The Fixer", 2009);
var lazaretto =  new Track("Jack White", "Lazaretto", 2014);

console.log(theFixer.trackInfo()); // The Fixer, by Pearl Jam, Year released: 2009

Prototype Pattern

var trackPrototype = {
    init: function(artist, title, year) {
        this.artist = artist;
        this.title = title;
        this.year = year;
    },
    trackInfo: function() {
        return this.title + ", by " + this.artist + ", Year released: " + this.year;
    }
};

function track(artist, title, year) {
    
    function F() {}
    F.prototype = trackPrototype;
    
    var f = new F();
    
    f.init(artist, title, year);
    return f;
}
 
var theFixer = track("Pearl Jam", "The Fixer", 2009);
var lazaretto = track("Jack White", "Lazaretto", 2014);

console.log(theFixer.trackInfo()); // The Fixer, by Pearl Jam, Year released: 2009

Object Literal Module Pattern

var playlist = {
    
    tracksCollection: [],
    
    lastTrack: function() {
        return tracksCollection[tracksCollection.length -1];
    },
    
    addTrack: function(track) {
        tracksCollection.push(track);
    },
    
    init: function(tracks) {
        tracks.forEach( function(track) {
           this.addTrack(track); 
        });
    }
}

playlist.init([new Track("Pearl Jam", "The Fixer", 2009)]);
playlist.lastTrack().trackInfo(); // The Fixer, by Pearl Jam, Year released: 2009
playlist.addTrack(new Track("Jack White", "Lazaretto", 2014));
playlist.lastTrack().trackInfo(); // Lazaretto, by Jack White, Year released: 2014

IIFE Module Pattern

var playlist = (function() {

    var tracksCollection = [];

    return {

        lastTrack: function() {
            return tracksCollection[tracksCollection.length -1];
        },

        addTrack: function(track) {
            tracksCollection.push(track);
        },

        init: function(tracks) {
            tracks.forEach( function(track) {
            this.addTrack(track);
            });
        }
    }

})();

playlist.init([new Track("Pearl Jam", "The Fixer", 2009)]);
playlist.lastTrack().trackInfo(); // The Fixer, by Pearl Jam, Year released: 2009
playlist.addTrack(new Track("Jack White", "Lazaretto", 2014));
playlist.lastTrack().trackInfo(); // Lazaretto, by Jack White, Year released: 2014

Resource

Learning JavaScript Design Patterns
A book by Addy Osmani

Google Maps API with Browserify

This post documents how to use local JavaScript modules with Browserify and the Google Maps JavaScript API. In this example, the Google Map contains a marker that can be dragged to reset the browser form with the marker position location data. Additionally, Browserify shim is used to require jQuery since it is already being loaded from a CDN.

Requirements

Install Browserify

After installing Node.js, using a Command shell, install Browserify globally. *

# global browserify install
npm install -g browserify

* If you have an older version of global Browserify, run npm rm –global browserify to remove the old version before installing the newer one.

Project Folders and Files

Create these folders and files. Browse, clone or download the source code if you prefer.

    • css
      • style.css
    • js
    • src
      • js
        • modules
          • gapi.js
          • gmap.js
          • jsonp.js
          • location.js
        • index.js
    • gulpfile.js
    • index.html

Web Page

This web page contains the map container, address and geocode location inputs. jQuery is being loaded from the Google Hosted Libraries CDN.

index.html
<!DOCTYPE html>
<html lang="en">
<head>
	<meta charset="utf-8">
	<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
	<title>Page Title</title>
	<link rel="stylesheet" href="/css/style.css">
</head>
<body>
	
	<p>Drag the marker to a location on the map and set input values.</p>
	
	<div id="map-canvas"></div>
	<label>Street
		<input type="text" id="street" name="street" />		
	</label>
	<label>Locality
		<input type="text" id="locality" name="locality" />
	</label>
	<label>Region
		<input type="text" id="region" name="region" />
	</label>
	<label>Country
		<input type="text" id="country" name="country" />
	</label>
	<label>Latitude
		<input type="text" id="latitude" name="latitude" />
	</label>
	<label>Longitude
		<input type="text" id="longitude" name="longitude" />
	</label>
	
	<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.3/jquery.min.js"></script>

        <!-- Prod / built version -->
	<!--script src="/js/bundle.js"></script-->

</body>
</html>

Application Entry Point

The index module is the starting point for the map application. The module dependency requirements are defined followed by a call to the load function exported by the gapi module. gapi.load uses a callback function before the location.init.

index.js
var $ = require('jQuery');
var gapi = require('./modules/gapi');
var location = require('./modules/location');

gapi.load( function () {
    location.init();
});

Google Map JavaScript API

Loading the Google Maps JavaScript API requires a global callback. For this I have created a jsonp module which accepts three parameters, a URL (url), a global callback function name (callbackname), and a callback function (done).

jsonp.js
module.exports = function (url, callbackname, done) {
    var script = document.createElement('script');
    script.type = 'text/javascript';
    script.src = url;
    var s = document.getElementsByTagName('script')[0];
    s.parentNode.insertBefore(script, s);

    window[callbackname] = done;
};

The gapi module makes use of the jsonp module to load the API:

gapi.js
var jsonp = require('./jsonp');

var gapiurl = 'http://maps.googleapis.com/maps/api/js?callback=__googleMapsApiOnLoadCallback';

exports.load = function (done) {
    jsonp(gapiurl, '__googleMapsApiOnLoadCallback', done);
};

Location Module

The location module initiates the google map using the gmap.init function exported by the gmap module. The gmap.init callback results data is handled by the location module to populate the address inputs.

location.js
var gmap = require('./gmap');

var latitude = 39.084014922903;
var longitude = -77.51372591791;

exports.init = function () {
    gmap.init(
        latitude,
        longitude,
        function(result){
            console.log('RESULT',result);

            if (result.address) {
                _setAddress(result.address);
            }

            $('#latitude').val(result.latitude);
            $('#longitude').val(result.longitude);
        }
    );
};

_setAddress = function(address) {
    var street = [];
    if (address.street_number) {
        street.push(address.street_number);
    }
    if (address.route) {
        street.push(address.route);
    }
    if (street.length > 0) {
        $('#street').val(street.join(' '));
    }
    if (address.locality) {
        $('#locality').val(address.locality);
    }
    if (address.administrative_area_level_1) {
        $('#region').val(address.administrative_area_level_1);
    }
    if (address.country) {
        $('#country').val(address.country);
    }
};

Google Map

The gmap module uses the Google Maps JavaScript API.

gmap.js
exports.init = function (lat, lng, callback) {
    var geocoder = new google.maps.Geocoder();
    var marker = new google.maps.Marker({
        position: new google.maps.LatLng(lat, lng),
        anchorPoint: new google.maps.Point(0, -29),
        draggable: true
    });
    map = new google.maps.Map(document.getElementById('map-canvas'), {zoom: 10});
    map.setCenter(marker.position);
    marker.setMap(map);

    google.maps.event.addListener(marker, 'dragend', function (evt) {

        var latlng = {lat: evt.latLng.lat(), lng: evt.latLng.lng()};
        var addrComponents = {
            street_number: 'short_name',
            route: 'long_name',
            establishment: 'long_name',
            locality: 'long_name',
            administrative_area_level_1: 'short_name',
            country: 'short_name',
            postal_code: 'short_name'
        };
        result = {
            address: {},
            latitude: 0,
            longitude: 0
        };

        geocoder.geocode({'location': latlng}, function(results, status) {
            if (status === google.maps.GeocoderStatus.OK) {
                if (results[0]) {

                    var i; var type = null;
                    for (i = 0; i < results[0].address_components.length; i++) {
                        type = results[0].address_components[i].types[0];
                        if (addrComponents[type]) {
                            result.address[type] = results[0].address_components[i][addrComponents[type]];
                        }
                    }
                    result.latitude = latlng.lat;
                    result.longitude = latlng.lng;
                    if(typeof callback == "function"){
                        callback(result);
                    }

                } else {
                    window.alert('No results found');
                }
            } else {
                window.alert('Geocoder failed due to: ' + status);
            }
        });
    });
}
style.css

Add this file into the css folder to declare some basic styles.

label {
	display: block;
	width: 300px;
	margin-bottom: 1em;
}
input[type='text'] {
	display: block;
	width: 100%;
}
#map-canvas {
	width: 300px;
	height: 300px;
	margin-bottom: 1em;
}

Build

Use the Gulp task and build runner to configure and run the build process. Install Gulp and the Node modules locally that are needed to properly bundle, minify and sourcemap the Browserify modules.

Install Gulp

Install gulp globally with the npm install -g command as follows: *

# global gulp install
npm install -g gulp

* If you have an older version of global Gulp, run npm rm –global gulp to remove the old version before installing the newer one.

Create package.json

The package.json file contains project metadata and lists dependencies that are available on npm.

# create package.json
npm init

Install node modules

# install dependencies and save to package.json
npm install browserify --save-dev
npm install browserify-shim --save-dev
npm install gulp --save-dev
npm install gulp-sourcemaps --save-dev
npm install gulp-uglify --save-dev
npm install gulp-util --save-dev
npm install gulp-webserver --save-dev
npm install vinyl-buffer --save-dev
npm install vinyl-source-stream --save-dev

Browserify Shim

Browserify shim is used so global jQuery can be required by modules. Near the bottom of the package.json file, add these browserify and browserify-shim properties. View the source code if needed.

"browserify": {
  "transform": ["browserify-shim"]
},
"browserify-shim": {
  "jQuery": "global:jQuery"
}

Create a gulpfile.js in the project root to define and configure the Gulp tasks. The default task is simply gulp. When gulp is executed, it will run the bundle task, start a local web server with livereload and open the app in a web browser.

gulpfile.js
'use strict';

var gulp        = require('gulp'),
    browserify  = require('browserify'),
    buffer      = require('vinyl-buffer'),
    gutil       = require('gulp-util'),
    source      = require('vinyl-source-stream'),
    sourcemaps  = require('gulp-sourcemaps'),
    uglify      = require('gulp-uglify'),
    webserver   = require('gulp-webserver');

gulp.task('bundle', function () {
  var b = browserify({
    entries: './src/js/index.js',
    debug: true
  });

  return b.bundle()
    .pipe(source('bundle.js'))
    .pipe(buffer())
    .pipe(sourcemaps.init({loadMaps: true}))
        .pipe(uglify())
        .on('error', gutil.log)
    .pipe(sourcemaps.write('./'))
    .pipe(gulp.dest('./js/'));
});

// static web server w/ livereload
gulp.task('server', function() {
    gulp.src('./')
        .pipe(webserver({
            livereload: true,
            directoryListing: false,
            open: true
        }));
});

gulp.task('default', ['bundle','server']);

Add watchify to the gulp workflow on the next page.

Source Code

Javascript Frameworks

After discovering yet another interesting Javascript framework for the web application development recently (Vue.js), I decided to put together this short list of some the emerging and more popular frameworks out there today for building user interfaces.

Emerging

Mithril is a light-weight robust MVC framework with no dependencies, a small API and small learning curve. Mithril implements a virtual DOM tree similar to React.js, however Mithril’s uncompiled templates run natively in the browser and compile into static Javascript data structures. Mithril.js was initially released in March, 2014 by Leo Horie. Source available at https://github.com/lhorie/mithril.

File Version Size
mithril.min.js 0.2.2-rc.1 19 kb

Vue.js is a library for building interactive web interfaces and its focus is on the view layer only. Vue.js extensions such as vue-router and vue-resource are available for inclusion into the application as needed. The Vue.js API is simple and easy to adapt compared to other frameworks. Vue.js was initially released in July, 2014 by Evan You. Source available at https://github.com/vuejs/vue.

File Version Size
vue.min.js 1.0.13 70 kb

Riot.js – A React-like user interface micro-library. Compare Riot.js with React and Polymer. Source available at https://github.com/riot/riot.

File Version Size
riot.min.js 2.3.12 19.2 kb

Polymer is a framework for creating and rendering custom web components. Web Components are the future of the web and Google has created the Polymer library set of polyfills for developers to dive into. Source available at https://github.com/Polymer/polymer.

Popular

AngularJS a framework by Google that binds HTML (views) to Javascript objects (models). AngularJS usage rivals that of BackboneJS, according to JavaScript analytics service Libscore, AngularJS is used on over 10,000 websites including The Weather Channel, Lego, Intel, Sprint and ABC News. AngularJS was initially released in September 2009 by Misko Hevery. Source available at https://github.com/angular/angular.js.

File Version Size
angular.min.js 1.4.8 145 kb

Ember.js is a framework based on the model-view-controller (MVC) pattern and follows Convention over Configuration (CoC). Ember has a large API that covers traditional MVC patterns and a broad range of helper utilities. Ember 2.0 has a brand new high performance rendering engine named Glimmer, and server-side rendering is provided with FastBoot. After Handlebars in 2010, on December 8, 2011, Yehuda Katz released Ember.js. Source available at https://github.com/emberjs/ember.js.

File Version Size
ember.min.js 2.2.0 435 kb

React.js is the framework that powers Facebook and Instagram’s user interface and is one of the fastest growing JavaScript frameworks today. Features include one-way data flow and a virtual-DOM using an in-memory data structure cache process so only applicable nodes on the client browser’s DOM are updated for high performance complex user interfaces. React.js was created by Jordan Walke, a ‎Software Engineer at Facebook and was initially released in May, 2013 by Facebook and Instagram. Source available at https://github.com/facebook/react.

File Version Size
react.min.js 0.14.6 133 kb
react-dom.min.js 0.14.6 1 kb
Total 134 kb

Backbone.js is a light weight framework that provides a RESTful JSON interface for data models, views, collections, and events. Backbone.js is used on over 12,000 websites according to Libscore including Linkedin, Pinterest, ESPN, Walmart, Target and Best Buy. Backbone.js was initially released on October 13, 2010 by Jeremy Ashkenas. Source available at https://github.com/jashkenas/backbone/.

File Version Size
backbone-min.js 1.2.3 22.5 kb
underscore-min.js 1.8.3 16.0 kb
Total 38.5 kb

Google Maps API RequireJS Module

This post documents how to create a RequireJS module that loads the Google Maps JavaScript API and is used by a web page to populate form inputs with location data. The Google Map contains a marker that can be dragged to reset the form with the marker position location data.

source code

Development Environment

In order to use the package managers, Gulp task runner, BrowserSync and RequireJS optimizer, you will need to have the following installed.

  1. Node.js
  2. Bower

Using a Command shell in the project root, follow these steps to get the local development environment setup.

Create package.json

The package.json file contains project metadata and lists dependencies that are available on npm. This is useful to have when you want to re-install or update the dependencies with the npm install command.

# create package.json
npm init

Install node modules

# install dependencies and save to package.json
npm install browser-sync --save-dev
npm install gulp --save-dev
npm install requirejs --save-dev

Create bower.json

The bower.json file contains project metadata and lists dependencies that are available on bower. Re-install / update bower components saved to this file with the bower install command.

# create bower.json
bower init

Install bower components

Only using the async plugin from this requirejs-plugins bundle for async dependency loading of the Google Maps JavaScript API.

# install bower components and save to bower.json
bower install --save requirejs-plugins

Project Folders and Files

Create these folders and files. Browse, clone or download the source code if you prefer.

  • css
  • js
  • src
    • js
      • modules
        • gmap.js
        • location.js
      • require
      • main.js
    • gulpfile.js
    • index.html
    • require.build.js

Web Page

This web page contains the map container, address and geocode location inputs. jQuery is loaded separately before the require.js module loader to demonstrate how to define jQuery when it is external.

index.html
<!DOCTYPE html>
<html lang="en">
<head>
	<meta charset="utf-8">
	<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
	<title>Page Title</title>
	<link rel="stylesheet" href="/css/style.css">
</head>
<body>
	
	<p>Drag the marker to a location on the map and set input values.</p>
	
	<div id="map-canvas"></div>
	<label>Street
		<input type="text" id="street" name="street" />		
	</label>
	<label>Locality
		<input type="text" id="locality" name="locality" />
	</label>
	<label>Region
		<input type="text" id="region" name="region" />
	</label>
	<label>Country
		<input type="text" id="country" name="country" />
	</label>
	<label>Latitude
		<input type="text" id="latitude" name="latitude" />
	</label>
	<label>Longitude
		<input type="text" id="longitude" name="longitude" />
	</label>
	
	<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.3/jquery.min.js"></script>
	
	<!-- Prod / built version -->
	<!--script data-main="/js/main" src="/js/require/require.js"></script-->
	<!-- Dev version -->
	<script data-main="/src/js/main" src="/src/js/require/require.js"></script>

</body>
</html>

Google Map JavaScript API

This module is dependent upon the RequireJS async plugin to load the google maps api. The gmap function accepts three parameters: the default latitude, default longitude and a callback function to handle the result data.

gmap.js
define(['async!//maps.googleapis.com/maps/api/js'],
    function(async){
    var gmap = {
        map: {},
        init: function gmap(lat, lng, callback) {
            var geocoder = new google.maps.Geocoder();
            var marker = new google.maps.Marker({
                position: new google.maps.LatLng(lat, lng),
                anchorPoint: new google.maps.Point(0, -29),
                draggable: true
            });
            map = new google.maps.Map(document.getElementById('map-canvas'), {zoom: 10});
            map.setCenter(marker.position);
            marker.setMap(map);

            google.maps.event.addListener(marker, 'dragend', function (evt) {

                var latlng = {lat: evt.latLng.lat(), lng: evt.latLng.lng()};
                var addrComponents = {
                    street_number: 'short_name',
                    route: 'long_name',
                    establishment: 'long_name',
                    locality: 'long_name',
                    administrative_area_level_1: 'short_name',
                    country: 'short_name',
                    postal_code: 'short_name'
                };
                result = {
                    address: {},
                    latitude: 0,
                    longitude: 0
                };

                geocoder.geocode({'location': latlng}, function(results, status) {
                    if (status === google.maps.GeocoderStatus.OK) {
                        if (results[0]) {

                            var i; var type = null;
                            for (i = 0; i < results[0].address_components.length; i++) {
                                type = results[0].address_components[i].types[0];
                                if (addrComponents[type]) {
                                    result.address[type] = results[0].address_components[i][addrComponents[type]];
                                }
                            }
                            result.latitude = latlng.lat;
                            result.longitude = latlng.lng;
                            if(typeof callback == "function"){
                                callback(result);
                            }

                        } else {
                            window.alert('No results found');
                        }
                    } else {
                        window.alert('Geocoder failed due to: ' + status);
                    }
                });
            });
        }
    };
    return gmap;
});

Location module

This module handles the result data from the gmap.js module and sets the input values.

location.js
define(['jquery','modules/gmap'],
    function($, gmap){
    var location = {
        // default map coordinates
        latitude: 39.084014922903,
        longitude: -77.51372591791,
        init: function(){
            gmap.init(
                location.latitude,
                location.longitude,
                function(result){
                    console.log('RESULT',result);

                    if (result.address) {
                        location._setAddress(result.address);
                    }

                    $('#latitude').val(result.latitude);
                    $('#longitude').val(result.longitude);
                }
            );
        },
        _setAddress: function(address) {
            var street = [];
            if (address.street_number) {
                street.push(address.street_number);
            }
            if (address.route) {
                street.push(address.route);
            }
            if (street.length > 0) {
                $('#street').val(street.join(' '));
            }
            if (address.locality) {
                $('#locality').val(address.locality);
            }
            if (address.administrative_area_level_1) {
                $('#region').val(address.administrative_area_level_1);
            }
            if (address.country) {
                $('#country').val(address.country);
            }
        }
    };
    location.init();
    return location;
});
main.js

JavaScript application entry point.

'use strict';

// since jQuery is already loaded from google cdn
define('jquery', [], function() {
    return jQuery;
});

require.config({
    paths : {
        //create alias to plugins (not needed if plugins are on the baseUrl)
        async: 'require/async'
    }
});

require(['modules/location'], function(location) {
     location.init();
});
gulpfile.js

This gulp file defines two tasks. 1. gulp copy copies node modules and bower components into the application. 2. gulp server runs the application in a static BrowserSync web server.

'use strict';
var gulp = require('gulp');
var browserSync = require('browser-sync').create();

// copy vendor libraries into app
gulp.task('copy', function() {
    gulp.src([
        './bower_components/requirejs-plugins/src/async.js',
        './node_modules/requirejs/require.js'
        ])
        .pipe(gulp.dest('./src/js/require/'));
});

// BrowserSync static server
gulp.task('server', function() {
    browserSync.init({
        server: {
            baseDir: './',
            directory: false,
            index: 'index.html'
        }
    });
});
require.build.js

This is the RequireJS Optimizer build configuration file. Once the entry point and modules are created, run node node_modules/requirejs/bin/r.js -o require.build.js to compile the modules into a single js/main.js file.

({
    baseUrl: './src/js',
    dir: './js',
    modules: [
        {
            name: 'main'
        }
    ],
    fileExclusionRegExp: /^(r|require.build)\.js$/,
    removeCombined: true,
    paths: {
        async: 'require/async'
    },
    preserveLicenseComments: false
})
style.css

Add this file into the css folder to declare some basic styles.

label {
	display: block;
	width: 300px;
	margin-bottom: 1em;
}
input[type='text'] {
	display: block;
	width: 100%;
}
#map-canvas {
	width: 300px;
	height: 300px;
	margin-bottom: 1em;
}

Overview

  1. Create package.json and bower.json
  2. Install node modules and bower components
  3. Create /index.html
  4. Create /src/js/modules/gmap.js
  5. Create /src/js/modules/location.js
  6. Create /src/js/main.js
  7. Create /gulpfile.js
  8. Create /require.build.js
  9. Create /css/style.css
  10. Copy the node modules and bower components into the application. In a command shell,
    run gulp copy
  11. Load the application in the web browser. In a command shell,
    run gulp server
  12. Optimize the application. In a command shell,
    run node node_modules/requirejs/bin/r.js -o require.build.js

Reference

Browserify with Sourcemaps

Browserify lets you write modular JavaScript in node.js style. At the beginning of each module you write, the respective dependencies are added using require statements. Then Browserify compiles the modules along with all of the dependencies into an optimized JavaScript file for use in the browser.

If jQuery has already been loaded by a CDN or otherwise, a Browserify Shim can be used to include global jQuery as a module.

Requirements

Install Browserify

Using a Command shell, enter npm commands as follows: *

# global browserify install
npm install -g browserify

* If you have an older version of global Browserify, run npm rm –global browserify to remove the old version before installing the newer one.

Create package.json

The package.json file contains project metadata and lists dependencies that are available on npm.

# create package.json
npm init

Install both jQuery and Lo-Dash from npm

# install dependencies
npm install jquery lodash --save
Updated on October 24, 2015

Local Modules

In the /src/js/modules folder, create a local application module. In this example, the service.js local module uses jQuery ajax to get data from a rest endpoint that returns json data. Then Lo-Dash is used to output the json data in the browser with a simple template.

service.js
var $ = require('jquery');
var _ = require('lodash');
var setElementHtml = require('./element');
module.exports = function() {

    var module = {

        init: function() {

            var tmplSource = $('#tmpl-artist').html(),
                template;

            $.getJSON('//jimfrenette.com/chinook/api/artists').done(function(data) {
                if (data) {

                    template = _.template(tmplSource)({
                        data: data
                    });

                    $(".artist-list").html(template);

                    setElementHtml('#some-id', 'Done');
                }
            });
        }
    };

    return module.init();

};

In the /src/js/modules folder, create a second local application module. This module depends on jQuery and contains a small function to update element HTML. Here we are simply using it to change the page heading as various steps are completed in this small application. It also shows how multiple module dependencies are defined and how they interact with each other.

element.js
var $ = require('jquery');
module.exports = function(selector, data) {

    return $(selector).html(data);

};

Application Entry Point

In the /src/js folder, create an index.js JavaScript file.

index.js
var setElementHtml = require('./modules/element');
var getServiceData = require('./modules/service');

setElementHtml('#some-id', 'Step 2...');

getServiceData();

Build

Use the Gulp task and build runner to configure and run the build process. Install Gulp and the Node modules locally that are needed to properly minify and sourcemap the Browserify JavaScript bundle.

Install Gulp

Install gulp globally with the npm install -g command as follows: *

# global gulp install
npm install -g gulp

* If you have an older version of global Gulp, run npm rm –global gulp to remove the old version before installing the newer one.

Install Remaining Node Modules

$ npm install gulp --save
$ npm install browserify --save
$ npm install vinyl-source-stream --save
$ npm install vinyl-buffer --save
$ npm install gulp-uglify --save
$ npm install gulp-sourcemaps --save
$ npm install gulp-util --save

Create a gulpfile.js in the project root for instructing gulp to perform the tasks.

gulpfile.js
'use strict';

var browserify = require('browserify');
var browserSync = require('browser-sync').create();
var gulp = require('gulp');
var source = require('vinyl-source-stream');
var buffer = require('vinyl-buffer');
var uglify = require('gulp-uglify');
var sourcemaps = require('gulp-sourcemaps');
var gutil = require('gulp-util');

gulp.task('javascript', function () {
  // set up the browserify instance on a task basis
  var b = browserify({
    entries: './src/js/index.js',
    debug: true
  });

  return b.bundle()
    .pipe(source('app.js'))
    .pipe(buffer())
    .pipe(sourcemaps.init({loadMaps: true}))
        // Add transformation tasks to the pipeline here.
        .pipe(uglify())
        .on('error', gutil.log)
    .pipe(sourcemaps.write('./'))
    .pipe(gulp.dest('./js/'));
});

// Static server
gulp.task('server', function() {
    browserSync.init({
        server: {
            baseDir: "./"
        }
    });

    gulp.watch('./js/*.js').on('change', browserSync.reload);

});
index.html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
  <title>Browserify App</title>
</head>
<body>
	<h1 id="some-id">Step 1 ...</h1>

	<!-- template wrapper -->
	<ul class="artist-list"></ul>

	<!-- template -->
	<script id="tmpl-artist" type="text/tmpl">
		<% _.forEach(data,function(artist) { %>
		<li data-artist-id="<%= artist.ArtistId %>"><%= artist.Name %></li>
		<% }); %>
	</script>
	<script type="text/javascript" src="/js/app.js"></script>
</body>
</html>

Run

Using a Command shell, run the gulp javascript task to Browserify the modules into a single JavaScript file, /js/app.js, with respective source map file, /js/app.js.map.

# run the task
$ gulp javascript

Server

Run the gulp server task to load the app in the browser using a BrowseSync static server. The gulp.watch event handler provides live reloading when the /js/app.js file is regenerated by the gulp javascript task.

# run the task
$ gulp server
Source Code

UPDATE: March 26, 2016

Browserify Shim jQuery

Browserify shim to use external / global jQuery loaded from a CDN. Source code is available for browsing and download at GitHub.

Source Code

Resources

AngularJS v2: Angular.io

At ng-conf 2015 last month it was announced that AngularJS 1.X will continue to reside at angularjs.org and Angular 2.0 will be hosted at angular.io. The new version of Angular is not a major update, it is a complete rewrite. In February, Brad Green announced that Angular 2 was officially Alpha in this First look at App Development in Angular2 video.

During the ng-conf 2015 keynote, Brad and Igor talked about some of the big work that is being done on Angular 1 is work that will be shared in Angular 2, including:

  • New Router that focuses on mobile and large complex apps.
  • New Internationalization (i18n) module
  • Better Performance
Angular 2
github.com/angular/angular

Best Practices

Here are a few Best Practices to follow when coding in JavaScript.

White Space

Follow these recommendations so commits and diffs will be easier to read.

  • Do not mix spaces and tabs.
  • Configure the editor to “show invisibles”. For example, in Sublime Text, the setting is "draw_white_space": "all".
  • Configure the editor to remove end of line whitespace.
  • Choose one, spaces or tab indentation for all JavaScript the team is writing for the project. Consider this, a space is always just one column. The code editor setup pages linked below have info on how to configure for consistent formatting of tabs to spaces and vice versa.
  • Use a consistent value throughout the project for the indent size, e.g., 4 columns.

Code Editor Setup

Comparison Operators

Use === and !==

The equality operator == implicitly tries to convert the values before comparing. The identity or “strict equals” operator === does not convert the values when comparing. Use the equality operator == and != only as needed.

Semicolons

Include all necessary semicolons. Configure your editor to use JSHint, a fork of JSLint, used to detect errors and potential problems in JavaScript.

Global Scope

Any variable that is defined outside of a function body is global in scope. Global variables can be accessed and altered in any scope. Global variables should be declared only as needed. For more information, read this YUI blog post.

Namespace

Using a namespace is a good practice to keep your code out of the global scope.

// global scope without a namespace
var foo = 'bar';

// namespace example
var app = {};
app.foo = 'bar';

// even better,
// namespace check before creation example
var app = app || {};

Line Length

80 characters per line should be sufficient enough to work with. When a statement will not fit on a single line, it may be necessary to break it. Place the break after an operator, ideally after a comma.

Eval

As Douglas Crockford states in his Code Conventions for the JavaScript Programming Language, “eval is Evil”. Improper use of eval makes your code unsafe and difficult to debug.

Curly Braces

For consistency and potential error prevention if additional statements are ever needed in the future, wrap them in curly braces. Minification will remove curly braces as needed for production.

// valid but not recommended
if (condition)
    alert('Condition true');
else
    alert('Condition false');

// recommended
if (condition) {
    alert('Condition true');
}
else {
    alert('Condition false');
}

Quotes

Use single quotes so strings containing double quotes don’t need to be escaped.

Arrays

Creating arrays should be done using the shorthand [] constructor instead of using new Array() notation.

// valid but not recommended
var city = new Array('Atlanta', 'Boston', 'Cleveland');
 
// recommended
var city = ['Atlanta', 'Boston', 'Cleveland'];

Objects

Object literal notation is recommended unless the object requires a specific prototype, then create the object by calling a constructor function with new.

// object literal notation
var config = {};

// constructor function with new
var cityGallery = new Gallery();

Object properties should be accessed using dot notation, unless the key is a variable, a reserved word, or a string that would not be a valid identifier.

prop = obj.propertyName;
prop = obj[variableKey];
prop = obj['default'];
prop = obj['key-with-hyphens'];

Script Element

The <script src="filename.js"></script> element or script tag as it is often referred to should be placed as far down in the body as possible. Ideally, just before the closing </body> tag. Some browsers try to load script files when they hit the script tag and the rest of the page is not parsed until script loading has finished. Additionally, async and defer attributes can be used to control when requested scripts are loaded.

async downloads the file during HTML parsing. The HTML parser will pause to execute the script when the download is completed.

defer downloads the file during HTML parsing. The script will execute after the HTML parser has completed. Multiple defer scripts execute in the order they appear in the document.

More info is available from this developers.google.com resource, Parser blocking versus asynchronous JavaScript

Miscellaneous

// object functions
app = {
  square: function( number ) {
    return number * number;
  },
  double: function( number ) {
    return number + number;
  }
};

Resources

JavaScript Templating Options

Javascript templating is a technique to render JSON data in HTML markup with JavaScript.

Handlebars is a semantic web template system, started by Yehuda Katz in 2010. Handlebars.js is a superset of Mustache, and can render Mustache templates in addition to Handlebars templates.

The document body will need an element that the template will be injected into. Since the template in this example will be a list item for each artist, an unordered list element is the logical choice.

Template Target
<!-- template wrapper -->
<ul class="artist-list"></ul>
Template Source
<!-- template -->
<script id="tmpl-artist" type="text/tmpl">
    {{#each this}}
    <li data-artist-id="{{ArtistId}}">{{Name}}</li>
    {{/each}}
</script>
JavaScript
//jQuery and Handlebars
$(function() {
    var tmplSource = $('#tmpl-artist').html(),
        template = Handlebars.compile(tmplSource);

    $.getJSON('//jimfrenette.com/chinook/api/artists').done(function(data) {
        if (data) {

            $(".artist-list").html(template(data));
        }
    });
});

A utility library delivering consistency, customization, performance, and extras.

Template Target (same as above)
Template Source
<!-- template -->
<script id="tmpl-artist" type="text/tmpl">
    <% _.forEach(data,function(artist) { %>
    <li data-artist-id="<%= artist.ArtistId %>"><%= artist.Name %></li>
    <% }); %>
</script>
JavaScript
//jQuery and Lo-Dash
$(function() {
    var tmplSource = $('#tmpl-artist').html(),
        template;

    $.getJSON('//jimfrenette.com/chinook/api/artists').done(function(data) {
        if (data) {

            template = _.template(tmplSource)({
                data: data
            });

            $(".artist-list").html(template);
        }
    });
});

jQuery only templates

A jQuery only templating solution may be all that you need if none of the other libraries that provide templating such as lodash, underscore or handlebars is present.

Template Target (same as above)
Template Source
<!-- template -->
<script id="tmpl-artist" type="text/tmpl">
    <li id="artist_0"></li>
</script>
JavaScript
//jQuery
$(function() {
    var tmplSource = $('#tmpl-artist').html(),
        artistTmplItem;

    $.getJSON('//jimfrenette.com/chinook/api/artists').done(function(data) {
        if (data) {

            for(var i = 0, len = data.length; i < len; i++) {

                artistTmpl = $(tmplSource);

                artistTmplItem = artistTmpl.attr('id','artist_'+data[i].ArtistId);

                artistTmplItem.text(data[i].Name);

                $('ul.artist-list').append(artistTmplItem);

            }
        }
    });
});

Just JavaScript

A JavaScript only solution that does not use jQuery or any other libraries.

Target element (same as Template Target above)
<ul class="artist-list"></ul>
JavaScript
var getJSON = function(url) {
    return new Promise(function(resolve, reject) {
        var xhr = new XMLHttpRequest();
        xhr.open('get', url, true);
        xhr.responseType = 'json';
        xhr.onload = function() {
            var status = xhr.status;
            if (status == 200) {
                resolve(xhr.response);
            } else {
                reject(status);
            }
        };
        xhr.send();
    });
};

getJSON('//jimfrenette.com/chinook/api/artists').then(function(data) {
    if (data) {
        // getElementsByClassName returns an array,
        // use the index [0] to get the first element match
        var artistList = document.getElementsByClassName('artist-list')[0],
            li = null,
            da = null;

        for(var i = 0, len = data.length; i < len; i++) {

            li = document.createElement('li');
            da = document.createAttribute('data-artist-id');
            da.value = data[i].ArtistId;
            li.setAttributeNode(da);
            li.appendChild(document.createTextNode(data[i].Name));
            artistList.appendChild(li);
        }
    }
});
Source Code

Benchmarks

Others ...

Resources

WordPress Theme Javascript Optimization

This post shows how to combine and minify multiple javascript files in a WordPress theme into one javascript file. The benefit is a single request to a javascript file that has been compressed by minification instead of multiple request to larger javascript files.

package.json

The package.json file contains meta data about your app or module and it includes the list of dependencies to install from Node Package Manager (NPM) when running npm install. NPM is bundled with Node.js; if you have not done so already, install Node.js so you have it. Then create and save this file in your themes root directory, for example /wp-content/my-theme/package.json. Then when you run npm install, the package.json file is read and the respective node modules are installed. More information is available here.

/* package.json */
{
  "name": "my-theme",
  "version": "0.1.0",
  "devDependencies": {
    "grunt": "~0.4.5",
    "grunt-contrib-jshint": "~0.10.0",
    "grunt-contrib-uglify": "~0.6.0"
  }
}
# install node_modules
$ npm install
gruntfile.js

The Grunt Javascript Task Runner will be used to configure and run the minification of /js/src/*.js into /js/main.js using UglifyJs.

/* gruntfile.js */
module.exports = function(grunt) {
  'use strict';
  // Project configuration.
  grunt.initConfig({
    pkg: grunt.file.readJSON('package.json'),
    uglify: {
      build: {
        files: {
          'js/main.js': ['js/src/*.js']
        }
      }
    }
  });
  // Load the plugin
  grunt.loadNpmTasks('grunt-contrib-uglify');
  // Default task(s).
  grunt.registerTask('default', ['uglify']);
};
file structure

Here is what the themes javascript file structure looks like for this example. The /js/src folder contains all of the individual javascript files for your theme. When you run grunt, these are all combined and minified into /js/main.js

javascript file location
Partial view of themes files showing location of javascript
functions.php

The functions.php file controls the loading of the themes javascript resources. In the functions.php file, the wp_enqueue_script() function links the script to the page. The pre-existing wp_enqueue_script() function calls are no longer valid since the javascript files have been moved to the /js/src/ folder. Here is an example of the code from the pre-existing functions.php that was linking the scripts:

function my-theme_scripts() {
  wp_enqueue_style( 'my-theme-style', get_stylesheet_uri() );

  wp_enqueue_script( 'my-theme-deflist', get_template_directory_uri() . '/js/deflist.js', array('jquery'), '20141011', true );

  wp_enqueue_script( 'my-theme-navigation', get_template_directory_uri() . '/js/navigation.js', array(), '20120206', true );
  
  wp_enqueue_script( 'my-theme-skip-link-focus-fix', get_template_directory_uri() . '/js/skip-link-focus-fix.js', array(), '20130115', true );

  if ( is_singular() && comments_open() && get_option( 'thread_comments' ) ) {
	wp_enqueue_script( 'comment-reply' );
  }
}
add_action( 'wp_enqueue_scripts', 'my-theme_scripts' );

Here is an example of the code from functions.php after making the changes to link /js/main.js instead. Since /js/src/deflist.js depends on jQuery, so does /js/main.js and the $deps array parameter is set to ‘jquery’. For more information, refer to the wp_enqueue_script function reference

function my-theme_scripts() {
  wp_enqueue_style( 'my-theme-style', get_stylesheet_uri() );

  wp_enqueue_script( 'my-theme-main', get_template_directory_uri() . '/js/main.js', array('jquery'), '20141011', true );

  if ( is_singular() && comments_open() && get_option( 'thread_comments' ) ) {
	wp_enqueue_script( 'comment-reply' );
  }
}
add_action( 'wp_enqueue_scripts', 'my-theme_scripts' );

Time to Grunt

Run Grunt when you are ready to create or overwrite /js/main.js

# run the default task(s)
$ grunt

Resources

Helper Functions

These are some JavaScript functions I saved here because I couldn’t find an equivalent in Underscore.js or lodash or didn’t need the overhead of the whole library.

find index by key value
// find index of an object in an array by key and value
var _findIndexOf = function(array, key, value) {
    var i;
    for(i = 0; i < array.length; i++) {
        if(array[i].hasOwnProperty(key) && array[i][key] === value) {
            return i;
        }
    }
    return -1;
}
filename from path
// get the filename from a path without using regex
var _filename = function(path) {
    return path.split('/').pop();
}
path only, remove filename
// get the path only without using regex
var _path = function(path) {
    return path.substring(0, path.lastIndexOf('/') +1);
}

Validation

// validate external URL syntax 
var _isExternalURL = function(str) {
    var a  = document.createElement('a');
    a.href = str;

    if (!a.host || location.host == a.host)
    {
        return false;
    }
    return true;
}
source

 

Math

/**
 * round to decimal place
 *
 * @param num number to round
 * @param dec number of decimal places to round to
 */ 
function _round(num, dec) {
    return Math.round(num * Math.pow(10, dec)) / Math.pow(10, dec);
}

Misc

//breakout of iframe / frame
function iframed() {
    try {
        return window.self !== window.top;
    } catch (e) {
        return true;
    }
}
if(iframed()) window.top.location.reload(false);
Resize Handler
window.addEventListener("resize", resizeThrottler, false);

var resizeTimeout;
function resizeThrottler() {
    // wait for resizeHandler execution to complete
    if ( !resizeTimeout ) {
        resizeTimeout = setTimeout(function next() {
            resizeTimeout = null;
            resizeHandler();

           // resizeHandler executes at 15fps
        }, 66);
    }
}

function resizeHandler() {
    // handle the resize event
    console.log(window.innerWidth);
}

jQuery and/or lodash resize handler

Orientation Change Listener

Register an event handler by calling addListener() on the MediaQueryList object named query. The callback function will receive this object as an argument when triggered.

var query = window.matchMedia("(orientation:landscape)");
var toggle = function(query) {
    if (query.matches) {
        console.log('------- LANDSCAPE -------')
    } else {
        console.log('------- PORTRAIT -------')    
    }
}
query.addListener(toggle);

// remove as needed
query.removeListener(toggle);

Grunt JavaScript Task Runner

This tutorial describes how to setup and use the Grunt JavaScript task runner to automate repetitive tasks such as minification and compilation. Grunt is installed using npm, the Node.js package manager. You will also need Git to work with the tagged source code. This makes it easy to reset and compare your working copy of the code at each step. I discovered commit tags while using the AngularJS tutorial.

If you have not done so already, make sure that you have Node.js, Grunt CLI and Git installed. For Windows systems, after installing Git, you may decide to use the Git bash shell for your CLI (command line interface) instead of the Command Prompt.

Install Grunt’s command line interface (CLI) globally.

#install grunt CLI
$ npm install -g grunt-cli

Source Code

Using Git, clone the GruntTutorial repository. This contains all the source code for the tutorial. As you work through the tutorial, you will be instructed to use git to reset the source code so it matches the step at that point. This will revert the source code to it’s original state for the respective tag and thus overwrite any changes you have made to it.

# git clone creates the GruntTutorial directory in your current directory
$ git clone https://github.com/jimfrenette/GruntTutorial.git

# reset to step 0
$ git checkout -f step-00

Step 1: Node.js Package

# reset to step 1
$ git checkout -f step-01

A npm (nodejs package manager) package.json file is added to the project root for npm to read its structure and know what to do when installing it.

Update: 12-20-2014 — Interactively create a package.json file with the npm init command. More information available at the npm cli commands doc.

# create package.json
$ npm init

Grunt and node modules are installed per the package.json dependencies.

package.json
{
  "name": "grunt-tutorial",
  "version": "0.1.0",
  "devDependencies": {
    "grunt": "~0.4.1",
    "grunt-contrib-jshint": "~0.6.3",
    "grunt-contrib-nodeunit": "~0.2.0",
    "grunt-contrib-uglify": "~0.2.2"
  }
}

These node_modules are already included in the step-01 source. To verify the process on your own, reset the tutorial source code to step-00 and run these npm install commands:

GruntTutorial – bash
# install the latest Grunt in your project folder
$ npm install grunt --save-dev

# install dependencies per package.json
$ npm install

Step 2: Combine and Minify Javascript files

# reset to step 2
$ git checkout -f step-02

Gruntfile.js is added to root of the project to specify the modules configuration, define tasks and load plugins. This Gruntfile.js specifies an uglify plugin to perform JavaScript minification. The banner option creates a comment on the first line of the minified file that is output. The JavaScript source (src) and destination (dest) paths are set in the build properties. Since our build source path has a wildcard * before the js filename extension, all of the js files in that directory will be minified into a single JavaScript file named grunt-tutorial.min.js as specified in the build destination property.

module.exports = function(grunt) {
  'use strict';
  // Project configuration.
  grunt.initConfig({
    pkg: grunt.file.readJSON('package.json'),
    uglify: {
      options: {
        banner: '/*! <%= pkg.name %> <%= grunt.template.today("yyyy-mm-dd") %> */\n'
      },
      build: {
        src: 'src/js/*.js',
        dest: 'build/<%= pkg.name %>.min.js'
      }
    }
  });
  // Load the plugin that provides the "uglify" task.
  grunt.loadNpmTasks('grunt-contrib-uglify');
  // Default task(s).
  grunt.registerTask('default', ['uglify']);
};

Time to test our Gruntfile.

# run the default task
$ grunt

You should recieve a message like this

Running "uglify:build" (uglify) task
File "build/js/grunt-tutorial.min.js" created.

Done, without errors. 

Step 3: Compass and Watch Plugin Install

# reset to step 3
$ git checkout -f step-03
OS X / Linux

For the Sass compile with the Compass & Watch Grunt plugins, you will need to have Ruby, Sass, and Compass version 0.12.2 or greater installed. Ruby comes pre-installed on OS X.

Windows

For the Sass compile with the Compass & Watch Grunt plugins, you will need to have the RubyInstaller for Windows. As of this writing, use Ruby 1.9.3 installers. These provide a stable language and a extensive list of packages (gems) that are compatible and updated. During the setup, check the option to Add Ruby executables to your PATH. After installing Ruby, install Sass and Compass using the Start Command Prompt with Ruby.

Start Command Prompt with Ruby
Start Command Prompt with Ruby

Install compass and watch Grunt plugins

By running these npm install commands with –save-dev, the package.json file will automatically be updated to include these two new dependencies.

GruntTutorial – bash
# install plugins and update package.json 
$ npm install grunt-contrib-compass --save-dev

$ npm install grunt-contrib-watch --save-dev

Compass and watch Grunt plugins added to the package.json devDependencies.

package.json
{
  "name": "grunt-tutorial",
  "version": "0.1.0",
  "devDependencies": {
    "grunt": "~0.4.2",
    "grunt-contrib-jshint": "~0.6.3",
    "grunt-contrib-nodeunit": "~0.2.0",
    "grunt-contrib-uglify": "~0.2.2",
    "grunt-contrib-compass": "~0.6.0",
    "grunt-contrib-watch": "~0.4.4"
  }
}

Step 4: Sass

# reset to step 4
$ git checkout -f step-04

Sass .scss files added to the source (src) directory and the Gruntfile.

Gruntfile.js
module.exports = function(grunt) {
  'use strict';
  // Project configuration.
  grunt.initConfig({
    pkg: grunt.file.readJSON('package.json'),
    compass: {
      dev: {
        // http://compass-style.org/help/tutorials/configuration-reference/#configuration-properties
        // these options will override (or extend) config.rb settings.
        options: {  
          cssDir: 'build/css/',
          sassDir: 'src/sass/'
        }
      }
    },
    uglify: {
      options: {
        banner: '/*! <%= pkg.name %> <%= grunt.template.today("yyyy-mm-dd") %> */\n'
      },
      build: {
        src: 'src/js/*.js',
        dest: 'build/js/<%= pkg.name %>.min.js'
      }
    },
    watch: {
      css: {
        files: '**/*.scss',
        tasks: ['compass']
      }
    }
  });
  // Load the plugins.
  grunt.loadNpmTasks('grunt-contrib-compass');
  grunt.loadNpmTasks('grunt-contrib-uglify');
  grunt.loadNpmTasks('grunt-contrib-watch');
  // Default task(s).
  grunt.registerTask('default', ['uglify', 'watch']);
};

Time to test our Gruntfile.

# run the default task(s)
$ grunt

Step 5: Compile Sass

# reset to step 5
$ git checkout -f step-05

The CLI / Terminal shows the status of the Grunt tasks that are run.

# run the default task(s)
$ grunt

Running "uglify:build" (uglify) task
File "build/js/grunt-tutorial.min.js" created.

Running "watch" task
Waiting...OK

Edit and save one of the Sass (.scss) files. The watch task waits for changes to files and then fires a task. In our Gruntfile, the watch is configured to run the the compass task whenever a .scss file is updated. The compass task is configured to compile the Sass files specified in the sassDir property (src/sass/) and output to the cssDir specified (build/css/).

CLI / Terminal output from changed Sass file.

>> File "src/sass/reset.scss" changed.

Running "compass:dev" (compass) task
overwrite build/css/reset.css (0.02s)
unchanged src/sass/style.scss
Compilation took 0.053s

Done, without errors.
Completed in 0.717s at Sat Nov 23 2013 17:14:57 GMT-0500 (EST) - Waiting...
Source Code
December 2014 — New Page added to cover using a static web server with live browser reloading.

Browser

The JavaScript Browser Object Model (BOM) represents the objects exposed by the web browser.

History

The history object represents the browsers URL history. Methods are back(), forward() and go().

// go back twice
history.go(-2);

Location

The location object represents the document URL.

// reload the current document
location.reload();

// current location properties

Navigator

The navigator object contains several properties with info about the browser.

// information about the browser
navigator.userAgent;

// current navigator.userAgent properties

Screen

The screen object contains properties with info about the display.

// show information about screen size
alert(screen.width + 'x' + screen.height);

// current screen properties