Module Pattern

Javascript is my favorite

Javascript is by far my favorite language for web development. At one point in my career, I was banned from using it because someone might have it disabled in their browser. Fortunately javascript is now ubiquitous with browsers and web dev. One of the most useful patterns I've picked up over the years is the module pattern. For the longest time I couldn't figure out what it was called. I've since found a couple good articles here and here that do a nice job of explaining what it is. I'd like to supplement these articles with some information on when to use the pattern.

Let's start with a messy approach

Often times we'll include a single script tag or external file and it sort of looks like this:

function doSomthingWithMap() {
    //foo
}

function doSomethingNotMapRelated() {
    //bar
}

The above code works well and we live our merry lives with harmony as two methods aren't terribly bad to manage like this. But when it starts to turn into this:

//map stuffs
function getMap() {
    //return map from Google
}

function doSomthingWithMap() {
    //foo
}


//non map stuffs
function doSomethingNotMapRelated() {
    //bar
}

//no longer sure we're even using this in our code anymore
function doSomethingFuther() {
	
}

//jQuery stuffs
$(function(){
    $("a").click(function() {
        var map = getMap();
    });
});

We now have to separate what belongs together in our mind rather than nice logical structures that speak for themselves. If someone else needs to manage the code afterwards, they have to look through the noise to see what are event handlers vs maps vs whatever. We also run a big risk that someone else may include a function named the same as any of ours. If that happens, whomever's gets declared last will override the former. This is known as polluting the global namespace. Ideally we should separate our code into separate modules that are closely related. In doing so, we only put a few items in the global scope which reduces our risk of function name collision. When we're done, two modules can have methods with the same name without issue as long as the module names are unique. We can easily separate the concerns of our code by using the module pattern to create a service. Let's do a quick Javascript review in case the module pattern looks too bizarre:

//a 'normal' function declaration
function foo() {

}

//can also be rewritten as an anonymous function
// the right hand side of the assignment is known as the anonymous part (it has no name directly)
var foo = function() {

}

//invoking foo requires parenthesis
foo();

//we can pass foo as a parameter as long as you don't add parenthesis
function anotherFunction(foo) {
    foo();//then call it in here
}

//we can also create an anonymous function that calls itself
var bar = (function(){
   alert('this runs immediately');
})();

//we can pass a parameter in as well
var bar = (function(arg){
    alert(arg);//hello alerts
})('hello');

//functions can also be assigned to JSON properties
var myObject = {
    foo: function() {
        alert('foo-ing')
    },
    bar: function(arg) {
        alert(arg);
    }
}

myObject.foo(); //foo-ing
myObject.bar('hello'); //hello

So far we've only written vanilla javascript with no jQuery. If we put together all of the bits together we can create a service module, in this case we'll create a map service module:

//an easy example

var mapService = (function(){
    //private methods go here

    //this can only be called from one of the public functions
    var _aPrivateMethodName = function(arg) {
        return arg * 2;
    }

    //public methods here
    return {
        foo: function() {
            alert('foo-ing');
        },
        bar: function(arg) {
            alert(arg);
        },
        double: function(number) {
            return _aPrivateMethodName(number);
        }
    }
})();

//we can then do this
mapService.foo();//foo-ing
mapService.bar('hello');//hello
mapService.double(2);//4
mapService._aPrivateMethodName(2);//error!

Ok, so creating a service is pretty easy. I'd recommend that you separate your javascript depending on what it's supposed to do. I typically create an application.js file that is responsible for handling UI events then calls the appropriate service:

//include your services as script tags before this script

//using jQuery now
//on document ready
$(function(){
   $('a').click(function(){
       mapService.foo();
   });
   
   $('.bar').click(function(){
       //combine services
       var value = anotherService.getValue();
       mapService.bar(value);
   });
});

A real map service would look more like this:

var mapService = (function () {
    //private methods here
    
    //public methods in the return object
    return {
        map: null, //could have made this private
        markers: [], //could have made this private
        bounds: null, //could have made this private
        infoWindow: null, //could have made this private
        getCities: function (options, callback) { //gets marker data that returns information about students
            var url = "/someurl";

            $.ajax({
                url: url,
                type: 'POST',
                contentType: 'application/json', 
                data: {
                    options: options
                },
                success: function (data) {
                    //the server responds with our markers as the 'data' object, we then execute
                    //the function the user passed in with the marker data
                    callback(data);
                }
            });
        },
        initMap: function (elementId) {
            if (!elementId) {
                elementId = "map";
            }

            this.map = new google.maps.Map(document.getElementById(elementId), {
                center: { lat: 41.6746991, lng: -86.3459244 },
                zoom: 4,
                scrollwheel: false,
                navigationControl: false
            });
        },
        resize: function() {
            google.maps.event.trigger(mapService.map, 'resize');
        },
        addMarkers: function (cities) {
            this.clearMarkers();

            this.bounds = new google.maps.LatLngBounds();
            var hasMarker = false;

            for (var i in cities) {
                this.addMarker(cities[i]);
                hasMarker = true;
            }

            if (hasMarker) {
                this.map.fitBounds(this.bounds);

                //limit how far zoomed in the map goes
                if (this.map.getZoom() > 10) {
                    this.map.setZoom(6);
                }
            }
        },
        addMarker: function (city) {
            var latLng = new google.maps.LatLng(city.Lat, city.Lng);
            var marker = new google.maps.Marker({
                position: latLng,
                map: this.map,
                title: city.Name
            });

            var html = "<ul class='list-unstyled'>";

            for (var i in city.Students) {
                var student = city.Students[i];

                html += "<li><i class='fa fa-envelope'></i><a href='#' data-student-id='" + student.Id + "' data-student-name='" + student.FirstName + " " + student.LastName + "'>" + student.FirstName + " " + student.LastName + "</a></li>";
            }

            html += "</ul>";

            html = "<div class='student-info-window'>" + html + "</div>";

            google.maps.event.addListener(marker, "click", function () {
                if (mapService.infoWindow) {
                    mapService.infoWindow.close();
                }

                mapService.infoWindow = new google.maps.InfoWindow(
                {
                    content: html,
                    height: 200,
                    maxHeight: 200,
                    width: 400,
                    disableAutoPan: false
                });

                mapService.infoWindow.open(mapService.map, marker);

                setTimeout(function () {
                    mapService.registerInfoWindowButtonClicker();
                }, 500);
            });

            this.markers.push(marker);
            this.bounds.extend(latLng);
        },
        registerInfoWindowButtonClicker: function () {
            $(".student-info-window a").click(function (e) {
                contactStudentService.handleEmailButtonClick($(this), e);
            });
        },
        setMapOnAll: function (map) {
            for (var i = 0; i < this.markers.length; i++) {
                this.markers[i].setMap(map);
            }
        },
        clearMarkers: function () {
            this.setMapOnAll(null);
        },
        deleteMarkers: function () {
            this.clearMarkers();
            this.markers = [];
        },
        showMarkers: function () {
            this.setMapOnAll(map);
        },
        handleBoundsChange: function (bounds) {

        }
    }
})();

//then in my application.js
mapService.getCities(options, function(cities) {
    mapService.addMarkers(cities);
});

Be sure to include your services before you call them. I keep them in separate files to keep things nice an separated. As you can see above you can create multiple services and have one get used by another. You can even split your service into multiple files, check out this article if you need to do so.

Summary

To sum up, the module pattern cleans up your code and global scope considerably. If you keep your modules uncoupled and focused, you can easily re-use your modules across projects. Javascript keeps getting faster and more useful. If you haven't spent a lot of time using it, I would highly recommend beginning with this pattern.