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 (but recommended!) src/obviel-template.js.

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 to use Obviel Template (optional but strongly recommended), you need to include it:

<script type="text/javascript" src="/path/to/obviel-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.

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.

obvt and obvt_url: Obviel Template

A combination of static HTML and jQuery scripting is certainly dynamic enough, but you can write a lot more compact and readable code by using a template language. Obviel includes a template language custom-written for Obviel called Obviel Template. Obviel also provides an API to extend it to support other template languages.

How do you use Obviel Template? The properties obvt and obvt_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 an Obviel Template, the object being rendered is combined with the template and the resulting DOM elements are added to the element on which render was invoked.

Obviel Template has many features, including the ability to directly render sub-objects within the template language. Read the template documentation for much more information.

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 like we did above, but Obviel Template offers a shorter and simpler way.

First, let’s look again at how we invoke a sub-view by hand in a render method:

obviel.view({
  html: '<div class="foo"></div>',
  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 you to do this from templates:

obviel.view({
  obvt: '<div data-view="attr"></div>
});

Wow, that’s a lot shorter!

For clarity we’ve spelled out everything before using the most low-level Obviel concepts. What you really should be doing is use Obviel Template. Here is the example of pages with lists and texts on it again, but using Obviel Template instead of JavaScript code:

obviel.view({
   iface: 'text',
   obvt: '<p>{text}</p>'
});

obviel.view({
   iface: 'list',
   obvt: '<ul><li data-each="entries">{@.}</li></ul>'
});


obviel.view({
   iface: 'page',
   obvt: '<div data-each="parts" data-view="@."></div>'
});

As you can see, that has cleaned up the code a lot!

Transformers

Sometimes the server does not return JSON objects with an iface already included, or in some other way returns objects that are not very suitable for rendering them with Obviel.

You can plug in a transformer function to transform JSON content retrieved from the server to a format more useful to your client-side views.

How this transformation is done in detail is up to you; you can plug in any transformation logic you like, as long as you return an object that can be rendered using Obviel.

What does this look like? Let’s imagine we have a web server that returns JSON objects that are just perfect for Obviel, except for the fact that the objects don’t have an iface or ifaces property. Instead, these objects have a type property. We would like use this type property as the iface.

We can hook in a transformer that does this for any object retrieved from the server using URI access:

obviel.transformer(function(obj, url, name) {
   obj.iface = obj.type;
   return obj;
});

In this case, we just modify the incoming JSON object adding an iface property in there. You can also return wholly different objects instead.

As you can see, the transformer function takes three arguments:

obj

The JSON object retrieved and to be transformed.

url

The URL from which the object was retrieved. This argument is optional.

name

The name of the view that is being looked up. This argument is optional.

The URL argument could be used to set the iface of an object based on the URL it was accessed with. This is less flexible than letting the server send an object with an iface marker, however, so if you have control over the server this is recommended.

Of course setting the iface is just one of the transformations you can apply; you can add any arbitrary property to objects or even return completely different objects altogether.

before: intervening before rendering starts

You can supply an optional before function to the view that will be called just before rendering starts. This can be useful to do additional client-side set up in the object being rendered just before the template renders it, so that the template can access extra information. Here is a simple example:

obviel.view({
  iface: 'text',
  before: function() {
      this.obj.length = this.obj.data.length;
  },
  jsont: 'The text "{data}" has {length} characters.'
});

el.render({
  iface: 'text',
  data: 'Hello world'
});

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.

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;
      $('#some_id', 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: {
         '#some_id': 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: {
       '#some_id': '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.

Declarative Object Events

You can also declaratively bind event handlers to the object when it is being rendered by the view. In your application you can then define custom events and trigger them on objects. The handlers are registered just like for normal events, either with a function or with a string indicating a method on the view.

Here is an example:

obviel.view({
  iface: 'foo',
  render: function() {},
  object_events: {
     'update': 'rerender'
  },
  rerender: function() {
     self.el.render(self.obj);
  }
});

This registers a handler that gets triggered when the custom update event is sent to the object. Let’s first render an object:

var my_obj = {iface: 'ifoo'};
some_el.render(my_obj);

and now let’s trigger the update event on my_obj. This is done by wrapping the object in $ first:

$(my_obj).trigger('custom');

This will cause rerender to be called, which will redraw the view. If my_obj has changed in the mean time, the view will be redrawn to reflect this.

As a convenience, rerender has been predefined on views, so you don’t have to define it like we did above. This is sufficient:

obviel.view({
  iface: 'foo',
  render: function() {},
  object_events: {
     'update': 'rerender'
  }
});

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 an 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 Element Association 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.