Backbone Stack tutorial
This tutorial will help you to get an overview of resthub-backbone-stack.
If you want to use this tutorial in a training mode, a version without answers is also available.
Code: you can find the code of the sample application on Github.
Step 1: Model and View
Find:
-
Backbone documentation and description of Backbone.Events, Model, Collection, Router, Sync, View
-
RequireJS documentation:
-
requirejs usage
RequireJS allows to define modules and dependencies between modules in order to load your js files
-
how to define a module as function
define(['backbone'], function(Backbone) { ... });
-
how to use it
require(['model/task', 'view/task-view'], function(Task, TaskView) { ... });
-
usage for config options shims and path
-
-
Description of Resthub-backbone standard project layout based on requireJS
Do:
-
Get an empty resthub-backbone project via an archetype and discover the base application layout
-
Create a Task model
```javascript define([‘backbone’], function(Backbone) {
var Task = Backbone.Model.extend();
return Task;
}); ```
-
Instantiate a task in app.js with attributes title and description
javascript var task = new Task({ title: 'Learn Backbone', description: 'To write great Rich Internet Applications.' });
-
Try to see your task object in the console. Make it work
attach task to window with
window.task = new Task(...)
-
Try to access to title and description. Is task.title working?
task.title does not work.
-
Inspect task and find where attributes are stored
In attributes.
-
Access title attribute value
task.get("title")
-
Change description attribute. What operation does backbone perform whena a model attrbute is modified?
task.set("description", "newDesc");
Backbone raise events on attribute modification (“change”, etc.) so we have to use getters / setters to manipulate attributes
-
Create a TaskView and implement render with a function that simply logs “rendered”
define(['backbone'], function(Backbone) { var TaskView = Backbone.View.extend({ render: function() { console.log("rendered"); return this; } }); return TaskView; });
-
Instantiate view in app and render it. Verify that “rendered” is logged. Try to render view multiple times in console
window.taskView = new TaskView(); taskView.render();
Output:
rendered >>> taskView.render() rendered Object { cid="view1", options={...}, $el=[1], more...} >>> taskView.render() rendered Object { cid="view1", options={...}, $el=[1], more...}
-
Instantiate the view with a task model in app. Modify TaskView render to log the title of the task. No other modification should be made on TaskView
app.js:
window.task = new Task({ title: 'Learn Backbone', description: 'To write great Rich Internet Applications.' }); window.taskView = new TaskView({model: task}); taskView.render();
view/task.js:
render: function() { console.log(this.model.get("title")); return this; }
Output:
Learn Backbone >>> taskView.render() Learn Backbone Object { cid="view1", options={...}, $el=[1], more...}
Write in DOM
View rendering is done in view relative el element that could be attached anywhere in DOM with jQuery DOM insertion API
Find:
-
backbone view’s DOM element documentation
see here
-
jquery documentation and search for $(), html(), append() methods
see here
Do:
-
Modify render to display a task inside a div with class=’task’ containing title in a h1 and description in a p
render: function() { this.$el.html("<div class='task'><h1>" + this.model.get("title") + "</h1><p>" + this.model.get("description") + "</p></div>"); return this; }
-
render the view and attach $el to the DOM ‘tasks’ element (in app.js)
$('#tasks').html(taskView.render().el);
Templating
Let’s render our task in DOM with a template engine: Handlebars
Find:
-
Handlebars documentation
see here
-
How to pass a full model instance as render context in backbone
see here
Do:
-
Create Task handlebars template to display task. Template should start with a div with class=’task’
<div class="task"> <h1>{{title}}</h1> <p>{{description}}</p> </div>
-
Load (with requirejs text plugin), compile template in view and render it (pass all model to template)
define(['backbone', 'text!template/task.hbs', 'handlebars'], function(Backbone, taskTemplate, Handlebars) { var TaskView = Backbone.View.extend({ template: Handlebars.compile(taskTemplate), render: function() { this.$el.html(this.template(this.model.toJSON())); return this; } }); return TaskView; });
-
Resthub comes with a hbs RequireJS extension to replace Handlebars.compile. Change TaskView to use this extension. Remove Handlebars requirement
define(['backbone', 'hbs!template/task'], function(Backbone, taskTemplate) { var TaskView = Backbone.View.extend({ render: function() { this.$el.html(taskTemplate(this.model.toJSON())); return this; } }); return TaskView; });
Model events
Find:
Do:
-
Update task in the console -> does not update the HTML
-
Bind model’s change event in the view to render. Update task in console: HTML is magically updated!
var TaskView = Backbone.View.extend({ initialize: function() { this.listenTo(this.model, 'change', this.render); }, render: function() { this.$el.html(taskTemplate(this.model.toJSON())); return this; } });
Step 2: Collections
-
Create a Tasks collection in
collection
directorydefine(['backbone'], function(Backbone) { var Tasks = Backbone.Collection.extend(); return Tasks; });
- Create a TasksView in
view
and a tasks template intemplate
. - Implement rendering in TasksView
- Pass the collection as context
-
Iterate through the items in the collection in the template. Template should start with an
ul
element with class=’task-list’// view define(['backbone', 'hbs!template/tasks'], function(Backbone, tasksTemplate) { var TasksView = Backbone.View.extend({ render: function() { this.$el.html(tasksTemplate(this.collection.toJSON())); return this; } }); return TasksView; });
// template <ul class="task-list"> {{#each this}} <li class="task">{{title}}</li> {{/each}} </ul>
-
In app: instanciate two task and add them into a new tasks collections. Instantiate View and render it and attach $el to ‘#tasks’ div
define(['model/task', 'collection/tasks', 'view/tasks'], function(Task, Tasks, TasksView) { var tasks = new Tasks(); var task1 = new Task({ title: 'Learn Backbone', description: 'To write great Rich Internet Applications.' }); var task2 = new Task({ title: 'Learn RESThub', description: 'Use rethub.org.' }); tasks.add(task1); tasks.add(task2); var tasksView = new TasksView({collection: tasks}); $('#tasks').html(tasksView.render().el); });
-
try adding an item to the collection in the console
require(['model/task', 'collection/tasks', 'view/tasks'], function(Task, Tasks, TasksView) { window.Task = Task; window.tasks = new Tasks(); ... });
Output:
>>> task3 = new Task() Object { attributes={...}, _escapedAttributes={...}, cid="c3", more...} >>> task3.set("title", "Learn again"); Object { attributes={...}, _escapedAttributes={...}, cid="c3", more...} >>> task3.set("description", "A new learning"); Object { attributes={...}, _escapedAttributes={...}, cid="c3", more...} >>> tasks.add(task3); Object { length=3, models=[3], _byId={...}, more...}
HTML was not updated.
-
Bind collection’s add event in the view to render
define(['backbone', 'hbs!template/tasks'], function(Backbone, tasksTemplate) { var TasksView = Backbone.View.extend({ initialize: function() { this.listenTo(this.collection, 'add', this.render); }, render: function() { this.$el.html(tasksTemplate(this.collection.toJSON())); return this; } }); return TasksView; });
Output:
>>> task3 = new Task() Object { attributes={...}, _escapedAttributes={...}, cid="c3", more...} >>> task3.set("title", "Learn again"); Object { attributes={...}, _escapedAttributes={...}, cid="c3", more...} >>> task3.set("description", "A new learning"); Object { attributes={...}, _escapedAttributes={...}, cid="c3", more...} >>> tasks.add(task3); Object { length=3, models=[3], _byId={...}, more...}
HTML is updated with the new task in collection.
-
Add a task to the collection in the console -> the whole collection in rerendered.
>>> task3 = new Task() Object { attributes={...}, _escapedAttributes={...}, cid="c3", more...} >>> task3.set("title", "Learn again"); Object { attributes={...}, _escapedAttributes={...}, cid="c3", more...} >>> task3.set("description", "A new learning"); Object { attributes={...}, _escapedAttributes={...}, cid="c3", more...} >>> tasks.add(task3); Object { length=3, models=[3], _byId={...}, more...}
Step 3: Nested Views
-
Remove the each block in template.
```html
```
-
Use TaskView in TasksView to render each tasks.
// view/tasks.js render: function() { this.$el.html(tasksTemplate(this.collection.toJSON())); this.collection.forEach(this.add, this); return this; },
-
Update a task in the console -> the HTML for the task is automatically updated.
// app.js ... window.task1 = new Task({ title: 'Learn Backbone', description: 'To write great Rich Internet Applications.' });
output:
>>> task1.set("title", "new Title"); Object { attributes={...}, _escapedAttributes={...}, cid="c0", more...}
-
Add tasks to the collection in the console -> the whole list is still rerendered.
>>> task3 = new Task() Object { attributes={...}, _escapedAttributes={...}, cid="c3", more...} >>> task3.set("title", "Learn again"); Object { attributes={...}, _escapedAttributes={...}, cid="c3", more...} >>> task3.set("description", "A new learning"); Object { attributes={...}, _escapedAttributes={...}, cid="c3", more...} >>> tasks.add(task3); Object { length=3, models=[3], _byId={...}, more...}
-
Update TasksView to only append one task when added to the collection instead of rendering the whole list again.
initialize: function() { this.render(); this.listenTo(this.collection, 'add', this.add); },
- Test in the console.
-
Remove automatic generated divs and replace them with lis
goal is to have:
<ul> <li class='task'></li> <li class='task'></li> </ul>
instead of:
<ul> <div><li class='task'></li></div> <div><li class='task'></li></div> </ul>
example:
// view/task.js var TaskView = Backbone.View.extend({ tagName:'li', className: 'task', ...
-
Manage click in TaskView to toggle task’s details visibility.
events: { click: 'toggleDetails' }, ... toggleDetails: function() { this.$('p').slideToggle(); }
Step 4: Rendering strategy
Find:
-
Resthub documentation for default rendering strategy
see here
Do:
-
Use Resthub.View for managing rendering in TaskView. Remove render method in TaskView and modify add method in TasksView to set root element
// view/task.js define(['backbone', 'resthub', 'hbs!template/task'], function(Backbone, Resthub, taskTemplate) { var TaskView = Resthub.View.extend({ template: taskTemplate, tagName: 'li', className: 'task', strategy: 'append', events: { click: 'toggleDetails' }, initialize: function() { this.listenTo(this.model, 'change', this.render); }, toggleDetails: function() { this.$('p').slideToggle(); } }); return TaskView; }); // view/tasks.js ... add: function(task) { var taskView = new TaskView({root: this.$('.task-list'), model: task}); taskView.render(); } ...
-
Use Resthub.View for managing rendering in TasksView. Call the parent render function.
define(['backbone', 'resthub', 'view/task-view', 'hbs!template/tasks'], function(Backbone, Resthub, TaskView, tasksTemplate) { var TasksView = Resthub.View.extend({ template: tasksTemplate, initialize: function() { this.render(); this.listenTo(this.collecion, 'add', this.add); }, render: function() { TasksView.__super__.render.apply(this); this.collection.forEach(this.add, this); return this; }, add: function(task) { var taskView = new TaskView({root: this.$('.task-list'), model: task}); taskView.render(); } }); return TasksView; });
-
In the console try adding a Task: thanks to the effect we can see that only one more Task is rendered and not the entirely list
>>> task3 = new Task() Object { attributes={...}, _escapedAttributes={...}, cid="c5", more...} >>> task3.set("title", "Learn again"); Object { attributes={...}, _escapedAttributes={...}, cid="c5", more...} >>> task3.set("description", "A new learning"); Object { attributes={...}, _escapedAttributes={...}, cid="c5", more...} >>> tasks.add(task3); Object { length=3, models=[3], _byId={...}, more...}
-
In the console, update an existing Task: thanks to the effect we can see that just this task is updated
>>> task3.set("title", "new Title"); Object { attributes={...}, _escapedAttributes={...}, cid="c5", more...}
Step 5: Forms
Do:
-
Create TaskFormView which is rendered in place when double clicking on a TaskView. Wrap your each form field in a div with
class='control-group'
. Addclass='btn btn-success'
on your input submit// view/task.js define(['backbone', 'resthub', 'view/taskform-view', 'hbs!template/task'], function(Backbone, Resthub, TaskFormView, taskTemplate) { var TaskView = Resthub.View.extend({ ... events: { click: 'toggleDetails', dblclick: 'edit' }, ... edit: function() { var taskFormView = new TaskFormView({root: this.$el, model: this.model}); taskFormView.render(); }, ... }); return TaskView; }); // view/taskform.js define(['backbone', 'resthub', 'hbs!template/taskform'], function(Backbone, Resthub, ,taskFormTemplate) { var TaskFormView = Resthub.View.extend({ template: taskFormTemplate, tagName: 'form', }); return TaskFormView; });
```html
-
When the form is submitted, update the task with the changes and display it again.
```javascript // view/taskform.js
… save: function() { this.model.save({ title: this.$(‘.title’).val(), description: this.$(‘.description’).val(), }); return false; } …
-
Add a button to create a new empty task. In TasksView, bind its click event to a create method which instantiate a new empty task with a TaskView which is directly editable. Add
class="btn btn-primary"
to this button<!-- template/tasks.hbs --> <ul class="task-list"></ul> <p> <button id="create" class="btn btn-primary" type="button">New Task</button> </p>
var TasksView = Resthub.View.extend({ template: tasksTemplate, events: { 'click #create': 'create' }, ... create: function() { var taskView = new TaskView({root: this.$('.task-list'), model: new Task()}); taskView.edit(); } });
-
Note that you have to add the task to the collection otherwise when you render the whole collection again, the created tasks disappear. Try by attach tasksView to windows and call render() from console
create: function() { var task = new Task(); this.collection.add(task, {silent: true}); var taskView = new TaskView({root: this.$('.task-list'), model: task}); taskView.edit(); }
-
Add a cancel button in TaskFormView to cancel task edition. Add a
class="btn cancel"
to this button<!-- template/taskform.hbs --> ... <input type="button" class="btn cancel" value="Cancel" />
javascript var TaskFormView = Resthub.View.extend({ ... events: { submit: 'save', 'click .cancel': 'cancel' }, ... cancel: function() { this.model.trigger('change'); } });
-
Add a delete button which delete a task. Add
class="btn btn-danger delete"
to this button. Remove the view associated to this task on delete click and remove the task from the collectionNote that we can’t directly remove it from the collection cause the TaskFormView is not responsible for the collection management and does not have access to this one.
Then use the model’s destroy method and note that Backbone will automatically remove the destroyed object from the collection on a destroy event
// view/taskform.js var TaskFormView = Resthub.View.extend({ ... events: { submit: 'save', 'click .cancel': 'cancel', 'click .delete': 'delete' }, ... delete: function() { this.model.destroy(); } }); // view/task.js ... initialize: function() { this.listenTo(this.model, 'change', this.render); this.listenTo(this.model, 'destroy', this.remove); }, ...
output:
// no click on delete >>> tasks Object { length=2, models=[2], _byId={...}, more...} // on click on delete >>> tasks Object { length=1, models=[1], _byId={...}, more...} // two clicks on delete >>> tasks Object { length=0, models=[0], _byId={...}, more...}
-
Note in the console that when removing a task manually in the collection, it does not disappear
>>> tasks Object { length=2, models=[2], _byId={...}, more...} >>> tasks.remove(tasks.models[0]); Object { length=1, models=[1], _byId={...}, more...}
But task is still displayed
-
Bind remove event on the collection to call
task.destroy()
in TasksView... initialize: function() { this.listenTo(this.collection, 'add', this.add); this.listenTo(this.collection, 'remove', this.destroyTask); }, ... destroyTask: function(task) { task.destroy(); }
-
Test again in the console
>>> tasks Object { length=2, models=[2], _byId={...}, more...} >>> tasks.remove(tasks.models[0]); Object { length=1, models=[1], _byId={...}, more...}
And task disapeared
Step 6: Validation
Find:
-
Backbone documentation about model validation
see here
-
Resthub documentation for populateModel
see here
Do:
-
Implement validate function in Task model: make sure that the title is not blank
define(['backbone'], function(Backbone) { var Task = Backbone.Model.extend({ validate: function(attrs) { if (/^\s*$/.test(attrs.title)) { return 'Title cannot be blank.'; } } }); return Task; });
-
In TaskFormView, on save method, get the result of set method call on attributes and trigger “change” event only if validation passes
save: function() { var success = this.model.set({ title: this.$('.title').val(), description: this.$('.desc').val(), }); // If validation passed, manually force trigger // change event even if there were no actual // changes to the fields. if (success) { this.model.trigger('change'); } return false; },
-
Update TaskForm template to add a span with class
help-inline
immediately after title input<div class="control-group"> <input class="title" type="text" placeholder="Title" value="{{model.title}}" /> <span class="help-inline"></span> </div>
-
In TaskFormView bind model’s error event on a function which renders validation errors. On error, add class “error” on title input and display error in span “help-inline”
initialize: function() { this.listenTo(this.model, 'add', this.add); this.model.on('invalid', this.invalid, this); }, ... invalid: function(model, error) { this.$('.control-group:first-child').addClass('error'); this.$('.help-inline').html(error); }
-
Use Backbone.Validation for easy validation management
// model/task.js define(['backbone'], function(Backbone) { var Task = Backbone.Model.extend({ validation: { title: { required: true, msg: 'A title is required.' } } }); return Task; }); // view/taskform.js define(['backbone', 'hbs!template/taskform'], function(Backbone, taskFormTemplate) { ... initialize: function() { this.listenTo(this.model, 'invalid', this.invalid); Backbone.Validation.bind(this); }, ... });
-
Note that Backbone.Validation can handle for you error displaying in your views: remove error bindings and method and ensure that you form input have a name attribute equals to the model attribute name
<div class="control-group"> <input class="title" type="text" name="title" placeholder="Title" value="{{model.title}}" /> <span class="help-inline"></span> </div> <div class="control-group"> <textarea class="description" rows="3" name="description" placeholder="Description">{{model.description}}</textarea> </div>
// view/taskform.js ... initialize: function() { Backbone.Validation.bind(this); }, ...
-
Rewrite save method using resthub
populateModel
and backboneisValid
save: function() { this.populateModel(this.$el); // If validation passed, manually force trigger // change event even if there were no actual // changes to the fields. if (this.model.isValid()) { this.model.trigger('change'); } return false; },
Step 7: Persist & Sync
- Our data are not persisted, after a refresh, our task collection will be reinitialized.
- Use Backbone local storage extension to persist our tasks into the local storage.
- Bind the collection’s reset event on TasksView.render to render the collection once synced with the local storage.
Step 8
- Download RESThub Spring tutorial sample project and extract it
- Create jpa-webservice/src/main/webapp directory, and move your JS application into it
- Run the jpa-webservice webapp thanks to Maven Jetty plugin
- Remove backbone-localstorage.js file and usage in JS application
-
Make your application retrieving tasks from api/task?page=no URL
// collection/tasks.js define(['backbone', 'model/task'], function(Backbone, Task) { var Tasks = Backbone.Collection.extend({ url: 'api/task', model: Task }); return Tasks; }); // app.js tasks.fetch({ data: { page: 'no'} });
- Validate that retrieve, delete, create and update actions work as expected with this whole new jpa-webservice backend