Documentation

Version: 0.1.3 (Sep 3, 2012)

Introduction

Agility is a Model-View-Controller (MVC) library for client-side Javascript with some specific design principles:

The overall goal is to improve code maintainability without sacrificing productivity. It is inspired by the principles behind Ruby on Rails and jQuery.

Agility's programming model is framed around the concept of self-contained MVC objects, where each object can be the prototype of, as well as the container of other MVC objects. This level of abstraction should encompass most applications.

See the home page for a quick overview of its syntax and usage.

Why MVC?

One might wonder, since DOM-querying/Ajax libraries like jQuery make it so easy to whip up a dynamic web app, why bother with an additional layer of complexity?

Short answer

For those who have built a complex web app "organically", i.e. purely through DOM querying and manual Ajax calls, the answer is immediate: although you were able to get that app up to speed so quickly, you probably dread maintaining and relearning that intertwined code, and wish you had known better!

Long answer

For those who haven't, some things you will likely end up doing with a pure jQuery-esque solution include: storing data in the DOM; querying the DOM to find your data; defining global callbacks to DOM events e.g. click/input change; having those callbacks neatly package your data to be sent to the server; retrieving data from the server and inserting them in the DOM with the right format and event handlers; etc.

Though that's all fine initially, sooner or later you will start running into maintainability problems: storing data in the DOM is very brittle, e.g. changing an id/class or restructuring the DOM requires revisiting the code just about everywhere; DOM elements that are logically related and need to be always in sync require manual updates in all callbacks associated with them; global callbacks lead to name collisions and hence cumbersome function names, as well as difficulties in finding just what function is responsible for doing X or Y; defining functions that package data for, or present data after Ajax calls is unnecessarily repetitive; etc.

One established answer to these problems is the Model-View-Controller approach, where your app is organized in "large" objects each having different parts responsible for managing content (Model), format/style (View), and behavior (Controller). For example, a series of DOM input elements such as Name, Address, Phone, etc, related to the abstract concept of "person" become part of an object "person", whose model contains the raw data, view contains the HTML/CSS presentation, and controller contains the actions that will be called in response to events in the former two.

MVC libraries like Agility typically offer built-in model, view, and controller methods that encompass most use case scenarios, so you don't have to reinvent the wheel or repeat yourself. That way all functions, formatting, and data related to an abstract concept (e.g. "person") are all in one place, the DOM is always in sync with the data, and the data is always ready to be sent to/retrieved from the server in one call.

Why Agility?

In response to the difficulties above, in the last few years several superb libraries have been introduced to bring MVC (or a variant thereof) to the browser. Although they do a good job of refactoring apps in terms of content, format, and behavior - and hence lead to more maintainable code - they do so at the expense of development speed: Most are fairly verbose, require a considerable amount of repetition, and/or require large library includes.

Agility borrows some useful concepts from the above frameworks, and makes rapid development a core part of its design principles. It's "write less, do more" with maintainability, if you will.

Features

Here are some of the features that Agility.js has aggregated into a single framework:

Architecture

Agility's architecture follows one of the simplest MVC patterns: users define Controller functions, which make direct calls to, and handle events from Models and Views. The diagram below illustrates this.

Architecture diagram

So for example, when a user clicks on a DOM element, an event signal is sent from the View to any Controller functions listening to it, and these functions in turn can make direct calls to Model and View functions.

Additionally, as illustrated below, every Agility object can serve as a container of other Agility objects. This is a natural abstraction for most applications, including simple lists, interactive tables, picture/video catalogs, etc, where each individual item might contain enough functionality (e.g. edit/remove buttons, mouse hover behavior, etc) to deserve its own MVC object. And because Agility objects are lightweight in memory (through pervasive use of prototypes), this comes at little performance cost.

Hierarchy diagram

Getting started

Agility.js depends on a recent version of jQuery (tested with 1.6.x, Zepto support coming soon). Other than that dependency, a single <script> tag in your Javascript code is all that's required, e.g.:

<script src="agility.js" type="text/javascript" charset="utf-8"></script>

Typically the <body> of your HTML will be empty, and will be populated programmatically by adding Agility objects to the global object $$.document.

Here's the full source of a "hello world" example:

<!DOCTYPE html>
<html>

<head>
  <meta http-equiv="Content-type" content="text/html; charset=utf-8">
  <title>Agility Hello World</title>

  <script src="jquery.min.js" type="text/javascript" charset="utf-8"></script>
  <script src="agility.js" type="text/javascript" charset="utf-8"></script>
</head>

<body>
  <script type="text/javascript">
    var message = $$({txt:'Hello World'}, '<div data-bind="txt"/>');
    $$.document.append(message);        
  </script>
</body>

</html>

The above template has been used for all examples throughout this documentation.

Creating objects

Agility is framed around the notion of all-in-one MVC objects, or simply "Agility objects". Such objects are created through the factory function $$(), either from scratch (by passing model, view, and/or controller initializers) or from a prototype object (by specifying an existing Agility object):

// Create object from scratch:
var proto = $$({}, '<p data-bind="name" style="color:red"/>');
// Create object from prototype object:
var obj = $$(proto, {name:'Joe Doe'});
$$.document.append(obj);

Refer to the examples in the home page and elsewhere in this document for several different uses of the factory function, and the factory function reference for syntax details.

Bindings

Agility offers painless two-way bindings to keep Models and Views in sync. Binding a given DOM element to a model property is as simple as specifying a data-bind attribute for the desired element:

// Two-way input binding (text)
var obj = $$({name:'Joe Doe'}, '<p><input type="text" data-bind="name"/> You typed: <span data-bind="name"/></p>');
$$.document.append(obj);

// Two-way input binding (search)
var obj = $$({query:'Type of query'}, '<p><input type="search" data-bind="name"/> Instant model change: <span data-bind="name"/></p>');
$$.document.append(obj);

You can also bind models to DOM element attributes in addition to the element HTML content using the following syntax for data-bind:

// data-bind syntax
[model_var] [, attribute1=model_var1 [, attribute2=model_var2] ... ]

where the first (single) argument is the model variable to be bound to the DOM element HTML content, and the subsequent comma-separated pairs specify the binding of a DOM element attribute to a model variable, like so:

// Bind model 'content' to element's HTML content, and model 'myStyle' to element's style attribute
var msg = $$({content:'Greetings!', myStyle:'color:red'}, '<p data-bind="content, style=myStyle"/>');
$$.document.append(msg);

More complex bindings are also supported for other input elements:

// Two-way input binding (radio)
var obj = $$(
  {opt:'opt-a'},
  "<div> \
      <input type='radio' name='test' value='opt-a' data-bind='opt'>a</input> \
      <input type='radio' name='test' value='opt-b' data-bind='opt'>b</input> \
      You selected: <span data-bind='opt'/> \
   </div>"
);
$$.document.append(obj);

// Two-way input binding (checkbox)
var obj = $$(
  {a:false, b:true},
  "<div> \
      <input type='checkbox' name='test' data-bind='a'/> checked: <span data-bind='a'/><br/> \
      <input type='checkbox' name='test' data-bind='b'/> checked: <span data-bind='b'/><br/> \
   </div>"
);
$$.document.append(obj);

// Two-way input binding (select)
var obj = $$(
  {opt:'opt-a'},
  "<div> \
    <select data-bind='opt'> \
      <option value='opt-a'>Option A</option>\
      <option value='opt-b'>Option B</option>\
    </select> \
    You selected: <span data-bind='opt'/> \
   </div>"
);
$$.document.append(obj);

Format and style

Agility's views require the specification of format (HTML), and encourage the use of style (CSS) in-object. This leads to better object reusability and maintainability: there is no need to fish out HTML/CSS parts from different files to reuse an existing object in a new project, and no need to maintain ids/classes throughout separate files. Content, style, and behavior are all contained in one object.

Formats are specified through an HTML string, containing one (and only one) root element that wraps all other elements, so the first two examples below are not valid:

// INVALID CODE!! (missing root view element)
var obj = $$({}, 'hey there');
$$.document.append(obj);

// INVALID CODE!! (more than one root elements)
var obj = $$({}, '<div>hey there</div> <button>OK</button>');
$$.document.append(obj);

// Valid code
var obj = $$({}, '<p>hey there</p>');
$$.document.append(obj);

Formats should always be specified upon object creation. Refer to the factory function for examples on how to initialize the format.

Specifying styles (CSS) in-object is optional, but again, it leads to better code reusability and maintainability. In-object CSS is implemented dynamically, so the object's style sheet is not introduced until the object is created.

To ensure CSS selectors apply only to the intended object, make sure all selectors are preceded by the root selector &. (In future versions this might not be necessary anymore).

// ANTI-PATTERN!! (applies CSS style globally)
var obj = $$({}, "<div><span>Please don't do this</span></div>", 'span { color:red; }');
$$.document.append(obj);

// Correct: applies style locally
var obj = $$({}, '<div><span>Do this</span></div>', '& span { color:blue; }');
$$.document.append(obj);

More complex formats and styles can be organized in multiple lines:

var obj = $$({
  view: {
    format:'<div>\
              <div id="hello">Hello</div>\
              <div id="world">World</div>\
            </div>',
    style:'& { border:5px solid green; color:white; }\
           & div { padding:10px 20px; }\
           & #hello { background:blue; }\
           & #world { background:red; }'
  }
});
$$.document.append(obj);

If your format and/or style are too large, it's probably time to split your object into more Agility objects. (Unless of course you are creating a mostly static page, in which case Agility is probably not the best solution).

Events

There are two types of events in Agility: DOM events and Agility events. Both are implicitly bound to controller functions by matching function and event names.

Controller functions can bind to multiple events at the same time by separating the events with a semicolon. For example, a controller function named click #a; click #b would fire when either click event occurred.

User-defined controllers extend (i.e. are called in addition to) built-in controllers.

DOM events

Usual DOM events such as click, dblclick, mouseenter, etc are supported through jQuery's event API. Please consult jQuery's API for a list of events supported.

When binding to controller functions, DOM events are distinguished from Agility events by the presence of a jQuery selector using the syntax:

// DOM event syntax for controller functions
'event selector': function(){}

In addition to jQuery's selectors, the root selector & is also supported to pick the root element of the view:

var button = $$({msg:'Click me'}, '<button data-bind="msg"/>', {
  'click &': function() {
    this.model.set({msg:"I've been clicked!"});
  }
});
$$.document.append(button);

Agility events

Agility events are fired by the object core, as well as Models and plugins. When binding to a controller function, they are never followed by a space:

// Agility event syntax for controller functions
'event': function(){}
'event:event_parameter': function(){}

Presently, the following Agility events are fired:

The example below defines both a DOM and a Model event handler:

var catcher = $$({msg:'Hover over me'}, '<p><span data-bind="msg"/></p>', {
  'mouseenter span': function() {
    this.model.set({msg:'Hovered!'});
  },
  'change:msg': function() {
    this.view.$().append('<p>Model changed!</p>');
  }
});
$$.document.append(catcher);

Agility event bubbling

Like DOM events, Agility events automatically bubble to the containers of objects. However, bubbling events do not trigger event handlers on the parent objects by default. An event handler must declare that it is interested in receiving bubbled events by including the child: prefix in its event specification:

var parent = $$({
  controller: {
    'child:testevent': function(){
      alert('testevent fired in descendant');
    }
  }
});

Events can bubble up multiple levels. Only a single child: prefix is required to catch an event in any descendant of an obect.

Auto-proxying

All user-defined controllers initialized by the factory function $$() have their this auto-proxied to the owner MVC object, for quick access and consistent behavior no matter what context:

var obj = $$({msg:'I only exist because of auto-proxying'}, '<div/>', {
  'myHandler': function(){
    this.view.$().html( this.model.get('msg') );
  }
});
$$.document.append(obj);

// Without auto-proxying the 'this' in myHandler would be 'window'
setTimeout(obj.controller.myHandler, 100);

If necessary, properties from the original context should be passed to the controller function.

Inheritance

Agility adopts prototype-based (differential) inheritance. To create a new Agility object from an existing one, pass the latter as the prototype argument to the factory function; additional model, view, controller initializers are passed as usual:

var proto = $$({}, '<p data-bind="msg"/>', '& {color:red}');
var obj = $$(proto, {msg:'Hey there!'});
$$.document.append(obj);

You can also bypass differential inheritance (which overrides existing methods) and instead extend controllers with the tilde (~) syntax:

var proto = $$({}, '<button>Click me</button>', {
  'click &': function(){
    alert('First controller');
  }
});
var obj = $$(proto, {}, {}, {
  '~click &': function(){
    alert('Second controller');
  }
});
$$.document.append(obj);

Since derived objects reuse as much of their ancestors as possible, you can create large numbers of descendants from a prototype without worrying about memory bloat due to redundant storage:

// Prototype of cell object with empty model
var cell = $$({
  model: {},
  view: {
    format: '<div data-bind="num"/>', 
    style: '& { float:left; width:50px; cursor:pointer; text-align:center; }\
            &:hover { background:#ccf }'
  },
  controller: {
    'click &': function(){
      this.destroy();
    }
  }
});
// Matrix of cell objects, all stemming from prototype above
var matrix = $$({}, '<div>Click to erase number: <div id="wrapper"/></div>', {
  'create': function(){
    for (var i=0;i<500;i++) {
      // Inherits from cell
      var newCell = $$(cell, {num:i});
      this.append(newCell, '#wrapper');
    }
  }
});
$$.document.append(matrix);

Persistence

Model persistence, such as server-side and local HTML5 storage, is bundled with the library as the plugin persist. This is not included in the core so as to keep it as simple as possible.

Reference

Factory $$()

Creates a new MVC object from the given model, view, and controller arguments, and optionally a prototype object.

Compact syntax:

$$([model [,view-format [,controller]]])
$$([model [,view-format [, view-style [,controller]]]])
$$([model [,view [,controller]]])
$$(prototype, [model [,view-format [,controller]]])
$$(prototype, [model [,view-format [, view-style [,controller]]]])
$$(prototype, [model [,view [,controller]]])

Verbose syntax:

$$([prototype,] [{
  model: {...},
  view: {...},
  controller: {...},
  user_defined_property: {...}
}])

where:

Examples:

Different view initialization methods:

// One string: format
var person1 = $$({name:'Foo Bar'}, '<div data-bind="name"/>');
// Two strings: format, style
var person2 = $$({name:'Foo Bar'}, '<div data-bind="name"/>', '& { color:red; font-weight:bold; }');
// Object: format, style
var person3 = $$(
  { name:'Foo Bar' }, 
  { format: '<div data-bind="name"/>', style: '& { color:blue; }' }
);
// Verbose
var person4 = $$({
  model: {
    name: 'Foo Bar'
  },
  view: {
    format: '<div data-bind="name"/>',
    style: '& { color:green; font-style:italic; }'
  }
});
$$.document.append(person1);
$$.document.append(person2);
$$.document.append(person3);
$$.document.append(person4);

Specifying controller functions - compact:

var button = $$({}, '<p><button>Click me</button></p>', {
  'click button': function(){
    alert('You clicked me!');
  }
});
$$.document.append(button);

and verbose:

var dataHolder = $$({
  model: {
    first:'Joe', 
    last:'Doe'
  }, 
  view: {
    format: '<p>Wait...</p>'
  }, 
  controller: {
    'change:first': function(){
      alert('First name changed!');
    }
  }
});
$$.document.append(dataHolder);

setTimeout(function(){
  dataHolder.model.set({first:'Mary'});
}, 2000);

Core methods

.bind()

Binds function to event.

Syntax:

.bind(event, fn)

Returns:

Owner Agility object (for chainable calls).

.trigger()

Triggers event, optionally passing parameters to listeners.

Syntax:

.trigger(event [,params])

Returns:

Owner Agility object (for chainable calls).

.destroy()

Erases self view, removes self from parent container.

Syntax:

.destroy()

Returns:

Nothing.

.parent()

Returns the parent container to which the object has been added, or null if it has not yet been added to a container.

Syntax:

.parent()

Returns:

Parent Agility object.

The methods below are specific to the object container.

.append()

Adds an Agility object to the object's container, and appends its view to containing object's view.

Syntax:

.append(object [,selector])

Returns:

Owner Agility object (for chainable calls).

.prepend()

Adds an Agility object to the object's container, and prepends its view to containing object's view.

Syntax:

.prepend(object [,selector])

Returns:

Owner Agility object (for chainable calls).

.before()

Adds an Agility object to the object's container, and inserts its view before given selector of containing object's view.

Syntax:

.before(object, selector)

Returns:

Owner Agility object (for chainable calls).

.after()

Adds an Agility object to the object's container, and inserts its view after given selector of containing object's view.

Syntax:

.after(object, selector)

Returns:

Owner Agility object (for chainable calls).

.remove()

Removes an Agility object from the object's container. [This function should rarely be invoked by the user; call instead .destroy() within the object to be removed].

Syntax:

.remove(id)

Returns:

Owner Agility object (for chainable calls).

.each()

Iterates over each Agility object in the object's container.

Syntax:

.each(fn)

where:

Returns:

Owner Agility object (for chainable calls).

.empty()

Removes all Agility objects from object's container by issuing a .destroy() for each contained object.

Syntax:

.empty()

Returns:

Owner Agility object (for chainable calls).

.size()

Returns number of objects within the object's container.

Syntax:

.size()

Returns:

Number of Agility objects in the object's container.

Model methods

.model.set()

Sets the model data. If model already exists, it's extended.

Syntax:

.model.set(object [,params])

Returns:

Owner Agility object (for chainable calls).

.model.get()

Gets model data.

Syntax:

.model.get([property])

Returns:

Desired property content if property is specified, or a Javascript object containing the entire model data if it's omitted.

.model.reset()

Resets model to its original value (at object creation time).

Syntax:

.model.reset()

Returns:

Owner Agility object (for chainable calls).

.model.each()

Iterates over each model property.

Syntax:

.model.each(fn)

Returns:

Owner Agility object (for chainable calls).

.model.size()

Gets number of model properties.

Syntax:

.model.size()

Returns:

Number of model properties.

View methods

.view.$()

Shortcut to jQuery object corresponding to root element or to given selector in the current view.

Syntax:

.view.$([selector])

Returns:

jQuery object of root element if no selector, jQuery object at given selector otherwise, restricted to the current view's DOM.

.view.render()

Updates View's main jQuery object according to .view.format. Automatically called upon creation.

Syntax:

.view.render()

Returns:

Owner Agility object (for chainable calls).

.view.stylize()

Applies CSS dynamically according to .view.style. Automatically called upon creation.

Syntax:

.view.stylize()

Returns:

Owner Agility object (for chainable calls).

.view.sync()

Synchronizes all view elements with model contents, according to established bindings. Automatically called upon creation.

Syntax:

.view.sync()

Returns:

Owner Agility object (for chainable calls).

Controller methods

Built-in controllers are intended for internal use and typically shouldn't be called by the user.

Please refer to the factory function and event types for syntax and usage examples of user-defined controllers.

Globals

$$.document

Main Agility object representing the document's body.

Typically you just .append() a new Agility object to it.

Built-in plugins

persist

The plugin persist lets you save and retrieve models to/from a storage server using a given adapter. Persistence is always manual, i.e. needs to be explicitly called by user.

All methods fire the generic events:

as well as the method-specific events:

.persist()

Initializes persistence plugin, creates persistence methods for owner object.

Syntax:

.persist([adapter, params])

where:

If the adapter-params pair is not given, the only method that can be invoked is gather.

Returns:

Owner Agility object (for chainable calls).

.load()

Refreshes model from server, using the id in the model property id.

Syntax:

.load()

Examples:

Loads model from server:

var person = $$({id:123}, '<p>Name: <span data-bind="name"/></p>').persist($$.adapter.restful, {collection:'people'});

$$.document.append(person);
person.load();

Returns

Owner Agility object (for chainable calls), with updated model.

.save()

Updates model on the server if id is present in the model, creates a new resource otherwise.

Syntax:

.save()

If the resource is to be created (i.e. model has no id), the server is expected to send back the new id either in the body, e.g.

{"id":123}

or in the Location header as the new resource URL, e.g.:

Location: http://your-site.com/api/people/123

Agility will parse either to extract the new id, and set the model accordingly. That way, further calls to .save() will update the model on the server.

Examples:

Creates new model on server:

var person = $$({name:'Joe Doe'}, '<p>Name: <span data-bind="name"/></p>').persist($$.adapter.restful, {collection:'people'});

$$.document.append(person);
person.save();

Updates model on server:

var person = $$({id:123, name:'Joe Doe'}, '<p>Name: <span data-bind="name"/></p>').persist($$.adapter.restful, {collection:'people'});

$$.document.append(person);
person.save(); // will update, since 'id' exists

Returns

Owner Agility object (for chainable calls), with new model id (if created new resource).

.erase()

Erases model from server, using the id given in the model.

Syntax:

.erase()

Returns

Owner Agility object (for chainable calls).

.gather()

Loads a collection of models and appends/prepends into container, using given prototype.

Each gathered MVC object will be added to the container, appended/prepended to the view (depending on specified method), and will be a direct descendant of given prototype object. All persistence information, including collection name, should be initialized in the prototype object.

Syntax:

.gather(proto, method, [,selector] [,query])

where:

Examples:

Loads a collection of persons from server:

// Prototype
var person = $$({}, '<li data-bind="name"/>').persist($$.adapter.restful, {collection:'people'});

// Container
var people = $$({}, '<div>People: <ul/></div>').persist();
$$.document.append(people);

people.gather(person, 'append', 'ul');

Same as above, with load button and "Loading..." Ajax message:

// Prototype
var person = $$({}, '<li data-bind="name"/>').persist($$.adapter.restful, {collection:'people'});

// Container
var people = $$({
  model: {},
  view: {
    format: 
      '<div>\
        <span>Loading ...</span>\
        <button>Load people</button><br/><br/>\
        People: <ul/>\
      </div>',
    style:
      '& {position:relative}\
       & span {position:absolute; top:0; right:0; padding:3px 6px; background:red; color:white; display:none; }'
  }, 
  controller: {
    'click button': function(){
      this.empty();
      this.gather(person, 'append', 'ul');
    },
    'persist:start': function(){
      this.view.$('span').show();
    },
    'persist:stop': function(){
      this.view.$('span').hide();
    }
  }
}).persist();
$$.document.append(people);

Returns

Owner Agility object (for chainable calls), with container filled with new proto descendants.

$$.adapter.restful

RESTful adapter.

This adapter sends GET, POST, etc requests as per RESTful specs, and expects JSON responses.

The default base URL is api/, but it can be overridden at initialization time with the parameter {baseUrl:'your_url/'} passed to persist(). The collection name and/or resource id will be appended to form URLs like

api/resource
api/resource/123