Obviel: Object/View/Element for jQuery

Introduction

So Obviel promises a better structure for your JavaScript applications.

What does Obviel really do? Obviel lets you associate views with JavaScript objects and DOM elements. You are the one who creates the views, and you will find that you can decompose much of your JavaScript application into views.

In the view definition, you write JavaScript code that can render the information in the JSON object into the browser DOM. This interplay of object, view and element is central to Obviel. It also inspires its name, Ob-vi-el.

What does Obviel ask you to do?

  • you must, typically on the server side, add simple type information to the JSON objects that you want to render with views on the client. This is done using the ifaces property. We also call such JSON objects model.
  • you must, on the client side, define views that know how to render the different JSON objects your server can send back.
  • you hook up the views to the JSON objects using the iface.
  • you can then render a view for an object on a DOM element by using a special render extension Obviel adds to jQuery.

All this is pretty dense, so we’ll go into much more detail about this now.

How to include Obviel on your web page

First you need to know how to include Obviel on a web page. You need to make sure that src/obviel.js is published somewhere on your web server. You also need jQuery, and optionally JSON template.

To include Obviel, you first need jQuery as a dependency:

<script type="text/javascript" src="/path/to/jquery-1.6.1.js"></script>

If you want Obviel’s json-template support, you can include JSON template (but this is optional):

<script type="text/javascript" src="/path/to/json-template.js"></script>

Finally, you need to include Obviel core itself:

<script type="text/javascript" src="/path/to/obviel.js"></script>

Obviel is now available as obviel in your JavaScript code.

Here is a suggestion on how to structure your code, using the JavaScript module pattern (global import):

(function($, obviel) {
   // .. views are defined here ..

   $(document).ready(function() {
     $(some_selector).render(some_object_or_url);
   });
})(jQuery, obviel);

We’ll go into what you can put in for some_selector and some_object_or_url below.

Rendering a view

Now that we have Obviel available, we’ll start with the last bit first: how do we actually render a view for an object on an element?

A view is a JavaScript component that can render an object into an element in the browser DOM tree. This is done using by calling the function render on the result of a JQuery selector:

$('#foo').render(model);

If you have Obviel installed, this render function will be available. Since the DOM needs to be available when you start rendering your views, you need to do your view rendering in the $(document).ready callback, or in code that gets called as a result of the first view rendering.

So what does this render call do? It will look up a view for the JavaScript object model and then ask that view to render the model on the element indicated by the jQuery selector #foo.

Typically you would use selectors that only match a single element, but if you use a selector that matches more than one element, view lookup is performed multiple times, once for each matching element.

Now let’s look at the pieces in more detail.

What is model? It’s just a JavaScript object with one special property: ifaces:

var model = {
  ifaces: ['example'],
  name: 'World'
};

Typically with Obviel models are JavaScript objects generated as JSON on the server, but you could basically use any JavaScript object, as long as it provides an ifaces property. The ifaces property lets models declare what type they have.

As you can see, ifaces is a list of strings; each string identifies an iface that this object declares – typically only one is enough.

What is a view? It’s a special JavaScript object registered with Obviel that at minimum says how to render a model on an element:

obviel.view({
   iface: 'example',
   render: function() {
      this.el.text("Hello " + this.obj.name + "!");
   }
});

You see how iface comes in again: this view knows how to render objects of iface example.

So imagine we have the following HTML in the browser DOM:

<div id="foo"></div>

What happens when you invoke the following?

$(‘#foo’).render(model);

The DOM will be changed so it reads this:

<div id="foo">Hello World!</div>

So, it has rendered "Hello World!", where World comes from the name property of the model object being rendered.

The steps taken are:

  • Obviel looks at the ifaces property of the model being rendered, in this case [`example`].
  • Obviel looks up the view registered for the iface example in its view registry.
  • Obviel creates a clone of the registered view object from this view that has as its el property the element being rendered on, and obj property the object being rendered.
  • call the render method on the view.
  • the render method then does whatever it wants, in particular manipulating the DOM using jQuery, as we do here to set the text of the div.

View lookup

Dynamic view lookup is what allows loose coupling between client and server. The primary mechanism of lookup is by the iface marker on the model. A model can declare with an iface what kind of model it is, and this way a view can declare what kind of model it is associated with.

An iface is in essence just a string marker:

var elephant = {
  ifaces: ['animal'],
  color: 'grey'
};

var lion = {
  ifaces: ['animal'],
  color: 'golden'
};

Each model can declare what kind of model it is using these iface markers.

When a view is registered, the iface it is associated with should be provided:

obviel.view({
   iface: 'animal',
   render: function() {
     this.el.text('The animal is ' + this.obj.color);
   };
});

If you now render a model that declares iface animal, the view will be used:

$('#animal').render(elephant);

will render in the element indicated by #animal the text:

The animal is grey

and this:

$('#animal').render(lion);

will render like this:

The animal is golden

What if we want to make an exception for elephants, though? We can do that too, by registering another view for the elephant iface and using that instead:

var elephant = {
  ifaces: ['elephant'],
  color: 'grey'
};

obviel.view({
   iface: 'elephant',
   render: function() {
     this.el.text('This very big animal is ' + this.obj.color);
   };
});

Now if we were to render a list of animals, and one of them happened to be an elephant, we’ll see that the exception for elephant will be used.

In some cases an iface is not enough, and you can further distinguish views by name. The name is really only needed when you want to have different ways of rendering the same object (or URL), perhaps depending on where a user clicks, or what tab is open, etc. Here’s an example:

obviel.view({
  iface: 'animal',
  name: 'alternate',
  render: function() {
    this.el.text("Color of animal is: " + this.obj.color);
  };
});

This named view can be explicitly invoked by passing its name as a second argument to the render function:

$('#animal').render(lion, 'alternate');

will result in:

Color of animal is: golden

As said before, names are optional, and aren’t used very often. By default the name is default.

The iface declaration for a view is optional too, though you should usually provide it. If you leave out an iface in a view registration you register a fallback view for all objects.

Note that the model property name is ifaces while the view property name is iface. The idea is that a model may have more descriptions of what the data is, but a view only knows how to render one type of data. (but may be a cause of errors that is easy to overlook, so we’re looking into changing this)

Properties available on views

When you render a view, a view instance is created that has several properties which you can access through this in the render function of a view. We’ve seen some of them before, but we’ll go through them systematically now.

el

The element that this view is being rendered on. This is a jQuery object, so all the usual jQuery functionality will work. The element is where the view expresses itself during rendering: it adds sub-elements to this element, or changes its text() value, hooks up event handlers, etc.

obj

This is the model that the view is supposed to render. You access properties of this object to determine what to render.

name

This is the name of the current view. By default it is default.

html and html_url

A view can be configured so that it renders a piece of static HTML into the element (using the jQuery .html function) before the render function is called. You do this by adding a html property to the view.

This is useful when you have a view that wants to insert a HTML structure into the DOM; this way you can avoid manual DOM manipulation. Doing this can also add to the clarity of the code.

Here’s an example:

obviel.view({
   ifaces: ['foo'],
   html: '<div class="a_class">Some HTML</div>',
   render: function() {
      var el = $('.a_class', this.el);
      el.text("Changed the text!");
   }
});

This will add the structure <div class="a_class">Some HTML</div> into the element the view is rendered on, and then calls the render function, which can now make some assumptions about what is in the element.

If the HTML fragment to insert has multiple lines, it is nicer to maintain it in a separate file instead of in an embedded string. The view can also refer to a static HTML resource on the server using the html_url property:

obviel.view({
   ifaces: ['foo'],
   html_url: 'http://www.example.com/some.html',
   render: function() {
      // ...
   }
});

The HTML referred to by html_url will be cached by the system, so when you render the view next time no more request will be made to the server to retrieve the HTML fragment.

In some cases you may want to let the server supply the HTML in the model instead of using it from the view. If the object to be rendered has a html or html_url property those will be interpreted as if they were on the view.

If both html and html_url are found on a view or a model, the html property has precedence. The html and html_url properties of the model have precedence over any defined on the view.

jsont and jsont_url: JSON template

A combination of static HTML and jQuery scripting is certainly dynamic enough, but sometimes using a template language can result in more readable code. By default support for JSON template is included. Obviel also provides an API to extend it to support other template languages.

The properties jsont and jsont_url work like html and html_url and can be provided both by the view and the model. Let’s look at an example:

obviel.view({
  iface: 'person',
  jsont: '<div>{name}</div>'
});

$('#somediv').render({
  iface: 'person',
  name: 'John'});

This will result in:

<div>John</div>

When rendering a JSON template, the object being rendered is combined with the template and the resulting HTML is inserted into the element that render was invoked for.

Rendering Sub-Objects

When presenting a complicated web page, it makes sense to split the underlying objects that this web page represents into individual sub objects. So, you might for instance have a JSON structure like this:

{
  ifaces: ['page'],
  parts: [
     {
       ifaces: ['text'],
       text: "Hello world"
     },
     {
       ifaces: ['list'],
       entries: ['foo', 'bar', 'baz']
     }
  ]
}

This has an outer JSON object with the iface page, and in there there are two parts, one of iface text and one of iface list.

How would you set out to render such a thing with Obviel views? Instead of creating one big view that does everything, we can decompose this into a number of subviews. Let’s first create a view for the text iface:

obviel.view({
   iface: 'text'
   render: function() {
      var p_el = this.el.append('<p>');
      p_el.text(this.obj.text);
   }
});

This view adds a p element to the DOM under which it is rendered, and renders the text property of the underlying object into it.

We’ll also create a view for list:

obviel.view({
   iface: 'list'
   render: function() {
      var self = this;
      var ul_el = self.el.append('<ul>');
      $.each(self.obj.entries, function(index, entry) {
         var li_el = ul_el.append('<li>');
         li_el.text(entry);
      });
   }
});

This creates a ul element in the DOM and renders each entry in the entries list as a li element with text in it. Note the use of the common JavaScript technique of assigning this to another local variable, self by convention in Obviel code, so we have an easy reference to it in the nested functions we define inside.

Now let’s create a view that renders all the page iface:

obviel.view({
   iface: 'page',
   render: function() {
      var self = this;
      $.each(self.obj.parts, function(index, part) {
         var div_el = self.el.append($('<div>');
         div_el.render(part);
      });
   }
});

This view creates a div for each part in the parts property. You can see how delegation to subviews comes in: we render each part individually. You can also see something else: the page view has no knowledge of what these sub views are, and could render any list of them – it’s entirely dependent on the object it is asked to render.

Partitioning code into views is useful: it’s the Obviel way. You’ll find it makes your code a lot easier to manage.

You can write the sub-view rendering code manually and often it is not cumbersome, but for some common cases Obviel provides a facility to automate this, see Additional methods.

Callbacks

In some cases you need to know when a view has finished rendering; this is particularly useful when you are writing automated tests that involve Obviel. The Obviel test suit itself is a good example of this. You can supply a callback by passing a function to the render method:

el.render(obj, function() { alert("Callback called!") };

You can use this in the callback to refer to the view that invoked the callback.

Additional methods

A view is just a JavaScript object, and you may therefore supply extra methods that it calls from the render method to assist in the rendering of the view and setting up of event handlers:

obviel.view({
  render: function() {
    this.foo();
  },
  foo: function() {
    // ...extra work...
  }
});

You can also add extra properties:

obviel.view({
  render: function() {
    this.foo();
  },
  extra: "An extra property"
});

View Inheritance

While in many cases the additional methods strategy as described previously is sufficient, in more complex cases it can be useful to be able to create a new view by inheriting from another view. The Obviel form system uses this approach for its widgts.

To understand how view inheritance works, you first need to understand that the following registration:

obviel.view({
  render: function() { ... }
});

is in fact a shorthand for this registration:

obviel.view(new obviel.View({render: function() { ... }}));

Obviel automatically creates a basic Obviel View if a bare object is passed to the view registration function.

You can however also create new view objects by subclassing View yourself:

var DivView = function(settings) {
  var d = {
    html: '<div></div>'
  };
  $.extend(d, settings);
  obviel.View.call(this, d);
};

DivView.prototype = new obviel.View;

DivView.render = function() {
  // ...
};

Now the new view can be registered like this:

obviel.view(new DivView());

You can also create your own base classes that derive from View that provide extra functionality, and inherit from them.

Subviews

As we have discussed earlier, many views are composed out of other views.

You can invoke sub-views by hand in a render method, like this:

obviel.view({
  render: function() {
     $('.foo', this.el).render(this.obj.attr);
  }
});

This will render a subview on the element matched by class foo for the model indicated by this.obj.attr. this.obj.attr may be a sub-object or a URL referring to another object.

Doing this by hand is not too bad, but Obviel also allows a shorter, declarative way to express this:

views.view({
  subviews: {
    '.foo': 'attr'
  }
});

This does the same thing as the previous example.

The subviews property, if available, should define a mapping from jQuery selector to model property name. If the view has a render function, subviews are rendered after the render function of the view has been executed.

So, if you have this view:

views.view({
  subviews: {
     '#alpha': 'alpha',
     '#beta': 'beta_url'
});

And render it with the following context object:

{
 alpha: {text: 'foo'},
 beta_url: '/beta.json'
}

the system will, in effect, call:

$('#alpha', this.el).render({text: 'foo'})

and:

$('#beta', this.el).render('/beta.json')

If you want to invoke a subview with a specific name, you can provide a name for subviews by passing an array instead of a string as the value of the subviews mapping:

views.view({
  subviews: {
      '#selector': ['foo', 'name']
  }
});

Here, a subview is registered for the selector #selector, the data is looked up on the context object using property name foo, and the view is looked up using name.

Note that if a callback is provided to render(), it will be called after the main view and all its subviews are done rendering.

Declarative Event Registration

Often a view will need to attach event handlers to elements rendered by the view. You can do this by hand:

obviel.view({
   iface: 'foo',
   render: function() {
      var self = this;
      self.el.click(function() {
         self.el.text("clicked!");
      });
   }
});

Like with subviews, Obviel allows a declarative way to hook up events. Here is the equivalent of the above:

obviel.view({
  iface: 'foo',
  render: function() {},
  events: {
     'click': function(ev) {
         ev.view.el.text('clicked!");
     }
  }
});

Like standard jQuery, the event handler gets an event object, but this object will have a special property view which is the view that this event is associated with.

There is another way to express this:

obviel.view({
  iface: 'foo',
  render: function() {},
  events: {
     'click': 'handle_click'
     }
  }
  handle_click: function(ev) {
     this.el.text('clicked!");
  }
});

In this case instead of directly hooking up the event handler, we refer to a method of the view itself as the event handler. You can refer to the view and its properties using this just like you do with render. The event handler also receives the usual event object as the first argument.

All declaratively defined events are registered after the view has been rendered.

Bootstrapping Obviel

Obviel can start working with just a single URL; two if you need templates or HTML snippets. All the other URLs in the application it can access by following hyperlinks in JSON.

This is an example of Obviel bootstrapping code in your application:

$(document).ready(function() {
  $('#main').render(app_url);
});

This renders the object at app_url when the DOM is ready, and will render it into the HTML element identified with the main id.

We call the object referred to by app_url the root object. The root object should include hyperlinks to other objects in your application, which it will then in turn render as sub-objects.

The question remains how to actually set app_url in your application. It is a URL that will be dependent on how your application is installed.

One way to do it is to exploit your web framework’s server-side templating system, and set it in a <script> element somewhere in your web page:

<script type="text/javascript">
   var app_url = "[the app url goes here using a template directive]";
</script>

Another way is to include a separate JavaScript file that you dynamically generate, that only sets app_url:

var app_url = "[the app url goes here, using server-side programming]";

There is a second URL that is handy to include using one of these methods as well: template_url. This is the URL that identifies your template (or HTML snippet) directory. It could for instance look like this:

http://example.com/templates/

Note how it ends with a forward slash (/).

Once template_url is available, your views can refer to individual templates like this:

v.view({
   html_url: template_url + 'some_snippet.html'
});

You can set up template_url in the same way you set up app_url, though there is one extra requirement: template_url must be known before any Obviel views are registered, whereas app_url only needs to be known when the DOM is ready. If you are going to set template_url it is therefore important to do this early, in a <script> tag that comes before the <script> tag that includes your code that registers views. For example:

<script type="text/javascript">
   var template_url = "http://example.com/templates/";
   var app_url = "http://example.com/app_root";
</script>
<script type="text/javascript" src="http://example.com/obviel_app.js"></script>

Element Association

When a view is rendered on an element, it remains associated with that element, unless the ephemeral property of the view is set to true. If a view is associated with an element, rendering an object of the view’s iface (and name) for any sub-element will render on the outer element instead. The sidebar has more background on this feature.

To retrieve the associated view of an element, you can use the $(el).view() function to get it back again.

To access a view in this element or its nearest parent, use (el).parent_view().

To remove the element association, you can call $(el).unview().

To re-render a view on the element again, use $(el).rerender().

Cleanup

When a view is rendered on an element that already had a view associated with it, or when a view is unrendered using unview, Obviel calls the cleanup method on the view. You can put in a special cleanup method on the view that gets called to perform the cleanup.

Events Sent by Obviel

Obviel triggers two kinds of events:

  • render-done.obviel
  • render.obviel

These will both be triggered on the element that the view is rendered on. Both event objects will also have a special view property with the view that triggered the event.

The render-done.obviel event can be used to take action when the view is done rendering entirely (including the rendering of any subviews).

The render.obviel event is an internal event of Obviel; Obviel sets up am event handler for this by default on the document, and also sets up an event handler for this for elements that have a view associated with it. The latter event handler will only take action if the view being rendered has the same iface and name properties as the view that was associated with the element – it is used to implement Bootstrapping Obviel behavior.

Iface extension

It is sometimes useful to be able to register an iface more generically, for a whole selection of related objects. We may have more particular person objects such as employee, contest_winner, etc, but if we register a view for person objects we want it to automatically apply to those other types of objects as well, unless we registered more specific views for the latter.

Let’s consider the following object describing a person:

>>> bob = {name: 'Bob', location: 'US', occupation: 'skeptic',
...        ifaces: ['person']}

>>> obviel.ifaces(bob)
['person', 'base', 'object']

So far nothing new. But ifaces themselves can have an extension relationship with each other: iface b can be declared to extend iface a. We’ve already seen an example of this, because person automatically extends the base iface base.

If a view is declared for a certain iface, it is also automatically declared for all ifaces that extend that iface.

So let’s imagine we have an iface employee that extends the person iface. We can tell the system about it like this:

>>> obviel.extendsIface('employee', 'person')

An iface may extend an arbitrary amount of other ifaces, but circular relationships are not allowed. The obviel.ifaces function knows about extensions. So, let’s say that we have an employee object:

>>> employee = {name: 'Bob', location: 'US', occupation: 'skeptic',
...             wage: 0, ifaces: ['employee']}

Since we said before that any employee is also a person, the following is true:

>>> views.ifaces(employee)
['employee', 'person', 'base', 'object']

Note that interfaces are sorted by topological sort, with the most specific interfaces at the start. When looking up a view for an object, this is useful to ensure that the views registered for the most specific interfaces are found first.