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
- Node.js which includes NPM.
- Browserify
- Gulp
- Command shell such as terminal, Cygwin or Windows Command prompt.
- Code Editor - I used VS Code to code this example and run Gulp tasks.
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
- modules
- js
- gulpfile.js
- index.html
- css
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
- Gulp - getting started docs
- https://github.com/substack/browserify-handbook
- https://www.npmjs.com/package/load-google-maps-api
- Fast browserify builds with watchify
Part 2 of 3 in the Google Maps API series.
Google Maps API RequireJS Module | Google Maps API with Webpack