Skip to content

Instantly share code, notes, and snippets.

@pindia
Created July 23, 2012 22:36
Show Gist options
  • Star 8 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save pindia/3166678 to your computer and use it in GitHub Desktop.
Save pindia/3166678 to your computer and use it in GitHub Desktop.
Step by step from jQuery to Agility.js

Step by step from jQuery to Agility.js

There's probably hundreds of Javascript MVC frameworks out there. But my favorite, Agility.js, seems to be barely ever mentioned. Agility has served me well in several projects both big and small, so I'm writing a tutorial in the spirit of Step by step from jQuery to Backbone for Agility.

What makes Agility different from other MVC frameworks? In short, the lack of boilerplate and the completeness. Other frameworks involve many, many lines of boilerplate that provide a significant barrier to entry and detract from the readability of the final product. Agility minimizes this, lets you get started fast, and stays out of your way. But it doesn't sacrifice either completeness or maintainability. Agility is feature-rich, including two-way model-view bindings, declarative controller-events, prototypical inheritance, server persistance, and everything else you'd expect from an MVC framework.

In this tutorial, we'll be working with the same initial code as the Backbone.js tutorial, modified slightly to run on JSFiddle.net (I'll be including links to working JSFiddles for each of the steps in this tutorial). This sample app allows the user to enter a status, posts it to the server, and adds it to a list in the DOM.

$(document).ready(function() {
    $('#new-status').submit(function(e) {
        e.preventDefault();
        $.ajax({
            url: '/echo/json/',
            type: 'POST',
            dataType: 'json',
            data: {'json': JSON.stringify({ text: $(this).find('textarea').val() })},
            success: function(data) {
                $('#statuses').append('<li>' + data.text + '</li>');
                $(this).find('textarea').val('');
            }
        });
    });
});

http://jsfiddle.net/hXZ9L/1/

The status item view

We're currently munging together HTML strings to create the <li> elements in our success callback. Let's replace this with an Agility object. Unlike some Javascript MVC frameworks that separate models, views, and controllers into separate objects, an Agility object combines all three concepts into a single object. We'll create a prototype called statusItemProto that represents a single status item:

+var statusItemProto = $$({
+    model: { text: '' },
+    view: { format: '<li data-bind="text" />' }
+});

 $(document).ready(function() {
     $('#new-status').submit(function(e) {
         e.preventDefault();

         $.ajax({
             url: '/echo/json/',
             type: 'POST',
             dataType: 'json',
             data: {'json': JSON.stringify({ text: $(this).find('textarea').val() })},
             success: function(data) {
-                $('#statuses').append('<li>' + data.text + '</li>');
+                $$.document.append( $$(statusItemProto, {
+                    model: {text: data.text}
+                }), '#statuses');
                 $(this).find('textarea').val('');
             }
         });
     });
 });

http://jsfiddle.net/hXZ9L/2/

Our statusItemProto defines a model with a single field text, and a view consisting of a single <li> tag. With the data-bind attribute on the <li> tag, we tell Agility that the contents of the element should be kept in sync with the text model attribute. Any changes to the model will automatically update the view.

In the success AJAX callback, we replace the raw jQuery append call. We first need to construct the statusItemProto instance to append to the DOM. The Agility factory function $$ takes an optional first argument: another Agility object to use as the prototype. We override the prototype's empty text model field with the data.text value. $$.document.append is the function used to append the new Agility object to the DOM proper. Its second argument is the jQuery selector for the element to append the object to.

Object persistence

Agility comes with a simple and flexible peristance plugin that allows Agility objects to be loaded from and saved to a server. We'll replace the use of jQuery's ajax with Agility persistance:

 var statusItemProto = $$({
     model: { text: ''}     ,
     view: { format: '<li data-bind="text" />' }
 });
+statusItemProto.persist($$.adapter.restful);

 $(document).ready(function() {
     $('#new-status').submit(function(e) {
         e.preventDefault();

-        $.ajax({
-            url: '/echo/json/',
-            type: 'POST',
-            dataType: 'json',
-            data: {'json': JSON.stringify({ text: $(this).find('textarea').val() })},
-            success: function(data) {
-                $$.document.append( $$(statusItemProto, {
-                    model: {text: data.text}
-                }), '#statuses');
-                $(this).find('textarea').val('');
-            }
-         });
+        var item = $$(statusItemProto, {
+            model: {text: $(this).find('textarea').val()}
+         });
+        item.bind('persist:save:success', function(){
+            $$.document.append(item, '#statuses');
+            $(this).find('textarea').val('');
+        });
+        item.save();
     });
 });

http://jsfiddle.net/hXZ9L/3/

The first step is to declare an Agility object as persistable with a call to the persist method. We'll use the built-in $$.adapter.restful adapter to interact with a RESTful Web service. In a real application you'll need to pass a second parameter to the persist method to define options like the base URL and collection (see the documentation).

We then need to break up the $$.document.append call, because we need to construct the status item before sending it to the server, but should only append it to the DOM when it is successfully sent. We construct the item as normal, then bind an event handler to the persist:save:success event. Agility defines a number of Agility events, special events that are dispatched by the framework. persist:save:success and persist:save:error are analogous to the success and error callbacks in jQuery's ajax, so we'll move all the code from success there. Finally, we call item.save() to start the process of saving it.

The status form view

We're currently binding the form's submit event imperatively in document.ready, and our entire codebase relies on a specific arrangement of DOM elements that isn't specified in the code at all. Let's fix these problems by continuing to move code into Agility objects. We'll create a new statusForm object that packages together the DOM structure of our status form and the its events:

 var statusItemProto = $$({
     model: { text: ''}     ,
     view: { format: '<li data-bind="text" />' }
 });
 statusItemProto.persist($$.adapter.restful);

+var statusForm = $$({
+    view: { format: '<form id="new-status"><textarea></textarea><input type="submit"><div id="statuses"></div></form>'},
+    controller: {
+        'submit &': function(e){
+            var _this = this;
+            e.preventDefault();
+
+            var item = $$(statusItemProto, {
+                model: {text: _this.view.$('textarea').val()}
+            });
+            item.bind('persist:save:success', function(){
+                $$.document.append(item, '#statuses');
+                _this.view.$('textarea').val('');
+            });
+            item.save();
+        }
+    }
+});

-$(document).ready(function() {
-    $('#new-status').submit(function(e) {
-        e.preventDefault();
-        var item = $$(statusItemProto, {
-            model: {text: $(this).find('textarea').val()}
-        });
-        item.bind('persist:save:success', function(){
-            $$.document.append(item, '#statuses');
-            $(this).find('textarea').val('');
-        });
-        item.save();
-    });
-});

+$(document).ready(function() {
+    $$.document.append(statusForm);
+});

http://jsfiddle.net/hXZ9L/4/

The statusForm object doesn't need a model because there's only ever one instance of it and nothing about it changes dynamically. We'll define a view for it that contains all of our app's HTML. For small amounts of HTML like this, embedding it directly into the Javascript file is simple and convenient, though for larger amounts it's advisable to place it elsewhere, like in a non-executable <script> tag, and reference it indirectly.

We come to the new part of statusForm, the controller. An Agility controller is an object that maps event strings to the handlers that should be called when the events occur. Controller methods are automatically proxied to their owner object, so this works correctly in the function bodies. DOM events are bound with a space-separated event string. The first component, in our case submit, is the jQuery event name. The second is a jQuery selector for the elements to which the event should be bound. The special selector & refers to the top-level element in the Agility object, in this case <form>.

Before, we could use $(this) to get the form element being submitted, because jQuery proxied the event handler to the element. However, Agility proxies the handler to the Agility object, so to get to the elements we need to use this.view.$(). This function, called with no arguments, returns the top-level element in the object, wrapped by jQuery. With an argument, it looks for elements matching the given selector in the object; this.view.$(sel) is essentially a shortcut for this.view.$().find(sel).

There's one final issue concerning our use of this. Agility proxies this in the submit & event handler, but not in the persist:save:success handler that is bound later. We need access to the statusForm in that handler, so we need to store it in _this beforehand.

Our document.ready function is now reduced to a single statement: adding statusForm to the DOM.

Two-way model binding

I said before that statusForm doesn't need a model because nothing about it changes. But that's not quite true: the status being entered by the user is part of the form, and it changes as the user is typing. This means if we make it a model field, we can take advantage of Agility's two-way model binding to avoid having to manually select the textarea and call its val() in jQuery.

 var statusItemProto = $$({
     model: { text: ''}     ,
     view: { format: '<li data-bind="text" />' }
 });
 statusItemProto.persist($$.adapter.restful);

 var statusForm = $$({
-    view: { format: '<form id="new-status"><textarea></textarea><input type="submit"><div id="statuses"></div></form>'},
+    model: { enteredText: ''},
+    view: { format: '<form id="new-status"><textarea data-bind="enteredText"></textarea><input type="submit"><div id="sta
     controller: {
         'submit &': function(e){
             var _this = this;
             e.preventDefault();

             var item = $$(statusItemProto, {
-                model: {text: _this.view.$('textarea').val()}
+                model: {text: _this.model.get('enteredText')}
             });
             item.bind('persist:save:success', function(){
                 $$.document.append(item, '#statuses');
-                _this.view.$('textarea').val('');
+                _this.model.set({enteredText: ''});
             });
             item.save();
         }
     }
 });

 $(document).ready(function() {
     $$.document.append(statusForm);
 });

http://jsfiddle.net/hXZ9L/5/

We added a new enteredText model field to the status form, and used data-bind to bind it to the input <textarea>. Now we can use this.model.get() to get the entered text, and this.model.set() to clear it after submission, and Agility will always keep it in sync with the value in the textarea. We could also bind to the Agility event change:enteredText to get an event every time the text changes, but we don't need that for this application.

Prototypical inheritance

We've been using Agility's prototypical inheritance for setting the initial values of model fields. But it's significantly more powerful than that; using inheritence we can also modify the view, add new controller methods, or extend existing methods. Let's take advantage of this to combine the three statements about item into one:

 var statusItemProto = $$({
     model: { text: ''}     ,
     view: { format: '<li data-bind="text" />' }
 });
 statusItemProto.persist($$.adapter.restful);

 var statusForm = $$({
     model: { enteredText: ''},
     view: { format: '<form id="new-status"><textarea data-bind="enteredText"></textarea><input type="submit"><div id="sta
     controller: {
         'submit &': function(e){
             var _this = this;
             e.preventDefault();

-            var item = $$(statusItemProto, {
-                model: {text: _this.model.get('enteredText')}
-            });
-            item.bind('persist:save:success', function(){
-                $$.document.append(item, '#statuses');
-                _this.model.set({enteredText: ''});
-            });
-            item.save();
+            $$(statusItemProto, {
+                model: {text: _this.model.get('enteredText')},
+                controller: {'persist:save:success': function(){
+                    _this.append(this, '#statuses');
+                    _this.model.set({enteredText: ''});
+                }}
+            }).save();
         }
     }
 });

 $(document).ready(function() {
     $$.document.append(statusForm);
 });

http://jsfiddle.net/hXZ9L/6/

Instead of using bind to bind an event after the creation of the object, we just declare the binding when constructing the object. There's no longer an item variable to close over in the persist:save:success handler, but since the handler is now a full controller method of the status item, it's automatically proxied to it, so we can just use this.

One final change we probably should have made earlier: we call _this.append rather than $$.document.append to add the new item to the DOM. All Agility objects have an append method (and also similar prepend, after, and before methods) that adds another object to its container. $$.document is simply a special Agility object that exists by default and whose view is the <body> tag. The status item should be appended to the status form so that it can be later accessed by Agility object methods like size() and each(), and so it is automatically destroyed if the status form is destroyed.

And we're done!

Here's our final code:

var statusItemProto = $$({
    model: { text: ''}     ,
    view: { format: '<li data-bind="text" />' }
});
statusItemProto.persist($$.adapter.restful);

var statusForm = $$({
    model: { enteredText: ''},
    view: { format: '<form id="new-status"><textarea data-bind="enteredText"></textarea><input type="submit"><div id="statuses"></div></form>'},
    controller: {
        'submit &': function(e){
            var _this = this;
            e.preventDefault();

            $$(statusItemProto, {
                model: {text: _this.model.get('enteredText')},
                controller: {'persist:save:success': function(){
                    _this.append(this, '#statuses');
                    _this.model.set({enteredText: ''});
                }}
            }).save();
        }
    }
});

$(document).ready(function() {
    $$.document.append(statusForm);
});

The code size has increased from 16 lines to 25. This small increase is more than made up for by increased abstraction and maintainability of the codebase. For example:

  • We've separated the concerns of the status item and status from each other, so if we wanted to change the markup associated with a status item, we change it statusItemProto without having to touch statusForm. Or if we want to add behavior to a status item, we can simply add a controller to the status item.
  • We're binding events declaratively, so if we wanted to get events as the user is typing, perhaps to implement a character counter, we can bind to the Agility model event change:enteredText instead of imperatively finding the textarea and binding to its 'change'
  • We're using Agility's persistance framework instead of hard-coding jQuery ajax, so if we wanted to save to localStorage instead of a server, we could just change $$.adapter.restful to $$.adapter.localStorage
  • We're using statusForm as an Agility container, so if we wanted to remove all the status items, we can just call empty() on it rather than manually selecting and removing all the <li> elements.

Because of its use of heavily nested objects in the $$ factory function, Agility benefits even more than the average Javascript code in readability from the use of CoffeeScript:

statusItemProto = $$
  model:
    text: ''
  view:
    format: '<li data-bind="text" />'
statusItemProto.persist $$.adapter.restful

statusForm = $$
  model:
    enteredText: ''
  view:
    format: '<form id="new-status"><textarea data-bind="enteredText"></textarea><input type="submit"><div id="statuses"></div></form>'
  controller:
    'submit &': (e) ->
      _this = this
      e.preventDefault()

      $$(statusItemProto,
        model:
          text: _this.model.get('enteredText')
        controller:
          'persist:save:success': ->
            _this.append this, '#statuses'
            _this.model.set enteredText: ''
      ).save()

$(document).ready ->
  $$.document.append statusForm

Want to learn more?

Hopefully this tutorial has helped introduce you to Agility.js. I've covered most of the Agility features, but have only scratched the surface in terms of the interesting ways that they can be combined. Unfortunately there's not much in the way of tutorials and examples online outside of the official site and official documentation. Please let me know if you come across anything else.

Once you understand the high-level concepts, the source code is well-commented and contained in a relatively short single file.

@himansudesai
Copy link

Awesome! Very useful - thank you so much

@esthersong
Copy link

Great tutorial! Thank you! :)

@eliot-akira
Copy link

This article was very helpful in learning about Agility.js. Its simplicity is refreshing even compared to Backbone, and so easy to get started. Really appreciate the effort in putting together a complete overview of features.

@SumeetGohil
Copy link

Very Nicely Explained

I have only on doubt

model data --> {"consumed":0, "limit": 1024}
format data --> out of

expected 0 out of 1024
result * out of 1024* --> not printing 0 value

I have to explicitly set 0 as '0' string then it is working

Any idea why it happened ?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment