Current version: 1.2.3
2.0 Beta Now Available
An opinionated Backbone application framework providing a filesystem structure, on demand module loading, model and collection view binding, inheritable view and DOM events, data loading helpers, form serialization / population and validation. Built using Backbone, Underscore, Zepto, Handlebars, Stylus and Lumbar.
Thorax can be used standalone but is designed to work best with Lumbar. The easiest way to setup a new Thoax + Lumbar project is with the thorax command line tool or by downloading the sample project it creates. To use the command line tools you'll need node and npm.
npm install -g lumbar thorax
thorax create project-name
cd project-name
npm start
This will create a hello world project, for a more complete example clone the Thorax Todos project (demo).
bin
- executable node scriptsgenerators
- code generation templates used by the command line interfacejs
- application code, models, collections, views, routerslumbar.json
- see Lumbar docsstatic
- static files / assets that will end up in thepublic
folderstyles
templates
thorax.json
- configuration for the command line interface
Start Thorax and create the Application.layout
object.
layout
- string css selector or Element where theApplication.layout
object will attach, defaults to.layout
scope
- object scope to configure, defaults to a new object in the global scopeApplication
templatePathPrefix
- Path where your templates are located. Defaults to "templates/"
In your lumbar.json
file you can specify the modules that compose your application. Each module is composed of routes, scripts, styles and templates. Thorax + Lumbar creates an internal router that listens to all routes in the application, lazily loading modules then calling a method on the router in that module as you would normally expect in a Backbone application.
Generate an Application.Router
subclass. The module
object will be automatically available inside the router file. Thorax expects that you create one router of the same name as the module, per module. In the example project there is a hello-world
module and a corresponding js/routers/hello-world.js
file.
Application.Router.create(module, {
index: function() {}
});
Each router method should redirect to another router method or call Application.layout.setView
Create a new view instance, looking it up by the name
property in the view's class definition.
Displays and manages views. By default there is only one layout object, Application.layout
which is created then attached to the page when Thorax.configure
is called. Additional layout objects having all of the same functionality as Application.layout
can be created by calling new Thorax.Layout()
.
Append the view to the Application.layout
object, displaying it on the page.
routerMethod: function(id) {
var view = this.view('view/name');
view.bind('ready', function() {
view.$('input:first')[0].focus();
});
Application.layout.setView(view);
}
This will trigger two events on the layout
object, both of which will receive the new view and the old view (if present):
change:view:start
- immediately aftersetView
callchange:view:end
- old view destroyed, new view in DOM and ready
By calling setView
on a layout object various events will be triggered on the view passed and the previous view that was passed if any.
initialize:before
- during constructor call, before initialize has been calledinitialize:after
- during construcor call, after initialize has been calledactivated
- immediately after setView was called with the viewready
- view.el attached to parentdeactivated
- setView called with the next view, view.el still attached to parentdestroyed
- after the view.el has been removed from parent, immediately before view.el and child views are destroyed
Layout objects listen for click a
on all elements inside them (therefore inside any views passed to setView
), triggering the corresponding route if one matches when clicked. Add a data-external
attribute on links you want to be ignored by anchorClick.
<a href="#/internal">Internal</a>
<a href="/external" data-external="true">External</a>
Thorax is primarily a view framework but provides Thorax.Model
and Thorax.Collection
classes which should be used when passed to setModel
or setCollection
. These are subclassed as Application.Model
and Application.Collection
in all of the example projects.
Calls fetch
on the model or collection ensuring the callbacks will only be called if the route does not change. callback
and failback
will be used as arguments to bindToRoute
. options
will be passed to the fetch
call on the model or collection if present.
routerMethod: function(id) {
var view = this.view('view/name');
var model = new Application.Model({id: id});
model.load(_.bind(function() {
//callback only called if browser still on this route
view.setModel(model);
Application.layout.setView(view);
}, this), function() {
//failback only called if browser has left this route
});
}
Used by model/collection.load
. Binds the callback to the current route. If the browser navigtates to another route in the time between when the callback is bound and when it is executed, callback will not be called. Else failback will be called if present.
routerMethod: function() {
var callback = this.bindToRoute(function() {
//callback called if browser is still on route
});
setTimeout(callback, 5000);
}
Thorax adds to Backbone's event handling by enhancing the view.events
hash, and providing a way of registering events for all views with registerEvents
and unregisterEvents
.
Thorax enhances the Backbone.View.events
hash handling in the following ways:
- accepts functions as a value to the hash in addition to a string method name
- accepts non DOM event names that will be treated as view events
- accepts a
collection
ormodel
hash of events that will be bound to the model or collection whensetModel
orsetCollection
are called, callbacks will be called with a context of the view instance
An example of a view implementing all of the above:
Application.View.extend({
name: 'view-name',
events: {
custom: '_onCustom',
'click a': '_onClick',
model: {
change: '_onChange'
},
collection: {
add: function(model){}
}
},
_onCustom: function(){},
_onClick: function(event){},
_onChange: function(){}
});
If a view has child views, the parent view by default will only listen for events triggered directly on the parent or DOM elements belonging directly to the parent, and not the children. The nested
keyword can be used in the events
hash or in a hash passed to registerEvents
to listen for events triggered by the parents or it's children.
Application.View.extend({
name: 'parent',
events: {
'nested eventName': function(view, arg) {
//called with a context of parent, the triggering
//view is always passed as the first argument followed
//by any other arguments passed to trigger, if any
},
'nested click': function(event) {
//always called with a context of parent
}
}
});
Add events to all instances of a view. Accepts a hash in the same format as described in Application.View.events
Application.View.registerEvents({
'click a': function() {
//called on any a click for all instances
//and subclasses of Application.View
}
});
Subclass = Application.View.extend({});
Subclass.registerEvents({
//events for all instances and subclasses of Subclass
});
Unregister events for all instances and subclasses of a given view class. Note that calling unregisterEvents
on Application.View
will unregister the built in events that make setModel
and setCollection
work.
Subclass.unregisterEvents(); //all events
Application.View.unregisterEvents('click a');
Application.View.unregisterEvents('model', 'change');
setModel
and setCollection
add event handlers to the view, call freeze to remove them. options
may contain a model
or collection
key that should contain the model or collection that was set with setModel
or setCollection
.
This method is never called directly, but can be specified to override the behavior of the events
hash or a hash passed to registerEvents
. For each event passed _addEvent
will be called with a hash containing:
- type "view" || "DOM"
- name (DOM events will begin with ".delegateEvents")
- originalName
- selector (DOM events only)
- handler
- nested (Boolean)
Thorax provides deep integration with Handlebars. By default one view maps to one Handlebars template of the same name. View attributes are made automatically availble to template as are model attributes if a model was set on a view with setModel
. Views having a collection set via setCollection
will look for corresponding view-name-item.handlebars
and view-name-empty.handlebars
templates. The view
and template
helpers are provided to allow the direct inclusion of other views or templates inside of templates.
Every view descending from Application.View must have a name attribute. render
will look for a corresponding handlebars template of the same name.
Application.View.extend({
name: 'view-name'
// templates/view-name.handlebars should exist
});
Application.Router
and Application.View
instances both have a view
method that will look up the view class by name and create new instance.
var instance = this.view('view-name');
Each DOM element on the page containing a view will have the name set on the data-view-name
attribute, allowing you to style your views with the following selector:
[data-view-name="view-name"] {
font-size: 2em;
}
Register a new helper that will be available in all handlebars templates. HTML generated from helpers should always be returned in a new Handlebars.SafeString
object.
Application.View.registerHelper('bold', function(content, options) {
//options.hash contains key, value pairs from named / html arguments
//to the helper
return new Handlebars.SafeString('<b>' + content + '</b>');
});
{{bold "Text"}}
Render a given template by file name sans extension. render
and renderCollection
both use this method. The scope inside of a template will contain all of the non function attributes of a view (which can be passed to the view constructor) and a cid
attribute which is a unique id for each rendering of a given template.
var klass = Application.View.extend({
name: 'view-name'
});
var view = new klass({
title: 'The Title'
});
console.log(view.template({
body: 'The Body'
}));
// templates/view-name.handlebars
<h1>{{title}}</h1>
<p>{{body}}</p>
This method is also available as a template helper, it will only render the template as a string, if there is a corresponding view it will not be initialized. The scope of the current template will be carried inward to the rendred template.
{{template "header" key="value"}}
<h1>{{title}}</h1>
<p>{{body}}</p>
{{template "footer"}}
Create a new view instance, looking it up by the name
property in the view's class definition.
Application.View.extend({
name: 'header'
});
Application.View.extend({
name: 'footer'
});
Application.View.extend({
name: 'main',
initialize: function() {
this.header = this.view('header');
}
});
This method is also available as a template helper which can receive a string name of a view to initialize and append, or a reference to an already initialized view.
// templates/main.handlebars
{{view header}}
<h1>{{title}}</h1>
<p>{{body}}</p>
{{view "footer"}}
Replace the HTML in a given view. The collection element and the child views appended by the {{view}}
helper will be automatically preserved if present.
Render a template with the filename of the view's name
attribute (sans extension), calling view.html()
with the result. Triggers the rendered
event.
To implement custom rendering behavior in a subclass override the method and pass a content
argument to render which may be an HTML string, DOM Element or an array of DOM Elements.
Application.View.extend({
name: 'child',
render: function() {
return Application.View.prototype.render.call(this, 'content');
}
});
Set the *modelattribute of the view. By default when the model is populated (either when it is passed to
setModelor after it is fetched) the
renderwill be called on the view, with the view's attributes and the model's
attributesavailable inside of the template. A
changeevent on the model (often triggered by the model's
setmethod) will cause the view to call
render` again.
fetch
- auto fetch the model if empty, defaults to true, if an object is passed it will be used as the options tofetch
success
- a callback to call when fetch() succeeds, defaults to falserender
- wether to call render after setModel and on the model's change event, defaults to truepopulate
- wether to auto call populate, defaults to true. if there is no form in the view populate will have no effecterrors
- wether to bubble the error event from the model to the view
setModel
will trigger the model set
event:
Application.View.extend({
name: 'view-name',
events: {
model: {
set: function(model) {}
}
}
});
Specify this function to override what attributes will be passed from a model set with setModel
to a template.
Application.View.extend({
name: 'view-name',
context: function(model) {
return _.extend({}, model.attributes, {
title: model.getTitle()
});
}
});
Set the collection attribute of the view. This will bind events on collection add
, remove
and reset
, updating the collection element (specified by the collection
view helper) as needed. options
may contain:
fetch
- auto fetch, defaults to true, if an object is passed it will be used as the options tofetch
success
- a callback to call when fetch() succeeds, defaults to falseerrors
- wether to bubble error events from the collection to the view, defaults to true
Collection rendering assumes that the following templates will be present.
templates/name.handlebars
- must contain the {{collection helper}}templates/name-item.handlebars
- must have at least one outer HTML elementtemplates/name-empty.handlebars
- must have at least one outer HTML element
To display a collection in your template use the {{collection}}
view helper. You can pass a custom tag
name (defaults to "div") or any HTML attributes.
{{collection tag="ul" class="my-list"}}
The following events will be triggered when the collection is rendered:
rendered:collection
- called whenrenderCollection
is called, receives the the collection elementrendered:item
- called for each item rendered in a non empty collection afterrenderCollection
is called, receives the item element or view after it has been renderedrendered:empty
- called whenrenderCollection
is called with an empty collection, receives the the collection element
Re-render the entire collection. If you need custom behavior when a collection is rendered it is better to use the rendered
or rendered:collection
events. This method looks for this.collection
which should be set by setCollection
and ignores any arguments passed.
Override this method to specify how an item is rendered. May return a string or another view.
renderItem: function(model) {
return new MyItemView({
model: model
});
}
renderItem: function(model, i) {
return this.template(this.name + '-item', this.itemContext(model, i));
}
Just like the context
method, but called for each item in the collection.
Override this method to specify what happens when renderCollection
is called when the collection is empty. May return a string or another view. The default implementation is:
renderEmpty: function() {
return this.template(this.name + '-empty');
}
Just like the context
method, but called when renderEmpty
is called.
Append and item at a given index. If no index is passed the index of the model in the current collection will be used, if the first argument is not a model, 0 will be used. item
may be:
- a model which will be passed to
renderItem
- an arbitrary html string which should contain exactly one outer element
- a view instance
Thorax provides helpers to assist with form handling, but makes no user interface decisions for you. Use the validate
and error
events to implement error messages in your application.
Application.View.registerEvents({
validate: function(attributes, errors) {
//clear previous errors if present
},
error: function(errors) {
errors.forEach(function(error) {
//lookup input by error.name
//display error from error.message
});
}
});
Serializes a form. callback
will receive the attributes from the form and will only be called if validateInput
returns nothing or an empty array. If an event
is passed a check will be run to prevent duplicate submission. options
may contain:
set
- defaults to true, wether or not to set the attributes if valid on a model if one was set withsetModel
validate - defaults to true, wether or not to call
validateInput` during serialization
Each form input in your application should contain a corresponding label. Since you may want to re-use the same form multiple times in the same view a cid
attribute with a unique value is provided to each render call of each template:
<label for="{{cid}}-last-name"/>
<input name="last-name" id="{{cid}}-last-name" value="Beastridge"/>
<label for="{{cid}}-last-name"/>
<input name="address[street]" value="123 Chestnut" id="{{cid}}-address[street]"/>
Phoenix.View.extend({
name: "address-form",
events: {
"submit form": "_handleSubmit"
},
_handleSubmit: function(event) {
this.serialize(event, function(attributes) {
attributes["last-name"] === "Beastridge";
attributes.address.street === "123 Chestnut";
});
}
});
serialize
Triggers the following events:
serialize
- called before validation with serialized attributesvalidate
- with an attributes hash and errors array aftervalidateInput
is callederror
- with an errors array, if validateInput returned an array with any errors
If your view uses inputs with non standard names (or no names, multiple inputs with the same name, etc), use the serialize
event:
this.bind('serialize', _.bind(function(attributes) {
attributes.custom = this.$('.my-input').val();
}, this));
Populate the form fields in the view with the given attributes. The keys of the attributes should correspond to the names of the inputs. populate
is automatically called with the response from view.context(view.model.attributes)
when setModel
is called.
view.populate({
"last-name": "Beastridge"
address: {
street: "123 Chestnut"
}
});
populate
triggers a populate
event. If your view uses inputs with non standard names (or no names, multiple inputs with the same name, etc), use this event:
this.bind('populate', _.bind(function(attributes) {
this.$('.my-input').val(attributes.custom);
}, this));
Validate the attributes created by serialize
, must return an array or nothing (if valid). It's recommended that the array contain hashes with name
and message
attributes, but arbitrary data or objects may be passed. If the array has a zero length the attributes are considered to be valid. Returning an array with any errors will trigger the error
event.
validateInput: function(attributes) {
var errors = [];
if (attributes.password && !attributes.password.match(/.{6,11}/)) {
errors.push({name: 'password', message: 'Invalid Password'});
}
return errors;
}
Create a named mixin. Callback will be called with the context of the view instance calling mixin
. methods
will be added to the view instance.
Application.View.registerMixin('mixin-name', function() {
}, {
methodName: function(){}
});
Application.View.extend({
name: 'view-name',
initialize: function() {
this.mixin('mixin-name');
}
});
Apply a given mixin by name. The mixin will only be applied once, thus duplicate calls mixin
with the same name
will not cause the mixin callback to be run multiple times.
It is possible to use the main thorax.js library completely standalone, but all of the documentation will assume you will be using a project structure created by the command line interface. To install the command line tools run:
npm install -g lumbar thorax
Create a new thorax project. All other thorax commands are run from inside the project directory.
thorax create todos
cd todos
Generate a router class and a module of the same name. A module is defined in lumbar.json and is composed of models, collections, views, templates, styles and a single router. Lumbar combines these files into a single js and single css file which are lazily loaded when one of the module's route's is visited. Running:
thorax router todos
- creates: app/routers/todos.js
- adds a JSON fragment for the todos module in lumbar.json
You'll need to fill in the routes hash inside lumbar.json with path: method name pairs to match your router class. This is how Lumbar / Thorax work together to lazily load your modules.
Generate a view class and template of the same name. Running:
thorax view todos header
- creates: app/views/header.js
- creates: app/templates/header.handlebars
- adds the appropriate JSON fragments in the main module in lumbar.json
Generate a view class which will render a collection and the appropriate templates of the same name. Running:
thorax collection-view todos todo-list
- creates: todo-list.js
- creates: app/templates/todo-list.handlebars
- creates: app/templates/todo-list-item.handlebars
- creates: app/templates/todo-list-empty.handlebars
- adds the appropriate JSON fragments in the main module in lumbar.json
Generate a model class. Running:
thorax model todos todo
- creates: app/models/todo.js
- adds the appropriate JSON fragments in the main module in lumbar.json
Generate a collection class. Running:
thorax collection todos todo-list
- creates: app/collections/todo-list.js
- adds the appropriate JSON fragments in the main module in lumbar.json
Starts your thorax application on port 8000, auto generating new JavaScript and CSS modules in public
as source files in your application change.
- load:start and load:end handling have been moved into a plugin
- nested event keyword now works with views, the callback will always be called with the context of the declaring view and will always recieve the triggering view as the first argument
- empty() the collection element before renderCollection()
- added {{collection}} helper
- _collectionSelector is now deprecated and internally defaults to [data-collection-cid], for backwards compatibility set it to ".collection" in your view classes
- added templatePathPrefix option to configure()
- unit tests!
- added nested event keyword
- added _addEvent method for subclasses to customize event registration
- registerEvents is now an instance method in addition to a class method
- added emptyContext method, called from renderEmpty
- checks for view.name property are now lazy
- exceptions are now thrown instead of using console.error