README.md

batman.js

batman.js is a framework for building rich single-page browser applications. It is written in CoffeeScript and its API is developed with CoffeeScript in mind, but of course you can use plain old JavaScript too.

You can observe the event with some callback, and fire it by just calling the event function directly. The observer callback gets whichever arguments were passed into the event function. But if the even function returns false, then the observers won't fire:

By default, these properties are stored like plain old JavaScript properties: that is, gadget.name would return "Batarang" just like you'd expect. But if you set the gadget's name with gadget.name = 'Shark Spray', then the observer function you set on gadget will not fire. So when you're working with batman.js properties, use get/set/unset to read/write/delete properties.

Custom Accessors

What's the point of using gadget.get 'name' instead of just gadget.name? Well, a Batman properties doesn't need to correspond with a vanilla JS property. Let's write a Box class with a custom getter for its volume:

batman.js's dependency tracking system makes sure that no matter how weird your object graph gets, your observers will fire exactly when they should.

Architecture

The MVC architecture of batman.js fits together like this:

Controllers are persistent objects which render the views and give them mediated access to the model layer.

Views are written in pure HTML, and use data-* attributes to create bindings with model data and event handlers exposed by the controllers.

Models have validations, lifecycle events, a built-in identity map, and can use arbitrary storage backends (Batman.LocalStorage and Batman.RestStorage are included).

A batman.js application is served up in one page load, followed by asynchronous requests for various resources as the user interacts with the app. Navigation within the app is handled via hash-bang fragment identifers, with pushState support forthcoming.

The App Class

Sitting in front of everything else is a subclass of Batman.App which represents your application as a whole and acts as a namespace for your other app classes. The app class never gets instantiated; your main interactions with it are using macros in its class definition, and calling run() on it when it's time to fire up your app.

The @global yes declaration just makes the class global on the browser's window object.

The calls to @controller and @model load external app classes with XHRs. For the controllers, this ends up fetching /controllers/app_controller.coffee and /controllers/gadgets_controller.coffee. The gadget model gets loaded from /models/gadget.coffee.

Routes

Routes are defined in a few different ways.

@route takes two strings, one representing a path pattern and the other representing a controller action. In the above example, 'faq/:questionID' matches any path starting with "/faq/" and having one other segment. That segment is then passed as a named param to the controller action function specified by the second string argument.

For the FAQ route, 'app#faq' specifies the faq function on BatBelt.AppController, which should take a params argument and do something sensible with params.questionID.

@root 'app#index' is just a shorthand for @route '/', 'app#index'.

The @resources macro takes a resource name which should ideally be the underscored-pluralized name of one of your models. It sets up three routes, as if you'd used the @route macro like so:

In addition to setting up these routes, the call to @resources keeps track of the fact that the Gadget model can be accessed in these ways. This lets you load these routes in your controllers or views by using model instances and classes on their own:

Now when you navigate to /#!/faq/what-is-art, the dispatcher runs this faq action with {questionID: "what-is-art"}. It also makes an implicit call to @render, which by default will look for a view at /views/app/faq.html. The view is rendered within the main content container of the page, which is designated by setting data-yield="main" on some tag in the layout's HTML.

Controllers are also a fine place to put event handlers used by your views. Here's one that uses jQuery to toggle a CSS class on a button:

Views

You write views in plain HTML. These aren't templates in the usual sense: the HTML is rendered in the page as-is, and you use data-* attributes to specify how different parts of the view bind to your app's data. Here's a very small view which displays a user's name and avatar:

The data-bind attribute on the <p> tag sets up a binding between the user's name property and the content of the tag. The data-bind-src attribute on the <img> tag binds the user's avatarURL property to the src attribute of the tag. You can do the same thing for arbitrary attribute names, so for example data-bind-href would bind to the href attribute.

batman.js uses a bunch of these data attributes for different things:

Binding properties

data-bind="foo.bar": for most tags, this defines a one-way binding with the contents of the node: when the given property foo.bar changes, the contents of the node are set to that value. When data-bind is set on a form input tag, a two-way binding is defined with the value of the node, such that any changes from the user will update the property in realtime.

data-bind-foo="bar.baz": defines a one-way binding from the given property bar.baz to any attribute foo on the node.

data-foreach-bar="foo.bars": used to render a collection of zero or more items. If the collection descends from Batman.Set, then the DOM will be updated when items are added, removed. If it's a descendent of Batman.SortableSet, then its current sort.

Handling DOM events

data-event-click="foo.bar": when this node is clicked, the function specified by the keypath foo.bar is called with the node object as the first argument, and the click event as the second argument.

data-event-change="foo.bar": like data-event-click, but fires on change events.

data-event-submit="foo.bar": like data-event-click, but fires either when a form is submitted (in the case of <form> nodes) or when a user hits the enter key when an <input> or <textarea> has focus.

Managing contexts

data-context="foo.bar": pushes a new context onto the context stack for children of this node. If the context is foo.bar, then children of this node may access properties on foo.bar directly, as if they were properties of the controller.

Rendering Views

data-yield="identifier": used in your layout to specify the locations that other views get rendered into when they are rendered. By default, a controller action renders each whole view into whichever node is set up to yield "main". If you want some content in a view to be rendered into a different data-yield node, you can use data-contentfor.

data-contentfor="identifier": when the view is rendered into your layout, the contents of this node will be rendered into whichever node has data-yield="identifier". For example, if your layout has "main" and "sidebar" yields, then you may put a data-contentfor="sidebar" node in a view and it will be rendered in the sidebar instead of the main content area.

data-partial="/views/shared/sidebar": renders the view at the path /views/shared/sidebar.html within this node.

data-route="/some/path" or data-route="some.model": loads a route when this node is clicked. The route can either be specified by a path beginning with a slash "/", or by a property leading to either a model instance (resulting in a resource's "show" action) or a model class (for the resource's "index" action).

Models

batman.js models:

can persist to various storage backends

only serialize a defined subset of their properties as JSON

use a state machine to expose lifecycle events

can validate with synchronous or asynchronous operations

Attributes

A model object may have arbitrary properties set on it, just like any JS object. Only some of those properties are serialized and persisted to its storage backends, however. You define persisted attributes on a model with the encode macro:

Given one or more strings as arguments, @encode will register these properties as persisted attributes of the model, to be serialized in the model's toJSON() output and extracted in its fromJSON(). Properties that aren't specified with @encode will be ignored for both serialization and deserialization. If an optional coder object is provided as the last argument, its encode and decode functions will be used by the model for serialization and deserialization, respectively.

By default, a model's primary key (the unchanging property which uniquely indexes its instances) is its id property. If you want your model to have a different primary key, specify it with the @id macro:

classUserextendsBatman.Model@encode'handle', 'email'@id'handle'

States

empty: a new model instance remains in this state until some persisted attribute is set on it.

loading: entered when the model instance's load() method is called.

loaded: entered after the model's storage adapter has completed loading updated attributes for the instance. Immediately transitions to the clean state.

dirty: entered when one of the model's persisted attributes changes.

validating: entered when the validation process has started.

validated: entered when the validation process has completed. Immediately after entering this state, the model instance transitions back to either the dirty or clean state.

saving: entered when the storage adapter has begun saving the model.

saved: entered after the model's storage adapter has completed saving the model. Immediately transitions to the clean state.

clean: indicates that none of an instance's attributes have been changed since the model was saved or loaded.

destroying: entered when the model instance's destroy() method is called.

destroyed: indicates that the storage adapter has completed destroying this instance.

Validation

Before models are saved to persistent storage, they run through any validations you've defined and the save is cancelled if any errors were added to the model during that process.

Validations are defined with the @validate macro by passing it the properties to be validated and an options object representing the particular validations to perform:

The options get their meaning from subclasses of Batman.Validator which have been registered by adding them to the Batman.Validators array. For example, the maxLength and lengthWithin options are used by Batman.LengthValidator.

Persistence

To specify a storage adapter for persisting a model, use the @persist macro in its class definition:

classProductextendsBatman.Model@persistBatman.LocalStorage

Now when you call save() or load() on a product, it will use the browser window's localStorage to retrieve or store the serialized data.

If you have a REST backend you want to connect to, Batman.RestStorage is a simple storage adapter which can be subclassed and extended to suit your needs. By default, it will assume your CamelCased-singular Product model is accessible at the underscored-pluralized "/products" path, with instances of the resource accessible at /products/:id. You can override these path defaults by assigning either a string or a function-returning-a-string to the url property of your model class (for the collection path) or to the prototype (for the member path). For example: