view.json

Low level markup

Nested view definition and child views

view.json supports nested view definition. You can write this:

{
  name: "Table",
  content: [{
    name: "ToggleAll",
    tag: "input"
  }]
}

and the result will be two view classes:

function Table() { ... }
function ToggleAll() { ... }

where calling Table.render() will create an instance of the ToggleAll view and call render() on it, placing the result in the correct place in the Table view (and all the other features will work normally on the child view).

view.json detects child views by looking at the "name" key. If any of the elements inside "content" have a "name" key, then they will be compiled into their own view classes and instantiated when the parent view is rendered.

What about reusing the same named view?

Referring to a view defined elsewhere:

{ child: "foo" }

Renders that child in place.

Bound elements

This view.json:

{
  name: "ItemView",
  content: ['Hello', '<b>', { tag: 'span' }, '</b>' ]
}

Describes:

<div id="0">Hello <b><span id="1">{{name}}</span></b></div>

Note how two of the elements have framework-generated ids (the real id's are slightly different).

Each addressable/bound element is an element. This makes it easy and fast to interact with the DOM, since selections can be made using the document.elementById() API. The downside is that updateable parts are always contained in elements; but this is a decent compromise between complexity and ease of use.

Responding to events and updating bound element content

What does it mean to have a template like this?

<div>Hello <span>{{name}}</span></div>

Well, it basically means that when the value represented by "{{name}}" changes, we should update the corresponding HTML automatically.

In view.json, this is accomplished via event subscriptions; event subcriptions are made via the "subs" key. Specifically, for each entry under "subs", the view calls on(eventname, callback), where eventname is the value of the "on" key and callback is a generated callback.

Here is an example event subscription:

{
  name: "ItemView",
  content: ['Hello', { tag: 'span' } ]
  subs: [ {
    on: "name.change",
    expr: "update(bound[1], window.name)"
  } ]
}

which is the same as:

on("name.change", function(bound) {
  update(bound[1], window.name);
});

The callback receives a parameter "bound", which is an array containing the ID's of the bound elements of the view. In the example markup:

<div>Hello <span>{{name}}</span></div>

bound[0] refers to the div element and bound[1] refers to the span element.

Helper functions and available variables in listeners

Listeners have access to the following helper functions:

  • update(boundId, value): sets the innerText of an element
  • updateAttr(boundId, name, value): sets the value of an attribute
  • updateCss(boundId, name, value): sets a CSS property
  • updateVisible(boundId, value): toggling visibility

These functions in combination with the automatically assigned element IDs allow you to update the HTML in response to events.

Listeners have the following variables accessible:

  • view: the current instance of the view
  • view.model: the model associated with the current view
  • view.bound: an array of bound element IDs

The bound array is an array of the bound element IDs, starting from the current view (named view), moving depth-first through all the bound elements. 0 is the container element of the current view.

Conditional views

Expressions such as:

if (expression)
  render bar
else
  render baz

are represented as replaceable subblocks. For example:

{
  name: "total",
  template: "conditional",
  listeners: [
    { on: "currentUser:change", expr: "view.update(); "}
  ],
  alternatives: [
    {
      expr: "(resolve('currentUser').get('id') % 2 == 0)",
      view: "FooView"
    }
  ]
}

Note:

  • the listeners key must be set so that updates occur
  • must have the conditions attr
  • to indicate "default", leave the expr out

Post render, updateAll() is triggered for the newly rendered view (and all it's subviews).

Collection views

view.json supports collection views. Collection views inherit from CollectionView, which is a view that has additional support for displaying a collection (array) of models.

{
  name: "todoList",
  tag: "ul",
  template: "collection",
  bind: "window.Todos",
  content: {
    tag: "li",
    name: "todoListItem",
    content: [ 'aaa']
  }
}

Note:

  • content must be a single child view
  • the child view must have a name
  • must have the bind attr

CollectionView has additional intelligence built into it so that it can update only the changed elements.

Event-based:

  • add(item, index)
  • remove(index)
  • reset(items)

Item changes are handled directly by each view.

In order to do this, we need an observable array:

emit('reset', [ { phone: '2222' } ]);
emit('add', { phone: '1234567'}, 0);
emit('remove', 0);
emit('reset', [ { phone: '666666' } ]);

Binding DOM events to code actions

Binding events is simple. The "on" key should contain one executable expression (JS code) for each DOM event. These events will then be bound to the element when the view is instantiated (and maintained over redraws/updates).

{
  name: "CheckboxView",
  tag: "input",
  attr: { type: "checkbox" },
  on: {
    click: "view.toggleDone();"
  }
}

is roughtly equivalent to:

$("el").on('click', function() { view.toggleDone(); });

TODO: event context and params doc.

DOM event handlers can access the following information:

this - is set to the source HTML element
e - is set to the (jQuery) normalized event object
e.data.view - is set to the view instance