Getting Started
Installation
Epoxy requires jQuery 1.7.0+, Underscore 1.4.3+, and Backbone 0.9.9+. To install Epoxy, download the Epoxy library (9k min, 2k gzip) and include its script tag in your document after all dependencies:
<script src="js/jquery-min.js"></script>
<script src="js/underscore-min.js"></script>
<script src="js/backbone-min.js"></script>
<script src="js/backbone.epoxy.min.js"></script>
You may choose to replace jQuery with Zepto, and/or Underscore with Lo-Dash. Also remember to include json2 when targeting IE6/7. Epoxy is open source under the MIT license; you may browse the full library source in its GitHub Repo.
Simple View Bindings
Let's start by setting up a simple binding between a few DOM elements who's content we want to update when their underlying model data changes:
- JavaScript
- HTML
var bindModel = new Backbone.Model({
firstName: "Luke",
lastName: "Skywalker"
});
var BindingView = Backbone.Epoxy.View.extend({
el: "#app-luke",
bindings: {
"input.first-name": "value:firstName,events:['keyup']",
"input.last-name": "value:lastName,events:['keyup']",
"span.first-name": "text:firstName",
"span.last-name": "text:lastName"
}
});
var view = new BindingView({model: bindModel});
<div id="app-luke" class="demo">
<label>First:</label>
<input type="text" class="first-name">
<label>Last:</label>
<input type="text" class="last-name">
<b>Full Name:</b>
<span class="first-name"></span>
<span class="last-name"></span>
</div>
In this example, we create a new instance of Epoxy.View, provide it a native Backbone model, then use the view's bindings hash to declare bindings between view selectors and model attributes.
Binding declarations are formatted as "handler:dataSource". Basically, that's a key/value pair where the key defines a handler method to perform the binding, and the value is a data source to populate the binding with. Epoxy provides a base set of binding handlers, and you're welcome to add your own. Data sources reference attributes on the view's model in most common binding implementations.
In the above example, value:firstName establishes a two-way binding between the text input's value property and the bound model's firstName attribute. Likewise, text:firstName establishes a one-way binding that populates the bound element's text with the model's firstName attribute. Lastly, events:['keyup'] is used to specify DOM events that the binding should respond to in addition to the default "change" event.
Inline Binding Declarations
Another popular approach to data binding is to declare bindings as attributes directly on the DOM elements that they target. This approach shifts bindings declarations out of the View and into the DOM. Epoxy does also supports this syntax, should you prefer it:
- JavaScript
- HTML
var bindModel = new Backbone.Model({
firstName: "Han",
lastName: "Solo"
});
var BindingView = Backbone.Epoxy.View.extend({
el: "#app-han",
bindings: "data-bind"
});
var view = new BindingView({model: bindModel});
<div id="app-han" class="demo">
<label>First:</label>
<input type="text" data-bind="value:firstName,events:['keyup']">
<label>Last:</label>
<input type="text" data-bind="value:lastName,events:['keyup']">
<b>Full Name:</b>
<span data-bind="text:firstName"></span>
<span data-bind="text:lastName"></span>
</div>
Here, the exact same binding scheme has been applied directly to the DOM using element attributes, and the view's bindings property simply defines the attribute name to query for (note that "data-bind" is Epoxy's default selector, so you don't need to specifically declare that within your view).
The two above examples are functionally identical, therefore the location of your binding declarations (view or DOM) is entirely a matter of preference—provided that they're all in one place. The proceeding tutorial examples will use the inline binding form to obviate element-to-binding relationships. However, don't interpret this as a bias: there are many advantages to declaring your bindings within the view (such as keeping all functional definition within the view, and maintaining DOM cleanliness).
Learn more about setting up view bindings in the Epoxy.View documentation.
Computed Model Attributes
Now let's add an Epoxy.Model into the mix. An Epoxy model introduces computed attributes, which operate as accessors and mutators. A computed attribute will get an assembled value derived from other model attributes, and will set one more more mutated values back to the model. Computed attributes may be get and set just like normal model attributes, and will trigger "change" events on the model when modified, however they do not exist within the model's attributes table, nor will they be saved with model data.
Let's start by adding an Epoxy computed attribute, which will assemble its value using other model values:
- JavaScript
- HTML
var BindingModel = Backbone.Epoxy.Model.extend({
defaults: {
firstName: "Obi-Wan",
lastName: "Kenobi"
},
computeds: {
fullName: function() {
return this.get("firstName") +" "+ this.get("lastName");
}
}
});
var view = new Backbone.Epoxy.View({
el: "#app-computed",
model: new BindingModel()
});
<div id="app-computed">
<label>First:</label>
<input type="text" data-bind="value:firstName,events:['keyup']">
<label>Last:</label>
<input type="text" data-bind="value:lastName,events:['keyup']">
<b>Full Name:</b>
<span data-bind="text:fullName"></span>
</div>
In this example, our Epoxy model includes a computed attribute, fullName, that assembles its value from other model values. Also, note our view's "text:fullName" binding. Because the computed fullName attribute can be get from the model just like any other attribute, it's able to bind seamlessly into an Epoxy view.
However, what happens to the computed fullName attribute if the firstName or lastName attributes change? Good news: Epoxy can automatically map computed attribute dependencies, and will register model "change:attribute" events to keep the computed value in sync. We'll discuss computed dependency management in the next section.
Learn more about computed model attributes in the Epoxy.Model documentation.
Managing Computed Dependencies
As discussed in the previous example, Epoxy can automatically map and bind computed model attribute dependencies. While this may smell of black magic, all that's really going on is that Epoxy has wrapped the Backbone model's native get method, and is using it to keep track of requested attribute names. As long as you use an Epoxy model's get method for all attribute access (no digging directly into a model's attributes table), then one or more Epoxy.Model instances will automatically map references between one another.
However, there's one big "gotcha" here... consider the following BROKEN example:
// BROKEN:
var BrokenModel = Backbone.Epoxy.Model.extend({
defaults: {
userName: "tatooine_luke",
fullName: "Luke Skywalker",
isOnline: true
},
computeds: {
displayName: function() {
return this.get("isOnline") ? this.get("fullName") : this.get("userName");
}
}
});
See the problem above? Because displayName uses conditional logic, one of the two conditional get calls will be unreachable (and therefore missed) while automatically mapping dependencies. This makes for a busted model. To fix this, you may take two approaches...
The first solution is to move all get calls outside of the conditional statement, and then let automatic mapping safely take its course. The following will work:
// FIXED by pre-collecting references:
var FixedModel = Backbone.Epoxy.Model.extend({
defaults: {
userName: "tatooine_luke",
fullName: "Luke Skywalker",
isOnline: true
},
computeds: {
displayName: function() {
var fullName = this.get("fullName");
var userName = this.get("userName");
return this.get("isOnline") ? fullName : userName;
}
}
});
In the above solution, we've elevated all get calls to above the conditional block where they'll all be reached. This is always a good practice to follow when using automatic mapping.
Alternatively, you may also declare computed dependencies manually, like so:
// FIXED by manual declarations:
var FixedModel = Backbone.Epoxy.Model.extend({
defaults: {
userName: "tatooine_luke",
fullName: "Luke Skywalker",
isOnline: true
},
computeds: {
displayName: {
deps: ["isOnline", "fullName", "userName"],
get: function( isOnline, fullName, userName ) {
return isOnline ? fullName : userName;
}
}
}
});
In the above solution, we've defined the computed attribute using a params object with a deps array. This array declares attribute names that the computed getter depends on, at which time those dependent attributes are mapped and injected as arguments into the getter method. Manually declaring dependencies will alleviate automation errors, yet may introduce a margin for human errors. It's your call on which direction seems safer.
Learn more about computed dependencies and automatic dependency mapping in the Model.addComputed documentation.
Computed Getters and Setters
So far we've only looked at computed attributes using read-only get functions. Now let's create a read-write computed attribute that will both get a computed value, and set one or more mutated values back to the model:
- JavaScript
- HTML
var PriceModel = Backbone.Epoxy.Model.extend({
defaults: {
productName: "Light Saber",
price: 5000
},
computeds: {
displayPrice: {
get: function() {
return "$"+this.get("price");
},
set: function( value ) {
return {price: parseInt(value.replace("$", "")||0, 10)};
}
}
}
});
var view = new Backbone.Epoxy.View({
el: "#app-readwrite",
model: new PriceModel()
});
<div id="app-readwrite">
<label>Price (updates on blur):</label>
<input type="text" data-bind="value:displayPrice">
<b>Display Price:</b>
<span data-bind="text:displayPrice"></span>
<b>Model Price:</b>
<span data-bind="text:price"></span>
</div>
Here, we've defined our computed attribute using a params object with both a get and set function. The get function will access our assembled model value, and the set function will mutate a raw value back into formatted model data. In the above example, the displayPrice computed attribute formats a currency string using its get method, and then reformats input as a valid number within its set method before submitting it back to the model.
Note that the setter function returns an attributes hash rather than calling set on its model directly. Attributes returned by a computed setter will get merged into the model's running set operation. This allows a computed setter to define multiple attribute modifications, all of which are performed synchronously with other queued model changes.
Learn more about computed getters and setters in the Model.addComputed documentation.
Computed View Properties
While computed model attributes are great for managing data, they start to break down when data needs to be formatted for specific display purposes; for example, when data needs to be formatted with HTML for its presentation. These cases are very specific to the view, and therefore should be computed in the view.
To accommodate view-specific formatting, an Epoxy view may define its own list of computed properties that will be made available to bindings. Let's try formatting a value for display using a computed view property:
- JavaScript
- HTML
var ComputedView = Backbone.Epoxy.View.extend({
el: "#app-view-computed",
computeds: {
nameDisplay: function() {
return "<b>"+ this.getBinding("lastName") +"</b>, "+ this.getBinding("firstName");
}
}
});
var view = new ComputedView({
model: new Backbone.Model({firstName: "Mace", lastName: "Windu"})
});
<div id="app-view-computed">
<label>First:</label>
<input type="text" data-bind="value:firstName,events:['keyup']">
<label>Last:</label>
<input type="text" data-bind="value:lastName,events:['keyup']">
<span data-bind="html:nameDisplay"></span>
</div>
The computed view property in the above example should look familiar. View computeds follow a similar pattern to model computeds, but differ in their data access. For computed view properties, we use the parent view's getBinding method to access view data. Note that the same conditional logic caveats discussed in Managing Computed Dependencies applies to computed view properties as well.
For more information on managing computed view properties and their dependencies, see computed view properties documentation.
View Binding Filters
Epoxy tries to strike a balance between robust binding options and clean binding definitions. While Epoxy uses a similar binding technique to Knockout.js, it intentionally discourages some of Knockout's inline-javascript allowances.
Instead, Epoxy provides filtering wrappers for formatting data directly within your bindings. Notice how the not() and format() filters are used in the following binding scheme:
- JavaScript
- HTML
var view = new Backbone.Epoxy.View({
el: "#app-filters",
model: new Backbone.Model({
firstName: "Luke",
lastName: "Skywalker"
})
});
<div id="app-filters">
<p>
<label>First name*</label>
<input type="text" data-bind="value:firstName,events:['keyup']">
<span data-bind="toggle:not(firstName)">Please enter a first name.</span>
</p>
<p>
<label>Last name*</label>
<input type="text" data-bind="value:lastName,events:['keyup']">
<span data-bind="toggle:not(lastName)">Please enter a last name.</span>
</p>
<p data-bind="text:format('Name: $1 $2',firstName,lastName)"></p>
<p class="req">* Required</p>
</div>
In the above example, filter wrappers are used to format binding data for specific implementations. The not() filter is used to negate a value's truthiness, and the format() filter is used to combine multiple values into a display string through a familiar RegEx backreference format.
The only catch with binding filters is that they may NOT be nested. This is a deliberate limitation about which Epoxy is fairly opinionated: application logic does not belong in your binding declarations. If a value requires more than a simple formatting pass, then it should be pre-processed within a computed property, or else applied to the view using a custom handler.
See a full list of available binding filters, or learn how to define your own custom filters.
Custom Binding Handlers
Binding handlers do the work of applying data values to DOM elements. Epoxy provides a collection of default binding handlers that cover many basic view operations. For everything else, developers are encouraged to write their own binding handlers for specific operations within the view. Custom bindings handlers are easy to define:
- JavaScript
- HTML
var model = new Backbone.Model({shipsList: []});
var BindingView = Backbone.Epoxy.View.extend({
el: "#app-custom",
bindingHandlers: {
listing: function( $element, value ) {
$element.text( value.join(", ") );
}
}
});
var view = new BindingView({model: model});
<div id="app-custom">
<label>Millennium Falcon:</label>
<input type="checkbox" value="Millennium Falcon" data-bind="checked:shipsList">
<label>Death Star:</label>
<input type="checkbox" value="Death Star" data-bind="checked:shipsList">
<b>Ships:</b>
<span data-bind="listing:shipsList"></span>
</div>
In the above example, we've set up a custom binding handler called listing to neatly print out an array of values. That custom handler may then be declared within the view's bindings, as seen in the "listing:shipsList" binding.
A binding handler is just a function that accepts two arguments: the first is a jQuery object wrapping the bound element, and the second is the data value being provided to the binding. Within a custom binding handler, you simply specify a process by which the value is formatted and then applied to the element. Note, the above example demonstrates a simple read-only binding.
Learn more about custom handlers and how to configure a two-way binding in the View.bindingHandlers documentation.
Binding Collections & Multiple Sources
So far we've only discussed the basic use case of binding a view to attributes of its model property. Now let's explore adding additional data sources, including Backbone.Collection instances.
First off, what is a data source? A data source provides itself and its attributes to the binding context — which is a compiled list of all data available in the view. Data sources may be instances of Backbone.Model or Backbone.Collection. By default, an Epoxy view's model and collection properties are automatically configured as data sources (you may also add additional sources if you need them).
Sources are included in the binding context under the alias "$sourceName". Therefore, the view's model and collection properties may be referenced within bindings as $model and $collection. These direct source references are used in cases such as the collection binding:
- JavaScript
- HTML
var ListItemView = Backbone.View.extend({
tagName: "li",
initialize: function() {
this.$el.text( this.model.get("label") );
}
});
var ListCollection = Backbone.Collection.extend({
model: Backbone.Model
});
var ListView = Backbone.Epoxy.View.extend({
el: "#bind-collection",
itemView: ListItemView,
initialize: function() {
this.collection = new ListCollection();
this.collection.reset([{label: "Luke Skywalker"}, {label: "Han Solo"}]);
}
});
var view = new ListView();
<div id="bind-collection">
<ul data-bind="collection:$collection"></ul>
</div>
In the above example, data-bind="collection:$collection" binds an unordered list's contents to the view's collection data source. However, what renders individual collection items? Note how an itemView property is defined on the list view class... That itemView property defines an item renderer for the collection.
For more details on setting up a collection binding, see the collection handler documentation. For more examples on using data source bindings, see the Epoxy ToDos demo below.
Epoxy ToDos
Per the status-quo of JavaScript MV* frameworks, let's build a small ToDos app using Epoxy view bindings paired with native Backbone models:
- JavaScript
- HTML
// Model for each ToDo item:
var TodoItemModel = Backbone.Model.extend({
defaults: {
todo: "",
complete: false
}
});
// Epoxy View for each ToDo item:
var TodoItemView = Backbone.Epoxy.View.extend({
el:"<li><input type='checkbox'> <input type='text' class='todo'></li>",
setterOptions: {save: true},
bindings: {
"input[type='text']": "value:todo,readonly:complete",
"input[type='checkbox']": "checked:complete"
},
bindingHandlers: {
readonly: function( $element, value ) {
$element.prop( "readonly", !!value );
}
}
});
// Collection for ToDo items:
var TodosCollection = Backbone.Collection.extend({
model: TodoItemModel,
localStorage: new Backbone.LocalStorage("todos")
});
// Epoxy View for main ToDos app:
var TodoAppView = Backbone.Epoxy.View.extend({
el: "#epoxy-todo-app",
collection: new TodosCollection(),
itemView: TodoItemView,
initialize: function() {
this.collection.fetch();
},
events: {
"click .add": "onAdd",
"click .cleanup": "onCleanup",
"keydown .todo-add": "onEnter"
},
onEnter: function( evt ) {
if ( evt.which == 13 ) this.onAdd();
},
onAdd: function() {
var input = this.$(".todo-add");
if ( input.val() ) {
this.collection.create({todo: input.val()});
input.val("");
}
},
onCleanup: function() {
_.invoke(this.collection.where({complete:true}), "destroy");
}
});
var app = new TodoAppView();
<div id="epoxy-todo-app">
<b>What do you need to do?</b>
<p>
<input type="text" class="todo-add">
<button class="add">Add</button>
</p>
<ul class="todos" data-bind="collection:$collection"></ul>
<button class="cleanup">Clear complete</button>
</div>
There are four components used within this application, including:
- TodoItemModel : this is a native Backbone model used to store the data required for each individual todo; in this example, each todo item has a todo caption, and a complete status.
- TodoItemView : this is an Epoxy view used for the display of each individual todo list item. This view constructs a DOM fragment with a checkbox and text input, and then binds those elements' values to the view's model. In addition, the view adds a few custom binding handlers to help manage the view: the readonly: handler toggles the text input's "readonly" property, and the save: binding is used used to call save on the bound model after elements are changed.
- TodosCollection : this is a native Backbone collection used to manage our active list of todos. It cites TodoItemModel as its model constructor.
- TodoAppView : finally, this Epoxy view manages the main application container view. It uses native Backbone events to setup the application's primary controls used to add and remove items from the TodosCollection instance. It also applies an Epoxy collection: binding to the view's default collection source (referenced as $collection). Also note, TodoItemView is provided as the itemView property for rendering individual collection items.
Mind you, this application certainly did not require data binding to make it work. In fact, data binding is overkill for many common application scenarios. Keep that in mind while assessing the goals and objectives of your projects. Ironically, the author of this library is a very reserved advocate of data binding: while data binding is a great tool at the moments when you need it, it should NOT be considered as an automatic choice approach when creating an interface; especially when using Backbone. However when a situation does lend itself to data binding... you'd like it to be done well.