WordPress Post from Front End using REST API and Vue.js Part II

The objective of this post is to demonstrate some Vue.js basics for working with the WordPress REST API. This post continues where the last post left off with the following additions.

  • List posts submitted by the current user
  • Select from the list to edit a post
  • Delete a post from the list

Source Code

Development

The original proof of concept details building the WordPress plugin, installing Vue.js, Webpack and configuring for development. Also included is information on creating the front end slug where the Vuejs app will run. The end product is a simple form to submit new posts from a custom WordPress page template. It is recommended that you read these prerequisites.

List Posts

App.vue

Above the form, replace the hardcoded heading text with a placeholder to render heading data that will be adding to the data model.

Below the form, add this markup to display the messages for loading and errors along with the list element for the posts. Note the vue.js v-if attributes to control visibility of the elements depending on the data model values. This is called conditional rendering.

<h3>{{ heading }}</h3>
<form v-on:submit.prevent="onSubmit">

...

</form>

<div v-if="loading">
    Loading...
</div>

<div v-if="error">
    {{ error }}
</div>

<div v-if="posts">
    <ul>
        <li v-for="post in posts">
            <a href="#" @click='editPost(post)'>{{ post.title.rendered }}</a>
        </li>
    </ul>
</div>
  • Note that an ellipsis … in the code snippets 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.

In the App.vue component script block, update the data model for the new elements.

...

data() {
  return {
    error: null,
    heading: 'Submit New Post',
    loading: null,
    post_id: null,
    post_title: null,
    post_content: null,
    posts: null
  }
},

...

Insert the getPosts () method to fetch all the posts for the current user and set the posts property of the data model. The endpoints for the WordPress REST API can be looked up at its base url.

...

methods: {
  getPosts () {
    this.error = this.posts = null
    this.loading = true
    var params = '?author='+ wp_api_vuejs_poc.current_user_id +'&status=any';

    $.ajax({
      url: wp_api_vuejs_poc.rest_url + 'wp/v2/posts' + params,
      beforeSend: function ( xhr ) {
        xhr.setRequestHeader( 'X-WP-Nonce', wp_api_vuejs_poc.nonce );
      }
    })
    .done( $.proxy( function( response ) {
      this.loading = false;
      this.posts = response;
    }, this ))
    .fail( $.proxy( function( response ) {
      this.error = response;
    }, this ));
  },

...

}

Insert the mounted function to call the getPosts method when the app has rendered. If there are any posts to show when the page loads, they should be listed.

App.vue
...

methods: {
  mounted: function () {
    this.$nextTick(function () {
      this.getPosts ();
    })
  },

...

}

Development Build

In your CLI, build in development mode which enables watch on the files and incrementally recompiles as needed. This is faster than a prodcution build. Additionally, this unminified build is compatible with the Vue DevTools extension for Chrome for debugging and object inspection.

npm run dev

After the build is completed and watch is running, reload the page. Submit a new post to make sure it is working and gets added to the list.

Edit / Add Posts

In the markup that lists posts, the titles link contains a @click='editPost(post) attribute to call the editPost method passing it the post object. Insert the editPost method to accept the post object from the list and set the properties of the data model. Since this is a simple proof of concept, we are not comparing the post against the version on the server to make sure it is hasn’t been modified since the last fetch. Note that the heading property is being set as well to display the appropriate text.

...

methods: {
  editPost( post ) {
    this.heading = 'Edit Post'
    this.post_id = post.id
    this.post_title = post.title.rendered
    this.post_content = post.content.rendered
  },

...

}

When you save the changes to the App.vue file, the watch detects the changes and incrementally rebuilds the code. Refresh the browser, select a post from the list and verify that it is working as expected.

We need a way to Submit New Post without reloading the page. Add this markup to the bottom of the form under the submit input so we have a link that will call a newPost method.

...

    <span v-if="post_id">
        <a href="#" @click="newPost">New Post</a>
    </span>
</form>

Insert the newPost method to reset the post and heading properties to their default values.

...

methods: {
  newPost() {
    this.heading = 'Submit New Post'
    this.post_id = null
    this.post_title = null
    this.post_content = null
  },

...

}

In most cases, we should reset the form after fetching posts. Insert a call to newPosts inside the ajax callback function right after the posts object is populated with the response.

...

methods: {
  getPosts () {

  ...

    .done( $.proxy( function( response ) {
      this.loading = false;
      this.posts = response;
      this.newPost();
    }, this ))

...

}

Delete Posts

Insert this link markup in the list item element after the post title link.

...
    <a href="#" @click='delPost(post)' title="DELETE">[–]</a>
</li>

Lastly, insert the delPosts method.

...

methods: {
  delPost( post ) {
    $.ajax({
      method: "DELETE",
      url: wp_api_vuejs_poc.rest_url + 'wp/v2/posts/' + post.id,
      beforeSend: function ( xhr ) {
        xhr.setRequestHeader( 'X-WP-Nonce', wp_api_vuejs_poc.nonce );
      }
    })
    .done( $.proxy( function() {
      this.getPosts();
    }, this ))
    .fail( $.proxy( function( response ) {
      console.log( response );
    }, this ));
  },

...

}

Resources

WordPress Post from Front End using REST API and Vue.js

This post is a simple proof of concept for using the new WordPress REST API to submit a new post draft from the front end. The form and user inputs are built using the Vue.js framework and vue-cli to create a simple Webpack build configuration.

Environment

This example was created using a fresh install of WordPress 4.7 served by XAMMP on Windows 10.
Nodejs version 6.9.1
NPM version 3.10.8
vue-cli version 2.6.0
git version 2.7.0

Plugin Development

Change to the plugins directory. For example,

cd c:/xampp/htdocs/wordpress/wp-content/plugins

Create the directory for the new plugin. For example,

mkdir wp-api-vuejs-poc

Entry Point

Create a plugin entry point php file in this new plugin directory. For example,

# change to the new directory
cd wp-api-vuejs-poc

# create a new file
touch wp-api-vuejs-poc.php

Add the following php code to the plugin entry point. At the very least, add the comment block at the top to register the plugin with WordPress.

wp-api-vuejs-poc.php
<?php
/**
 * Plugin Name: WP API Vue.js Proof of Concept
 * Plugin URL: http://example.com
 * Description: WordPress plugin to draft posts from the front end using the new REST API and Vue.js
 * Version: 1.0
 * Author:
 * Author URI: http://example.com
 * Text Domain: wp-api-vuejs-poc
 */

if ( ! defined( 'ABSPATH' ) ) {
    exit; // Exit if accessed directly.
}

if ( ! class_exists( 'WP_API_Vuejs_PoC' ) ) :

/**
 * Main WP_API_Vuejs_PoC Class.
 */
final class WP_API_Vuejs_PoC {

    /**
     * The single instance of the class.
     */
    protected static $_instance = null;

    /**
     * Ensures only one instance of WP_API_Vuejs_PoC is loaded or can be loaded.
     */
    public static function instance() {
        if ( is_null( self::$_instance ) ) {
            self::$_instance = new self();
        }
        return self::$_instance;
    }

    public function __construct() {
        $this->define_constants();
        $this->includes();
    }

    /**
     * Define Constants.
     */
    private function define_constants() {
        $this->define( 'WAVP_PLUGIN_PATH', plugin_dir_path( __FILE__ ) );
        $this->define( 'WAVP_PLUGIN_URL', plugin_dir_url( __FILE__ ) );
    }

    private function define( $name, $value ) {
        if ( ! defined( $name ) ) {
            define( $name, $value );
        }
    }

    /**
     * What type of request is this?
     *
     * @param  string $type admin, ajax, cron or frontend.
     * @return bool
     */
    private function is_request( $type ) {
        switch ( $type ) {
            case 'admin' :
                return is_admin();
            case 'ajax' :
                return defined( 'DOING_AJAX' );
            case 'cron' :
                return defined( 'DOING_CRON' );
            case 'frontend' :
                return ( ! is_admin() || defined( 'DOING_AJAX' ) ) && ! defined( 'DOING_CRON' );
        }
    }

    public function includes() {
        if ( $this->is_request( 'frontend' ) ) {
            include( 'class-api-vpoc-page.php' );
        }
    }

}

endif;

/**
 * Main instance of WP_API_Vuejs_PoC.
 * Returns the main instance of WAVP to prevent the need to use globals.
 */
function WAVP() {
    return WP_API_Vuejs_PoC::instance();
}

// Global for backwards compatibility.
$GLOBALS['wp-api-vuejs-poc'] = WAVP();

Class for Front End Page Request

In the class of the plugin entry point file above, the includes function contains a request type check. When the request is made from the front end, include the class file for the page. Let’s create that class file now, for example,

touch class-api-vpoc-page.php

Add the following php code to create the class. The constructor method is called on the newly-created class object where we are subscribing to events using hooks.

The action hook is called during the page processing event for script loading, at this point, call our page_scripts function to load the app javascript.

The filter hook is called during data processing, when the content is being loaded, call our page_content function to create the app bootstrap element.

class-api-vpoc-page.php
<?php

if ( ! defined( 'ABSPATH' ) ) {
	exit; // Exit if accessed directly
}

/**
 * WP_API_Vuejs_PoC_Page class.
 */
class WP_API_Vuejs_PoC_Page {

    /**
     * Access
     */
    private static $user_can = 'edit_posts';
    private static $page_slug = 'api-test';

    /**
     * Constructor.
     */
    public function __construct() {
        add_action( 'wp_enqueue_scripts', array( $this, 'page_scripts' ) );
        add_filter( 'the_content', array( $this, 'page_content' ) );
    }

    public function page_scripts() {
        if ( is_page( self::$page_slug ) ) {
            // load the Vue.js app
            wp_enqueue_script( 'wp-api-vuejs-poc', WAVP_PLUGIN_URL . 'dist/build.js', array(), false, true );

            // localize data for script
            wp_localize_script( 'wp-api-vuejs-poc', 'wp_api_vuejs_poc', array(
                'rest_url' => esc_url_raw( rest_url() ),
                'nonce' => wp_create_nonce( 'wp_rest' ),
                'success' => __( 'Post submitted', 'wp-api-vuejs-poc' ),
                'failure' => __( 'Post could not be processed.', 'wp-api-vuejs-poc' ),
                'current_user_id' => get_current_user_id()
                )
            );
        }
    }

    public function page_content($content) {
        if ( is_page( self::$page_slug ) ) {
            // output only to logged in users who can edit posts
            if ( is_user_logged_in() && current_user_can( self::$user_can ) ) {
                // app bootstrap element
                ob_start();?>
                <div id="app"></div>
                <?php
                $content .= ob_get_clean();
            }else{
                $content .=  sprintf( '<a href="%1s">%2s</a>', esc_url( wp_login_url() ), __( 'Log in', 'wp-api-vuejs-poc' ) );
            }
        }

        return $content;
    }

}

return new WP_API_Vuejs_PoC_Page();

The next page covers the Vue.js front end.


Laravel JWT Auth with Vue.js 2

This post is a refresh of Laravel JWT Auth with Vue.js I posted in September that applies to the 1.x version of Vue.js. Evan You released vue.js version 2 shortly thereafter and this post will cover building the Laravel JSON Web Token based authentication with it.

February 11, 2017 – A lot has changed in the last 4 months with Vue.js and Webpack. Since Vue.js 2.1.x and Webpack 2.2.x were released last month, this post originally published in November has been updated for these new versions.

Included are some notes about refactoring the application from vue.js 1.x to version 2.1. Changes also include using NPM scripts with webpack version 2 for the front end build process instead of Elixir and Browserify.

Environment

For this tutorial, Laravel 5.2 has been installed locally and it is being served by XAMMP on Windows 10.

Command Line Tools
Nodejs version 6.9.4
NPM version 3.10.10
vue-cli version 2.8.1
git version 2.10.2

User Model

After setting the database connection parameters in env.php, run the database migration script to create the users, password_resets and migrations tables. In my Laravel application, I have moved the Users class into a Models folder.

php artisan migrate

mkdir app/Models

mv app/User.php app/Models

Edit app/Models/User.php, change 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, config/auth.php. Update the authentication drivers user provider for the App\Models\User namespace change as follows.

auth.php
'providers' => [
    'users' => [
        'driver' => 'eloquent',
        'model' => App\Models\User::class,
    ],

    ...

Webpack Vue.js CLI

A simple webpack vue-loader setup is being used for the front end build process. First step is to install vue-cli globally.

npm install -g vue-cli
  • If you have an older version of vue-cli, you will need to update it to version 2.8.
npm update -g vue-cli

Using vue-cli from the laravel root directory, install the webpack-simple template into the existing laravel resources folder.

vue init webpack-simple resources

After the webpack-simple template has been installed. Change to the resources directory and run the npm install. This will install vue 2.1.x, vue-loader, vue-template-compiler, webpack 2.2.x and other dependencies. For details, inspect the install manifest resources/package.json.

cd resources

npm i
  • For example, the vue-cli 2.8.0 release from January, 2017 installs vue 2.1.10, vue-loader 10.0.2, vue-template-compiler 2.1.10 and webpack 2.2.0.

Vue Router and Components

Install the router for vue.js 2.x.

npm i vue-router --save-dev

Create a resources/assets/components folder and add an empty App.vue, Dashboard.vue, Home.vue, Register.vue and Signin.vue component.

mkdir assets/components

touch assets/components/App.vue

touch assets/components/Dashboard.vue

touch assets/components/Home.vue

touch assets/components/Register.vue

touch assets/components/Signin.vue

Add the following code to the App.vue base layout component. The new <router-link> component replaces the the deprecated v-link directive from vue.js 1.x.

App.vue
<template>
    <div class="panel panel-default">
        <div class="panel-heading">
            <nav>
                <ul class="list-inline">
                    <li>
                        <router-link :to="{ name: 'home' }">Home</router-link>
                    </li>
                    <li class="pull-right">
                        <router-link :to="{ name: 'register' }">Register</router-link>
                    </li>
                </ul>
            </nav>
        </div>
        <div class="panel-body">
            <router-view></router-view>
        </div>
    </div>
</template>

Add the following code to the Home.vue component.

Home.vue
<template>
    <h1>Laravel 5</h1>
</template>

Add the following code to the Register.vue component. Note the change from version 1.0 of this component. A div has been added at the root of the template since components now must have exactly one root element.

Register.vue
<template>
    <div>
        <div class="alert alert-danger" v-if="error && !success">
            <p>There was an error, unable to complete registration.</p>
        </div>
        <div class="alert alert-success" v-if="success">
            <p>Registration completed. You can now sign in.</p>
        </div>
        <form autocomplete="off" v-on:submit="register" v-if="!success">
            <div class="form-group" v-bind:class="{ 'has-error': error && response.username }">
                <label for="name">Name</label>
                <input type="text" id="name" class="form-control" v-model="name" required>
                <span class="help-block" v-if="error && response.name">{{ response.name }}</span>
            </div>
            <div class="form-group" v-bind:class="{ 'has-error': error && response.email }">
                <label for="email">E-mail</label>
                <input type="email" id="email" class="form-control" placeholder="gavin.belson@hooli.com" v-model="email" required>
                <span class="help-block" v-if="error && response.email">{{ response.email }}</span>
            </div>
            <div class="form-group" v-bind:class="{ 'has-error': error && response.password }">
                <label for="password">Password</label>
                <input type="password" id="password" class="form-control" v-model="password" required>
                <span class="help-block" v-if="error && response.password">{{ response.password }}</span>
            </div>
            <button type="submit" class="btn btn-default">Submit</button>
        </form>
    </div>
</template>

JavaScript

Create a resources/assets/js folder for the JavaScript.

mkdir assets/js

Create a resources/assets/js/app.js JavaScript file.

touch assets/js/app.js

Add the following code to the app.js file to import vue modules, components and define the router. Exporting will allow other modules in this project to import them.

Note the changes between version 1.0 of app.js.

  1. router.map has been replaced with an array on the new routes option.
  2. router is passed to the new Vue instance as option since starting an app with routing no longer requires a special method.
app.js
import Vue from 'vue';
import VueRouter from 'vue-router';
import App from '../components/App.vue';
import Dashboard from '../components/Dashboard.vue';
import Home from '../components/Home.vue';
import Register from '../components/Register.vue';
import Signin from '../components/Signin.vue';

Vue.use(VueRouter);

export default Vue;

export var router = new VueRouter({
    routes: [
        {
            path: '/',
            name: 'home',
            component: Home
        },
        {
            path: '/register',
            name: 'register',
            component: Register
        }
    ]
});

new Vue({
    el: '#app',
    router: router,
    render: app => app(App)
});

Web Page

Edit the resources/views/welcome.blade.php template.

Replace the entire contents of the file with this markup.

welcome.blade.php
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <meta name="csrf-token" content="{!! csrf_token() !!}">
    <title>Laravel</title>

    <link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">

</head>
<body>
    <div class="container">
        <div id="app"></div>
    </div>
    <script src="/js/app.js"></script>
</body>
</html>

Webpack

Edit the resources/webpack.config.js entry and output values.

REPLACE

module.exports = {
  entry: './src/main.js',
  output: {
    path: path.resolve(__dirname, './dist'),
    publicPath: '/dist/',
    filename: 'build.js'
  },
  ...
}

WITH

module.exports = {
  entry: './assets/js/app.js',
  output: {
    path: path.resolve(__dirname, '../public/js'),
    publicPath: '/js/',
    filename: 'app.js'
  },
  ...
}

Test drive the updated webpack configuration with the npm build script.

npm run build

Your build output should be similar to this. The laravel public folder should now contain js/app.js and a respective js/app.js.map source map.

> resources@ build C:\xampp\htdocs\laravel\resources
> cross-env NODE_ENV=production webpack --progress --hide-modules

Hash: 260fd0b01225d3f55a16
Version: webpack 2.2.1
Time: 6031ms
     Asset    Size  Chunks             Chunk Names
    app.js  118 kB       0  [emitted]  main
app.js.map  884 kB       0  [emitted]  main
Laravel JWT Auth Vue.js 2.0 Home Page

The next page covers Vue Resource for request handling, Form Request Validation, API authorization with JWT Auth, User endpoint and Sign in.


Laravel JWT Auth with Vue.js

This post documents using Laravel to build JSON Web Token based authentication with a Vue.js 1.0 user interface. UPDATE: vue.js 2.0 version published.

A fresh install of Laravel 5.2 for local development is required. Head over to the Laravel 5.2 docs to get setup with Composer and Laravel as needed. This post contains some info on how I installed Laravel 5.2 on Windows. Once Laravel 5.2 is installed, the database created and the .env file updated to connect to it, run the migration to create the users table.

php artisan migrate

User Model

One of the first things I like to do is create a Models folder and move the User model into it. To organize models within their own directory, start by creating 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, change 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, config/auth.php. Update the authentication drivers user provider for the App\Models\User namespace change as follows.

auth.php
'providers' => [
    'users' => [
        'driver' => 'eloquent',
        'model' => App\Models\User::class,
    ],

    ...

Laravel Elixir Vueify

Using NPM, install the dependencies including laravel-elixer and gulp that should already be defined in the package.json file.

npm install

Install the Laravel Elixir and the Browserify Vueify plugin wrapper, laravel-elixir-vueify. This node package also includes vue.js.

For Laravel Elixer 5
npm install laravel-elixir-vueify@1.0.6 --save-dev

Laravel Elixir is a build tool wrapper for Gulp. To use laravel-elixir-vueify, require laravel-elixir-vueify and update the elixir call with mix.browserify in the gulpfile.js as follows.

REPLACE

elixir(function(mix) {
    mix.sass('app.scss');
});

WITH

require('laravel-elixir-vueify');

elixir(function(mix) {
    mix.browserify('app.js');
    mix.sass('app.scss');
});

Create a resources/assets/js folder for the JavaScript.

mkdir resources/assets/js

Create a resources/assets/js/app.js JavaScript file.

touch resources/assets/js/app.js

Test the gulp build. A browserified app.js file that includes an external source map file should get compiled and written to public/js/app.js. The pre-existing app.scss Sass file should also get compiled.

gulp
Default Gulp task output
Default Gulp task output

Vue Router and Components

Install the router for Vue.js.

npm install vue-router@0.7.13 --save-dev
  • The npm install vue-router command above has been updated to install the version supported by the 1.x version of vue.js. This is the vue.js version installed with laravel-elixir-vueify@1.0.6.

Create a resources/assets/components folder and add an empty App.vue, Dashboard.vue, Home.vue, Register.vue and Signin.vue component.

mkdir resources/assets/components

touch resources/assets/components/App.vue

touch resources/assets/components/Dashboard.vue

touch resources/assets/components/Home.vue

touch resources/assets/components/Register.vue

touch resources/assets/components/Signin.vue

Add the following code to the empty App.vue file which will be the base layout component.

App.vue
<template>
    <div class="panel panel-default">
        <div class="panel-heading">
            <nav>
                <ul class="list-inline">
                    <li><a v-link="{ name: 'home' }">Home</a></li>
                    <li class="pull-right">
                        <a v-link="{ name: 'register' }">Register</a>
                    </li>
                </ul>
            </nav>
        </div>
        <div class="panel-body">
            <router-view></router-view>
        </div>
    </div>
</template>

Add the following code to the empty Home.vue file. The content in this template will populate the App.vue router-view element by default.

Home.vue
<template>
    <h1>Laravel 5</h1>
</template>

Add the following code to the empty Register.vue file for the user account creation form.

Register.vue
<template>
    <div class="alert alert-danger" v-if="error && !success">
        <p>There was an error, unable to complete registration.</p>
    </div>
    <div class="alert alert-success" v-if="success">
        <p>Registration completed. You can now sign in.</p>
    </div>
    <form autocomplete="off" v-on:submit="register" v-if="!success">
        <div class="form-group" v-bind:class="{ 'has-error': error && response.username }">
            <label for="name">Name</label>
            <input type="text" id="name" class="form-control" v-model="name" required>
            <span class="help-block" v-if="error && response.name">{{ response.name }}</span>
        </div>
        <div class="form-group" v-bind:class="{ 'has-error': error && response.email }">
            <label for="email">E-mail</label>
            <input type="email" id="email" class="form-control" placeholder="gavin.belson@hooli.com" v-model="email" required>
            <span class="help-block" v-if="error && response.email">{{ response.email }}</span>
        </div>
        <div class="form-group" v-bind:class="{ 'has-error': error && response.password }">
            <label for="password">Password</label>
            <input type="password" id="password" class="form-control" v-model="password" required>
            <span class="help-block" v-if="error && response.password">{{ response.password }}</span>
        </div>
        <button type="submit" class="btn btn-default">Submit</button>
    </form>
</template>

Add the following code to the empty app.js file to require vue, the vue-router, import vue components, setup the router. Export Vue and the router to allow modules to import them.

app.js
var Vue = require('vue');
var VueRouter = require('vue-router');

import App from '../components/App.vue';
import Dashboard from '../components/Dashboard.vue';
import Home from '../components/Home.vue';
import Register from '../components/Register.vue';
import Signin from '../components/Signin.vue';

Vue.use(VueRouter);

export default Vue;
export var router = new VueRouter

router.map({
    '/': {
        name: 'home',
        component: Home
    },
    '/register': {
        name: 'register',
        component: Register
    }
});

router.start(App, '#app');

Web Page

Edit the resources/views/welcome.blade.php template.

Replace the entire contents of the file with this markup.

welcome.blade.php
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <meta name="csrf-token" content="{!! csrf_token() !!}">
    <title>Laravel</title>

    <link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">

</head>
<body>
    <div class="container">
        <div id="app"></div>
    </div>
    <script src="/js/app.js"></script>
</body>
</html>

Now is a good time to test that everything builds and you have a Bootstrap styled page with with links to Home and Register in a horizontal navigation bar and a content panel under it with “Laravel 5” heading text. Select the Register link to diplay the form in the content panel.

gulp
Laravel JWT Auth Vuejs User Registration Form
New User Registration Form

The next page covers Vue Resource for request handling, Form Request Validation, API authorization with JWT Auth, User endpoint and Sign in.


Handlebars Templates with Browserify

This post documents how to use the hbsfy precompiler plugin for Browserify to compile Handlebars templates into javascript. With this design, templates can be required as javascript modules in the views that use them.

Requirements

Global Packages

List the global packages that are installed with the npm list –global command. Then install these if they are not already installed or need to be updated.

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.

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.

Project Packages

Create package.json

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

# create package.json
npm init

Install Packages

These are the project node module packages needed to browserify and bundle the Handlebars template modules. Since all of the modules are bundled into a single minified javascript file, source maps are included in the bundle task to make it easier to debug in the browser’s developer tools. For convenience, a local web server module has been included.

$ npm install browserify --save-dev
$ npm install handlebars --save-dev
$ npm install hbsfy --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-source-stream --save-dev
$ npm install vinyl-buffer --save-dev

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 browserify bundle task, start a local web server and open the default web page in a 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');

var hbsfy = require('hbsfy');

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

  return b.transform(hbsfy).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
gulp.task('server', function() {
    gulp.src('./')
        .pipe(webserver({
            port: 3000,
            directoryListing: false,
            open: true
        }));
});

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

Source

Create a Handlebars template in /src/hbs/

people.hbs
{{#each people}}
<li>{{firstName}} {{lastName}}</li>
{{/each}}

Create a data module in /src/js/modules/ for this template demo since we are not calling a rest API to get data.

data.js
module.exports = {
    people: [
        {firstName: "Yehuda", lastName: "Katz"},
        {firstName: "Carl", lastName: "Lerche"},
        {firstName: "Alan", lastName: "Johnson"}
    ]
}

In the /src/js folder, create the main node, index.js.

index.js
var data = require('./modules/data');
var list = require('../hbs/people.hbs');

document.querySelector('#people').innerHTML = list(data);

Web Page

Create this html file to load the javascript and render the template output.

index.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Handlebars with Browserify</title>
</head>
<body>
    <ul id="people"></ul>
    <script src="js/bundle.js"></script>
</body>
</html>

Project Folders and Files

Here is a tree view of the projects folders and files. Browse, clone or download the source code if you prefer.

    • js
    • src
      • hbs
        • people.hbs
      • js
        • modules
          • data.js
        • index.js
    • gulpfile.js
    • package.json
    • index.html

The next page covers custom Handlebars helper registration


Laravel User Registration with Email Activation

This post documents how to add an e-mail confirmation to the Laravel User registration that is generated by the Artisan console. In this example, the registered user will not be able to login until the activation link that is sent to the users registered e-mail account has been loaded into the browser.

Requirements

An instance of Laravel 5.2 setup for local development with authentication scaffolding is required. Refer to the first page of my recent post on Laravel User Authentication with Ajax Validation for more information on installing Laravel 5.2 using Composer and generating the authentication scaffolding with artisan.

User Model

I like placing model classes in their own directory. This can be done with just a few minor changes after moving the User class into a new app/Models folder.

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, checkout the email-activation branch in 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 Migration Files

Generate a database migration file for creating the user_activations table. This table stores the activation codes until they are used.

create_user_activations_table
php artisan make:migration create_user_activations_table --create=user_activations

Edit the migration file up function to create these three fields in the user_activations table.

public function up()
{
    Schema::create('user_activations', function (Blueprint $table) {
        $table->integer('user_id')->unsigned();
        $table->string('token')->index();
        $table->timestamp('created_at');
    });
}
alter_users_table_add_activated_col

Generate a database migration file for adding a column to the users table.

php artisan make:migration alter_users_table_add_activated_col

Edit the migration file up function to add the activated field to the users table.

public function up()
{
    Schema::table('users', function($table) {
        $table->boolean('activated')->default(false);
    });
}

Run the migration

php artisan migrate

ActivationRepository Class

Create a Repositories folder in the app directory

mkdir app/Repositories

Create the ActivationRepository class file in app/Repositories. This class contains functions for generating activation tokens and updating the tables in the database.

ActivationRepository.php
<?php

namespace App\Repositories;

use Carbon\Carbon;
use Illuminate\Database\Connection;

class ActivationRepository
{
    protected $db;
    protected $table = 'user_activations';

    public function __construct(Connection $db)
    {
        $this->db = $db;
    }

    public function getActivation($user)
    {
        return $this->db->table($this->table)->where('user_id', $user->id)->first();
    }

    public function getActivationByToken($token)
    {
        return $this->db->table($this->table)->where('token', $token)->first();
    }

    public function deleteActivation($token)
    {
        $this->db->table($this->table)->where('token', $token)->delete();
    }

    public function createActivation($user)
    {
        $activation = $this->getActivation($user);

        if (!$activation) {
            return $this->createToken($user);
        }
        return $this->regenerateToken($user);
    }

    protected function getToken()
    {
        return hash_hmac('sha256', str_random(40), config('app.key'));
    }

    private function regenerateToken($user)
    {
        $token = $this->getToken();
        $this->db->table($this->table)->where('user_id', $user->id)->update([
            'token' => $token,
            'created_at' => new Carbon()
        ]);
        return $token;
    }

    private function createToken($user)
    {
        $token = $this->getToken();
        $this->db->table($this->table)->insert([
            'user_id' => $user->id,
            'token' => $token,
            'created_at' => new Carbon()
        ]);
        return $token;
    }
}

ActivationFactory Class

Create a Factories folder in the app directory

mkdir app/Factories

Create the ActivationFactory class file in app/Factories.

ActivationFactory.php
<?php

namespace App\Factories;

use App\Models\User;
use App\Repositories\ActivationRepository;
use Illuminate\Mail\Mailer;
use Illuminate\Mail\Message;

class ActivationFactory
{
    protected $activationRepo;
    protected $mailer;
    protected $resendAfter = 24;

    public function __construct(ActivationRepository $activationRepo, Mailer $mailer)
    {
        $this->activationRepo = $activationRepo;
        $this->mailer = $mailer;
    }

    public function sendActivationMail($user)
    {
        if ($user->activated || !$this->shouldSend($user)) {
            return;
        }

        $token = $this->activationRepo->createActivation($user);

        $link = route('user.activate', $token);
        $message = sprintf('Activate account %s', $link, $link);

        $this->mailer->raw($message, function (Message $m) use ($user) {
            $m->to($user->email)->subject('Activation mail');
        });
    }

    public function activateUser($token)
    {
        $activation = $this->activationRepo->getActivationByToken($token);

        if ($activation === null) {
            return null;
        }

        $user = User::find($activation->user_id);

        $user->activated = true;

        $user->save();

        $this->activationRepo->deleteActivation($token);

        return $user;
    }

    private function shouldSend($user)
    {
        $activation = $this->activationRepo->getActivation($user);
        return $activation === null || strtotime($activation->created_at) + 60 * 60 * $this->resendAfter < time();
    }
}

Route

Create a route for the e-mail activation link in app/Http/routes.php. This user.activate route should be added after Route::auth()

routes.php
<?php
...
Route::auth();

Route::get('user/activation/{token}', 'Auth\AuthController@activateUser')->name('user.activate');

AuthController

Make the following updates to app/Http/Controllers/Auth/AuthController.php

REPLACE

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

WITH

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

REPLACE

public function __construct()
{
    $this->middleware($this->guestMiddleware(), ['except' => 'logout']);
}

WITH

protected $activationFactory;

public function __construct(ActivationFactory $activationFactory)
{
    $this->middleware($this->guestMiddleware(), ['except' => 'logout']);
    $this->activationFactory = $activationFactory;
}

At the bottom of the controller, add these three functions after the create function. The register function we are adding will override the register method inherited from the AuthenticatesAndRegistersUsers trait.

...

public function register(Request $request)
{
    $validator = $this->validator($request->all());

    if ($validator->fails()) {
        $this->throwValidationException(
            $request, $validator
        );
    }

    $user = $this->create($request->all());

    $this->activationFactory->sendActivationMail($user);

    return redirect('/login')->with('activationStatus', true);
}

public function activateUser($token)
{
    if ($user = $this->activationFactory->activateUser($token)) {
        auth()->login($user);
        return redirect($this->redirectPath());
    }
    abort(404);
}

public function authenticated(Request $request, $user)
{
    if (!$user->activated) {
        $this->activationFactory->sendActivationMail($user);
        auth()->logout();
        return back()->with('activationWarning', true);
    }
    return redirect()->intended($this->redirectPath());
}

Authentication Language Lines

For the activation status messages that we need to display to the user, update the resources/lang/en/auth.php file.

en/auth.php
<?php

return [

...

    'activationStatus' => 'We sent you an activation code. Please check your e-mail.',
    'activationWarning' => 'You need to confirm your account. We have sent you an activation code, please check your e-mail.',

...

];

Add the following inside the form at resources/views/auth/login.blade.php Blade view to render the activation messages.

login.blade.php
...

@if (session('activationStatus'))
    <div class="alert alert-success">
        {{ trans('auth.activationStatus') }}
    </div>
@endif

@if (session('activationWarning'))
    <div class="alert alert-warning">
        {{ trans('auth.activationWarning') }}
    </div>
@endif

Source Code

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.


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

Xdebug for XAMPP on OS X

After discovering how nice the vscode-php-debug extension works in Visual Studio Code on my Windows 10 laptop, I wanted this same setup for a PHP project on my work Mac Book Pro running OS X Yosemite (10.10). The version of XAMPP I currently have installed on the Mac is 5.6.11-0 and for this tutorial, there are a few requirements to consider:

Requirements

Use homebrew to install the autoconf utility for producing configure scripts.

# install autoconf
$ brew install autoconf

phpize

XAMPP comes with phpize which we will want to utilize to prepare the build environment for the Xdebug PHP extension. If another version of phpize is installed, it will need to be renamed. Check to see if phpize is installed in /usr/bin:

$ cd /usr/bin
$ ls -al | grep phpize
-rwxr-xr-x     1 root   wheel      4508 Sep  9  2014 phpize

If the grep search returns phpize, similar to what is shown above, then rename it to phpize_bak.

# rename / backup phpize
$ sudo mv phpize phpize_bak

Create a new symbolic link in /usr/bin to target the XAMPP version of phpize.

# navigate to the /usr/bin directory
$ cd /usr/bin

# create symbolic link to XAMPP phpize
$ sudo ln -s /Applications/XAMPP/bin/phpize-5.6.11 phpize

# check phpize version
$ cd /
$ phpize -v
Configuring for:
PHP Api Version:         20131106
Zend Module Api No:      20131226
Zend Extension Api No:   220131226

phpize -v command output should be similar to what is shown above.

Xdebug Installation

Open the XAMPP phpinfo.php file in a web browser, for example, http://localhost/dashboard/phpinfo.php. In another browser window or tab, open https://xdebug.org/wizard.php and copy the phpinfo page content in the first window or tab and paste it into the textbox on the xdebug.org page. Then submit for analysis to determine the Xdebug source to download. The response should be a Summary and Tailored Installation Instructions.

Example Summary
  • Xdebug installed: no
  • Server API: Apache 2.0 Handler
  • Windows: no
  • Zend Server: no
  • PHP Version: 5.6.11
  • Zend API nr: 220131226
  • PHP API nr: 20131226
  • Debug Build: no
  • Thread Safe Build: no
  • Configuration File Path: /Applications/XAMPP/xamppfiles/etc
  • Configuration File: /Applications/XAMPP/xamppfiles/etc/php.ini
  • Extensions directory: /Applications/XAMPP/xamppfiles/lib/php/extensions/no-debug-non-zts-20131226
Example Instructions
  1. Download xdebug-2.4.0.tgz
    continued…

You can follow the Tailored Installation Instructions from xdebug.org or you can follow the instructions here that I have found to work well.

  1. Unpack the downloaded file
    # navigate to the downloaded file
    $ cd ~/Downloads
    
    $ tar -xvzf xdebug-2.4.0.tgz
    $ cd xdebug-2.4.0
    
  2. run phpize
    $ phpize
    
    # example output
    Configuring for:
    PHP Api Version:         20131106
    Zend Module Api No:      20131226
    Zend Extension Api No:   220131226
    
  3. Configure with XAMPP php-config.
    $ ./configure --enable-xdebug --with-php-config=/Applications/XAMPP/xamppfiles/bin/php-config
    
  4. run make
    $ make
  5. In the Tailored Installation Instructions from xdebug.org, locate the step after make that contains the cp command and paths for copying the xdebug.so extension file into XAMPP, for example,
    $ cp modules/xdebug.so /Applications/XAMPP/xamppfiles/lib/php/extensions/no-debug-non-zts-20131226
    
  6. Edit /Applications/XAMPP/xamppfiles/etc/php.ini
    After “;zend_extension”
    Add the zend_extension setting from the xdebug.org instructions, for example:

    zend_extension=/Applications/XAMPP/xamppfiles/lib/php/extensions/no-debug-non-zts-20131226/xdebug.so
    
  7. Additionally, add these xdebug configuration settings in php.ini after the zend_extension setting:
    xdebug.remote_enable=1
    xdebug.remote_handler=dbgp
    xdebug.remote_host=localhost
    xdebug.remote_autostart = 1
    xdebug.remote_port=9000
    xdebug.show_local_vars=1
    xdebug.remote_log=/Applications/XAMPP/logs/xdebug.log
    
    ;---------------------------------
    ; uncomment these dev environment
    ; specific settings as needed
    ;---------------------------------
    ;xdebug.idekey = "netbeans-xdebug"
    ;xdebug.idekey = "sublime.xdebug"
    
    ;some pages in your Drupal site will not work default = 100
    ;xdebug.max_nesting_level=256
    

    Optional – for development only
    Find “max_execution_time” and set to unlimited:

    max_execution_time=0
    
  8. Restart Apache using XAMPP’s manager-osx
  9. Reload http://localhost/dashboard/phpinfo.php and verify that xdebug section exists. Another verification method is to copy and paste the phpinfo into the form at https://xdebug.org/wizard.php and resubmit it for analysis.

Customizing Media Manager

These examples show how to load copies of com_media views and templates instead of their core counterparts for customization without hacking any core Joomla files.

Here is the secret sauce to override com_media views and templates. A system plugin with an onAfterInitialise() event handler to capture request data and evaluate parameters using JInput. For example,

/plugins/system/mcm/mcm.php
// no direct access
defined ( '_JEXEC' ) or die ( 'Restricted access' );

jimport('joomla.plugin.plugin');

class plgSystemMcm extends JPlugin { 

    public function onAfterInitialise() {

        $input = JFactory::getApplication()->input;

        if('com_media' == $input->getCMD('option')) {

            $this->loadView($input->getCMD('view'));

        }

        return true;
    }

    protected function loadView($view)
    {
        if (('images' == $view) || ('imagesList' == $view)) {

            $overridePath = FOFPlatform::getInstance()->getTemplateOverridePath('com_media', true) . '/' . $view;

            require_once $overridePath . '/view.html.php';
        }
    }
}

Application Specific onAfterInitialise()

This version of the onAfterInitialise() function shows how to apply the override for just the frontend (site) views and templates.

public function onAfterInitialise() {

    $app = JFactory::getApplication();

    if ($app->isSite()) {

        if('com_media' == $app->input->getCMD('option')) {

            $this->loadView($app->input->getCMD('view'));

        }

        return true;
    }

    return false
}

Create the new custom templates and views by copying the folders from the com_media component into your template folders. For example, if you want to customize media manager for your protostar template, copy the com_media folders into the protostar template as follows:

  1. copy
    /administrator/components/com_media/views/images
    to
    /templates/protostar/html/com_media/images
  2. copy
    /administrator/components/com_media/views/imageslist
    to
    /templates/protostar/html/com_media/imageslist

Directory structure of com_media protostar template overrides.

  • templates
    • protostar
      • html
        • com_media
          • images
            • tmpl
              • default.php
          • view.html.php
          • imageslist
            • tmpl
              • default_folder.php
              • default_image.php
              • default.php
          • view.html.php

Then modify both copies of view.html.php to include their respective default.php template copies.

At the end of the display function, comment out or replace,
parent::display($tpl);

with an include directive to the template copy,
include( dirname(__FILE__) . '/tmpl/default.php');

for example, in both the images and imageslist folders:

/templates/protostar/html/com_media/images/view.html.php
/templates/protostar/html/com_media/imageslist/view.html.php
//parent::display($tpl);
include( dirname(__FILE__) . '/tmpl/default.php');

Also comment or replace both loadTemplate('folder') and loadTemplate('image') function calls in the imageslist default.php template and include their file and folder default.php template copies.

for example,

/templates/protostar/html/com_media/imageslist/tmpl/default.php
<?php for ($i = 0, $n = count($this->folders); $i < $n; $i++) :
	$this->setFolder($i);
	//echo $this->loadTemplate('folder');
	include( dirname(__FILE__) . '/default_folder.php');
endfor; ?>

<?php for ($i = 0, $n = count($this->images); $i < $n; $i++) :
	$this->setImage($i);
	//echo $this->loadTemplate('image');
	include( dirname(__FILE__) . '/default_image.php');
endfor; ?>

The com_media views and templates will now be loaded instead of their core counterparts and can be customized without hacking any core Joomla files. The plugin code above can be found here on GitHub. The next page covers extending the com_media controller to handle base64 image uploads.

source code

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

Joomla Hello World Module

This post documents my experience while following the Creating a simple module Joomla 3.x tutorial. I am brand new to Joomla and wanted to share my experience for anyone else trying to develop, install and demo this module in their Joomla 3 environment.

After following the steps in the tutorial, you can compare your mod_helloworld files to the files I have pushed to github. Additionally, here is the file structure of the module.

Files

  • mod_helloworld
    • sql
      • mysql
        • install.mysql.utf8.sql
        • uninstall.mysql.utf8.sql
    • tmpl
      • default.php
      • index.html
    • helper.php
    • index.html
    • mod_helloworld.php
    • mod_helloworld.xml

Install the Module

I created the module directly in the Joomla modules folder. For example,
C:\xampp\htdocs\joomla3\modules\mod_helloworld\

Using XAMPP for Windows as my local Joomla development environment.

In this example, install the module as follows:

  1. Copy the mod_helloworld folder into the Joomla modules folder if it isn’t already there.
  2. In the Joomla Administration page (/administrator), navigate to Extensions > Manage
  3. Select Discover from the sub menu.
  4. Select the Discover button.
  5. Check the box next to the Hello World module.
  6. Select the Install button.
Joomla 3 Module Discovery Install
Joomla 3.x Module Discovery Install

Add the module as follows:

  1. Navigate to Extensions > Modules
  2. Select the New button
  3. Under Select a Module Type: scroll down to the Hello World! module and select it.
  4. Fill out the mod_helloworld form, enter a Title, select a language and Position.
  5. Select the Save & Close button
Joomla 3.x Hello World Module Form
Joomla 3.x Hello World Module Form

In this example, Bonjour tout le monde (Hello World in French) is displayed in position-1 at the top of the home page.

Joomla3 Home page with Hello World module
Joomla3 Home page with Hello World module

WordPress Page Specific Styles or Scripts

I wanted an efficient way to load page or post specific stylesheets and or scripts which lead me to see if I could utilize custom fields for this. Using custom fields combined with wp_enqueue_style and wp_enqueue_script worked perfectly. This solution allows me to link css or javascript files for only the posts or pages I want.

1. Add a couple of custom fields, one for styles and one for scripts to a page using the editor:

Add bn_styles custom field
WordPress editor – Add bn_styles custom field
bn_styles and bn_scripts custom fields added
WordPress editor – bn_styles and bn_scripts custom fields added

Loading WordPress theme stylesheets and scripts is typically done within the functions.php file using wp_enqueue_style and wp_enqueue_script respectively. Using the Twenty Fifteen functions.php file as an example, take a look at the twentyfifteen_scripts function:

function twentyfifteen_scripts() {
    // Add custom fonts, used in the main stylesheet.
    wp_enqueue_style( 'twentyfifteen-fonts', twentyfifteen_fonts_url(), array(), null );

    // Add Genericons, used in the main stylesheet.
    wp_enqueue_style( 'genericons', get_template_directory_uri() . '/genericons/genericons.css', array(), '3.2' );

    // Load our main stylesheet.
    wp_enqueue_style( 'twentyfifteen-style', get_stylesheet_uri() );

    // Load the Internet Explorer specific stylesheet.
    wp_enqueue_style( 'twentyfifteen-ie', get_template_directory_uri() . '/css/ie.css', array( 'twentyfifteen-style' ), '20141010' );
    wp_style_add_data( 'twentyfifteen-ie', 'conditional', 'lt IE 9' );

    // Load the Internet Explorer 7 specific stylesheet.
    wp_enqueue_style( 'twentyfifteen-ie7', get_template_directory_uri() . '/css/ie7.css', array( 'twentyfifteen-style' ), '20141010' );
    wp_style_add_data( 'twentyfifteen-ie7', 'conditional', 'lt IE 8' );

    wp_enqueue_script( 'twentyfifteen-skip-link-focus-fix', get_template_directory_uri() . '/js/skip-link-focus-fix.js', array(), '20141010', true );

    if ( is_singular() && comments_open() && get_option( 'thread_comments' ) ) {
        wp_enqueue_script( 'comment-reply' );
    }

    if ( is_singular() && wp_attachment_is_image() ) {
        wp_enqueue_script( 'twentyfifteen-keyboard-image-navigation', get_template_directory_uri() . '/js/keyboard-image-navigation.js', array( 'jquery' ), '20141010' );
    }

    wp_enqueue_script( 'twentyfifteen-script', get_template_directory_uri() . '/js/functions.js', array( 'jquery' ), '20150330', true );
    //wp_enqueue_script( 'twentyfifteen-script', get_template_directory_uri() . '/js/main.js', array( 'jquery' ), '20150527', true );
    wp_localize_script( 'twentyfifteen-script', 'screenReaderText', array(
        'expand'   => '' . __( 'expand child menu', 'twentyfifteen' ) . '',
        'collapse' => '' . __( 'collapse child menu', 'twentyfifteen' ) . '',
    ) );
}
add_action( 'wp_enqueue_scripts', 'twentyfifteen_scripts' );

Edit functions.php

Add some code to the twentyfifteen_scripts function to get the custom field data and use it to load stylesheets and scripts.

2. At the top of the twentyfifteen_scripts function, add the get_post_custom function to retrieve the custom field data:

function twentyfifteen_scripts() {

    // retrieve custom fields from the current post
    $custom_fields = get_post_custom();

3. After the last stylesheet is queued for loading, add the code that checks for and loads our styles custom_field stylesheets. In this example, add the code after wp_style_add_data( ‘twentyfifteen-ie7’, ‘conditional’, ‘lt IE 8’ ); as shown here:

    wp_style_add_data( 'twentyfifteen-ie7', 'conditional', 'lt IE 8' );

    // styles custom_field
    if (!empty($custom_fields['bn_styles'])) {
        foreach ( $custom_fields['bn_styles'] as $key => $value ) {
            wp_enqueue_style('twentyfifteen_custom_field_' . $key, get_template_directory_uri() . '/css/' . $value, false, null);
        }
    }

4. At the end of the twentyfifteen_scripts function, add the code that checks for and loads our scripts custom_field javascript as shown here:

    // scripts custom_field
    if (!empty($custom_fields['bn_scripts'])) {
        foreach ( $custom_fields['bn_scripts'] as $key => $value ) {
            wp_enqueue_script('twentyfifteen_custom_field_' . $key, get_template_directory_uri() . '/js/' . $value, [], null, true);
        }
    }
}
add_action( 'wp_enqueue_scripts', 'twentyfifteen_scripts' );

5. Here is the entire twentyfifteen_scripts function after the modifications above:

function twentyfifteen_scripts() {

    // retrieve custom fields from the current post
    $custom_fields = get_post_custom();

    // Add custom fonts, used in the main stylesheet.
    wp_enqueue_style( 'twentyfifteen-fonts', twentyfifteen_fonts_url(), array(), null );

    // Add Genericons, used in the main stylesheet.
    wp_enqueue_style( 'genericons', get_template_directory_uri() . '/genericons/genericons.css', array(), '3.2' );

    // Load our main stylesheet.
    wp_enqueue_style( 'twentyfifteen-style', get_stylesheet_uri() );

    // Load the Internet Explorer specific stylesheet.
    wp_enqueue_style( 'twentyfifteen-ie', get_template_directory_uri() . '/css/ie.css', array( 'twentyfifteen-style' ), '20141010' );
    wp_style_add_data( 'twentyfifteen-ie', 'conditional', 'lt IE 9' );

    // Load the Internet Explorer 7 specific stylesheet.
    wp_enqueue_style( 'twentyfifteen-ie7', get_template_directory_uri() . '/css/ie7.css', array( 'twentyfifteen-style' ), '20141010' );
    wp_style_add_data( 'twentyfifteen-ie7', 'conditional', 'lt IE 8' );

    // styles custom_field
    if (!empty($custom_fields['bn_styles'])) {
        foreach ( $custom_fields['bn_styles'] as $key => $value ) {
            wp_enqueue_style('twentyfifteen_custom_field_' . $key, get_template_directory_uri() . '/css/' . $value, false, null);
        }
    }

    wp_enqueue_script( 'twentyfifteen-skip-link-focus-fix', get_template_directory_uri() . '/js/skip-link-focus-fix.js', array(), '20141010', true );

    if ( is_singular() && comments_open() && get_option( 'thread_comments' ) ) {
        wp_enqueue_script( 'comment-reply' );
    }

    if ( is_singular() && wp_attachment_is_image() ) {
        wp_enqueue_script( 'twentyfifteen-keyboard-image-navigation', get_template_directory_uri() . '/js/keyboard-image-navigation.js', array( 'jquery' ), '20141010' );
    }

    wp_enqueue_script( 'twentyfifteen-script', get_template_directory_uri() . '/js/functions.js', array( 'jquery' ), '20150330', true );
    //wp_enqueue_script( 'twentyfifteen-script', get_template_directory_uri() . '/js/main.js', array( 'jquery' ), '20150527', true );
    wp_localize_script( 'twentyfifteen-script', 'screenReaderText', array(
        'expand'   => '' . __( 'expand child menu', 'twentyfifteen' ) . '',
        'collapse' => '' . __( 'collapse child menu', 'twentyfifteen' ) . '',
    ) );

    // scripts custom_field
    if (!empty($custom_fields['bn_scripts'])) {
        foreach ( $custom_fields['bn_scripts'] as $key => $value ) {
            wp_enqueue_script('twentyfifteen_custom_field_' . $key, get_template_directory_uri() . '/js/' . $value, [], null, true);
        }
    }
}
add_action( 'wp_enqueue_scripts', 'twentyfifteen_scripts' );

Handle Script Dependencies

A nice feature of the wp_enqueue_script function is that it allows us to pass in dependencies as a parameter. The optional dependencies parameter consists of an array of handles that map to javascript files. To apply this feature to our scripts custom field code, we need to create a $deps array that will retrieve optional handles added to the custom field value. Replace the code block at the bottom of function twentyfifteen_scripts() as follows:

REPLACE
    // scripts custom_field
    if (!empty($custom_fields['bn_scripts'])) {
        foreach ( $custom_fields['bn_scripts'] as $key => $value ) {
            wp_enqueue_script('twentyfifteen_custom_field_' . $key, get_template_directory_uri() . '/js/' . $value, [], null, true);
        }
    }
WITH
    // scripts custom_field
    if (!empty($custom_fields['bn_scripts'])) {
        foreach ( $custom_fields['bn_scripts'] as $key => $value ) {
            $script = explode(',', $value);
            $deps = array();
            foreach ( $script as $i => $v ) {
                if ($i > 0) { // values after $script[0]
                    $deps[] = trim($v);
                }
            }
            wp_enqueue_script('twentyfifteen_custom_field_' . $key, get_template_directory_uri() . '/js/' . $value, $deps, null, true);
        }
    }

For a detailed list of handle names that can be used, see Handles and Their Script Paths Registered by WordPress.

6. Here is the entire twentyfifteen_scripts function after updating the scripts custom field parser to handle script dependencies:

function twentyfifteen_scripts() {

    // retrieve custom fields from the current post
    $custom_fields = get_post_custom();

    // Add custom fonts, used in the main stylesheet.
    wp_enqueue_style( 'twentyfifteen-fonts', twentyfifteen_fonts_url(), array(), null );

    // Add Genericons, used in the main stylesheet.
    wp_enqueue_style( 'genericons', get_template_directory_uri() . '/genericons/genericons.css', array(), '3.2' );

    // Load our main stylesheet.
    wp_enqueue_style( 'twentyfifteen-style', get_stylesheet_uri() );

    // Load the Internet Explorer specific stylesheet.
    wp_enqueue_style( 'twentyfifteen-ie', get_template_directory_uri() . '/css/ie.css', array( 'twentyfifteen-style' ), '20141010' );
    wp_style_add_data( 'twentyfifteen-ie', 'conditional', 'lt IE 9' );

    // Load the Internet Explorer 7 specific stylesheet.
    wp_enqueue_style( 'twentyfifteen-ie7', get_template_directory_uri() . '/css/ie7.css', array( 'twentyfifteen-style' ), '20141010' );
    wp_style_add_data( 'twentyfifteen-ie7', 'conditional', 'lt IE 8' );

    // styles custom_field
    if (!empty($custom_fields['bn_styles'])) {
        foreach ( $custom_fields['bn_styles'] as $key => $value ) {
            wp_enqueue_style('twentyfifteen_custom_field_' . $key, get_template_directory_uri() . '/css/' . $value, false, null);
        }
    }

    wp_enqueue_script( 'twentyfifteen-skip-link-focus-fix', get_template_directory_uri() . '/js/skip-link-focus-fix.js', array(), '20141010', true );

    if ( is_singular() && comments_open() && get_option( 'thread_comments' ) ) {
        wp_enqueue_script( 'comment-reply' );
    }

    if ( is_singular() && wp_attachment_is_image() ) {
        wp_enqueue_script( 'twentyfifteen-keyboard-image-navigation', get_template_directory_uri() . '/js/keyboard-image-navigation.js', array( 'jquery' ), '20141010' );
    }

    wp_enqueue_script( 'twentyfifteen-script', get_template_directory_uri() . '/js/functions.js', array( 'jquery' ), '20150330', true );
    //wp_enqueue_script( 'twentyfifteen-script', get_template_directory_uri() . '/js/main.js', array( 'jquery' ), '20150527', true );
    wp_localize_script( 'twentyfifteen-script', 'screenReaderText', array(
        'expand'   => '' . __( 'expand child menu', 'twentyfifteen' ) . '',
        'collapse' => '' . __( 'collapse child menu', 'twentyfifteen' ) . '',
    ) );

    // scripts custom_field
    if (!empty($custom_fields['bn_scripts'])) {
        foreach ( $custom_fields['bn_scripts'] as $key => $value ) {
            $script = explode(',', $value);
            $deps = array();
            foreach ( $script as $i => $v ) {
                if ($i > 0) { // values after $script[0]
                    $deps[] = trim($v);
                }
            }
            wp_enqueue_script('twentyfifteen_custom_field_' . $key, get_template_directory_uri() . '/js/' . $value, $deps, null, true);
        }
    }
}
add_action( 'wp_enqueue_scripts', 'twentyfifteen_scripts' );
This code is also available at gist.github.com

7. Now the bn_scripts custom field can be updated to include dependencies. List the dependency handles after the script separated by a comma as shown below.

bn_styles and bn_scripts custom fields added
WordPress editor – bn_scripts custom field dependencies added

More often than not jQuery will already be loaded. However, if your script is dependent upon jQuery you should add its handle to the dependencies comma separated list in the bn_scripts custom field value. wp_enqueue_script prevents double loading of scripts and their dependencies.

Given the default theme location, if you view source of the page that has the custom fields added and search for tree.css or tree.js, they should be linked to /wp-content/themes/twentyfifteen/css/tree.css and /wp-content/themes/twentyfifteen/js/tree.js. Any dependencies will also be linked.


Bourboneat version

If you would like to explore using this code with the bourboneat WordPress starter theme, here is the modified /functions/asset.php assets function that is used to enqueue stylesheets and scripts:

function assets() {
    wp_enqueue_style('bn_css', asset_path('styles/main.css'), false, null);

    $custom_fields = get_post_custom();

    // styles custom_field
    if (!empty($custom_fields['bn_styles'])) {
        foreach ( $custom_fields['bn_styles'] as $key => $value ) {
            wp_enqueue_style('bn_custom_field_' . $key, asset_path('styles/' . $value), false, null);           
        }
    }

    if ( is_singular() && comments_open() && get_option( 'thread_comments' ) ) {
        wp_enqueue_script( 'comment-reply' );
    }

    //wp_enqueue_script('modernizr', asset_path('scripts/modernizr.js'), [], null, true);
    wp_enqueue_script('bn_js', asset_path('scripts/main.js'), ['jquery'], null, true);

    // scripts custom_field
    if (!empty($custom_fields['bn_scripts'])) {
        foreach ( $custom_fields['bn_scripts'] as $key => $value ) {
            $script = explode(',', $value);
            $deps = array();
            foreach ( $script as $i => $v ) {
                if ($i > 0) { // values after $script[0]
                    $deps[] = trim($v);
                }
            }
            wp_enqueue_script('bn_custom_field_' . $key, asset_path('scripts/' . $script[0] ), $deps, null, true);
        }
    }
}

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('http://responsiveresourcesgroup.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
Available for browsing and download at GitHub: https://github.com/jimfrenette/BrowserifyGulp

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

Visual Studio MVC Bourbon Web App

It is now 2015 and with it a new version of Visual Studio that includes tools for Bower front end package management, Node Package Manager (NPM) and the Grunt JavaScript Task Runner. Not surprisingly, Microsoft has its own way of implementing Grunt, Bower and Node Modules in their upcoming Visual Studio debut. You don’t have to wait for Visual Studio 2015. Grunt, Bower, Node Modules and Bourbon can easily be included in a Visual Studio 2012 MVC 4 web application.

1. Create a New Project

New ASP.NET MVC 4 Web Application – MvcAppBourbon
New ASP.NET MVC 4 Web Application – MvcAppBourbon

2. Select the Empty Project Template

New ASP.NET MVC 4 Project – Empty Project Template
New ASP.NET MVC 4 Project – Empty Project Template

3. Ruby and Sass 3.3+

To install or update Sass you will need to have Ruby installed. For Windows, you can use the RubyInstaller for Windows. See this post for more info, Compass on Windows. These next step are done using a CLI, Windows PowerShell is what I will be using here. Git for Windows provides BASH emulation which also works great.

Install Sass

Ruby uses Gems to manage its code packages, to install the Sass Ruby Gem, enter gem install sass in the CLI.

Windows PowerShell – Install Sass
Windows PowerShell – Install Sass

4. Node Package Manager (NPM)

Install Node.js

NPM is included with Node.js. The best way to install NPM is to install node. The easiest way to install Node.js for Windows is with the Windows Installer (.msi) avaialble here.

5. Bower

Use NPM to install Bower globally on your system.

Install Bower
# global bower install
npm install -g bower

Change to the directory where you app is located. For me this is:
cd ‘~\Documents\Visual Studio 2012\Projects\MvcAppBourbon\MvcAppBourbon’

Install Bourbon
# bourbon install
bower install bourbon

Installed packages are located in the bower_components directory. This is created in the folder which the bower program was executed.

Install Bourbon Neat

Neat is a lightweight semantic grid framework for Sass and Bourbon.

# bourbon neat install
bower install neat

6. Bitters

Install Bourbon Bitters

Bitters jump start projects with a predefined set of basic Sass variables, default element style and project structure. Bitters are not installed with bower since the styles and variables are intended to be customized as needed. Change to the root directory of the app if not already there from bourbon install above. For me this is:
cd ‘~\Documents\Visual Studio 2012\Projects\MvcAppBourbon\MvcAppBourbon’

# bourbon bitters install
gem install bitters

# create a directory for sass files and folders
# this is done from within the MvcAppBourbon app root

mkdir sass

# change to the new sass directory
cd sass

# install bitters into the sass folder
bitters install

# navigate back to the MvcAppBourbon app root
cd ../

# create a directory for css files
mkdir css

Bitters files are installed to base/base. In-depth instructions on GitHub.

7. Grunt

Install Grunt

Use NPM to install Grunt’s command line interface (CLI) globally on your system. Then use NPM init to create a package.json that stores node package data for the app. The third command to run, npm install grunt –save-dev installs the latest version of Grunt into the project and adds it to the package.json devDependencies. More info is avaialable here.

# grunt CLI install
npm install -g grunt-cli

# create package.json
npm init

# grunt install to app, updates package.json
npm install grunt --save-dev

Installed npm packages are located in the node_modules directory. This is created in the folder which the npm install program was executed.

Install Grunt Plugins

The Grunt task that handles sass compilation to css when a .scss file is updated and saved is accomplished with these two plugins, grunt-contrib-watch and grunt-contrib-compass.

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

# grunt-contrib-compass install
npm install grunt-contrib-compass --save-dev
Create Gruntfile.js

In Visual Studio > Solution Explorer;
Right click MvcAppBourbon;
Select: Add > New Item > JavaScript File;
Name: Gruntfile.js

Add this JavaScript to the Gruntfile.

Gruntfile.js
'use strict';

module.exports = function(grunt) {
	// Project configuration.
	grunt.initConfig({
	    pkg: grunt.file.readJSON('package.json'),
	    compass: {
	      dist: {
	        options: {  
	          cssDir: 'css/',
	          sassDir: 'sass/',
	          outputStyle: 'compressed'
	        }
	      }
	    },
	    watch: {
			css: {
				files: '**/*.scss',
				tasks: ['compass']
			}
	    }
	});

	// Load the plugins.
	grunt.loadNpmTasks('grunt-contrib-compass');
	grunt.loadNpmTasks('grunt-contrib-watch');

	// default task
	grunt.registerTask('default', ['watch']);
}

8. SassyStudio Extension

In Visual Studio 2012, select Tools > Extensions and Updates

Search for and install the SassyStudio extension. This free utility provides a small amount of support for .scss files in Visual Studio. More info

9. Sass

In Visual Studio Solution Explorer;
Select Show All Files;

Show All Files – MvcAppBourbon

Right click sass > Include In Project;

Include In Project – MvcAppBourbon

Open sass/base/_grid-settings.scss and change the import path: @import “neat-helpers” to @import “../../bower_components/neat/app/assets/stylesheets/neat-helpers”.

_grid-settings.scss
//not in Rails
@import "../../bower_components/neat/app/assets/stylesheets/neat-helpers";
Create main.scss

In Visual Studio > Solution Explorer;
Right click MvcAppBourbon > sass;
Select: Add > New Item > SASS File;
Name: main.scss

Add this Sass to the main.scss file to import Bitters (base/base) and Neat. Neat is imported after Bitters.

main.scss
@import "../bower_components/bourbon/app/assets/stylesheets/bourbon.scss";
@import "base/base";
@import '../bower_components/neat/app/assets/stylesheets/neat';

Prior to making anymore Sass edits, in the CLI, load grunt to watch and compile changes.

Windows PowerShell – Grunt
Windows PowerShell – Grunt

Since we are using Neat, uncomment the grid-settings import in sass/base/_base.scss:

_base.scss
// Neat Settings -- uncomment if using Neat -- must be imported before Neat
@import "grid-settings";

After saving sass/main.scss, the running grunt tasks should should indicate that css/main.css has been written in the CLI and css/main.css should be updated to contain all of the base styles from the bitters import. Refresh Solution Explorer to see the new css.

Windows PowerShell – Grunt Compass
Windows PowerShell – Grunt Compass

On the next page, controller, views and some Bourbon Refills added for demonstration.

Resources

Bourbon

This post is not about a barrel-aged distilled spirit made primarily from corn. Instead, it is about a Sass mixin library. To add Bourbon to your web project, visit the Bourbon GitHub repo and take a look at the requirements.

The first requirement is obviously Sass, since this is a mixin library for it. As of this post, the latest Bourbon requires Sass 3.3+. To install or update Sass you will need to have Ruby installed. Ruby comes pre-installed on OS X. For Windows, you can use the RubyInstaller for Windows. When I set this up last year, I used the Ruby 1.9.3 installers. These next step are performed using a CLI. Git for Windows provides a BASH emulation that you can use instead of the Command Prompt.

Sass check / update

# version check
$ sass --v
Sass 3.2.12 (Media Mark)

# update
$ gem update sass
Updating installed gems
Updating sass
Fetching: sass-3.4.5.gem (100%)
Successfully installed sass-3.4.5
Gems updated: sass
Installing ri documentation for sass-3.4.5...
Installing RDoc documentation for sass-3.4.5...

Install Bourbon

Option 1

The installation as documented at the bourbon repo is pretty straight forward.

# Bourbon gem install
$ gem install bourbon
Fetching: thor-0.19.1.gem (100%)
Fetching: bourbon-4.0.2.gem (100%)
Successfully installed thor-0.19.1
Successfully installed bourbon-4.0.2
2 gems installed
Installing ri documentation for thor-0.19.1...
Installing ri documentation for bourbon-4.0.2...
Installing RDoc documentation for thor-0.19.1...
Installing RDoc documentation for bourbon-4.0.2...

# change to your project directory
# e.g., cd ~/mywebsite
# install Bourbon into your project
$ bourbon install
Bourbon files installed to bourbon/
Option 2

A good alternative is to use Bower to install Bourbon and manage packages for your project. You will need to have the Node Package Manger (NPM) which is bundled with Node.js. If you have not done so already, install Node.js. Then, use the NPM to install Bower

# global bower install
$ npm install -g bower

Next, using bower init, generate a bower.json file for your project. You will be prompted for answers to generate the file. Answering each is up to you, accept the defaults by selecting enter. After the bower.json file is created in your project, install Bourbon using bower install –save bourbon:

# change to your project directory
# e.g., cd ~/mywebsite
$ bower init
? name: BourbonTutorial
? version: 0.0.1
? description:
? main file:
? what types of modules does this package expose?:
? keywords:
? authors: jimfrenette <jim@jimfrenette.com>
? license: MIT
? homepage:
? set currently installed components as dependencies?: Yes
? add commonly ignored files to ignore list?: Yes
? would you like to mark this package as private which prevents it from being accidentally published to the registry?: No

{
  name: 'BourbonTutorial',
  version: '0.0.1',
  authors: [
    'jimfrenette '
  ],
  license: 'MIT',
  ignore: [
    '**/.*',
    'node_modules',
    'bower_components',
    'test',
    'tests'
  ]
}

? Looks good?: Yes

# install Bourbon into your project using Bower,
# this command installs the package and the --save option
# adds the dependency to the bower.json file

$ bower install --save bourbon
bower cached        git://github.com/thoughtbot/bourbon.git#4.0.2
bower validate      4.0.2 against git://github.com/thoughtbot/bourbon.git#*
bower install       bourbon#4.0.2

bourbon#4.0.2 bower_components\bourbon

Using Bourbon

Now we can create a Sass file and test drive some Bourbon mixins. Create a folder named sass in your web project root. And in that folder create a main.scss file with the code/text editor of your choice. Add the following Sass code to your new main.scss file and save it. NOTE: if you used option 1 to install Bourbon, adjust your import path as needed since /bower_components unavailable.

main.scss
@import "../bower_components/bourbon/dist/_bourbon.scss";

button {
    @include button(shiny, #ff0000);
}

Next, create a folder name css for you compiled sass output. Your directory structure should now look something like this depending on which Bourbon install option you used.

option 1
# | bourbon/
# | css/
# | sass/
#     | -- main.scss
 
option 2
# | bower_components/
#     | -- bourbon/
# | css/
# | sass/
#     | -- main.scss
# | bower.json
Setup sass to compile main.scss automatically

From within your projects sass folder, execute the sass watch command. Whenever your main.scss file is saved, the respective main.css will be written with all of the applicable browser extensions, reusable properties, etc..

# change to your sass folder in your project directory
# e.g., cd ~/mywebsite/sass

$ sass --watch  main.scss:../css/main.css
>>> Sass is watching for changes. Press Ctrl-C to stop.
←[32m      write ../css/main.css
←[0m←[32m      write ../css/main.css.map
←[0m

Here is what your /css/main.css file should look like after the Sass compile.

button {
  border: 1px solid #8a0000;
  border-bottom: 1px solid #810000;
  border-radius: 5px;
  box-shadow: inset 0 1px 0 0 #ff1d0c;
  color: white;
  display: inline-block;
  font-size: inherit;
  font-weight: bold;
  background-color: #ff0000;
  background-image: -webkit-linear-gradient(top, #ff0000 0%, #c70000 50%, #a90000 50%, #b00000 100%);
  background-image: linear-gradient(to bottom,#ff0000 0%, #c70000 50%, #a90000 50%, #b00000 100%);
  padding: 7px 18px;
  text-align: center;
  text-decoration: none;
  text-shadow: 0 -1px 1px #730000; }
  button:hover:not(:disabled) {
    cursor: pointer;
    background-color: #f20000;
    background-image: -webkit-linear-gradient(top, #f20000 0%, #bd0000 50%, #a20000 50%, #a90000 100%);
    background-image: linear-gradient(to bottom,#f20000 0%, #bd0000 50%, #a20000 50%, #a90000 100%); }
  button:active:not(:disabled), button:focus:not(:disabled) {
    box-shadow: inset 0 0 20px 0 #900000; }
  button:disabled {
    opacity: 0.5;
    cursor: not-allowed; }

/*# sourceMappingURL=main.css.map */

This is just the tip of the iceberg. With Sass, you can develop modular, reusable styles in small chunks to help make your CSS DRY and organized.

Bourbon Neat with Bitters

Neat is a lightweight semantic grid framework for Sass and Bourbon. Bitters is a set of Sass styles, variables and structure for Bourbon projects.

Going to change gears a little here and use Grunt to handle the watch & compile task for the Sass (.scss) files. Therefore, create this package.json file for NPM in the project root.

package.json
{
  "name": "bourbon-tutorial",
  "version": "0.1.0",
  "devDependencies": {
    "grunt": "~0.4.5",
    "grunt-contrib-compass": "~1.0.1",
    "grunt-contrib-watch": "~0.6.1"
  }
}

Using Bower install Option 2, your file structure should look like this:

# option 2
# | bower_components/
#     | -- bourbon/
# | css/
# | sass/
#     | -- main.scss
# | bower.json
# | package.json

From the project root directory, run the NPM install which will add the grunt node modules to the project specified in package.json

# install node modules 
$ npm install

Install Neat and Bitters

# install neat using Bower,
# this command installs the package and the --save option
# adds the dependency to the bower.json file

$ bower install --save neat

# install bitters
$ gem install bitters

# add the library to the project,
# change to the sass directory,
# e.g., cd sass
$ bitters install

A base directory will be created in /sass containing the Bitters library. Detailed instructions are available here.

Create Grunt config

A Grunt configuration file is needed to run the watch and compile tasks. Create this gruntfile.js in the project root.

gruntfile.js
module.exports = function(grunt) {
  'use strict';
  // Project configuration.
  grunt.initConfig({
    pkg: grunt.file.readJSON('package.json'),
    compass: {
      dist: {
        // these options will override (or extend) config.rb settings.
        options: {  
          cssDir: 'css/',
          sassDir: 'sass/',
          outputStyle: 'compressed'
        }
      }
    },
    watch: {
      css: {
        files: '**/*.scss',
        tasks: ['compass']
      }
    }
  });
  // Load the plugins.
  grunt.loadNpmTasks('grunt-contrib-compass');
  grunt.loadNpmTasks('grunt-contrib-watch');
  // Default task(s).
  grunt.registerTask('default', ['watch']);
};

Run grunt

$ grunt
Running "watch" task
Waiting...

Step 2

This tutorial is not using Ruby on Rails, so edit sass/base/_grid-settings.scss
changing the import path as needed for neat-helpers.

_grid-settings.scss
//not in Rails
@import "../../bower_components/neat/app/assets/stylesheets/neat-helpers";

Edit sass/main.scss to import Bitters (base/base) and Neat. Neat is imported after Bitters.

main.scss
@import "../bower_components/bourbon/dist/_bourbon.scss";
@import "base/base";
@import "../bower_components/neat/app/assets/stylesheets/neat";

button {
    @include button(shiny, #ff0000);
}

UPDATE – Bourbon 4.1 path changes **

main.scss
// note changes to path in Bourbon 4.1
@import "../bower_components/bourbon/app/assets/stylesheets/_bourbon.scss";

** Bourbon 4.1.0 released on December 30, 2014
For Bower users, the Bourbon’s directory structure has been changed. Instead of a dist directory, which was just a duplicate of the entire Bourbon library for Bower to use, now point Bower to app/assets/stylesheets.

Bitters is a scaffold of scss files to get you started, add to or edit them as needed. Since we are using Neat, uncomment the grid-settings import in base/_base.scss:

_base.scss
// Neat Settings -- uncomment if using Neat -- must be imported before Neat
@import "grid-settings";

After saving sass/main.scss, per the running grunt tasks the terminal output should indicate that css/main.css has been written. css/main.css should be updated to contain all of the base styles from the bitters import.

>> File "sass\main.scss" changed.
Running "compass:dist" (compass) task
    write css/main.css (0.875s)

Done, without errors.
Source Code

Resources

XAMPP Windows Setup

Documenting my XAMPP for Windows setup for myself and anyone else who finds it useful. For those of you that do not know, XAMPP is a completely free, easy to install Apache distribution containing MySQL, PHP, and Perl. I still have my Virtual Machine for LAMP Development which I will use for test deployments of my localhost LAMP development prior to production deployment. This local XAMPP setup will be better for debugging, working with node modules and grunt tasks for preparing the deployment build.

XAMPP Configuration

Since my Windows 8.1 system is already using port 80 for IIS, I decided to setup XAMMP to use port 8080.

C:\xampp\apache\conf\httpd.conf
# on or near line 58
# change listen to 8080
Listen 8080

# on or near line 219
ServerName localhost:8080
C:\xampp\apache\conf\extra\httpd-vhosts.conf
# virtual hosts
NameVirtualHost *:8080

<VirtualHost *:8080>
  DocumentRoot C:/xampp/htdocs
  ServerName localhost
</VirtualHost>

# For htaccess rewrites to work AllowOverride
# needs to be set to All instead of None
<VirtualHost *:8080>
  DocumentRoot "C:/xampp/htdocs/mysite"
  ServerName mysite.dev
  ServerAlias www.mysite.dev
  SetEnv APPLICATION_ENV development
  <Directory "C:/xampp/htdocs/mysite">
    Options Indexes MultiViews FollowSymLinks
    AllowOverride All
    Order allow,deny
    Allow from all
  </Directory>
</VirtualHost>

If you want to use a localhost tunneling solution such as pagekite to access your virtual host, change the ServerAlias to the URL your localhost tunneling provider has enabled. For example mysite.pagekite.me

C:\xampp\apache\conf\extra\httpd-vhosts.conf
# pagekite.me server alias
<VirtualHost *:8080>
  DocumentRoot "C:/xampp/htdocs/mysite"
  ServerName mysite.dev
  ServerAlias mysite.pagekite.me
  SetEnv APPLICATION_ENV development
  <Directory "C:/xampp/htdocs/mysite">
    Options Indexes MultiViews FollowSymLinks
    AllowOverride All
    Order allow,deny
    Allow from all
  </Directory>
</VirtualHost>
C:\xampp\xampp-control.ini
# add to bottom so when
# control panel starts there
# are not Apache port errors
[ServicePorts]
Apache=8080
C:\Windows\System32\drivers\etc\hosts
# port numbers not allowed
127.0.0.1   localhost
127.0.0.1   mysite.dev

PHP Configuration

C:\xampp\php\php.ini
# enable PHP short open tags
short_open_tag=On

# development only
max_execution_time = 0

# XDebug
# uncomment these lines
zend_extension = "C:\xampp\php\ext\php_xdebug.dll"
xdebug.remote_enable = 1
xdebug.remote_handler = "dbgp"
xdebug.remote_host = "127.0.0.1"

I use NetBeans for PHP development and prefer not having XDEBUG_SESSION_START=netbeans-xdebug querystring tacked onto my URL’s in order to debug.

C:\xampp\php\php.ini
# more xdebug settings
xdebug.remote_autostart = 1
xdebug.idekey = "netbeans-xdebug"

After starting up Apache in the XAMPP Control Panel, loading the site in the browser is now as easy as http://mysite.dev:8080/

XAMPP Windows Update

If you want to install a new version of XAMPP, the installer will not allow you to install over an existing copy. Follow these steps to:

  • backup the existing XAMPP installation
  • install the new version of XAMPP
  • copy config and mysql data into the new version
  1. Launch the XAMPP control panel and Stop all services.
  2. Quit the control panel
  3. Rename the installation folder, for example, change it from C:\xampp to C:\xampp_orig
  4. Create a new folder with the same name as the original installation. For example C:\xampp
  5. Install the new version of XAMPP
  6. Copy your website folders from C:\xampp_orig\htdocs\ to C:\xampp\htdocs\. Do not copy xampp and other folders that are installed with xampp, such as dashboard and webalizer.
  7. Copy any changes you made to C:\xampp_orig\apache\conf\httpd.conf and C:\xampp_orig\apache\conf\extra\httpd-vhosts.conf into the new versions of these apache configuration files.
  8. Create databases with the same names as before. A new folder for each database will be created along with its respective db.opt file.
  9. Copy C:\xampp_orig\mysql\data\ibdata1 to C:\xampp\mysql\data\ibdata1
  10. Copy all of the files except for db.opt from each database folder in C:\xampp_orig\mysql\data\*\ to C:\xampp\mysql\data\*\. Make sure you do not copy the db.opt file(s).
  11. Launch XAMPP control panel and start apache and mysql

Bootstrap Customization

This tutorial covers using Git to clone, branch and merge the latest Bootstrap source code, modifying and compiling your modifications to the source LESS variables using Grunt and viewing your compiled changes with a local Node.js web server.

If you are not familiar with Git, Node.js and Grunt, please read my previous post, Grunt JavaScript Task Runner to help get you started.

You could just use the Bootstrap customize and download form to customize the LESS variables and download a pre-compiled Bootstrap package. However, as of this writing, the user interface at the form doesn’t provide a preview option to monitor your changes before you download the package. And since the Bootstrap version 3 source now includes Grunt, we can use it to compile the LESS source and preview the changes via localhost.

Source Code

Using Git, clone the Bootstrap repository. To keep our changes to the source separate and to make upgrading the bootstrap source easier, create a develop branch.

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

# create and checkout develop branch
$ git checkout -b develop

Load a Bootstrap example

I like to use the Node.js web-server.js that is bundled with the AngularJS tutorial. For example, here is how I start the NodeJs web-server.js that is included in the angular-phonecat repository on GitHub. This example presumes your CLI is in the bootstrap directory and the angular-phonecat repository is cloned to its own directory one level up.

UPDATED 1/4/2014

Repository contents rearranged over the holidays and the examples have been moved to the docs folder. You can always take a look at the latest source on GitHaub.

# directory structure (partial)
# | angular-phonecat/
#   | -- scripts/
#     | -- web-server.js
# | bootstrap/
#   | -- docs/
#     | -- examples/
#       | -- starter-template/
#         | -- index.html
#         | -- starter-template.css

# start the web server from the bootstrap directory
$ node ../angular-phonecat/scripts/web-server.js

Http Server running at http://localhost:8000/

Now open a web browser and go to http://localhost:8000/docs/examples/starter-template/index.html.

Modify LESS variables

Open the bootstrap/less/variables.less file in a text editor and find the @navbar-inverse-color and @navbar-inverse-link-color variables and change their values from @gray-light to @gray-lighter. Also, find @navbar-inverse-bg and change its value to #FF8C00.

Install node modules and compile LESS

The Bootstrap source contains a package.json for node module installation and a fully configured gruntfile.js to run Grunt tasks.

Open another bash window CLI and navigate to the bootstrap directory to install the node modules. After the node modules are installed, run the Grunt dist-css task which compiles the less files to dist/css/ followed by task dist-docs which copies the css files created in the previous task to docs/dist/css/.

# install node modules
$ npm install

# run grunt task to compile less to dist/css/
$ grunt dist-css

# run grunt task to copy to docs/dist/css/
$ grunt dist-docs

Refresh the web browser to see the compiled changes. The navigation bar at the top of the document in the starter template should now be orange instead of black.

Upgrading the source

Since we are only modifying the variables.less file from the source, upgrading is not that difficult. Using Git, merge the newer Bootstrap source master branch into the develop branch we created.

# checkout master branch
$ git checkout master

# get the latest bootstrap source
$ git pull origin master

# checkout develop
$ git checkout develop

# merge master into it
$ git merge master