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

Watchify

Having to manually run the gulp bundle task after saving source code edits is how the gulp workflow from the previous page is setup. The following changes to the gulpfile will add watchify to the gulp workflow and improve efficiency.

Install watchify and lodash.assign

# dependencies for watchify gulp js task

npm install watchify --save-dev
npm install lodash.assign --save-dev

This updated gulpfile includes watchify for persistent browserify bundling that watches files for changes and only rebuilds what it needs to. To avoid EADDRINUSE errors, the gulp webserver is being set to use port 3000.

gulpfile.js
'use strict';

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

// custom browserify options
var customOpts = {
    entries: ['./src/js/index.js'],
    debug: true
};
var opts = assign({}, watchify.args, customOpts);
var b = watchify(browserify(opts));

gulp.task('js', bundle); // run `gulp js` to build the file
b.on('update', bundle); // on any dep update, runs the bundler
b.on('log', gutil.log); // output build logs to terminal

function bundle() {
    return b.bundle()
    // log errors if they happen
    .on('error', gutil.log.bind(gutil, 'Browserify Error'))
    .pipe(source('bundle.js'))
    // optional, remove if you don't need to buffer file contents
    .pipe(buffer())
    // optional, remove if you dont want sourcemaps
    .pipe(sourcemaps.init({loadMaps: true})) // loads map from browserify file
    // Add transformation tasks to the pipeline here.
    .pipe(uglify()) // minify
    .pipe(sourcemaps.write('./')) // writes .map file
    .pipe(gulp.dest('./js'));
}

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

gulp.task('default', ['js', 'server']);
Source Code

Resources


Part 2 of 3 in the Google Maps API series.

Google Maps API RequireJS Module | Google Maps API with Webpack

comments powered by Disqus