Historically, state was managed server side with session cookies. So, whenever users navigated to a new page, the previous pageâs state was lostâonly the cookies persisted. JavaScript applications, however, are confined to a single page, which means we can now store state on the client's memory.
One of the major advantages to storing state on the client is a really responsive interface. A user gets immediate feedback when interacting with the page, rather than waiting a few seconds for the next page to load. Speed greatly improves the user experience, making many JavaScript applications a real pleasure to use.
However, storing state on the client causes challenges as well. Where exactly should it be stored? In local variables? Perhaps in the DOM? This is where a lot of developers get led astray, which is an unfortunate state of affairs because storing state properly is one of the most critical areas to get right.
First, you should avoid storing data or state in the DOM. Thatâs just a slippery slope leading to an entangled mess and anarchy! In our caseâsince weâre using the tried and tested MVC architectureâstate is stored inside our applicationâs controllers.
What exactly is a controller? Well, you can think of it as the glue between the applicationâs views and models. Itâs the only component aware of the applicationâs views and models, tying them together. When the page loads, your controller attaches event handlers to views and processes callbacks appropriately, interfacing with models as necessary.
You donât need any libraries to create controllers, although they can be useful. The only essential part is that controllers are modular and independent. Ideally, they shouldnât be defining any global variables, instead functioning as fairly decoupled components. An excellent way of ensuring this is with the module pattern.
The module pattern is a great way to encapsulate logic and prevent global namespace pollution. Itâs all made possible by anonymous functions, which are arguably the single best feature of JavaScript. Weâll just create an anonymous function and execute it immediately. All the code residing within the function runs inside a closure, providing a local and private environment for our applicationâs variables:
(function(){ /* ... */ })();
We have to surround the anonymous function with braces ()
before we can execute it. JavaScript requires this so it
can interpret the statement correctly.
Variable definitions inside the module are local, so they canât be accessed outside in the global namespace. However, the applicationâs global variables are all still available, and they can be readily accessed and manipulated inside the module. Itâs often not obvious which global variables are being used by a module, especially when your modules get larger.
In addition, implied globals are slower to resolve because the JavaScript interpreter has to walk up the scope chain to resolve them. Local variable access will always be faster and more efficient.
Luckily, our modules provide an easy way to resolve these problems. By passing globals as parameters to our anonymous function, we can import them into our code, which is both clearer and faster than implied globals:
(function($){ /* ... */ })(jQuery);
In the example above, weâre importing the global variable jQuery
into our module and aliasing it to
$
. Itâs obvious which global
variables are being accessed inside the module, and their lookup is
quicker. In fact, this is the recommended
practice whenever you want to use jQueryâs $
shortcut, which ensures that your code wonât conflict with any
other libraries.
We can use a similar technique when it comes to exporting
global variables. Ideally, you should be using as few global variables
as possible, but thereâs always the odd occasion when theyâre needed. We
can import the pageâs window
into our
module, setting properties on it directly, thereby exposing variables
globally:
(function($, exports){ exports.Foo = "wem"; })(jQuery, window); assertEqual( Foo, "wem" );
The fact that weâre using a variable called exports
to set any global variables means the
code is clearer, making it obvious which global variables a module is
creating.
Using a local context is a useful way of structuring
modules, especially when it comes to registering callbacks to events. As it stands, the context inside our module is globalâthis
is equal to window
:
(function(){ assertEqual( this, window ); })();
If we want to scope the context, we need to start adding functions onto an object. For example:
(function(){ var mod = {}; mod.contextFunction = function(){ assertEqual( this, mod ); }; mod.contextFunction(); })();
The context inside contextFunction()
is now local to our mod
object. We can start using this
without worrying about creating global
variables. To give you a better indication of how it would be used in
practice, letâs further flesh out that example:
(function($){ var mod = {}; mod.load = function(func){ $($.proxy(func, this)); }; mod.load(function(){ this.view = $("#view"); }); mod.assetsClick = function(e){ // Process click }; mod.load(function(){ this.view.find(".assets").click( $.proxy(this.assetsClick, this) ); }); })(jQuery);
Weâre creating a load()
function
that takes a callback, executing it when the page has
loaded. Notice that weâre using jQuery.proxy()
to ensure that the callback is invoked in the correct
context.
Then, when the page loads, weâre adding a click handler onto an
element, giving it a local function, assetsClick()
, as a callback. Creating a
controller doesnât need to be any more complicated than that. Whatâs
important is that all of the controllerâs state is kept local and
encapsulated cleanly into a module.
Letâs abstract that library out so we can reuse it with
other modules and controllers. Weâll include the existing load()
function and add new ones like proxy()
and include()
:
(function($, exports){ var mod = function(includes){ if (includes) this.include(includes); }; mod.fn = mod.prototype; mod.fn.proxy = function(func){ return $.proxy(func, this); }; mod.fn.load = function(func){ $(this.proxy(func)); }; mod.fn.include = function(ob){ $.extend(this, ob); }; exports.Controller = mod; })(jQuery, window);
proxy()
ensures that functions
are executed in the local context, which is a useful pattern for event
callbacks. The include()
function is
just a shortcut for adding properties onto the controller, saving some
typing.
Weâre adding our library to the exports
object, exposing it as the global
Controller
variable. Inside the
module we can instantiate a Controller
object using its constructor
function. Letâs go through a simple example that toggles an elementâs
class depending on whether the mouse is over the element:
(function($, Controller){ var mod = new Controller; mod.toggleClass = function(e){ this.view.toggleClass("over", e.data); }; mod.load(function(){ this.view = $("#view"); this.view.mouseover(this.proxy(this.toggleClass), true); this.view.mouseout(this.proxy(this.toggleClass), false); }); })(jQuery, Controller);
When the page loads, weâre creating a view
variable and attaching some event
listeners. They in turn call toggleClass()
when the mouse moves over the element, toggling the
elementâs class. You can see the full example in this bookâs
accompanying files, in
assets/ch04/modules.html.
Granted, using context rather than local variables means there is
probably more code to write, what with all the usage of this
. However, the technique gives us much
greater scope for reusing code and including mixins. For example, we
could add a function onto every Controller
instance by setting a property on
its prototype
:
Controller.fn.unload = function(func){ jQuery(window).bind("unload", this.proxy(func)); };
Or, we could extend an individual controller by using the include()
function we defined earlier, passing
it an object:
var mod = new Controller; mod.include(StateMachine);
The StateMachine
object, in
this example, could be reused over and over again with our other
modules, preventing us from duplicating code and keeping things DRY
(donât repeat yourself).
As it stands, some parts of our controllers are being loaded before the DOM, and other parts are in callbacks to be invoked after the pageâs document has loaded. This can be confusing because the controllerâs logic is being executed under different states, resulting in a lot of document load callbacks.
We can solve this in one fell swoop by loading controllers after the DOM. I personally advocate this approach because it ensures that you donât need to think constantly about what state the pageâs DOM is in when accessing elements.
Letâs first take advantage and clear up our library, making our
controllers a bit cleaner. The Controller
class doesnât need to be a
constructor function because the context switch needed when generating
subcontrollers is unnecessary here:
// Use global context, rather than the window // object, to create global variables var exports = this; (function($){ var mod = {}; mod.create = function(includes){ var result = function(){ this.init.apply(this, arguments); }; result.fn = result.prototype; result.fn.init = function(){}; result.proxy = function(func){ return $.proxy(func, this); }; result.fn.proxy = result.proxy; result.include = function(ob){ $.extend(this.fn, ob); }; result.extend = function(ob){ $.extend(this, ob); }; if (includes) result.include(includes) return result; }; exports.Controller = mod; })(jQuery);
Now we can use our new Controller.create()
function to create
controllers, passing in an object literal of instance properties. Notice
that the entire controller is wrapped in jQuery(function(){ /* ... */ })
. This is an
alias for jQuery.ready()
, and it
ensures that the controller is loaded only after the pageâs DOM has
fully initialized:
jQuery(function($){ var ToggleView = Controller.create({ init: function(view){ this.view = $(view); this.view.mouseover(this.proxy(this.toggleClass), true); this.view.mouseout(this.proxy(this.toggleClass), false); }, this.toggleClass: function(e){ this.view.toggleClass("over", e.data); } }); // Instantiate controller, calling init() new ToggleView("#view"); });
The other significant change weâve made is passing in the view element to the controller upon instantiation, rather than hardcoding it inside. This is an important refinement because it means we can start reusing controllers with different elements, keeping code repetition to a minimum.
A common pattern is to have one controller per view. That view has an ID, so it can be passed to controllers easily. Elements inside the view then use classes, rather than IDs, so they donât conflict with elements in other views. This pattern provides a good structure for a general practice, but it should not be conformed to rigidly.
So far in this chapter weâve been accessing views by using the
jQuery()
selector, storing a local reference to the view inside the
controller. Subsequent searches for elements inside the view are then
scoped by that view reference, speeding up their lookup:
// ... init: function(view){ this.view = $(view); this.form = this.view.find("form"); }
However, it does mean that controllers fill up with a lot of selectors, requiring us to query the DOM constantly. We can clean this up somewhat by having one place in the controller where selectors are mapped to variables names, like so:
elements: { "form.searchForm": "searchForm", "form input[type=text]": "searchInput" }
This ensures that the variables this.searchForm
and this.searchInput
will be created on the
controller when itâs instantiated, set to their respective elements.
These are normal jQuery objects, so we can manipulate them as usual,
setting event handlers and fetching attributes.
Letâs implement support for that elements
mapping inside our controllers, iterating over all the selectors
and setting local variables. Weâll do this inside our init()
function, which is called when our controller is
instantiated:
var exports = this; jQuery(function($){ exports.SearchView = Controller.create({ // Map of selectors to local variable names elements: { "input[type=search]": "searchInput", "form": "searchForm" }, // Called upon instantiation init: function(element){ this.el = $(element); this.refreshElements(); this.searchForm.submit(this.proxy(this.search)); }, search: function(){ console.log("Searching:", this.searchInput.val()); }, // Private $: function(selector){ // An `el` property is required, and scopes the query return $(selector, this.el); }, // Set up the local variables refreshElements: function(){ for (var key in this.elements) { this[this.elements[key]] = this.$(key); } } }); new SearchView("#users"); });
refreshElements()
expects every
controller to have a current element property, el
, which will scope any selectors. Once
refreshElements()
is called, the
this.searchForm
and this.searchInput
properties will be set on the
controller and are subsequently available for event binding and DOM
manipulation.
You can see a full example of this in this bookâs accompanying files, in assets/ch04/views.html.
We can also take a stab at cleaning up all that event
binding and proxying by having an events
object that
maps event types and selectors to callbacks. This is going to be very
similar to the elements
object, but
instead will take the following form:
events: { "submit form": "submit" }
Letâs go ahead and add that to our SearchView
controller. Like refreshElements()
, weâll have a delegateEvents()
function that will be called
when the controller is instantiated. This will parse the controllerâs
events
object, attaching event
callbacks. In our SearchView
example,
we want the search()
function to be
invoked whenever the viewâs <form />
is submitted:
var exports = this; jQuery(function($){ exports.SearchView = Controller.create({ // Map all the event names, // selectors, and callbacks events: { "submit form": "search" }, init: function(){ // ... this.delegateEvents(); }, search: function(e){ /* ... */ }, // Private // Split on the first space eventSplitter: /^(\w+)\s*(.*)$/, delegateEvents: function(){ for (var key in this.events) { var methodName = this.events[key]; var method = this.proxy(this[methodName]); var match = key.match(this.eventSplitter); var eventName = match[1], selector = match[2]; if (selector === '') { this.el.bind(eventName, method); } else { this.el.delegate(selector, eventName, method); } } } });
Notice weâre using the delegate()
function inside delegateEvents()
, as well as the bind()
function. If the event selector isnât provided, the event will be
placed straight on el
. Otherwise, the
event will be delegated, and it will be
triggered if the event type is fired on a child matching the selector.
The advantage of delegation is that it often reduces the amount of event
listeners requiredâi.e., listeners
donât have to be placed on every element selected because events are
caught dynamically when they bubble up.
We can push all those controller enhancements upstream to our
Controller
library so they can be
reused in every controller. Hereâs the finished example; you can find
the full controller library in
assets/ch04/finished_controller.html:
var exports = this; jQuery(function($){ exports.SearchView = Controller.create({ elements: { "input[type=search]": "searchInput", "form": "searchForm" }, events: { "submit form": "search" }, init: function(){ /* ... */ }, search: function(){ alert("Searching: " + this.searchInput.val()); return false; }, }); new SearchView({el: "#users"}); });
State machinesâor to use their proper term, Finite State Machines (FSMs)âare a great way to program UIs. Using state machines, you can easily manage multiple controllers, showing and hiding views as necessary. So, what exactly is a state machine? At its core, a state machine consists of two things: states and transitions. It has only one active state, but it has a multitude of passive states. When the active state switches, transitions between the states are called.
How does this work in practice? Well, consider having a few application views that need to be displayed independentlyâsay, a view for showing contacts and a view for editing contacts. These two views need to be displayed exclusivelyâwhen one is shown, the other view needs to be hidden. This is a perfect scenario to introduce a state machine because it will ensure that only one view is active at any given time. Indeed, if we want to add additional views, such as a settings view, using a state machine makes this trivial.
Letâs flesh out a practical example that will give you a good idea
of how state machines can be implemented. The example is simple and
doesnât cater to different transition types, but it is sufficient for our
needs. First, weâre going to create an Events
object that will use jQueryâs event API
(as discussed in Chapter 2) to add the ability
to bind and trigger events on our state machine:
var Events = { bind: function(){ if ( !this.o ) this.o = $({}); this.o.bind.apply(this.o, arguments); }, trigger: function(){ if ( !this.o ) this.o = $({}); this.o.trigger.apply(this.o, arguments); } };
The Events
object is essentially
extending jQueryâs existing event support outside the DOM so that we can
use it in our own library. Now letâs set about creating the StateMachine
class, which will have one main
function, add(
)
:
var StateMachine = function(){}; StateMachine.fn = StateMachine.prototype; // Add event binding/triggering $.extend(StateMachine.fn, Events); StateMachine.fn.add = function(controller){ this.bind("change", function(e, current){ if (controller == current) controller.activate(); else controller.deactivate(); }); controller.active = $.proxy(function(){ this.trigger("change", controller); }, this); };
The state machineâs add()
function adds the passed controller to the list of states and creates an
active()
function. When active()
is called, the active state will
transition to the controller. The state machine will call activate()
on the active controller and deactivate()
on all the other controllers. We
can see how this works by creating two example controllers, adding them to
the state machine, and then activating one of them:
var con1 = { activate: function(){ /* ... */ }, deactivate: function(){ /* ... */ } }; var con2 = { activate: function(){ /* ... */ }, deactivate: function(){ /* ... */ } }; // Create a new StateMachine and add states var sm = new StateMachine; sm.add(con1); sm.add(con2); // Activate first state con1.active();
The state machineâs add()
function works by creating a callback for the change
event, calling the activate()
or deactivate()
function, depending on which is appropriate. Although the
state machine gives us an active()
function, we can also change the state by manually triggering the
change event:
sm.trigger("change", con2);
Inside our controllerâs activate()
function, we can set up and display
its view, adding and showing elements. Likewise, inside the deactivate()
function, we can tear down anything
that is hiding the view. CSS classes offer a good way of hiding and
showing views. Simply add a classâsay, .active
âwhen the view is active, and remove it
upon deactivation:
var con1 = { activate: function(){ $("#con1").addClass("active"); }, deactivate: function(){ $("#con1").removeClass("active"); } }; var con2 = { activate: function(){ $("#con2").addClass("active"); }, deactivate: function(){ $("#con2").removeClass("active"); } };
Then, in your stylesheets, make sure that the views have a .active
class; otherwise, theyâre hidden:
#con1, #con2 { display: none; } #con1.active, #con2.active { display: block; }
You can see the full examples in assets/ch04/state_machine.html.
Our application is now running from a single page, which means its URL wonât change. This is a problem for our users because theyâre accustomed to having a unique URL for a resource on the Web. Additionally, people are used to navigating the Web with the browserâs back and forward buttons.
To resolve this, we want to tie the applicationâs state to the URL. When the applicationâs state changes, so will the URL. The reverse is true, tooâwhen the URL changes, so will the applicationâs state. During the initial page load, weâll check the URL and set up the applicationâs initial state.
However, the pageâs base URL canât be changed without
triggering a page refresh, which is something weâre trying to avoid.
Luckily, there are a few solutions. The traditional way to manipulate
the URL was to change its hash. The hash is never sent to the server, so
it can be changed without triggering a page request. For example, hereâs
the URL for my Twitter page, the hash being #!/maccman
:
http://twitter.com/#!/maccman
You can retrieve and alter the pageâs hash using the location
object:
// Set the hash window.location.hash = "foo"; assertEqual( window.location.hash , "#foo" ); // Strip "#" var hashValue = window.location.hash.slice(1); assertEqual( hashValue, "foo" );
If the URL doesnât have a hash,
location.hash
is an empty string. Otherwise,
location
.
hash
equals the URLâs hash fragment, prefixed with the #
character.
Setting the hash too often can really hurt performance, especially on mobile browsers. So, if youâre setting it frequentlyâsay, as a user scrolls through a listâyou may want to consider throttling.
Historically, changes to the hash were detected rather
crudely with a polling timer. Things are improving, though, and modern
browsers support the hashchange event. This is fired on the window
, and you can listen for it in order to
catch changes to the hash:
window.addEventListener("hashchange", function(){ /* ... */ }, false);
Or with jQuery:
$(window).bind("hashchange", function(event){ // hash changed, change state });
When the hashchange
event fires, we can make
sure the application is in the appropriate state. The event has good
cross-browser support, with implementations in all the latest versions
of the major browsers:
IE >= 8
Firefox >= 3.6
Chrome
Safari >= 5
Opera >= 10.6
The event isnât fired on older browsers; however, thereâs a useful jQuery plug-in that adds the hashchange event to legacy browsers.
Itâs worth noting that this event isnât fired when the page initially loads, only when the hash changes. If youâre using hash routing in your application, you may want to fire the event manually on page load:
jQuery(function(){ var hashValue = location.hash.slice(1); if (hashValue) $(window).trigger("hashchange"); });
Because they donât execute JavaScript, search engine crawlers canât see any content thatâs created dynamically. Additionally, none of our hash routes will be indexed; as in the eyes of the crawlers, theyâre all the same URLâthe hash fragment is never sent to the server.
This is obviously a problem if we want our pure JavaScript applications to be indexable and available on search engines like Google. As a workaround, developers would create a âparallel universeâ of content. Crawlers would be sent to special static HTML snapshots of the content, while normal browsers would continue to use the dynamic JavaScript version of the application. This resulted in a lot more work for developers and entailed practices like browser sniffing, something best avoided. Luckily, Google has provided an alternative: the Ajax Crawling specification.
Letâs take a look at my Twitter profile address again (notice the exclamation mark after the hash):
http://twitter.com/#!/maccman
The exclamation mark signifies to Googleâs crawlers that our site conforms to the Ajax Crawling spec. Rather than request the URL as-isâexcluding the hash, of courseâthe crawler translates the URL into this:
http://twitter.com/?_escaped_fragment_=/maccman
The hash has been replaced with the _escaped_fragment_
URL parameter. In the
specification, this is called an ugly URL, and itâs
something users will never see. The crawler then goes ahead and fetches
that ugly URL. Since the hash fragment is now a URL parameter, your
server knows the specific resource the crawler is requestingâin this
case, my Twitter page.
The server can then map that ugly URL to whatever resource it represented and respond with a pure HTML or text fragment, which is then indexed. Since Twitter still has a static version of their site, they just redirect the crawler to that.
curl -v http://twitter.com/?_escaped_fragment_=/maccman 302 redirected to http://twitter.com/maccman
Because Twitter is using a temporary redirect (302) rather than a
permanent one (301), the URL shown
in the search results will typically be the hash addressâi.e., the
dynamic JavaScript version of the site (http://twitter.com/#!/maccman
). If you donât
have a static version of your site, just serve up a static HTML or text
fragment when URLs are requested with the _escaped_fragment_
parameter.
Once youâve added support for the Ajax Crawling spec to your site, you can check whether itâs working using the Fetch as Googlebot tool. If you choose not to implement the scheme on your site, pages will remain indexed as-is, with a good likelihood of not being properly represented in search results. In the long term, however, itâs likely that search engines like Google will add JavaScript support to their crawlers, making schemes like this one unnecessary.
The History API is part of the HTML5 spec and essentially allows you to replace the current location with an arbitrary URL. You can also choose whether to add the new URL to the browserâs history, giving your application âback buttonâ support. Like setting the locationâs hash, the key is that the page wonât reloadâits state will be preserved.
Supported browsers are:
Firefox >= 4.0
Safari >= 5.0
Chrome >= 7.0
IE: no support
Opera >= 11.5
The API is fairly straightforward, revolving mostly around the
history.pushState()
function. This takes three arguments: a data object, a title, and
the new URL:
// The data object is arbitrary and is passed with the popstate event var dataObject = { createdAt: '2011-10-10', author: 'donnamoss' }; var url = '/posts/new-url'; history.pushState(dataObject, document.title, url);
The three arguments are all optional, but they control whatâs pushed onto the browserâs history stack:
- The
data
object This is completely arbitraryâyou specify any custom object you want. Itâll be passed along with a popstate event (which weâll cover in depth later).
- The
title
argument This is currently ignored by a lot of browsers, but according to the spec will change the new pageâs title and appear in the browserâs history.
- The
url
argument This is a string specifying the URL to replace the browserâs current location. If itâs relative, the new URL is calculated relative to the current one, with the same domain, port, and protocol. Alternatively, you can specify an absolute URL, but for security reasons, itâs restricted to the same domain as the current location.
The issue with using the new History API in JavaScript
applications is that every URL needs a real HTML representation.
Although the browser wonât request the new URL when you call history.pushState()
, it will be requested if
the page is reloaded. In other words, every URL you pass to the API
needs to existâyou canât just make up fragments like you can with
hashes.
This isnât a problem if you already have a static HTML representation of your site, but it is if your application is pure JavaScript. One solution is to always serve up the JavaScript application regardless of the URL called. Unfortunately, this will break 404 (page not found) support, so every URL will return a successful response. The alternative is to actually do some server-side checking to make sure the URL and requested resource is valid before serving up the application.
The History API contains a few more features. history.replaceState()
acts exactly the same
as history.pushState()
, but it
doesnât add an entry to the history stack. You can navigate through the
browserâs history using the history.back(
)
and
h
istory.forward()
functions.
The popstate event mentioned earlier is triggered when the page is loaded or
when history.pushState()
is called.
In the case of the latter, the event
object will
contain a state
property that holds
the data object given to history.pushState()
:
window.addEventListener("popstate", function(event){ if (event.state) { // history.pushState() was called } });
You can listen to the event and ensure that your applicationâs state stays consistent with the URL. If youâre using jQuery, you need to bear in mind that the event is normalized. So, to access the state object, youâll need to access the original event:
$(window).bind("popstate", function(event){ event = event.originalEvent; if (event.state) { // history.pushState() was called } });
Get JavaScript Web Applications now with the O’Reilly learning platform.
O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.