MVC framework making it easy to write realtime, collaborative applications that run in both Node.js and browsers.
var app = require('derby').createApp(module);
// Templates define both HTML and model <- -> view bindings
app.view.make('Body'
, 'Holler: <input value="{message}"><h1>{message}</h1>'
);
// Routes render on client as well as server
app.get('/', function (page, model) {
// Subscribe specifies the data to sync
model.subscribe('message', function () {
page.render();
});
});
var express = require('express')
, expressApp = express()
, server = require('http').createServer(expressApp);
// The server-side store syncs data over Socket.IO
var store = require('derby').createStore({listen: server});
expressApp
.use(express.static(__dirname + '/public'))
// The store creates models for incoming requests
.use(store.modelMiddleware())
// App routes create an Express middleware
.use(require('./hello').router());
server.listen(3000);
app = require('derby').createApp module
# Templates define both HTML and model <- -> view bindings
app.view.make 'Body',
'Holler: <input value="{message}"><h1>{message}</h1>'
# Routes render on client as well as server
app.get '/', (page, model) ->
# Subscribe specifies the data to sync
model.subscribe 'message', ->
page.render()
express = require 'express'
expressApp = express()
server = require('http').createServer expressApp
# The server-side store syncs data over Socket.IO
store = require('derby').createStore listen: server
expressApp
.use(express.static __dirname + '/public')
# The store creates models for incoming requests
.use(store.modelMiddleware())
# App routes create an Express middleware
.use(require('./hello').router())
server.listen 3000
Derby includes a powerful data synchronization engine called Racer. While it works differently, Racer is to Derby somewhat like ActiveRecord is to Rails. Racer automatically syncs data between browsers, servers, and a database. Models subscribe to changes on specific objects and queries, enabling granular control of data propagation without defining channels. Racer supports offline usage and conflict resolution out of the box, which greatly simplifies writing multi-user applications. Derby makes it simple to write applications that load as fast as a search engine, are as interactive as a document editor, and work offline.
HTML templates: Handlebars-like templates are rendered into HTML on both the server and client. Because they render on the server, pages display immediately—even before any scripts are downloaded. Templates are mostly just HTML, so designers can understand and modify them.
View bindings: In addition to HTML rendering, templates specify live bindings between the view and model. When model data change, the view updates the properties, text, or HTML necessary to reflect the new data. When the user interacts with the page—such as editing the value of a text input—the model data updates.
Client and server routing: The same routes produce a single-page browser app and an Express server app. Links render instantly with push/pop state changes in modern browsers, while server rendering provides access to search engines and browsers without JavaScript.
Model syncing: Model changes are automatically synchronized with the server and all clients subscribed to the same data over Socket.IO.
Customizable persistence: Apps function fully with in-memory, dynamic models by default. Apps can also use the racer-db-mongo plugin to add MongoDB support with no change to the application code. Any changes to data made within the app are automatically persisted. Adding support for other databases is simple.
Conflict resolution: The server detects conflicts, enabling clients to respond instantly and work offline. Multiple powerful techniques for conflict resolution are included.
Derby represents a new breed of application frameworks, which we believe will replace currently popular libraries like Rails and Backbone.
Adding dynamic features to apps written with Rails, Django, and other server-side frameworks tends to produce a tangled mess. Server code renders various initial states while jQuery selectors and callbacks desperately attempt to make sense of the DOM and user events. Adding new features typically involves changing both server and client code, often in different languages.
Many developers now include a client MVC framework like Backbone to better structure client code. A few have started to use declarative model-view binding libraries, such as Knockout and Angular, to reduce boilerplate DOM manipulation and event bindings. These are great concepts, and adding some structure certainly improves client code. However, they still lead to duplicating rendering code and manually synchronizing changes in increasingly complex server and client code bases. Not only that, each of these pieces must be manually wired together and packaged for the client.
Derby radically simplifies this process of adding dynamic interactions. It runs the same code in servers and browsers, and it syncs data automatically. Derby takes care of template rendering, packaging, and model-view bindings out of the box. Since all features are designed to work together, no code duplication and glue code are needed. Derby equips developers for a future when all data in all apps are realtime.
Derby eliminates the tedium of wiring together a server, server templating engine, CSS compiler, script packager, minifier, client MVC framework, client JavaScript library, client templating and/or bindings engine, client history library, realtime transport, ORM, and database. It eliminates the complexity of keeping state synchronized among models and views, clients and servers, multiple windows, multiple users, and models and databases.
At the same time, it plays well with others. Derby is built on top of popular libraries, including Node.js, Express, Socket.IO, Browserify, Stylus, LESS, UglifyJS, MongoDB, and soon other popular databases and datastores. These libraries can also be used directly. The data synchronization layer, Racer, can be used separately. Other client libraries, such as jQuery, and other Node.js modules from npm work just as well along with Derby.
When following the default file structure, templates, styles, and scripts are automatically packaged and included in the appropriate pages. In addition, Derby can be used via a dynamic API, as seen in the simple example above.
See source and installation instructions for the demos
The demos for the 0.3 version are no longer hosted online
Derby and Racer are alpha software. While Derby should work well enough for prototyping and weekend projects, it is still undergoing major development. APIs are subject to change.
New projects should use the 0.5 version.
As with all Node.js modules, first install Node. The Node installer will also install npm.
Install Derby with:
$ npm install -g derby
Derby includes a simple project generator:
$ cd ~
$ derby new first-project
$ cd first-project
or, for CoffeeScript:
$ cd ~
$ derby new --coffee first-project
$ cd first-project
$ make
make will execute the coffee compiler with the watch option, so leave it running in a separate terminal.
Then, simply fire up Node:
$ node server.js
The default file structure is:
/lib
/app
index.js
/server
index.js
serverError.js
/public
/img
/gen
/styles
/app
index.styl
404.styl
base.styl
reset.styl
/views
/app
index.html
404.html
/ui
/connectionAlert
index.html
index.js
index.js
.gitignore
package.json
README.md
server.js
In CoffeeScript projects, the lib directory is generated by the compiler, and script files should be edited in the src directory instead. The project generator will create a Makefile for compiling CoffeeScript projects.
Derby uses a filename based convention similar to Node.js modules. A file named demo.js and a directory demo containing a file index.js both define an app with the name “demo.” The same applies for styles and views, which can either be demo.styl or demo\index.styl and demo.html or demo\index.html.
Apps are associated with their respective styles and views by filename only. Derby automatically includes them when rendering. Both support importing, so shared styles and templates may be defined in separate files.
Static files can be placed in the public folder. (Note that the contents of the public folder map to the root URL, so the image stored at the file public/img/logo.png would be served from the URL /img/logo.png.) The default Express server created by the Derby project generator sets a cache time of one year for all static files. Therefore, new file versions must be given new filenames. Derby compiles scripts for the browser into the public\gen folder by default. Each script’s filename is generated from a hash, so that it can be cached long term.
The ui directory contains a component library, which can be used to create custom components for the containing project. These are re-usable templates, scripts, and styles that can be used to create custom HTML tags for use in applications. General purpose component libraries can be created as separate npm modules. See Component Libraries.
Derby projects support one or more single-page apps as well as static pages. Apps have a full MVC structure, including a model provided by Racer, a template and styles based view, and controller code with application logic and routes (which map URLs to actions). Static pages consist of only templates and styles.
On the server, apps provide a router middleware for Express. One or more app routers as well as server only routes can be included in the same Express server.
Derby packages up all of an app’s templates, routes, and application code when rendering. Regardless of which app URL the browser requests initially, the app is able to render any other state within the same application client-side. If the app cannot handle a URL, it will fall through and request from the server. Errors thrown during route handling also cause requests to fall through to the server.
Derby works great with only a single app, though developers may wish to create separate apps if only certain sets of pages are likely to be used together. For example, a project might have a separate desktop web app and mobile web app. Or a project might have an internal administration panel app and a public content app.
Apps are created in the file that defines the app’s controller code. They are then associated with a server by requiring the app within the server file.
app =derby.createApp( module )module: Derby uses the module object to create an app. The app’s name is taken from its filename, and Derby exports a number of methods on the app.
app: Returns an app object, which is equivalent to
module.exports.
The app’s filename is used to determine the name of the app. App names are used to automatically associate an app with template and styles files of the same name.
The app name is also used as the name of the global variable that the application exposes in the browser. Therefore, app names should be valid JavaScript variable names, starting with a letter and containing only alphanumeric characters and underscores.
The createApp method adds a number of methods to the app. On both the client and the server, these are view, render, ready, get, post, put, del, and hook. On the server only, Derby also adds router, createStore, and session.
The Derby project generator outputs an Express server for a typical setup. Because Derby shares most code between server and client, Derby server files can be very minimal.
The server includes an app with a standard Node.js require statement. It can then use the app.router() method to create a router middleware for Express that handles all of the app’s routes.
The server also needs to create a store object, which is what sets up Socket.IO, creates models, coordinates data syncing, and interfaces with databases. Stores are created via the derby.createStore() method. See Creating stores.
Derby can also render static pages from templates and styles not associated with an app. This is useful for error pages and other pages that don’t need dynamic content.
staticPages =derby.createStatic( root )root: The root path that contains the “views” and “styles” directories.
staticPages: Returns a staticPages object, which has a render method. (While unused, static is a reserved JavaScript keyword, and it cannot be a variable name.)
The staticPages object keeps a reference to the directory root and provides a staticPages.render() method. It is intended for use in server-only Express routes. See Rendering.
Typically, writing Derby apps begins with HTML templates. These templates define the rendered HTML as well as model-view bindings.
Derby compiles a collection of HTML-based templates into a page based on a number of pre-defined names. Pages usually define at least a Title and Body template. Templates may be created programmatically via the view.make() method:
var view = require('derby').createApp(module).view;
view.make('Body', '<h1>Howdy!</h1>');
{view} = require('derby').createApp module
view.make 'Body', '<h1>Howdy!</h1>'
However, they are generally placed in template files within the views directory. Each app automatically looks for a template file that shares the same name and calls view.make for each template. Templates placed in a template file are also automatically bundled with the application scripts so that they can be rendered on the client.
Template files are also HTML, but each template is wrapped in a tag that names the template. This name must end in a colon to differentiate it from a normal HTML tag. These tags need not be closed. For example:
<Title:>
Silly example
<Body:>
<h1>Howdy!</h1>
By default, Derby includes templates with the names Doctype, Root, Charset, Title, Head, Header, Body, Footer, Scripts, and Tail when it renders a page on the server.
In the browser, only the Root, Title, Header, Body, and Footer templates are re-rendered. Thus, model-view bindings may only be defined within these templates.
Some of pre-defined templates have names that also are the names of HTML tags, but only Title wraps the template inside of a <title> tag. Derby does not include any non-required HTML elements, such as <html>, <head>, and <body> by default.
By convention, Pre-defined template names are capitalized to indicate that the page renderer will include them automatically. However, since HTML tags are case-insensitive, Derby template names are also case insensitive. Thus, Body, BODY, and body all represent the same template.
Note that template files don’t contain boilerplate HTML, such as doctype definitions, stylesheets, and script includes. By default, Derby includes these items in an order optimized for fast load times. Also to optimize load time, it sends pages a number of chunks:
Doctype: Standard HTML5 doctype—<!DOCTYPE html>—unless overriddenRoot: Optional location for an <html> element if desired. This template should not include any other elementsCharset: <meta charset=utf-8> unless overriddenTitle: The text content of the page’s <title> elementHead: Optional location for meta tags, scripts that must be placed in the HTML <head>, and manually included stylesheetsHeader: Optional location for a page header that will be sent with the initial response chunk. Note that this is actually part of the HTML <body>, but it should render correctly by itself. It is separated out so that it can be displayed to the user before the rest of the page if the remainder of the page takes a while to download. Typically this includes fixed content, such as a logo and a top navigation barBody: The page’s main contentFooter: Optional location for content to include after the body. Used for copyright notices, footer links, and other content repeated at the bottom of multiple pagesinline.js or added via the view.inline() method. Scripts are typically included this way if they are needed to properly render the page, such as resizing an element based on the window sizeScripts: Optional location for external scripts loaded before the client scripts. For example, this is where a script tag that includes jQuery would be placed. Note that this template is just a location within the page, and it is not wrapped in a script tagTail: Optional location for additional scripts to be included at the very end of the pageTemplates can be imported from another file for making multiple page apps and sharing templates among multiple pages. File paths are expressed relatively, similar to how Node.js modules are loaded. Like in Node.js modules, either pageName.html or pageName/index.html can be imported as pageName.
<!-- all templates from "./home.html" with the namespace "home" -->
<import: src="home">
<!-- all templates from "./home.html" into the current namespace -->
<import: src="home" ns="">
<!-- one or more specific templates with the namespace "home" -->
<import: src="home" template="message alert">
<!-- one template as a different name in the current namespace -->
<import: src="home" template="message" as="myMessage">
Templates defined in a parent namespace are inherited unless they are overridden by a template with the same name in the child namespace. Thus, it often makes sense to place common page elements in a main file that imports a number of other files and override the part of the page that is different.
Template components are referenced relative to their current namespace. Namespaces are separated by colons, and a namespace can be passed to the page.render() method to render a specific page or application state.
<profile:>
<div class="profile">
...
</div>
<import: src="shared">
<Body:>
Welcome to the home page
<!-- include component from an imported namespace -->
<app:shared:profile>
<import: src="home">
<import: src="contact">
<import: src="about">
<Body:>
Default page content
<Footer:>
<p><small>© {{year}}</small></p>
page.render('home', {
year: 2012
});
page.render 'home',
year: 2012
See Components for more info on defining template components.
Derby’s template syntax is largely based on Handlebars, a popular semantic templating language similar to Mustache.
If you use Sublime Text 2 or TextMate, you can use our fork of the HTML5 bundle to get proper syntax highlighting of Derby templates. You might want to also try our Clean color theme, which highlights each type of template tag appropriately.
A simple Handlebars template:
Hello {{name}}
You have just won ${{value}}!
{{#if inCalifornia}}
Well, ${{taxedValue}}, after taxes.
{{/if}}
Given the following data context:
{
name: "Chris",
value: 10000,
taxedValue: 10000 - (10000 * 0.4),
inCalifornia: true
}
Will produce the following:
Hello Chris
You have just won $10000!
Well, $6000.0, after taxes.
Semantic templates better enforce separation of logic from presentation by restricting the ability to embed logic within views. Instead of conditional statements and loops, there is a small set of template tags. During rendering, data are passed to the template, and template tags are replaced with the appropriate values. This data is often referred to as the “context.”
With Handlebars, application code generates a context object before rendering the view. It then passes that object along with the template at render time. Derby templates can be used this way as well. However, in addition to looking for objects in a context object, Derby assumes that the model is part of the context. Even better, Derby is able to automatically establish live bindings between the view and objects in the model. Derby slightly extends the Handlebars syntax in order to support these features.
The other major difference between Handlebars and Derby templates is that Derby templates must be valid HTML first. Handlebars is language agnostic—it can be used to compile anything from HTML to source code to a document. However, Derby templates are first parsed as HTML so that the parser can understand how to bind data to the surrounding DOM objects. Template tags are only allowed within elements or text, within attribute values, and surrounding elements.
<!-- INVALID: Within element names -->
<{{tagName}}>Bad boy!</{{tagName}}>
<!-- INVALID: Within attribute names -->
<b {{attrName}}="confused" {{booleanAttr}}>Bad boy!</b>
<!-- INVALID: Splitting an html tag -->
<b{{#if maybe}}>Bad boy!</b{{/}}>
<!-- INVALID: Splitting an element -->
{{#if maybe}}<b>{{/}}Bad boy!</b>
<!-- Within an element -->
Let's go <b>{{activity}}</b>!
<!-- Within text -->
<b>Let's go {{activity}}!</b>
<!-- Within attribute values -->
<b style="color:{{displayColor}}">Let's go running!</b>
<!-- Surrounding one or more elements and text -->
{{#if maybe}}<b>Let's go dancing!</b>{{/}}
Before parsing, all HTML comments, leading whitespace, and new lines are removed from templates. This reduces page size, and it keeps template code more readable when spaces are not desired between inline elements. Whitespace at the end of lines is maintained, in case a space is desired in the HTML output.
The contents of <script> and <style> tags are passed through literally, except for whitespace removal. This whitespace removal can be disabled within an element by adding an x-no-minify attribute.
<script type="application/x-yaml" x-no-minify>
firstName: Sam
lastName : Reed
</script>
Derby’s HTML parser should be able to parse any valid HTML, including elements that don’t require closing tags and unquoted attributes. However, it is recommended that you always include closing tags for elements like <p> and <li> that might not require a closing tag. The rules around how tags are automatically closed are complex, and there are certain cases where template sections may be included within an unexpected element.
HTML attribute values only need to be quoted if they are the empty string or if they contain a space, equals sign, or greater than sign. Since Derby templates are parsed as HTML first, any of these characters within a template tag require an attribute to be escaped. Using quotes around all attribute values is recommended.
Because it understands the HTML context, Derby’s HTML escaping is much more minimal than that of most templating libraries. You may be surprised to see unescaped > and & characters. These only need to be escaped in certain contexts, and Derby only escapes them when needed. If you are skeptical, an HTML5 validator will detect most escaping bugs.
Throughout these docs, the output of templates is shown indented and on multiple lines for the sake of readability. However, Derby’s renderer would not output any indentation or line breaks. In addition, output attribute values are quoted, but Derby only includes quotes around attribute values if they are needed.
Variables insert a value from the context or model with a given name. If the name isn’t found, nothing will be inserted. Values are HTML escaped by default. The unescaped keyword may be used to insert a value without escaping.
<Body:>
<p>{{name}}</p>
<p>{{age}}</p>
<p>{{location}}</p>
<p>{{unescaped location}}</p>
page.render({ name: 'Parker', location: '<b>500 ft</b> away' });
page.render name: 'Parker', location: '<b>500 ft</b> away'
<p>Parker</p>
<p></p>
<p><b>500 ft</b> away</p>
<p><b>500 ft</b> away</p>
Sections set the scope of the context for their contents. In the case of if, unless, else if, else, and each, they also cause their contents to be conditionally rendered. with is used to only set the scope and always render. In Handlebars, sections begin and end with the same block type, but Derby requires only an ending slash.
As in Handlebars, falsey values include all falsey JavaScript values (false, null, undefined, 0, '', and NaN) as well as empty arrays ([]). All other values are truthy.
<Body:>
<h1>
{{#if visited}}
Welcome back!
{{else}}
Welcome to the party!
{{/}}
</h1>
<ul>
{{#each users}}
<li>{{name}}: {{motto}}</li>
{{/}}
</ul>
{{#unless hideFooter}}
{{#with meta}}
<small>Copyright © {{year}} Party Like It's.</small>
{{/}}
{{/}}
page.render({
visited: true
, users: [
{ name: 'Billy', motto: "Shufflin', shufflin'" }
, { name: 'Ringo', motto: "Make haste slowly." }
]
, meta: {
year: 1999
}
});
page.render
visited: true
users: [
{ name: 'Billy', motto: "Shufflin', shufflin'" }
{ name: 'Ringo', motto: "Make haste slowly." }
]
meta:
year: 1999
<h1>Welcome back!</h1>
<ul>
<li>Billy: Shufflin', shufflin'</li>
<li>Ringo: Make haste slowly</li>
</ul>
<small>Copyright © 1999 Party Like It's.</small>
Note how in the above example, the context becomes each array item inside of the #each users section. Similarly, sections set scope when referring to the name of an object. In addition to the local scope, template tags may refer to anything in the parent scope.
<Body:>
{{#with users.jill}}
I like <a href="{{link}}">{{favorite}}</a>.
{{/}}
page.render({
users: {
jill: {
favorite: 'turtles'
}
}
, link: 'http://derbyjs.com/'
});
page.render
users:
jill:
favorite: 'turtles'
link: 'http://derbyjs.com/'
I like <a href="http://derbyjs.com/">turtles</a>.
Model-view binding is a relatively recent approach to adding dynamic interaction to a page. Its use of declarative syntax dramatically lowers the amount of repetitive, error-prone DOM manipulation code in an application. With Derby’s bindings system, it should rarely be necessary to write any DOM code at all.
Derby templates declare bindings by using single curly braces instead of double curly braces. If a left curly brace ({) character is desired in HTML output, use the HTML entity {.
Bound template tags output their values in the initially rendered HTML just like unbound tags. In addition, they create bindings that update the view immediately whenever the model changes. If bindings are used for elements that change upon user interaction—such as form inputs—Derby will update the model automatically as their values change.
Any template tag may be live bound, except for within an id attribute. The id must be set at render time and not change until the element is re-rendered, since it is used to figure out which element to update.
Bindings only work for data in the model. Context data is passed in at render time, and it doesn’t change dynamically. If a binding tag uses a name not in the context object or the model at render time, it is still bound to the model, since the path may be defined later.
<Body:>
Holler: <input value="{message}"><h1>{message}</h1>
model.set('message', 'Yo, dude.');
page.render();
model.set 'message', 'Yo, dude.'
page.render()
Holler: <input value="Yo, dude." id="$0"><h1 id="$1">Yo, dude.</h1>
Note that the value in the model at render time is inserted into the HTML, as with a non-bound template tag. In addition, Derby establishes an event listener for the input element that sets the value of message whenever the user modifies the text of the input element. It also sets up a listeners for both the input and the h1 element to update their displayed values whenever message changes.
Rather than re-rendering the entire template when a value changes, only the individual elements are updated. In the case of the input, its value property is set; in the case of the h1, its innerHTML is set. Since neither of these elements have an id attribute specified in the template, Derby automatically creates ids for them. All DOM ids created by Derby begin with a dollar sign ($). If an element already has an id, Derby will use that instead.
Derby associates all DOM event listeners with an id, because getting objects by id is a fast DOM operation, it makes dealing with DOM events more efficient, and event listeners continue working even if other scripts modify the DOM unexpectedly. Derby internally tracks events via ids, allowing it to render pages on the server and then re-establish the same event listeners on the client efficiently.
If a bound template tag or section is not fully contained by an HTML element, Derby will wrap the template by placing comment markers before and after the location of the template. Comments are used, because they are valid in any location. A number of HTML elements have restrictions that make it impossible to wrap a template in an additional element. For example, <tr> elements may only contain <td> and <th> elements.
<Body:>
Welcome to our {adjective} city!
model.set('adjective', 'funny');
page.render();
model.set 'adjective', 'funny'
page.render()
Welcome to our <!--$0-->funny<!--$$0--> city!
For items in the context object, objects from the parent scope can still be referred to directly from within sections. However, bindings are set up when templates are initially compiled, and objects defined in the model may change. Thus, model paths must refer to the full path regardless of location within the template.
Yet, a template might need to define how each item in an array should be rendered as well as bind to those items. In this case, relative model paths may be used. Paths relative to the current scope begin with a dot (.).
<Body:>
<ul>{#each items}<app:item>{/}</ul>
<item:>
<li><a href="{{url}}">{.name}</a>: ${.price}</li>
model.set('items', [
{ name: 'Can', price: 5.99, url: '/p/0' }
, { name: 'Fin', price: 10.99, url: '/p/1' }
, { name: 'Bot', price: 24.95, url: '/p/2' }
]);
page.render();
model.set 'items', [
{ name: 'Can', price: 5.99, url: '/p/0' }
{ name: 'Fin', price: 10.99, url: '/p/1' }
{ name: 'Bot', price: 24.95, url: '/p/2' }
]
page.render()
<ul id="$0">
<li><a href="/p/0" id="$1">Can</a>: $<!--$2-->5.99<!--$$2--></li>
<li><a href="/p/1" id="$3">Fin</a>: $<!--$4-->10.99<!--$$4--></li>
<li><a href="/p/2" id="$5">Bot</a>: $<!--$6-->24.95<!--$$6--></li>
</ul>
In the above example, note that the url is not bound, and it does not start with a dot. Since the context will be set to the array item at render time, this will render the value correctly, but it will not update if the value changes. .name and .price start with a dot, because they are bound to paths in the model relative to the item being rendered. Whenever the name or the price of an item changes, the appropriate fields will be updated in realtime. In addition, the entire list is bound. If a new item is added, an item is removed, or the items are reordered, the list will be updated in realtime.
Aliases to a specific scope may be defined, enabling relative model path references within nested sections. Aliases begin with a colon (:), and can be defined at the end of a section tag with the as keyword.
<Body:>
<h2>Toys in use:</h2>
{#each toys as :toy}
{#if :toy.inUse}
<app:toyStatus>
{/}
{/}
<h2>All toys:</h2>
{#each toys as :toy}
<app:toyStatus>
{/}
<toyStatus:>
<p>{{name}} on the {:toy.location}</p>
model.set('toys', [
{ name: 'Ball', location: 'floor', inUse: true }
, { name: 'Blocks', location: 'shelf' }
, { name: 'Truck', location: 'shelf' }
]);
page.render();
model.set 'toys', [
{ name: 'Ball', location: 'floor', inUse: true }
{ name: 'Blocks', location: 'shelf' }
{ name: 'Truck', location: 'shelf' }
]
page.render()
<h2>Toys in use:</h2>
<!--$0-->
<!--$1--><p>Ball on the <!--$2-->floor<!--$$2--></p><!--$$1-->
<!--$3--><!--$$3-->
<!--$4--><!--$$4-->
<!--$$0-->
<h2>All toys:</h2>
<!--$5-->
<p>Ball on the <!--$6-->floor<!--$$6--></p>
<p>Blocks on the <!--$7-->shelf<!--$$7--></p>
<p>Truck on the <!--$8-->shelf<!--$$8--></p>
<!--$$5-->
Derby follows the semantic templating approach of Handlebars and Mustache, which helps to reduce bleeding of logic into templates. However, because of Derby’s automatic bindings between the view and model, it can be useful to have computed values that are created only for the view.
View helper functions are reactive, and they are evaluated when rendering as well as whenever any bound inputs change. In addition, they can work as both getters and setters. This is especially useful when binding to form elements, such as selected options or radio buttons:
// Remove all whitespace from a string
view.fn('unspace', function(value) {
return value && value.replace(/\s/g, '')
})
# Remove all whitespace from a string
view.fn 'unspace', (value) ->
value && value.replace(/\s/g, '')
{{#with home}}
<h1 style="color:{unspace(.title.color)}">
Welcome in {.title.color}!
</h1>
<select>
{#each .colors}
<option selected="{equal(.name, home.title.color)}">
{{.name}}
</option>
{/}
</select>
{{/}}
There are two default view helper functions, equal and not, that are aways available. It is also possible to define custom view helper functions, such as unspace in the example above. The equal and not functions can act as both getters and setters. In this example, when the page renders, the option with a name equal to the value of home.title.color will have a selected attribute and the others will not. When the user selects a different option from the drop down, home.title.color will be set to the value of the option that is now selected.
Note that helper functions provide enough flexibility to introduce logic into templates, which is considered bad practice. For example:
<!-- WARNING: Not recommended -->
{#if lessThan(5, _user.score)}
<b>Let's try that again!</b>
{/}
<!-- A little better: -->
{#if lessThan(lowScoreCutoff, _user.score)}
<b>Let's try that again!</b>
{/}
<!-- Preferred: -->
{#if isLowScore(_user.score)}
<b>Let's try that again!</b>
{/}
The first example is basically just straight logic embedded within the template. This is not recommended, because as business rules change (such as changing scoring so that 20 is now a low score), templates should not need to be modified. It is typically better to define constants in the controller code and store them in the model or pass them in as context data. Better still is to define a function specifically for each purpose, as what determines the low score could change entirely to a function of an additional input and no longer a simple cutoff.
When defining view helpers, try to avoid basic comparison functions like lessThan. While tempting, such functions are likely to produce less maintainable code in the long run.
Components are similar to Handlebars partials, but they are much more powerful. Like partials, they inherit the scope of the parent context where they are used. In addition, Derby’s components let you supply additional arguments as attributes and HTML content. Both for code readability and for more efficient template compilation, it is best to keep individual templates relatively simple and use components for each significant unit.
Any Derby template can be used as a component. They are included like custom HTML tags with a special namespace. Components defined within an app are all accessed from the app namespace.
<Body:>
<app:nav>
<nav:>
<ul>{{each navItems}}<app:navItem>{{/}}</ul>
<navItem:>
<li><a href="{{link}}">{{title}}</a></li>
page.render({
navItems: [
{ title: 'Home', link '/' }
, { title: 'About', link '/about' }
, { title: 'Contact us', link '/contact' }
]
});
page.render
navItems: [
{ title: 'Home', link '/' }
{ title: 'About', link '/about' }
{ title: 'Contact us', link '/contact' }
]
<ul>
<li><a href="/">Home</a></li>
<li><a href="/about">About</a></li>
<li><a href="/contact">Contact us</a></li>
</ul>
Literal values or variable values can be passed to components. These component attributes are available in template tags, prefixed with an @ character.
<Body:>
<h1><app:greeting message="Hello" to="{_user.name}"></h1>
<greeting:>
{#if @to}
{{@message}}, {@to}!
{else}
{{@message}}!
{/}
produces the same output as:
<Body:>
<h1>
{#if _user.name}
Hello, {_user.name}!
{else}
Hello!
{/}
</h1>
By default, all components are void HTML elements. This means that they must only have an opening tag and no closing tag, just like the <img> and <br> elements. A component can be defined as nonvoid, which means that it must have both a starting and a closing tag. Nonvoid components have access to a special content attribute that makes it possible to pass HTML content to the component. For example:
<Body:>
Welcome!
<app:fancyButton>
<b>Click me{{#if isUrgent}} now!{{/}}</b>
</app:fancyButton>
<fancyButton: nonvoid>
<button class="fancy">
{{@content}}
</button>
produces the same output as:
<Body:>
Welcome!
<button class="fancy">
<b>Click me{{#if isUrgent}} now!{{/}}</b>
</button>
Nonvoid components may also
Documentation is forthcoming. Until then, please see the Widgets example for usage and the Boot source.
Derby provides a few extensions to HTML that make it easier to bind models and views.
Custom attributes used during template parsing start with the prefix x- to avoid conflicts with future extensions to HTML. Note that Derby uses this prefix instead of data-, since that prefix is intended for custom data attributes that are included in the DOM. Derby removes x- attributes as it parses, and the output HTML does not include these non-standard attributes.
The x-bind attribute may be added to any HTML element to bind one or more DOM events to a controller function by name. The bound function must be exported on the app. Bound functions are passed the original event object, the element on which the x-bind attribute was placed, and a next() function that can be called to continue event bubbling.
Browsers emit DOM events on the target and then each of its parent nodes all the way up to the document’s root element, unless a handler calls e.stopPropogation(). Derby performs event bubbling more like routes—the handler function for the target element or the first bound parent element is called and then event bubbling stops. The handler can call the next() function to continue bubbling the event up.
The x-capture attribute may be used if the handler should always be called whenever a child element emits a given event. This may be especially useful on the root <html> element for handling events like mousemove.
If the click event is bound on an <a> tag without an href attribute, Derby will add the attributes href="#" and onclick="return false" automatically. If the submit event is bound on a <form> tag, onsubmit="return false" will be added to prevent a default redirect action.
Event bindings can also delay the callback’s execution after a timeout. This can be useful when handling events like paste, which is fired before new content is inserted. The value of the delay in milliseconds is included after the name of the event, such as x-bind="paste/0: afterPaste".
Internally, Derby only binds each type of event once to the document and performs event delegation. It uses element ids to keep track of which elements should be bound to which events. Thus, much like with model-view bindings, Derby will add an automatically generated id attribute to an element that uses x-bind if it does not already have an id.
<Root:>
<!-- always called regardless of event bubbling -->
<html x-capture="mousemove: move">
<Body:>
<button x-bind="click: start">Start</button>
<!-- href="#" and onclick="return false" will be added -->
<a x-bind="click: cancel">Cancel</a>
<!-- onsubmit="return false" will be added -->
<form x-bind="submit: search"> </form>
<!-- Multiple events on one element -->
<img src="example.png" x-bind="mousedown: down, mouseup: up">
<!-- Wait for timeout of 50ms before calling back -->
<input x-bind="paste/50: afterPaste">
It is often useful to relate back a DOM element to the model path that was used to render the item. For example, one might want to remove an item from a list when a button is clicked. Derby extends the model.at() method to accept a DOM node or jQuery object. When passed one of these, the method will return a scoped model that is scoped to the context of the closest bound path in the template.
<Body:>
<ul>
{#each _users}
<li x-bind="click: upcase">{.name}</li>
{/}
</ul>
exports.upcase = function (e, el, next) {
user = model.at(el);
// Logs something like "_users.3"
console.log(user.path());
user.set('name', user.get('name').toUpperCase());
}
exports.upcase = (e, el, next) ->
user = model.at el
# Logs something like "_users.3"
console.log user.path()
user.set 'name', user.get('name').toUpperCase()
In HTML, boolean attributes are true when they are included and false when they are excluded. Since Derby only allows template tags inside attribute values, this makes it difficult to bind such attributes to model objects. Therefore, Derby uses a slightly modified syntax that works more naturally with the templating syntax for the attributes checked, selected, and disabled, which are likely to be bound to data.
Boolean attribute values can be inverted via the built-in view helper function not().
<Body:>
<!-- Outputs:
<input type="checkbox" checked>
- or -
<input type="checkbox">
-->
<input type="checkbox" checked="{{active}}">
<!-- Bound to model -->
<input type="checkbox" checked="{active}">
<!-- Inverted value -->
<input type="checkbox" disabled="{not(active)}">
Binding the selected attribute of <option> elements in a <select> is difficult, because the change event is only fired on the <select> element, and the selected attribute must be place on the options. Therefore, Derby distributes the change event to each of the children of a select element, raising the event on each of the options as well. This makes it possible to bind the selected state on each of the options.
For radio buttons, change events are only fired on the element that is clicked. However, clicking a radio button unchecks the value of all other radio buttons with the same name. Thus, Derby also emits the change event on other elements with the same name so that each radio button’s checked attribute may be bound.
While Derby’s rendering performance has yet to be benchmarked and optimized, its architecture will ultimately enable it to outperform most current web application rendering approaches in real usage.
When large chunks of a page require updating, rendering HTML and then updating the innerHTML of an element is the fastest approach. However, when small changes to one item in a template occur, rerendering the entire template and replacing an entire section of the DOM is much slower than simply updating a single property or single element’s innerHTML.
In addition, only rendering certain sections or an entire page client-side dramatically slows page loads. Even an extremely fast client-only renderer causes the browser to wait for the page to load a script (most likely via an additional request), interpret the script, render the template, and update the DOM before it has a chance to start performing layout of the HTML content.
Derby’s architecture optimizes time to load the page initially, to re-render sections or the entire page client-side, and to update individual elements in realtime. It makes it easy for designers and developers to create application views with HTML-based templates, and it provides instant responsiveness with model-view bindings.
Derby uses Stylus and/or LESS to automatically compile and includes styles for each page. Both of these languages extend CSS with variables, mixins, functions, and other awesome features.
Derby also includes Nib with Stylus, which adds a number of convenient CSS3 mixins. Nib takes care of adding vendor prefixes, makes CSS gradients much easier, and has bunch of other useful features.
Stylus requires that files end in a .styl extension. It supports importing other files, including support for index.styl files. Since Node.js, Derby templates, and Stylus all support similar file importing conventions, it is easy to use the same directory structure for analogous files in the lib/src, views, and styles directories.
Derby includes compiled CSS at the top of each page. Inlining CSS almost always decreases load time, and Stylus or LESS importing makes it easy to break up shared styles into files included in the appropriate pages. Note, however, that it is not optimal to include a very large amount of CSS, such as large data URI encoded images, at the top of the page. Large images are best loaded as separate files or inline at the bottom of the page, so that the rest of the page may be displayed first.
Views are rendered in response to routes. Most routes should be defined inside of an app so that they can be handled both on the client and the server. Views can also be rendered in response to server only routes.
In each render method, namespace, model, context, and status arguments may be in any order or omitted.
page.render
( [namespace], [context], [status] )namespace: (optional) A namespace within which to render. Typically is the name of a page or type of page.
context: (optional) Object specifying additional context objects to use in rendering templates.
status: (optional) Number specifying the HTTP status code. 200 by default. Has no effect when rendering on the client.
App routes supply a page object, which provides a consistent interface for rendering an entire page on both server and client. On the server, the page is rendered by calling Node.js response object methods like res.write. On the client, Derby renders the page locally. It then replaces the document.title and document.body.innerHTML, and updates the URL with history.pushState.
The page’s render function implicitly renders in the context of the app’s model. An additional context object may be supplied for items that are only needed at render time.
app.render
( res, model, [namespace], [context], [status] )res: Response object passed to the Express routing callback
model: A Derby model object used for rendering. The contents of the model will be serialized and initialized into the same state in the browser once the page loads.
namespace: (optional) A namespace within which to render. Typically is the name of a page or type of page.
context: (optional) Additional context objects to use in rendering templates.
status: (optional) Number specifying the HTTP status code. 200 by default.
Apps may also be rendered within server only Express routes. In this case, it is necessary to provide the renderer with a response object and model. When using the store.modelMiddleware, the model for a request is retrieved from req.getModel(). Otherwise, models can be made directly from the store via the store.createModel() method.
staticPages.render
( name, res, [model], [namespace], [context], [status] )name: Name of the view and style files to render
res: Response object passed to the Express routing callback
model: (optional) A Derby model object. A model object may be used for rendering, but it will not be serialized and included with a static page. Static pages don’t have an associated app, and they don’t include scripts by default.
namespace: (optional) A namespace within which to render. Typically is the name of a page or type of page.
context: (optional) Additional context objects to use in rendering templates.
status: (optional) Number specifying the HTTP status code. 200 by default.
For creating error pages and other static pages, Derby provides a staticPages object that renders a template and script file specified by name. Typically, this is used without a model object, but it is possible to supply a model object that is used for rendering only. See Static pages.
Derby adds an app.view object for creating and rendering views.
view.make
( name, template )name: Name of the template
template: A string containing the Derby template. Note that this should be only the content of the template, and it should not have a template name element, such as
<Body:>at the start.
Apps should typically place all templates in a template file in the views folder instead of calling view.make() directly. However, templates may be added to an app this way as well.
Note that calling view.make() only renders the template; it does not include the template in the external script file separately. Thus, it must be called again on the client when the app loads.
view.inline
( fn )fn: Function to be inlined in the page and called immediately when the page loads.
This method is intended solely for server use and has no effect when called in the browser.
Scripts should be included inline in the page if needed to properly render the page. For example, a script might adjust the layout based on the window size, or it might autofocus a sign in box in browsers that don’t support the HTML5 autofocus attribute.
Usually, it is preferable to place such scripts in a separate file called inline.js in the same directory as the app. This file will be automatically inlined when the app is created. Calling view.inline() directly does the same thing, but it is redundant to send the script inline and also include it in the app’s external script file.
Derby controllers are defined in the script file that invokes derby.createApp(). Typically, controllers are located at lib\app_name\index.js or src\app_name\index.coffee. See Creating apps.
Controllers include routes, user event handlers, and application logic. Because Derby provides model-view bindings and syncs models automatically, directly manipulating the DOM and manually sending messages to the server should rarely be necessary.
Routes map URL patterns to actions. Derby routes are powered by Express, which is similar to Sinatra. Within apps, routes are defined via the get, post, put, and del methods of the app created by derby.createApp().
app.get
( pattern, callback(page, model, params, next) )app.post
( pattern, callback(page, model, params, next) )app.put
( pattern, callback(page, model, params, next) )app.del
( pattern, callback(page, model, params, next) )pattern: A string containing a literal URL, an Express route pattern, or a regular expression. See Express’s routing documentation for more info.
callback: Function invoked when a request for a URL matching the appropriate HTTP method and pattern is received. Note that this function is called both on the server and the client.
page: Object with the methods
page.render()andpage.redirect(). All app routes should call one of these two methods or pass control by callingnext().model: Derby model object
params: An object containing the matching URL parameters. The
url,query, andbodyproperties typically available onreqare also added to this object.next: A function that can be called to pass control to the next matching route. If this is called on the client, control will be passed to the next route defined in the app. If no other routes in the same app match, it will fall through to a server request.
page.redirect
( url, [status] )url: Destination of redirect. Like Express, may also be the string ‘home’ (which redirects to ’/’) or ‘back’ (which goes back to the previous URL).
status: (optional) Number specifying HTTP status code. Defaults to 302 on the server. Has no effect on the client.
Unlike Express, which provides direct access to the req and res objects created by Node HTTP servers, Derby returns page, model, and params objects. These provide the same interface on the client and the server, so that route handlers may be executed in both environments.
Express is used directly on the server. On the client, Derby includes Express’s route matching module. When a link is clicked or a form is submitted, Derby first tries to render the new URL on the client.
Derby can also capture form submissions client-side. It provides support for post, put, and del HTTP methods using the same hidden form field override approach as Express.
In the client, there are a number of situations where it makes sense to update the URL but only part of the UI needs to update. For example, one might want to show a lightbox on top of a given page or update only the content area of a page and not the surrounding chrome.
In these cases, transitional routes provide a more efficient and flexible solution to updating the page. On the server or from a different page, calling a transitional route renders the entire page like a normal route. However, when coming from the same page in the client, a transitional route only runs code to update the model and the appropriate view bindings.
Transitional routes make it possible to use CSS animations, since only the relevant elements are updated. If the full page were re-rendered, the current HTML elements would be replaced with new ones. Then, the CSS animation would not be able to figure out how a given element’s styles had changed.
Transitional routes use the same get, post, put, and del methods, but they take both a from and to pattern as well as a forward and back callback. Since transitional routes cannot render the entire page but only update data in the model, their callbacks do not have a page argument.
get('/photo/:id', function (page, model, params, next) {
// Normal page rendering code goes here
...
// Any state set in the `forward` route callback should be
// reset in the main route, in case the user navigates to
// a different page and then back to this page directly
model.del('_showLightbox');
// The transitional route callback will execute right before
// the render method is called
page.render();
})
get({from: '/photo/:id', to: '/photo/:id/lightbox'}, {
forward: function (model, params, next) {
model.set('_showLightbox', true);
}
, back: function (model, params, next) {
model.del('_showLightbox');
}
});
get '/photo/:id', (page, model, params, next) ->
# Normal page rendering code goes here
...
# Any state set in the `forward` route callback should be
# reset in the main route, in case the user navigates to
# a different page and then back to this page directly
model.del '_showLightbox'
# The transitional route callback will execute right before
# the render method is called
page.render()
get from: '/photo/:id', to: '/photo/:id/lightbox',
forward: (model, params, next) ->
model.set '_showLightbox', true
back: (model, params, next) ->
model.del '_showLightbox'
Transitional routes support literal string routes and patterned routes with named parameters like :id and wildcard captures like *. However, they do not support arbitrary regular expressions.
When a to route is requested on the server or from a different page, the router first pretends like the from route was called. It replaces any named parameters and wildcards based on their equivalents in the from route, and then does a lookup for the from route.
After that, the original route is executed up to the page.render() call. Next, the forward callback of the transitional route is called. This simulates first navigating to the original route and then transitioning to the new route. Finally, page.render() is executed.
In the above example, the possible transitions would be:
| Starting from | Going to | Effect |
|---|---|---|
| From server or another page | /photo/42 | Run the original route normally |
| /photo/42 | /photo/42/lightbox | Run the `forward` callback only |
| /photo/42/lightbox | /photo/42 | Run the `back` callback only |
| From server or another page | /photo/42/lightbox | Run the original route for `/photo/42` up to `page.render()`, then the `forward` callback, then render |
For the most part, updating the URL client-side should be done with normal HTML links. The default action of requesting a new page from the server is canceled automatically if the app has a route that matches the new URL.
To update the URL after an action other than clicking a link, scripts can call methods on view.history. For example, an app might update the URL as the user scrolls and the page loads more content from a paginated list.
view.history.push
( url, [render], [state], [e] )view.history.replace
( url, [render], [state], [e] )url: New URL to set for the current window
render: (optional) Re-render the page after updating the URL if true. Defaults to true
state: (optional) A state object to pass to the
window.history.pushStateorwindow.history.replaceStatemethod.$renderand$methodproperties are added to this object for internal use when handlingpopstateeventse: (optional) An event object whose
stopPropogationmethod will be called if the URL can be rendered client-side
Derby’s history.push and history.replace methods will update the URL via window.history.pushState or window.history.replaceState, respectively. They will fall back to setting window.location and server-rendering the new URL if a browser does not support these methods. The push method is used to update the URL and create a new entry in the browser’s back/forward history. The replace method is used to only update the URL without creating an entry in the back/forward history.
view.history.refresh
( )Re-render the current URL client-side
For convenience, the navigational methods of window.history can also be called on view.history.
view.history.back
( )Call
window.history.back(), which is equivalent to clicking the browser’s back button
view.history.forward
( )Call
window.history.forward(), which is equivalent to clicking the browser’s forward button
view.history.go
( i )Call
window.history.go()i: An integer specifying the number of times to go back or forward. Navigates back if negative or forward if positive
Derby automates a great deal of user event handling via model-view binding. This should be used for any data that is tied directly to an element’s attribute or HTML content. For example, as users interact with an <input>, value and checked properties will be updated. In addition, the selected attribute of <option> elements and edits to the innerHTML of contenteditable elements will update bound model objects.
For other types of user events, such as click or dragover, Derby’s x-bind attribute can be used to tie DOM events on a specific element to a callback function in the controller. Such functions must be exported on the app module.
Even if controller code is responding to a DOM event, it should typically only update the view indirectly by manipulating data in the model. Since views are bound to model data, the view will update automatically when the correct data is set. While this way of writing client code may take some getting used to, it is ultimately much simpler and less error-prone.
Model events are emitted in response to changes in the model. These may be used directly to update other model items and the resulting views, such as updating a count of the items in a list every time it is modified.
Application logic executes in response to routes, user events, and model events. Code that responds to user events and model events should be placed within the app.ready() callback. This provides the model object for the client and makes sure that the code is only executed on the client.
app.ready
( callback(model) )callback: Function called as soon as the Derby app is loaded on the client. Note that any code within this callback is only executed on the client and not on the server.
model: The Derby model object for the given client
Application logic should be written to share as much code between servers and clients as possible. For security reasons or for direct access to backend services, it may be necessary to only perform certain functions on servers. However, placing as much code as possible in a shared location allows Derby apps to be extremely responsive and work offline by default.
Derby models are powered by Racer, a realtime model synchronization engine. Racer enables multiple users to interact with the same data objects via sophisticated conflict detection and resolution algorithms. At the same time, it provides a simple object accessor and event interface for writing application logic.
On the server, Racer creates a store, which manages data updates. It is possible to directly manipulate data via asynchronous methods on a store, similar to interacting with a database. Stores also create model objects, which provide a synchronous interface more like interacting directly with objects.
Models maintain their own copy of a subset of the global state. This subset is defined via subscriptions to certain paths. Models perform operations independently, and they automatically synchronize their state with the associated store over Socket.IO.
When models are modified, they will immediately reflect the changes. In the background, Racer turns operations into transactions that are sent to the server and applied or rejected. If the transactions are accepted, they are sent to all other clients subscribed to the same data. This optimistic approach provides immediate interaction for the user, makes writing application logic easier, and enables Racer to work offline.
Model mutator methods provide callbacks invoked after success or failure of a transaction. These callbacks can be used to provide application-specific conflict resolution UIs. Models also emit events when their contents are updated, which Derby uses to update the view in realtime.
Currently, Racer defaults to applying all transactions in the order received, i.e. last-writer-wins. For realtime-connected clients, this will typically result in expected behavior. However, offline users interacting with the same data are likely to produce conflicting updates that could lead to unexpected behavior.
Therefore, Racer supports conflict resolution via a combination of Software Transactional Memory (STM), Operational Transformation (OT), and Diff-match-patch techniques.
These features are not fully implemented yet, but the Racer demos show preliminary examples of STM and OT. Letters uses STM mode to automatically detect conflicts when different users move the same letters at the same time. Pad uses OT for a minimal collaborative text editor.
To perform these algorithms, Racer stores a journal of all transactions. When new transactions arrive, their paths and versions are compared to the transactions already committed to the journal. STM accepts a transaction if it is not in conflict with any other operations on the same path. STM works well when changes need to be either fully accepted or rejected, such as updating a username. In contrast, OT is designed for situations like collaborative text editing, where changes should be merged together. In OT, transactions are modified to work together in any order instead of being rejected.
The default server produced by the Derby project generator will create a store and add a modelMiddleware to the Express server before any app routers. The modelMiddleware adds a req.getModel() function which can be called to create or get a model (if one was already created) for a given request.
store =derby.createStore( options )options: An object that configures the store
store: Returns a Racer store object
Typically, a project will have only one store, even if it has multiple apps. It is possible to have multiple stores, though a given page can only have one model, and a model is associated with one store.
Typically a listen option is specified, which is used to setup Socket.IO. This option may be an Express server or a port number. In addition to listen, a namespace argument may be provided to setup Socket.IO under a namespace. See “Restricting yourself to a namespace” in the Socket.IO guide.
Alternatively, options may specify sockets and socketUri if the Socket.IO sockets object is already created. The sockets option should be the object returned from Socket.IO’s io.listen().sockets or io.listen().of().
More information about configuring Racer to run with various PubSub, database, and journal adapters is coming soon.
Derby’s models are powered by Racer. By default, Racer stores data in memory, so nothing will be persisted between server restarts. This is an easy way to get started and prototype an app. Adding persistence merely requires including an adapter for a given database. This is configured when creating the store:
derby.use(require('racer-db-mongo'));
app.createStore({
listen: server
, db: {type: 'Mongo', uri: 'mongodb://localhost/database'}
});
derby.use(require 'racer-db-mongo')
app.createStore
listen: server
db: {type: 'Mongo', uri: 'mongodb://localhost/database'}
Each of the db adapters are separate npm modules. As with all npm modules, each adapter should be added as a dependency within a project’s package.json file. After adding a new dependency, be sure to re-run $ npm install.
Racer paths are translated into database collections and documents using a natural mapping:
collection.documentId.document
All synced paths (anything that doesn’t start with an underscore) must follow this convention. In other words, all model data stored at the first two path segments should be an object and not a string, number, or other primitive type.
// Examples:
model.set('todos.id_0.completed', true);
model.set('rooms.lobby.messages.5.text', 'Call me');
model.set('meta', {
app: {
title: 'Hi there'
, author: 'Erik Mathers'
}
});
// The first and second segments in root paths must be objects
model.set('title', 'Hi there'); // WARNING INVALID
model.set('app.title', 'Hi there'); // WARNING INVALID
// However, any type may be stored at any private path, which
// starts with an underscore and is not synced back to the server
model.set('_title', 'Hi there'); // OK
# Examples:
model.set 'todos.id_0.completed', true
model.set 'rooms.lobby.messages.5.text', 'Call me'
model.set 'meta',
app:
title: 'Hi there'
author: 'Erik Mathers'
# The first and second segments in root paths must be objects
model.set 'title', 'Hi there' # WARNING INVALID
model.set 'app.title', 'Hi there' # WARNING INVALID
// However, any type may be stored at any private path, which
// starts with an underscore and is not synced back to the server
model.set '_title', 'Hi there' # OK
The document’s id (the second path segment) is automatically added as the id property of the document, so that it can be retrieved from the datastore.
model.set('meta', {
app: {
title: 'Hi there'
, author: 'Erik Mathers'
}
});
// Logs: {id: 'app', title: 'Hi there', author: 'Erik Mathers'}
console.log(model.get('meta.app'));
model.set 'meta'
app:
title: 'Hi there'
author: 'Erik Mathers'
# Logs: {id: 'app', title: 'Hi there', author: 'Erik Mathers'}
console.log model.get('meta.app')
Derby provides a model when calling application routes. On the server, it creates an empty model from the store associated with an app. When the server renders the page, the model is serialized. It is then reinitialized into the same state on the client. This model object is passed to the app.ready() callback and app routes rendered on the client.
Derby uses the model supplied by the store.modelMiddleware by calling req.getModel(). To pass data from server-side express middleware or routes, the model can be retrieved via this same method and data can be set on it before passing control to the app router.
model =store.createModel( )model: A Racer model object associated with the given store
Server-side routes can use the model from req.getModel(). Otherwise, they can manually create a model via store.createModel().
All model operations happen on paths which represent literal nested objects. These paths must be globally unique within a particular store.
For example, the model:
{
title: 'Fruit store',
fruits: [
{ name: 'banana', color: 'yellow' },
{ name: 'apple', color: 'red' },
{ name: 'lime', color: 'green' }
]
}
Would have paths like title, fruits.1, and fruits.0.color. Paths consist of valid JavaScript variable names—alphanumeric characters or underscore (_), beginning with a letter or underscore—or array indices joined by dots (.). They should not contain dollar signs ($), which are reserved for internal use.
Paths that contain a segment starting with an underscore (e.g. _showFooter or flowers.10._hovered) have a special meaning. These paths are considered “private,” and they are not synced back to the server or to other clients. Private paths are frequently used with references and for rendering purposes.
Models provide a method to create globally unique ids. These can be used as part of a path or within mutator methods.
guid =model.id( )guid: Returns a globally unique identifier that can be used for model operations
Model mutator methods are applied optimistically. This means that changes are reflected immediately, but they may ultimately fail and be rolled back. All model mutator methods are synchronous and provide an optional callback.
These methods can be used on any model path to get, set, or delete an object.
value =model.get( [path] )path: (optional) Path of object to get. Not supplying a path will return all data in the model
value: Current value of the object at the given path. Note that objects are returned by reference and should not be modified directly
All model mutators have an optional callback with the arguments callback(err, methodArgs...). If the transaction succeeds, err is null. Otherwise, it is a string with an error message. This message is 'conflict' if when there is a conflict with another transaction. The method arguments used to call the original function (e.g. path, value for the model.set() method) are also passed back to the callback.
previous =model.set( path, value, [callback] )path: Model path to set
value: Value to assign
previous: Returns the value that was set at the path previously
callback: (optional) Invoked upon completion of a successful or failed transaction
obj =model.del( path, [callback] )path: Model path of object to delete
obj: Returns the deleted object
Models allow getting and setting to nested undefined paths. Getting such a path returns undefined. Setting such a path first sets each undefined or null parent to an empty object.
var model = store.createModel();
model.set('cars.DeLorean.DMC12.color', 'silver');
// Logs: { cars: { DeLorean: { DMC12: { color: 'silver' }}}}
console.log(model.get());
model = store.createModel()
model.set 'cars.DeLorean.DMC12.color', 'silver'
# Logs: { cars: { DeLorean: { DMC12: { color: 'silver' }}}}
console.log model.get()
obj =model.setNull( path, value, [callback] )path: Model path to set
value: Value to assign only if the path is null or undefined
obj: Returns the object at the path if it is not null or undefined. Otherwise, returns the new value
num =model.incr( path, [byNum], [callback] )path: Model path to set
byNum: (optional) Number specifying amount to increment or decrement if negative. Defaults to 1
num: Returns the new value that was set after incrementing
The model.setNull() and model.incr() methods provide a more convenient way to perform common get and set combinations. Internally, they perform a model.get() and a model.set(), so the model events raised by both of these methods are set events and not setNull or incr. Note that incr can be called on a null path, in which case the value will be set to byNum.
Array methods can only be used on paths set to arrays, null, or undefined. If the path is null or undefined, the path will first be set to an empty array before applying the method.
length =model.push( path, items..., [callback] )path: Model path to an array
items: One or more items to add to the end of the array
length: Returns the length of the array with the new items added
length =model.unshift( path, items..., [callback] )path: Model path to an array
items: One items to add to the beginning of the array
length: Returns the length of the array with the new items added
length =model.insert( path, index, items..., [callback] )path: Model path to an array
index: Index at which to start inserting. This can also be specified by appending it to the path instead of as a separate argument
items: One or more items to insert at the index
length: Returns the length of the array with the new items added
item =model.pop( path, [callback] )path: Model path to an array
item: Removes the last item in the array and returns it
item =model.shift( path, [callback] )path: Model path to an array
item: Removes the first item in the array and returns it
removed =model.remove( path, index, [howMany], [callback] )path: Model path to an array
index: Index at which to start removing items. This can also be specified by appending it to the path instead of as a separate argument
howMany: (optional) Number of items to remove. Defaults to 1
removed: Returns an array of removed items
item =model.move( path, from, to, [callback] )path: Model path to an array
from: Starting index of the item to move. This can also be specified by appending it to the path instead of as a separate argument
to: New index where the item should be moved
item: Returns the item that was moved
Models inherit from the standard Node.js EventEmitter, and they support the same methods: on, once, removeListener, emit, etc.
Racer emits events whenever it mutates data via model.set, model.push, etc. These events provide an entry point for an app to react to a specific data mutation or pattern of data mutations.
model.on and model.once accept a second argument for these types of events. The second argument may be a path pattern or regular expression that will filter emitted events, calling the handler function only when a mutator matches the pattern.
listener =model.on( method, path, eventCallback )method: Name of the mutator method - e.g., “set”, “push”
path: Pattern or regular expression matching the path being mutated
eventCallback: Function to call when a matching method and path are mutated
listener: Returns the listener function subscribed to the event emitter. This is the function that should be passed to
model.removeListener
The event callback receives a number of arguments based on the path pattern and method. The arguments are:
eventCallback
( captures..., args..., out, isLocal, passed )captures: The capturing groups from the path pattern or regular expression. If specifying a string pattern, a capturing group will be created for each
*wildcard and anything in parentheses, such as(one|two)args: The arguments to the method. Note that optional arguments with a default value (such as the
byNumargument ofmodel.incr) will always be includedout: The return value of the model mutator method
isLocal:
trueif the model mutation was originally called on the same model andfalseotherwisepassed:
undefined, unless a value is specified viamodel.pass. See description below
In path patterns, wildcards (*) will only match a single segment in the middle of a path, but they will match a single or multiple path segments at the end of the path. In other words, they are non-greedy in the middle of a pattern and greedy at the end of a pattern.
// Matches only model.push('messages', message)
model.on('push', 'messages', function (message, messagesLength) {
...
});
// Matches model.set('todos.4.completed', true), etc.
model.on('set', 'todos.*.completed', function (todoId, isComplete) {
...
});
// Matches all set operations
model.on('set', '*', function (path, value) {
...
});
# Matches only model.push('messages', message)
model.on 'push', 'messages', (message, messagesLength) ->
...
# Matches model.set('todos.4.completed', true), etc.
model.on 'set', 'todos.*.completed', (todoId, isComplete) ->
...
# Matches all set operations
model.on 'set', '*', (path, value) ->
...
This method can be chained before calling a mutator method to pass an argument to model event listeners. Note that this value is only passed to local listeners, and it is not sent to the server or other clients.
// Logs:
// 'red', undefined
// 'green', 'hi'
model.on('set', 'color', function (value, out, isLocal, passed) {
console.log(value, passed);
});
model.set('color', 'red');
model.pass('hi').set('color', 'green');
# Logs:
# 'red', undefined
# 'green', 'hi'
model.on 'set', 'color', (value, out, isLocal, passed) ->
console.log value, passed
model.set 'color', 'red'
model.pass('hi').set 'color', 'green'
Scoped models provide a more convenient way to interact with commonly used paths. They support the same methods, and they provide the path argument to accessors, mutators, and event subscribers.
scoped =model.at( path, [absolute] )path: The reference path to set. Note that Derby also supports supplying a DOM node instead of a path string
inputPaths: (optional) Will replace the model’s reference path if true. By default, the path is appended
scoped: Returns a scoped model
scoped =model.parent( [levels] )levels: (optional) Defaults to 1. The number of path segments to remove from the end of the reference path
scoped: Returns a scoped model
path =model.path( )path: Returns the reference path for the model that was set via
model.atormodel.parent
segment =model.leaf( )segment: Returns the last segment for the reference path. This may be useful for getting indices or other properties set at the end of a path
room = model.at('_room');
// These are equivalent:
room.at('name').set('Fun room');
room.set('name', 'Fun room');
// Logs: {name: 'Fun room'}
console.log(room.get());
// Logs: 'Fun room'
console.log(room.get('name'));
// Array methods can take a subpath as a first argument
// when the scoped model points to an object
room.push('toys', 'blocks', 'puzzles');
// When the scoped model points to an array, no subpath
// argument should be supplied
room.at('toys').push('cards', 'dominoes');
room = model.at '_room'
# These are equivalent:
room.at('name').set 'Fun room'
room.set 'name', 'Fun room'
# Logs: {name: 'Fun room'}
console.log room.get()
# Logs: 'Fun room'
console.log room.get('name')
# Array methods can take a subpath as a first argument
# when the scoped model points to an object
room.push 'toys', 'blocks', 'puzzles'
# When the scoped model points to an array, no subpath
# argument should be supplied
room.at('toys').push 'cards', 'dominoes'
Note that Derby also extends model.at to accept a DOM node as an argument. This is typically used with e.target in an event callback. See x-bind.
WARNING
This section is out of date. Please see the Queries Readme for now.
Models have access to an expressive, chainable query API. Queries enable a more versatile approach than Paths (see above) to subscribe to a set of data. For example, with paths, it is not possible to specify a subscription to all users who are older than 25. Queries enable subscribing to a set of documents that satisfy some set of conditions.
model.query
(namespace)namespace: The namespace containing the docs you want to query.
The model.query method returns a Query instance that can be used to build up any number of queries on documents in namespace. For, example, suppose you have documents at the following paths:
{id: '1', name: 'Lars'}{id: '2', name: 'Gnarls'}{id: '3', name: 'Evil Global Corp'}Then model.query('users') will return a Query that has the ability to find the docs at ‘users.1’ and/or ‘users.2’. Such a Query would never find the doc at ‘groups.1’, which has a namespace of ‘groups’.
After generating a Query instance with model.query(namespace), the query instance can begin to add conditions and options via a set of chainable methods.
var query = model.query('users').where('name').equals('Lars');
query = model.query 'users', where: {name: 'Lars'}
This will generate a query that finds any documents under the ‘users’ namespace whose ‘name’ property is equal to ‘Lars’. In our example data set of size 3 above, it would find {id: '1', name: 'Lars'}.
Here are examples using the other query methods:
// Find users
model.query('users')
// With name 'Gnarls'
.where('name').equals('Gnarls')
// With gender != 'female'
.where('gender').notEquals('female')
// With 21 < age <=30
.where('age').gt(21).lte(30)
// With 100 <= numFriends < 200
.where('numFriends').gte(100).lt(200)
// With tags that include both 'super' and 'derby'
.where('tags').contains(['super', 'derby'])
// With shoe as either 'nike' or 'adidas'
.where('shoe').within(['nike', 'adidas'])
// Pagination ftw!
.skip(10).limit(5);
# Find users
model.query 'users',
where:
# With name 'Gnarls'
name: 'Gnarls'
# With gender != female
gender: {notEquals: 'female'}
# With 21 < age <= 30
age:
gt: 21
lte: 30
# With 100 <= numFriends < 200
numFriends:
gte: 100
lt: 200
# With tags that include both 'super' and 'derby'
tags:
contains: ['super', 'derby']
# With shoe as either 'nike' or 'adidas'
shoe:
within: ['nike', 'adidas']
# Pagination ftw!
skip: 10
limit: 5
Notice how we can chain query descriptors together to build up our query.
If you happen to know the id of the document you want to find, then you can use the query method, byId. So for example,
model.query('users').byId('1');
model.query 'users', byId: '1'
This will find the single document {id: '1', name: 'Lars'}.
Queries also support pagination. If we want the 11th to 15th users older than 21, then we could write that as:
model.query('users').where('age').gt(21).skip(10).limit(5);
model.query 'users',
where:
age:
gt: 21
skip: 10
limit: 5
Queries can limit what properties of a document it wants to include or exclude:
// This will find documents but strip out all properties except
// 'id', 'name', and 'age' before passing the results back to the app.
model.query('users').where('age').gte(30).only('id', 'name', 'age');
// This will find documents and strip our the given property 'name'
// before passing the results back to the application.
model.query('users').where('age').gte(30).except('name');
# This will find documents but strip out all properties except
# 'id', 'name', and 'age' before passing the results back to the app.
model.query 'users',
where:
age:
gte: 30
only: ['id', 'name', 'age']
# This will find documents and strip our the given property 'name'
# before passing the results back to the application.
model.query 'users',
where:
age:
gte: 30
except: 'name'
Queries can also sort the results it gets:
// This will sort the results in age-ascending, name-descending order
model.query('users')
.where('age').gte(25)
.sort('age', 'asc', 'name','desc');
# This will sort the results in age-ascending, name-descending order
model.query 'users'
where:
age:
gte: 25
sort: ['age', 'asc', 'name', 'desc']
The model.subscribe method populates a model with data from its associated store and declares that this data should be kept up to date as it changes. It is possible to define subscriptions in terms of path patterns or queries.
Typically, subscriptions are set up in response to routes before rendering a page. However, the subscribe method may be called in any context on the server or in the browser. All subscriptions established before rendering the page on the server will be re-established once the page loads in the browser.
model.subscribe
( targets..., [callback] )targets: One or more path patterns or queries to subscribe to
callback: (optional) Called after subscription succeeds and the data is set in the model or upon an error
The subscribe callback takes the arguments callback(err, scopedModels...). If the transaction succeeds, err is null. Otherwise, it is a string with an error message. This message is 'disconnected' if Socket.IO is not currently connected. The remaining arguments are scoped models that correspond to each subscribe target’s path respectively.
If a model is already subscribed to a target, calling subscribe again for the same target will have no effect. If all targets are already subscribed, the callback will be invoked immediately.
model.unsubscribe
( [targets...], [callback] )targets: (optional) One or more path patterns or queries to unsubscribe from. All of the model’s current subscriptions will be unsubscribed if no targets are specified
callback: (optional) Called after unsubscription succeeds or upon an error
The unsubscribe callback takes the argument callback(err). Like subscribe, err is null when unsubscribe succeeds, and it is 'disconnected' if Socket.IO is not currently connected.
Calling unsubscribe with no specified targets removes all subscriptions for a model. Unsubscribe removes the subscriptions, but it does not remove any data from the model.
Path patterns are specified as strings that correspond to model paths. A path pattern subscribes to the entire object, including all of its sub-paths. For example, subscribing to rooms.lobby subscribes to all data set under that path, such as rooms.lobby.name or rooms.lobby.items.3.location.
It is also possible to use an asterisk as a wildcard character in place of a path segment. For example, rooms.*.playerCount subscribes a model to the playerCount for all rooms but no other properties. The scoped model passed to a subscribe callback is scoped to the segments up to the first wildcard character. For this example, the model would be scoped to rooms.
var roomName = 'lobby';
model.subscribe('rooms.' + roomName, function (err, room) {
// Logs: 'rooms.lobby'
console.log(room.path());
// A reference is frequently created from a parameterized
// path pattern for use later. Refs may be created directly
// from a scoped model
model.ref('_room', room);
});
roomName = 'lobby'
model.subscribe "rooms.#{roomName}", (err, room) ->
# Logs: 'rooms.lobby'
console.log room.path()
# A reference is frequently created from a parameterized
# path pattern for use later. Refs may be created directly
# from a scoped model
model.ref '_room', room
More complex subscriptions can be specified via queries.
var query = model.query('posts').where('authorId').equals(userId);
model.subscribe(query, function (err, posts) {
// Logs: 'posts'
console.log(posts.path());
// A reference is frequently created from a parameterized
// path pattern for use later. Refs may be created directly
// from a scoped model
model.ref('_posts', posts);
});
query = model.query 'posts'
where:
authorId: userId
model.subscribe query, (err, posts) ->
# Logs: 'posts'
console.log posts.path()
# A reference is frequently created from a parameterized
# path pattern for use later. Refs may be created directly
# from a scoped model
model.ref '_posts', posts
In addition to subscribe, models have a fetch method with the same format. Like subscribe, fetch populates a model with data from a store based on path patterns and queries. However, fetch only retrieves the data once, and it does not establish any ongoing subscriptions. Fetch may be used for any data that need not be updated in realtime and avoids use of the PubSub system.
model.fetch
( targets..., callback )targets: One or more path patterns or queries
callback: Called after a fetch succeeds and the data is set in the model or upon an error
The fetch callback has the same arguments as subscribe’s: callback(err, scopedModels...)
Reactive functions provide a simple way to update a computed value whenever one or more objects change. While model events respond to specific model methods and path patterns, reactive functions will be re-evaluated whenever any of its inputs or their properties change in any way.
out =model.fn( path, inputPaths..., fn )path: The location at which to create a reactive function. This must be a private path, since reactive functions must be declared per model
inputPaths: One or more paths for function inputs. The function will be called whenever one of the inputs or its sub-paths are modified
fn: The function to evaluate. The function will be called with each of its inputs as arguments
out: Returns the result of the function
Note that reactive functions must be pure functions. In other words, they must always return the same results given the same input arguments, and they must be side effect free. They should not rely on any state from a closure, and all inputs should be explicitly declared.
Reactive functions created on the server are sent to the client as a string and reinitialized when the page loads. If the output of a function is used for rendering, it should be created on the server.
model.set('players', [
{name: 'John', score: 4000}
, {name: 'Bill', score: 600}
, {name: 'Kim', score: 9000}
, {name: 'Megan', score: 3000}
, {name: 'Sam', score: 2000}
]);
model.set('cutoff', 3);
// Sort the players by score and return the top X players. The
// function will automatically update the value of '_leaders' as
// players are added and removed, their scores change, and the
// cutoff value changes.
model.fn('_leaders', 'players', 'cutoff', function (players, cutoff) {
// Note that the input array is copied with slice before sorting
// it. The function should not modify the values of its inputs.
return players.slice().sort(function (a, b) {
return a.score - b.score;
}).slice(0, cutoff - 1);
});
model.set 'players', [
{name: 'John', score: 4000}
{name: 'Bill', score: 600}
{name: 'Kim', score: 9000}
{name: 'Megan', score: 3000}
{name: 'Sam', score: 2000}
]
model.set 'cutoff', 3
# Sort the players by score and return the top X players. The
# function will automatically update the value of '_leaders' as
# players are added and removed, their scores change, and the
# cutoff value changes.
model.fn '_leaders', 'players', 'cutoff', (players, cutoff) ->
# Note that the input array is copied with slice before sorting
# it. The function should not modify the values of its inputs.
players.slice().sort((a, b) -> a.score - b.score)
.slice(0, cutoff - 1)
References make it possible to write business logic and templates that interact with the model in a general way. They redirect model operations from a reference path to the underlying data, and they set up event listeners that emit model events on both the reference and the actual object’s path.
References must be declared per model, since calling model.ref creates a number of event listeners in addition to setting a ref object in the model. When a reference is created, a set model event is emitted. Internally, model.set is used to add the reference to the model.
fn =model.ref( path, to, [key] )path: The location at which to create a reference. This must be a private path, since references must be declared per model
to: The location that the reference links to. This is where the data is actually stored. May be a path or scoped model
key: (optional) A path whose value should be added as an additional property underneath
towhen accessing the reference. May be a path or scoped modelfn: Returns the function that is stored in the model to represent the reference. This function should not be used directly
model.set('colors', {
red: {hex: '#f00'}
, green: {hex: '#0f0'}
, blue: {hex: '#00f'}
});
// Getting a reference returns the referenced data
model.ref('_green', 'colors.green');
// Logs {hex: '#0f0'}
console.log(model.get('_green'));
// Setting a property of the reference path modifies
// the underlying data
model.set('_green.rgb', [0, 255, 0]);
// Logs {hex: '#0f0', rgb: [0, 255, 0]}
console.log(model.get('colors.green'));
// Setting or deleting the reference path modifies
// the reference and not the underlying data
model.del('_green');
// Logs undefined
console.log(model.get('_green'));
// Logs {hex: '#0f0', rgb: [0, 255, 0]}
console.log(model.get('colors.green'));
// Changing a reference key updates the reference
model.set('selected', 'red');
model.ref('_selectedColor', 'colors', 'selected');
// Logs '#f00'
console.log(model.get('_selectedColor.hex'));
model.set('selected', 'blue');
// Logs '#00f'
console.log(model.get('_selectedColor.hex'));
model.set 'colors'
red: {hex: '#f00'}
green: {hex: '#0f0'}
blue: {hex: '#00f'}
# Getting a reference returns the referenced data
model.ref '_green', 'colors.green'
# Logs {hex: '#0f0'}
console.log model.get('_green')
# Setting a property of the reference path modifies
# the underlying data
model.set '_green.rgb', [0, 255, 0]
# Logs {hex: '#0f0', rgb: [0, 255, 0]}
console.log model.get('colors.green')
# Setting or deleting the reference path modifies
# the reference and not the underlying data
model.del '_green'
# Logs undefined
console.log model.get('_green')
# Logs {hex: '#0f0', rgb: [0, 255, 0]}
console.log model.get('colors.green')
# Changing a reference key updates the reference
model.set 'selected', 'red'
model.ref '_selectedColor', 'colors', 'selected'
# Logs '#f00'
console.log model.get('_selectedColor.hex')
model.set 'selected', 'blue'
# Logs '#00f'
console.log model.get('_selectedColor.hex')
Racer also supports a special reference type created via model.refList. This type of reference is useful when a number of objects need to be rendered or manipulated as a list even though they are stored as properties of another object. A reference list supports the same mutator methods as an array, so it can be bound in a view template just like an array.
fn =model.refList( path, to, key )path: The location at which to create a reference list. This must be a private path, since references must be declared per model
to: The location of an object that has properties to be mapped onto an array. Each property must be an object with a unique
idproperty of the same value. May be a path or scoped modelkey: A path whose value is an array of ids that map the
toobject’s properties to a given order. May be a path or scoped modelfn: Returns the function that is stored in the model to represent the reference. This function should not be used directly
// refLists may only consist of objects with an id that matches
// their property on their parent
model.set('colors', {
red: {hex: '#f00', id: 'red'}
, green: {hex: '#0f0', id: 'green'}
, blue: {hex: '#00f', id: 'blue'}
});
model.set('_colorIds', ['blue', 'red']);
model.ref('_myColors', 'colors', '_colorIds');
model.push('_myColors', {hex: '#ff0', id: 'yellow'});
// Logs: [
// {hex: '#00f', id: 'blue'},
// {hex: '#f00', id: 'red'},
// {hex: '#ff0', id: 'yellow'}
// ]
console.log(model.get('_myColors'));
# refLists may only consist of objects with an id that matches
# their property on their parent
model.set 'colors',
red: {hex: '#f00', id: 'red'}
green: {hex: '#0f0', id: 'green'}
blue: {hex: '#00f', id: 'blue'}
model.set '_colorIds', ['blue', 'red']
model.ref '_myColors', 'colors', '_colorIds'
model.push '_myColors', {hex: '#ff0', id: 'yellow'}
# Logs: [
# {hex: '#00f', id: 'blue'},
# {hex: '#f00', id: 'red'},
# {hex: '#ff0', id: 'yellow'}
# ]
console.log model.get('_myColors')
Note that if objects are added to a refList without an id property, a unique id from model.id() will be automatically added to the object.