Thursday, June 13, 2013

PushState, RequireCSS and RequestResponse

Today was the day of lots of new things. As I mentioned yesterday, I was in the market for something that will help me load my CSS.

While I agree that requirejs is a great tool to manage js dependencies, lately I am feeling that it leaves the job half done. A modular piece of JS, in many cases, will come with accompanying CSS and html templates. For example, almost every jquery plugin has a JS and a CSS file. Requirejs provides no API for declaring dependency on a whole module.

One way to achieve this with rjs is as following. First find a rjs plugin that loads html templates and css. Then write a wrapper module declaring dependency on both js and css. Now use this wrapper module wherever you needed the original. But this means writing a wrapper for every plugin or library you want to use. Given that rjs wants one module per file, this can mean a lot of files! One way of working around that is:

define('external', [], function() {
    var exports = {};
    exports.plugin1 = {
      ...
    };

    exports.plugin2 = {
      ...
    };

    return exports;
});
Then we can add a dependency on 'external' and use the required module. But now every external module will be loaded as a dependency for your module which is again not very palatable.

Loading CSS using RequireJS

For loading CSS, there are some plugins available. The primary issue is that a lot of modern browsers seem to be lacking a onload event on link elements. This means while it is easy to add a link element in the dom and thus load the CSS, there is no way to know when the sheet has loaded. One workaround is to keep polling the dom stylesheets to see if the new sheet has come in. The other approach is more clever. You create a script tag and use the css url as src. Browser loads the css and tries to interpret as a script and throws an error. So the onerror event on script tag can be treated as the onload for the css file.

There are bunch of drawbacks. One is that onerror gets called for 404 errors also. This means that the sheet was not found. This can be overcome with checking for the presence of stylesheet in the dom.

The other issue is that invalid script tag throws errors in the console. The workaround for that is to use img tag instead of script tag. With img tag, there is a non-threatening warning in the console and that's it.

There are couple of rjs plugins available that implement the above ideas: RequireCSS & CSSLoader. I am using a fork of RequireCSS that uses the img tag technique described above.

RequestResponse

RequestResponse is a simple object on which you can do two things:
  • set handlers for particular requests by name
  • make a particular request. The handler for the request will be executed and response returned.
Marionette applications come with a RequestResponse object on them. One use I am making of them is to make a central datastore for my models and collections. All other parts of application request the data from this central place. Updates are also done in the datastore. Other components use events to indicate what needs to be done.

This means any changes to a model or collection are immediately propagated to all the views. It also helps in supporting both the option of getting data: through a bootstrap variable dumped by the server in initial file or through a fetch from server.

// datastore module

var store = {};
// The library for the current user
function getLibrary(){
 if(!store.library){
  if(typeof pubs === 'undefined'){
   store.library = new Library();
   store.library.fetch();
  }
  else{
   store.library = new Library(pubs);
  }
 }
 return store.library;
}

var reqres = new Backbone.Wreqr.RequestResponse();
reqres.setHandlers({
 "collection:library": getLibrary,
 "model:publication": getPublication
});

return reqres;


// using the datastore in a view
// Request the library collection to be displayed
this.collection = DataStore.request("collection:library");

This pattern is useful when trying to build support for pushstate in your application. In case of pushstate, the view can either be rendered directly from the server or it might be reached through some interaction on the frontend. If the view is being rendered on the server side, it makes sense to provide the bootstrap data along with the template thus avoiding a extra call for data fetch. On the other hand, if we reach the view through some frontend interactions, we have to fetch the data from server through an AJAX call. A central datastore encapsulates the logic of doing the right thing in both the cases.

No comments:

Post a Comment