Rendering views in Javascript

Rendering views in Javascript

 
This post was originally published on the New Bamboo blog, before New Bamboo joined thoughtbot in London.

This is possibly the hardest part of a Javascript MVC framework to standardise on. More than anywhere else, it is open to interpretation and differences of opinion. I will briefly run through a number of different options moving towards the kind of thing I like.
One of the key things to think about first when deciding on your javascript strategy is accessibility and SEO. This will determine whether you need the majority of your DOM to be rendered by the server, and whether you are going to present different parts of your application as different “pages”.
If you are going to build a traditional web site where doing things goes to different pages, you can then decide on how much you want to add “Degradable” enhancements into the mix.
I am going to focus on non-degradable functionality, and am tallking about the types of RIAs that would previously have been done in Flash. I’m a firm believer in the capabilities of using HTML 5 to build almost native feeling apps.

How to make the HTML?

With dynamic state changing in your client’s browser, these objects first need to be rendered, and then updated if they change.
The simplest thing that works is often something like this:
document.write("<p>"+ myObject.name+"</p>")
Since this is obviously too simple for a real application, the next evolution of this would be to put the new data in a specific place (using JQuery).
$('#my_list_of_objects').append("<p>"+ myObject.name+"</p>")
At this point it becomes necessary to choose an overall strategy for updating the UI and the choices can be classed as follows:
  • Simple string interpolation
  • Injecting fragments of HTML rendered by a server
  • Employing a templating language
  • Modifying and cloning existing parts of the DOM

Libraries

There are several libraries available for rendering JS objects as HTML, which I will cover briefly.

The clone/modify camp

There are several options:
These work by cloning a fragment of HTML you have rendered with your server, and then replacing parts of it with the attributes of a data object. This often means that an object such as this:
{
  name: "max"
}
Can be fed into the following HTML and in this case populate the “text” attribute of the span with “max”:
<span id="my_template" class="name"></span>
goes to:
<span id="my_template" class="name">max</span>
There are often nifty options when piping in the data so that it can bind to changes on the data object itself, or other parts of your UI such as a form.
One of the drawbacks I have found from this sort of library is that customising the view can become difficult. In the example above, it would be difficult to make the following object:
{
  id: 3,
  color: "#eee",
  name: "foo"
}
into (note inline style, and id attribute):
<div id="my_object_3" style="background: #eee;">foo</div>
Flexibility can often be achieved by passing in a bunch of configuration options, but this seems a little unwieldy to me.

The templating camp

These essentially mean that you end up writing something like eRB in Javascript. They aren’t really my favourite option, as I think introducing a templating language into a frontend app violates some sort of long term maintainability principle.
What’s more, we already have enough opinionated people arguing about HAML vs Erb Vs whatever-else in our server side code, without introducing another fight on the client end.
If you like this sort of thing some of them are pretty advanced. I believe Sammy templates can be requested from the server as GET requests, and also cached if necessary.

The “injecting fragments of HTML rendered by a server” camp (eg RJS)

This is not really something I like doing, as it fits into my concept of “Pseudo state”.
If a part of a UI needs to be updated by a change of state, this method means your client app goes crying to Daddy server (via Ajax) to request a snippet of HTML which is rendered for it. It then inserts the HTML often by using some sort of evaluated Javascript which has been sent in the request.
I could go into more detail about this, but it just feels like an anti-pattern, and I can’t really be bothered.

Going back to MVC

Rather than using a library for creating HTML that might mean conforming to data-binding conventions I don’t agree with, I tend go for an approach which is flexible and javascripty. This means building things up from the simplest solution that works, to a case that deals with a lot of complexity.
The technique borrows from the clone-modify pattern, but adding a “builder” type of pattern to modifying it. The end result can be extended to incorporate behaviour and event handling.

Item Views vs Container Views

This is quite interesting distinction, but is worth thinking about. Often you can get away with only using item views.
The easiest way of thinking about it is in terms of Rails’ partials. The following is adding a container view (myfriendlist) to the page, which corresponds to a widget:
<%= render :partial => "my_friend_list" %>
In this partial you might have the following erb, which renders an item view(“myfrienditem”) based on a collection of friends:
<h3> My friends </h3>
<ul>
<%= render :partial => "my_friend_item", :collection => @my_friends %>
</ul>
Instances of item view classes render one or more similar objects and container view classes are the holder for a collection of item views. These aren’t hard and fast rules, but are a kind of principle.
Imagine the following scenario based on a collection of people.
myFriendCollection = [
  {
    name: "Max"
  },
  {
    name: "Frodo"
  }
]
A likely widget on a page might be a list of your friends:
<ul>
  <li>Max</li>
  <li>Frodo</li>
</ul>
In this example, a “friendListView” class might be in charge of iterating through the “myFriendCollection” array and adding a “friendItemView” to the UL for each person it encounters.
In this way, an instance of “friendItemView” would receive a single object it is to render. If items are added or removed from the collection, the “friendListView” will need to redraw its children.

Making simple item View classes

Here’s how I tend to build-up the complexity of something like the example above. This involves a similar “clone and modify” technique that some other libraries use, but rather than abstracting the data mapping behind a bunch of configuration options, I will simply build it up with basic JQuery. This may not be to everyone’s taste, but I think it strikes a nice balance between complexity and simplicity.

Step 1.

Identify the place in the view where you want to make changes.
<ul id="my_friend_list"></div>

Step 2.

Make a view class to represent an item to reside in that list:
var friendItemView = function(holder_elem, person){
  ...
}
Note that holder_elem is the DOM element we created earlier($('#my_friend_list')), and person is the data object we are passing to it.

Step 3.

Put some logic in the class to draw the object you have been given into the holder element (in this case a simple string):
var friendItemView = function(holder_elem, object){

  var draw = function(){
    holder_elem.append("<li>"+ object.name + "</li>");
  };

  // run when initialised
  draw()

}

Step 4.

Instantiate the class with the holder element and an object to be rendered:
new friendItemView($('#my_friend_list'), person);

Step 5.

If you need to render a more complex template, you can grab it from a “templates” section in your HTML (not related to the above example):
<div style="display: none" id="templates">
  <ul>
  <li id="my_object_template">
    <h3></h3>
    <p class="description"></p>
  </li>
  </ul>
</div>
This then means altering your class to clone the template, and build the data within it:
var friendItemView = function(holder_elem, person, template){
  elem = template.clone().attr("id", null); // nullify id so it isn't duplicated

  var draw = function(){
    elem.find('h3').text(person.name)
    elem.find('.description').text(person.description)
  };

  // run when initialised
  draw()
}
And changing how the object is initialised:
new friendItemView($('#my_friend_list'), person, $('#my_object_template'));

Updating views

These item based views nicely encapsulate all the data needed to render themselves. However, in these examples, I have used a private method (“draw”) to render them. What happens if the data changes, and the view needs to be re-rendered? It is possible to make “draw” public, so it can be called externally, but that often makes things a bit messy. It leads to the following code being duplicated all over the place, not to mention the fact that something global needs to keep track of these views in order to modify them:
// do some stuff to change the data object
// find the view which corresponds with the object
// then call:
instance_of_specific_view.draw()
Events simplify this greatly. Rather than going into detail on events, I will just give a couple of examples.
In the examples above, there is a one-to-one relationship between a view instance and the data object it is passed. This means that the view can bind to events triggered by the object which is passed to it. Changing the represented state in the HTML is therefore handled internally. Note that entering this territory means maintaining your data objects in sensible “Model” classes (the M in MVC). We use JS-Model to handle this.
The following illustrates an example:
var friendItemView = function(elem, person){
  ...
  person.bind('update', draw);
}
Hopefully you can see that this makes it easy maintain.

Chucking in a bit of behaviour

You can also use the view as a place to bind behaviour to your HTML. The following demonstrates the need to “drag” friends out of the list (kinda pointless on its own):
var friendItemView = function(elem, person){
  ...
  elem.draggable();
}

Container View class

These are generally less used than the item views and need less code.
If you want a container which draws the collection of objects, it is generally as easy as doing:
var friendListView = function(elem, myFriendCollection){

  var draw = function() {
    for (i in myFriendCollection){
      new friendItemView($('#my_friend_list'), myFriendCollection[i], $('#my_object_template'));
    }
  }

}
Sometimes these holder classes may need to dictate dimensions to their children (such as height), which can only be ascertained by holding knowledge of the collection eg:
...
for (i in myFriendCollection){
  friend_item = new friendItemView($('#my_friend_list'), myFriendCollection[i], $('#my_object_template'));
  friend_item.height = height_of_container / myFriendCollection.length
}
...

Events

These collections are more likely to listen to events on the collection such as when a new item is added. On occasion, they may need to purge all the item views and redraw.
var friendListView = function(elem, collection){
  ...
  var add_new_view = function(evt, object){
    new friendItemView(elem, object, $('#my_object_template'));
  }

  collection.bind('create', add_new_view);
}

Conclusion

While this technique is a little more involved than some of the templating languages, it offers far more control and crucially it feels really Javascripty.
SHARE

Oscar perez

Arquitecto especialista en gestion de proyectos si necesitas desarrollar algun proyecto en Bogota contactame en el 3006825874 o visita mi pagina en www.arquitectobogota.tk

  • Image
  • Image
  • Image
  • Image
  • Image
    Blogger Comment
    Facebook Comment

0 comentarios:

Publicar un comentario