Composer.js

Controller

The controller is the piece that ties your view (generally HTML rendered in a browser) to your models/collections.

It’s main function is to listen to events from both the models (updating the view according to new data) and to the view (clicks, form posts, etc). It is the piece that coordinates between your view (the DOM) and your data layer (your models).

It also provides a handful of utilities to make rendering views and cleaning things up a bit easier.

Note: Composer doesn’t actually have a templating engine. You are free to use any engine you wish, or none at all (hand-made HTML strings).

Events

These are the event(s) fired by controllers.

release

Triggered when a controller is released (removed from the view). Arguments passed:

  • the controller being released

Composer.Controller

The main controller class. Extends Composer.Base, giving it access to all of Base’s abilities.

el :: attribute(false)

A param holding the current DOM element the controller is attached to. This is the element the controller renders into and removes when it is released.

If this param is a string, it is assumed to be a selector and that element is used as the el. If not found, a new element of type tag is created.

inject :: attribute(false)

A param holding a selector (or element) that we want to inject the controller’s main element into on creation.

tag :: attribute(“div”)

The type of tag that el will be created as. For instance, if the controller was to describe an item in a list, you might set tag to be “li”.

class_name :: attribute(false)

If present, sets this.el.className on initialization, providing an easy way to give a CSS classname to the controller’s main element.

elements :: attribute({})

An object that provides selector -> attribute mapping. Note that the elements found by the selectors are searched for within the el element and are updated after calling html.

var MyController = Composer.Controller.extend({
    elements: {
        'h1': 'the_title',
        'p': 'the_text'
    },

    init: function()
    {
        this.render();
    },

    render: function()
    {
        this.html('<h1>Test</h1><p>Hello</p>');
    }
});
var con = new MyController();
alert('Text: '+ con.the_text.innerHTML);

events :: attribute({})

An object that provides event monitoring for elements within el. It sets up events in ‘ [selector]' -> 'function_name' mapping:

var MyController = Composer.Controller.extend({
    events: {
        'click h1': 'click_title',
        'click input[type=button]': 'close'
    },

    init: function()
    {
        var tpl = '<input type="button" value="Close" style="float:right">';
        tpl += '<h1>Click me!</h1>';
        this.html(tpl);
        this.el.style.background = '#fcc';
    },

    click_title: function(e)
    {
        this.el.style.background = '#cfc';
    },

    close: function(e)
    {
        this.release();
    }
});
new MyController({ inject: '#event-test' });

initialize :: function(params, options)

Controller constructor. Don’t extend unless you know what you’re doing (use init) instead.

params is a collection of parameters to set (top-level) into the controller. For instance you can choose your el or inject selector here.

options can contain the following items:

  • clean_injection - If true, will clear the object that contains the controller’s el before injecting the el. Otherwise (the default) is to append the controller into the container (at the end).
var MyController = Composer.Controller.extend({
    model: null,

    init: function()
    {
        if(!this.model) return this.release();
        ...
    }
});
// all parameters can be changed on instantiation, although it's recommended to
// keep the event/element/function definitions in your class definition and use
// instantiation parameters for more temporary data. here we set the inject and
// model params (very common to change on instantiation):
var mymodel = new Composer.Model({ says: 'hello' });
var con = new MyController({
    inject: '#setting-on-initialize',
    model: mymodel
});

init :: function()

Per-controller initialization function. Gets run on each controller instantiation. Override me!

var MyController = Composer.Controller.extend({
    num_messages: false,
    init: function()
    {
        alert('Hello, Mr. Wayne. You have '+ this.num_messages +' new messages.');
    }
});
var con = new MyController({ num_messages: 17 });

render :: function()

Your controller’s render function. This function’s purpose is to generate the view for your controller and update it (probably using html). Override me!

var MyController = Composer.Controller.extend({
    init: function()
    {
        this.render();
    },

    render: function()
    {
        // you can generate your HTML any way you like...pure javascript strings
        // or some form of templating engine. here we do strings for simplicity.
        this.html('<h1>HELLO!!</h1>');
    }
});
var con = new MyController({ inject: '#render-test' });

html :: function(element_or_string, options)

Update the controller’s el’s inner HTML. This also refreshes the controller’s elements, making sure they reflect the elements in the passed HTML.

See render for example usage.

Also see the xdom documentation for an overview of how to make html much faster and easier to use. Composer’s xdom system runs patches against the DOM instead of completely replacing it (like html does by default) which can make managing your views much, much easier.

Note that this function can take document fragments as of v1.1.12.

For an overview of what options can contain, check out the html() section of the xdom docs.

with_bind :: function(object, ev, fn, name)

This function wraps object’s bind and tracks the binding internally in the controller. When release is called on the controller, all the bindings created with with_bind are unbound automatically.

This is mainly to alleviate a common pattern of having to do manually clean up bound events in a custom release function. When your controller binds to events on, say, a model, it previously had to remember to unbind the event otherwise the controller would never be garbage collected (even after all references to it are gone from your app) because the model still holds a reference to its function(s) that were bound.

Note: objects passed to with_bind must extend Composer.event.

Here’s an example of the traditional way of binding/cleaning up:

var MyController = Composer.Controller.extend({
    model: false,

    init: function()
    {
        // re-render when the model's data changes. note we have to name the
        // binding so we can reference it later
        this.model.bind('change', this.render.bind(this), 'model:change:render');

        // manually unbind our 'change' event on release
        this.bind('release', function() {
            this.model.unbind('change', 'model:change:render');
        }.bind(this));
    }
});

Here’s how it works now:

var MyController = Composer.Controller.extend({
    model: false,

    init: function()
    {
        // notice we don't need to use a named event here because the controller
        // mangages the binding for us. you can still specify a name if you plan
        // to unbind the event yourself, but it's optional.
        this.with_bind(this.model, 'change', this.render.bind(this));
    }
});

with_bind_once :: function(object, ev, fn, name)

This function is exactly like with_bind, except that instead of calling object.bind, it calls object.bind_once. This lets you add one-time events on any object that extends Composer.Event without worrying about cleaning them up if your controller is released.

See with_bind for full usage examples.

Note: objects passed to with_bind_once must extend Composer.Event.

sub :: function(name, [create_fn])

A common pattern is for a controller to have one or more subcontrollers. So you may have a Dashboard controller and it could have a UserList controller and a RecentComments controller.

When the Dashboard controller releases, you want it to release its subcontrollers automatically. sub does this for you! It keeps track of subcontrollers you are using and frees them on release.

name is the (string) name you wish to give your subcontroller. It must be unique from other subcontrollers you’re tracking. This is because the name is used to release and re-create the subcontroller if it already exists.

create_fn is a function of zero arguments that must return a controller instance. This function is where you create, set up, and return your subcontroller.

If you omit create_fn, sub will return the controller stored under name.

Note that sub will check if a controller is already stored under the given name. If one exists, it is released and removed from tracking before the new one is put into its place. This is very useful in situations where your master controller re-creates its subcontrollers on render and you don’t want to have to worry about dangling controllers not being cleaned up. subs handles everything for you.

var UserListController = Composer.Controller.extend({
    init: function()
    {
        this.render();
    },

    render: function()
    {
        this.html('<ul><li>user1</li><li>user2</li>...</ul>');
    }
});
var DashboardController = Composer.Controller.extend({
    elements: {
        'div.users': 'user_list'
    },

    init: function()
    {
        this.render();
    },

    render: function()
    {
        this.html('<h1>Dashboard</h1><div class="users"></div>');
        // track our UserListController. if DashboardController renders again,
        // the subcontroller will be released and recreated. if the Dashboard
        // is released, so is the subcontroller.
        this.sub('users', function() {
            return new UserListController({
                // put the subcontroller into our <div class="users"> element
                inject: this.user_list
            });
        }.bind(this));

        // we can use `sub` the pull the subcontroller out
        this.sub('users').bind('rendered', function() {
            console.log('rendered!');
        });
    }
});

In the above, DashboardController will automatically clean its UserListController subcontroller each time it renders (or when it’s released) so you can focus on building your app and not a bunch of boilerplate cleanup BS.

remove :: function(name, options)

Recently used sub? Use remove is a helper function that removes and releases a tracked subcontroller by the name given in the call to sub. It allows you to manually untrack and release a subcontroller without having to pull it out via sub and check if it exists.

options can contain the following items:

  • skip_release - If true, remove the subcontroller from the parent’s tracking without releasing it. There are very few cases where this would ever be needed.

release :: function(options)

Remove the controller from the DOM (removes el) and perform any cleanup needed (such as unbinding events bound using with_bind).

Fires the release event.

Note that options can contain silencing directives.

See the events section for a release example.

replace :: function(element)

Replace the controller’s el with another DOM element. Once the replace is complete, the elements and events are refreshed for the controller.

Composer.find_parent :: function(selector, element)

This is a utility for (recursively) finding the first parent of element that matches the selector. If element matches selector, it is returned without grabbing any parent elements.

For instance, if you bind an event to an <a> element but there is an image/button/etc inside of the <a>, often times when the event comes through, event.target will be the image/button/etc instead of the <a> element. In that case, you’d use find_parent to grab the correct element:

var ParentTestController = Composer.Controller.extend({
    events: {
        'click a.say-hi': 'say_hi'
    },

    init: function()
    {
        this.render();
    },

    render: function()
    {
        var html = '';
        html += '<div>';
        html += '<a href="#" class="say-hi" rel="larry"><img src="http://killtheradio.net/images/uploads/turtl.png" width="128" height="128"><br>CLICK THIS ^</a>';
        html += '</div>';
        this.html(html);
    },

    say_hi: function(e)
    {
        e.preventDefault();
        e.stopPropagation();

        // instead of trusting e.target is the element we want, make sure of it.
        var atag = Composer.find_parent('a.say-hi', e.target);
        var name = atag.getAttribute('rel');
        alert('Hello, '+ name);
    }
});
var test = new ParentTestController({inject: '#example-find-parent'});