Using A Basic JavaScript Module Pattern
In my last post I described a basic JavaScript module pattern and how you can use it to tidy up your global scope. But it is now time to make it real, yo. Example time! You can view the demo page here.
This is demo-ware but at least it beats a very abstract discussion. As these posts go on we will add more functionality but for now this will do. You have a form and when someone puts in data and hits the button, that data gets saved (into local storage) and displayed. You can view the source (it is, after all, client-side code) for the example at the url above.
I wrote the JavaScript code in two different ways. The first way, which you will find here, is something like how I would have written that script a few years ago. All the script is in one file and is just a mess of functions. If you have to assign a pattern name, you probably wouldn't go wrong with Big Ball of Mud.
The second way is actually running at the demo url. The scripts are broken up into three pieces. One of them is very small (base.js), one is about right (form-persistence.js) and the other is too big (form.js). This latter one we'll be improving on in the next post. Let's start our discussion with the first.
base.js
'use strict';
this.FormThingy = {};
$(document).ready(function () {
FormThingy.Form.init();
});
This script is simple. I am setting up my namespace and then, whenever the document is ready, I am calling init on the Form object to setup my behavior for the page.
form-persistence.js
'use strict';
(function (context) {
var persistence = {
save: function (obj) {
var peeps = this.serializePeeps();
peeps.push(obj);
localStorage.setItem('peeps', JSON.stringify(peeps));
},
get: function (obj) {
return this.serializePeeps();
},
remove: function (index) {
var peeps = this.serializePeeps();
peeps.splice(index, 1);
localStorage.setItem('peeps', JSON.stringify(peeps));
},
serializePeeps: function () {
return JSON.parse(localStorage.getItem('peeps')) || []
}
};
context.FormThingy.FormPersistence = persistence;
})(this);
Hey look, it's that module pattern. This is my module for doing persistence. It stores people in local storage as an array. As you can see above, you need to convert whatever you are going to store as strings, otherwise I would have had an array of [object Object] (the string value of my objects) stored in local storage, which would have been uncool. I am not using a database so I don't have an id to hang on to for deleting entries, so I just use the index in the array as the id.
form.js
This is the module that controls the primary logic of the page. It is too big, not because it has 69 lines but because it does too much. It contains the logic for both the form and the history display and I would like to separate them out. But that will be in the next post. Since this is a bit big, we will take it one piece at a time (you can see the whole file here).
'use strict';
(function (context, $, FormPersistence) {
var obj = {
//...
context.FormThingy.Form = obj;
})(this, jQuery, this.FormThingy.FormPersistence);
This is the module setup. The module depends on having a reference to the context (the window object), jQuery and the FormPersistence bits we just discussed.
elements: {},
init: function () {
this.attachElements();
this.template = _.template($('#historyTemplate').html());
this.updateHistory();
},
attachElements: function () {
this.elements.firstName = $('#firstName');
this.elements.lastName = $('#lastName');
this.elements.age = $('#age');
this.elements.button = $('#save');
this.elements.inputs = $('input[type=text]');
this.elements.history = $('#history');
this.elements.button.click($.proxy(this.save, this));
this.elements.history.on('click', 'button', function () {
obj.destoryHistoryItem($(this));
});
},
Here is how I setup modules that control page logic. In MV* patterns, you might view this as a controller. The init function is the entry point and mostly just calls other methods to setup things. I also go ahead and generate the template function for the underscore templating stuff for generating the history entries on the right side of the page.
The attachElements method sets up a hash lookup for the elements on the page. As a general rule, I don't want to have selectors scattered throughout my scripts. This gives me a central place to do this logic and the elements hash gives me an easy way to reference elements on the page. I also setup my event bindings so the buttons do what I want them to do.
If you are a user of the various MV* JavaScript frameworks, you may say "Hey, they have some nifty shortcuts for some of this in framework 'X'" and you are right, some do. They are very nice and do some of this kind of stuff for you.
save: function () {
var person = {};
person.age = this.elements.age.val();
person.firstName = this.elements.firstName.val();
person.lastName = this.elements.lastName.val();
FormPersistence.save(person);
this.updateHistory();
this.clearForm();
},
clearForm: function () {
this.elements.inputs.val('');
},
There are two form-related actions to do, saving and clearing. Clearing is super easy as you can see. Saving involves getting the values, putting them in a JavaScript object and sending things off to be persisted.
updateHistory: function () {
this.elements.history.html('');
var peeps = FormPersistence.get();
for (var i in peeps) {
var obj = peeps[i];
obj.id = i;
this.elements.history.append(this.template(obj));
}
this.elements.history.show();
},
destoryHistoryItem: function (buttonClicked) {
var id = buttonClicked.data('id');
FormPersistence.remove(id);
this.updateHistory();
}
And finally, the code for managing the history widget on the right. One function handles the need to update the view based on changes in the data, the other handles the need to delete an item from the data. Note that both depend on the FormPersistence object for the logic dealing with the persistence stuff.
What Did We Get From This?
If you compare the total number of lines of code between the Big Ball of Mud approach and the modularized approach, you will notice that the BBOM approach actually contains less code. This is because the module pattern requires just a bit of setup and the functionality didn't really change the between the two. But that is okay because we weren't doing this to have less lines of code but to make the code easer to read. For small JavaScript files, the difference between modular and non-modular will not make much difference in readability. But as the functionality grows, the old approach really does become a Big Ball of Mud but the modularized approach continues to break the code into manageable size chunks. So it's really about mental organization and patterns than lines of code.
Here is a corrollary. When I started using client-side MV* libraries (I spent most of my time with Backbone and Spine), I found that my line count barely differed as I compared the MV* versions of the scripts to their previous state. I suppose that was a little disappointing. But I realized that this didn't really matter. JavaScript isn't hard to maintain when there are large amounts of it; JavaScript is hard to maintain when it is hard to read, a rule that applies to languages in general. It's all about breaking things into understandable chunks and readability.
So What Now?
There are a number of things that still need to be discussed. What if we wanted to change out our persistence later? Since our persistence logic is in a separate module, we will later see what changes we need to make to make things easily swappable.
Right now our person object is just a plain old JavaScript object. It would be nice to have that as a "real" object with functionality attached. In this case a "fullName" function would be nice to have for the templating. Local storage stores things as strings, not as objects, so this means we would need to do a little work to make this happen.
How would we write tests for this? With some slight modifications, this code should be fairly easy to test. I have found that testing JavaScript can be a pretty awkward experience so we will see what it takes to make this as easy as possible. But that is a discussion for another time.
Next time we will create a new module for the history section on the right to separate it out from the form. As the functionality of both grow as we build this sample app, the separation of concerns will help us keep the code easier to maintain. We will also talk about using eventing to do the communication so we can decouple the widgets on the page.