DerbyJS: Introducing the onrender method render event and its possibilities (Updated!)

22nd of June, 2012 04:02 AM

Although not yet accepted into the master branch of Derby on GitHub, I am confident the onrender method will make it in. I have (very) briefly discussed the issue with Nate (one of two creators) and tried various solutions, gone through their positive and negative sides and I cannot for my life see how a different implementation could be advantageous over the onrender method, without trying to be too much.

Pre-requisites: Basic understanding of Derby and jQuery.

Update: As of 22nd of June, Nate implemented and made a commit to fire a render event upon rendering. It will end up (I assume) in the next version of Derby, meaning 0.3.11. If you can't wait, you can always grab Derby straight from the master branch which contains this fix. I have updated this post to reflect this.

What's the issue?

Using the current version of DerbyJS for development, there is a problem with controlling rendering. Whenever rendering is done, you have no possibility to do any action without having to worry about whether the page has actually been updated already or are still in the midst of it. Below you can find a simplified example of when this can become tedious - I am sure there are a thousand issues all stemming from the same problem.

app.ready(function (model) {
  
  // Bind something just for the sake of it
  $("#button").click(function() {
    
    // Push Derby to render a new page, which will make 
    // Derby go through appropriate routers, etcetera
    // This utilizes the same mechanisms as what is
    // happening whenever you click a link and Derby takes
    // over your click.
    view.history.push('/new-page/');
    
    // Won't work: What I want do after rendering
    $("#new-element-only-on-new-page") // This will return an empty jQuery object
      .css({width: $(window).width() + "px"});
    
  });
  
});
The onrender method render event

In order to cope with this problem, after doing some thorough investigation of the issue (you can find my monologue/rambling on the pertaining github issue), I decided to go ahead and implement an onrender method. I considered implementing an event that would fire upon rendering that the developer using Derby could catch as well as implement a full scale routing system based upon the existing routing system, but after completing. I did actually implement the full scale routing system into Derby, but once completed, I realized that it was overly complex, making it hard to develop further as well as to start develop with. I also realized it had its limitations, and that a much simpler, but yet as elegant, solution would suffice (the onrender method with some added sugar). The event firing solution... erhm... I did not like. Hard to control, a new pattern not seen earlier in Derby (Derby uses surprisingly few, if any, events like that) which would cause a lot of headache. However, later Nate introduced a render event which will be the preferred way to do it from here on forward.

How it works

So, the onrender method works almost exactly the same as similar to the ready method. It belongs to app (which you get after calling createApp) and takes in the same callback function with the same arguments. You are able to to bindings to app (should be done inside the ready method) and attach a callback, which gets The callback is executed right after the ready callback, but also upon any re-rendering. This is quite important to think about - any re-rendering means any time you call page.render from within a router's callback and that router's callback is being executed (due to a match). To sum it up, this callback will fire on any transitional routing back and forth, any normal routing made by clicks, by calling view.history.push, on initialization of your app (and hence, any other normal HTTP requests) and any other way you would be able to trigger the routes mechanisms of Derby. It could be used like this: (note there is an additional way of using the render event; together with namespaces, see Nate's comment on his commit)

app.onrender(function (model) {
  
  alert('This will pop up on any rendering!');
  
});

// Where the ctx parameter is the context object one can send in as a parameter to page.render.
app.on('render', function(ctx) {
  
  alert('This will pop up on any rendering!');
  
});
What to think about when implementing

First of all, my commit has not yet been pushed into the master branch - and much less into a new version. You can find the version of Derby with the onrender method included on my github pages here. Let's hope Nate includes it ASAP into the main branch and updates the version so we can grab it through npm. My version is based upon 0.3.9 including any fixes Nate's done (and actually it looks like he's been quite busy), since up until the 20th of June.

Second of all, the onrender callback should not really contain any rendering of contents. Why? Well, rendering of content belongs in the normal routes and the templates. This way, we will be able to produce the same (or similar) results whether we render first on the server and then send it to the browser/client, or whether we render it purely through browser/client-side processed routes. Note that this is possible, but very much not advisable.

Some sugar: Lightpage

Alright, so going through this process, I started thinking how cool it would be to actually have a proper routing system for browser/client-specific code. I know I have been thinking of a good way to handle what code to execute on what page before, and soon I discovered page.js, made by TJ Holowaychuk who also makes Express. Then it struck me, most of this is already implemented in Derby and what we really need is only the basics, a way to better structure our code based upon routes (in other words, call only certain parts of our code based upon paths matching the current location). Also, to use page.js as is, it would most likely break DerbyJS as they both try to take over any clicks and such. After all, Derby already has a way to do routing browser/client -side (really, that's a major part of the Derby's offering). But, Derby's existing system doesn't allow us to do browser/client -side coding in an efficient, non-hacky manner.

So, I stripped page.js and tuned it to better fit into the Derby style of development, and, voilà(!), lightpage! Basically what lightpages allow you to do is specify routes, which you can process (only) by "manually" invoking a process method. You can read more about how it works in detail on the github pages of lightpage (I also suggest taking a good look again at page.js which really is a nice piece of work).

How to use lightpage with Derby

As a coincidence, (erhm, or not...) it happens to work extremely well with the onrender method render event and Derby! All you have to do is to install it as any other package available in the npm, for example by specifying "lightpage": "*" as a dependency of your app in your package.json file and then do npm install in your app's directory. Then, to use it, you need to require it at the top and then you are ready to go! Note that any routes should be defined in the ready method callback to avoid A: server-side setup of the routes (which will most likely break your app completely) and B: the routes being setup more than once (which will make your callbacks fire more than once... not good). Also, I suggest putting the processing/triggering of the routes in the onrender method render event callback, as they will then be triggered/called on every render, which most likely is your intended behavior. This might sound complicated, but it really is quite easy (this is an example that would fit well with the bundled app that comes whenever you create a new Derby project):

require('lightpage');

... 

// Normal routes and what not

...

app.ready(function (model) {
  
  // Set up your routes, just like normal Express routes.
  // The difference lies in that you cannot specify get/post/...
  // since this is not always known to the browser/client
  // (for example a POST to the server).
  // Also, much like Derby, a params parameter instead of a 
  // context parameter is provided to the callback. Since 
  // app.ready already have the model variable, you could use
  // that one as you would in any other place in your app. 
  // No page parameter is provided since this is not really
  // a relevant place for such a parameter.
  page('/:roomName?', function ( params, next ) {
    
    alert('Just rendered a room with name: ' + params.roomName);
    
  });
  
  app.on('render', function(ctx) {
      
      // Trigger processing of lightpage routes on every render
      page.process(location.pathname)
      
    });
  
});

app.onrender(function (model) {  
    
    // Trigger processing of lightpage routes on every render
    page.process(location.pathname);
  
  });
End notes

These two additions to Derby should severely simplify and better structure any browser/client -specific code you may have/need. If you stumble upon any issues I try to be as much available as possible on the Derby IRC channel on freenode and I am constantly checking the Derby Google group. And of course, feel free to share and comment here and I will answer as soon as I can! I am always happy to help!

For anyone wondering about simpler tutorials how to use and get started with Derby, they will come! Stay tuned!

The Weblog

Carl-Johan Blomqvist