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 elementupdateAttr(boundId, name, value)
: sets the value of an attributeupdateCss(boundId, name, value)
: sets a CSS propertyupdateVisible(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 viewview.model
: the model associated with the current viewview.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