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

BrowserSync SSI Recipe

BrowserSync is my Node.js web browser app development server of choice. This gulpfile is a work in progress to address support for server side includes when using a BrowserSync static server.

gulpfile.js
var gulp = require('gulp');
var browserSync = require('browser-sync').create();
var minimist = require('minimist');

var serverConfig = {
    site: './src',
    directory: false,
    host: 'localhost',
    ssi: '.inc',
    index: 'index.html'
}

// from https://github.com/gulpjs/gulp/blob/master/docs/recipes/pass-arguments-from-cli.md
// seems to make sense since gulp itself uses minimist.
var optionsConfig = {
    string: 'host',
    default: { host: process.env.NODE_ENV || serverConfig.host }
};
var options = minimist(process.argv.slice(2), optionsConfig);

// Static server
gulp.task('server', function() {
    browserSync.init({
        server: {
            baseDir: serverConfig.site,
            directory: serverConfig.directory,
            index: serverConfig.index
        }
    });
});

// BrowserSync static server with ssi support
// https://www.npmjs.com/package/ssi#supported-instructions
// ------------------------------------------------
// Known issues:
//   virtual include directives to an absolute URL path
//   do not work as expected. For example, given this file context,
//   http://localhost:3000/subdir/index.html
//     works:
//     <!--#include virtual="../incl/head.inc" -->
//     does NOT work correctly, include directives in this include may not be found:
//     <!--#include virtual="/incl/head.inc" -->
//
//     workaround: use gulp server-proxy task
// ------------------------------------------------
function ssiMiddleware (req, res, next) {

    var fs  = require('fs');
    var path = require('path');
    var ssi = require("ssi");
    var url = require('url');

    var pathname = url.parse(req.originalUrl || req.url).pathname;
    var filename = path.join(serverConfig.site, pathname.substr(-1) === '/' ? pathname + serverConfig.index : pathname);

    if (fs.existsSync(filename)) {

        var matcher = '/**/*' + serverConfig.ssi;
        var parser = new ssi(serverConfig.site, serverConfig.site, matcher);

        var contents = parser.parse(filename, fs.readFileSync(filename, {
            encoding: 'utf8'
        })).contents;

        res.writeHead(200, {
            'Content-Type': 'text/html'
        });
        res.end(contents);

        browserSync.emitter.on('service:running', function(data) {
            if (!snippet) {
                snippet = data.options.get('snippet');
            }
            inject();
        });
    }
    else {
        next();
    }
}
gulp.task('server-ssi', function() {
    browserSync.init({
        server: {
            baseDir: serverConfig.site,
            directory: serverConfig.directory,
            index: serverConfig.index
        },
        middleware: ssiMiddleware
    });
});

// Proxy
// http://www.browsersync.io/docs/options/#option-proxy
// useful if apache needed for .htaccess, ssi, rewrite, etc.
// usage: $ gulp server-proxy --host [host]
// e.g.,  $ gulp server-proxy --host local.dev
gulp.task('server-proxy', function() {
    browserSync.init({
        proxy: options.host
    });
});
Required Modules

$ gulp server-ssi

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

HTML 5 Template

Template with html5.js shiv *
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta http-equiv="x-ua-compatible" content="ie=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>My Super Site!</title>
  <meta name="description" content="My Super Site!">
  <meta name="author" content="My Name">

  <link rel="stylesheet" href="css/styles.css">

  <!--[if lt IE 9]>
  <script src="http://html5shiv.googlecode.com/svn/trunk/html5.js"></script>
  <![endif]-->
</head>
<body>

</body>
</html>
* Inspired by HTML5 Boilerplate
Emmet

If the Emmet plugin is installed in your code editor, you can use the ! alias or html:5 abbreviation then tab to stub out the template.


Easy Browsersync Server

Easy Browsersync Server is an HTML 5 Template that comes with a NPM package config to install a minimal set of development tools including a Browsersync server and Gulp task runner. After installation, a simple gulp server command loads the html 5 template in a browser using Browsersync. Any edits made to the html or sass (scss) files are processed instantly and reloaded into the browser.

source code

  1. Requires Node.js

  2. Download or clone this repsoitory.

  3. Change directory to /uiCookbook/easybs

  4. Run npm install from a bash terminal or command prompt. This will read the package.json file and install the dependencies into the easybs project.

$ npm install
  • Finally, run gulp server to load the index.html file in a web browser
$ gulp server

If you wish to include normalize.scss in your sass build, run this task:

$ gulp cp

Then uncomment the normalize import statement at the top of /src/sass/base/_base.scss

Additional reading:


Easy Web Server

If you don’t need all the overhead of the Browsersync server, this Gulp webserver plugin with LiveReload is a good alternative. The uiCookbook – gulp-webserver branch contains an example to get you started.

source code

Or static http-server that can run from any project folder, npm install -g http-server