Attaching Callbacks to Hashchanges, A Javascript Templating Scheme
If you’ve spun your wheels in front end for a while, you may have found grinding out CRUD after CRUD UI gets a little… repetitive sometimes, even if you’re using you your favorite framework (vue, angular, react) coding javascript can become pretty… unremarkable. So it’s kind of exciting when you develop a technique on your own, I’ve decided to take a break from work to share one of mine.
On a personal project, I’m building a prototype UI in plain Javascript/JQuery, with the hopes of a smooth integration later into PhoneGap/native mobile integration. The interface features a template loader triggered by hash change navigation. Lets dive in.
The Setup
The first thing we need is to tell the page to do something when the hash changes, so lets register a function with the hash change event.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | /* * Create an app object */ var app = new App({}); /* * Register some default behaviors */ $(document).ready(function(){ // Get hash value var hash = window.location.hash.substr(1); $(window).on('hashchange', function() { $("#body-container").html("loading..."); app.load(window.location.hash.substr(1)); }); // Load default app.load(window.location.hash.substr(1)); app.init(); }); |
That’s pretty straight forward. The idea behind hash navigation is that the page and template is completely reactive to changes to the URL, just as a traditional, stateless GET request would be. This is a fairly elegant strategy if it’s enforced. Unfortunately it’s very easy to trip up and create a function that changes something about the template, risking the hash and template becoming out of sync.
Next we need to create a function that changes the template by manipulating the hash.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | ... this.navigate = function(ui, params) { // Detect params if(typeof(params) != "object") { params={}; } // Params from args var args = ""; if(!$.isEmptyObject(params)) { args = "&"+$.param(params); } window.location.hash = 'v='+ui+args; }; ... |
To call a template in the app, I can now make a call like app.navigate("profile",{"user":3});
This will result in setting a hash value of my.url.com/#v=profile&user=3. A familiar pattern for web connoisseurs everywhere! Now comes the reactive part, we’ve told the page to call an app.load function when the hash changes, so let’s define it
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | this.load = function(hash) { // Close all modals $('.modal').not($(this)).each(function () { $(this).modal('hide'); }); // Split our uri into a PHP like array self=this; var ret = hash.split('&').reduce(function(ret,item) { var parts = item.split('='); self.getArray[parts[0]] = parts[1]; },{}); if(!self.getArray['v']) { return; } // Determine the UI from the hash var ui = this.getArray['v']; if(typeof(ui) == "undefined") { ui = "404"; // Programmer defined } // Load our corresponding HTML file $("#body-container").html("loading "+ui); $("<div></div>").load(ui+".html", function(){ // Load it $("#body-container").html($(this).html()); }); }; |
Now we have a simple templating framework, that allows us to load html file content into our page container by only changing the hash value in the URL. Simple but elegant.
The Problem
Now we can successfully navigate between templates in our app, but there’s a problem. I need to attach behavior to my app once the template changes. Specifically I’d like to define a function – anonymous or not – to execute specific logic once we navigate. So using navigate with a callback? Yeah that’d be extra nice. Lets start by giving navigate() the ability to accept and stash a callback:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | ... this.navigate = function(ui, params, cb) { // Detect params if(typeof(params) != "object") { params={}; } // Detect callback and register it if(typeof(cb) == "function") { var funcId = "fn"+generateId(32); // generateId(32) returns a random len(32) varchar this.callbacks[funcId] = cb; params["_cb"]= funcId; } // Params from args var args = ""; if(!$.isEmptyObject(params)) { args = "&"+$.param(params); } window.location.hash = 'v='+ui+args; }; ... |
No sorcery here, we are generating a function name for our callback and stashing it as a reference to a callback function in a local data structure (this.callbacks). Keeping in mind that that data structure could as easily be window.callbacks if your function definitions are not as object-y. Then we add the reference to our url via params.
Lastly lets handle the sweet, sweet callback:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | this.load = function(hash) { // Close all modals $('.modal').not($(this)).each(function () { $(this).modal('hide'); }); // Split our uri into a PHP like array self=this; var ret = hash.split('&').reduce(function(ret,item) { var parts = item.split('='); self.getArray[parts[0]] = parts[1]; },{}); if(!self.getArray['v']) { return; } // Determine the UI from the hash var ui = this.getArray['v']; if(typeof(ui) == "undefined") { ui = "404"; // Programmer defined } // Load our corresponding HTML file $("#body-container").html("loading "+ui); $("<div></div>").load(ui+".html", function(){ // Load it $("#body-container").html($(this).html()); // Run a callback if we have one var cb = self.getArray['_cb']; if(typeof(self.callbacks[cb]) == "function") { self.callbacks[cb](); delete self.callbacks[cb]; } var obj = self; }); }; |
Only a tiny snippet of code needs to be added, in a couple lines we see if the callback parameter has been passed, if so we reach into our data structure to execute and remove it.
And there you have it. A simple templating technique and magical page jumping function callbacks.
0 Comments