jsfiddle
I'm trying to create a message client using this tutorial at this point, the view is supposed to be updated when a new message is typed into the field and the add button is clicked.
For some reason the "addMessage" Event is failing to add a new model to the collection.
var messagesjson = [
{
id: 3,
message: "This is the message",
sender: "gabriel",
receiver: "gabriel",
has_been_read: false,
has_been_reported: false,
created_at: "2014-10-23T19:55:20+0200",
is_friend: false
},
{
id: 5,
message: "I'm loving this ",
sender: "gabriel",
receiver: "gabriel",
has_been_read: true,
has_been_reported: false,
created_at: "2014-10-23T20:02:34+0200",
is_friend: false
}];
var MessageModel = Backbone.Model.extend({
defaults:
{
id: 3,
message: "This is the message",
sender: "gabriel",
receiver: "gabriel",
has_been_read: false,
has_been_reported: false,
created_at: "2014-10-23T19:55:20+0200",
is_friend: false
}
});
var MessageView = Backbone.View.extend({
tagName: "div",
className: "listview",
template: $('#messageTemplate').html(),
render: function()
{
var tmpl = _.template(this.template);
console.log(this.model);
this.$el.html(tmpl(this.model.toJSON()));
return this;
}
});
var MessageCollection = Backbone.Collection.extend({
model: MessageModel
});
var MessageCollectionView = Backbone.View.extend({
el: $('#messages'),
initialize: function()
{
this.collection = new MessageCollection(messagesjson);
this.render();
this.collection.on("add", this.renderMessage, this);
},
render: function()
{
var that = this;
_.each(this.collection.models, function(item){
that.renderMessage(item);
},this);
},
events:{
"click #add":"addMessage"
},
renderMessage: function(item)
{
var messageview = new MessageView({
model: item
});
this.$el.append(messageview.render().el);
},
addMessage: function(e)
{
e.preventDefault();
var formData = {};
$("#addMessage").children("input").each(function (i, el) {
formData[el.id] = $(el).val();
});
messagesjson.push(formData);
this.collection.add(new MessageModel(formData));
console.log(messagesjson);
}
});
var messagecollectionview = new MessageCollectionView();
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
<script src="http://cdnjs.cloudflare.com/ajax/libs/backbone.js/1.1.2/backbone-min.js"></script>
<script src="http://cdnjs.cloudflare.com/ajax/libs/underscore.js/1.7.0/underscore-min.js"></script>
<div id="messages">
<form id="addMessage" action="#">
<div>
<label for="messageText">Message: </label>
<input id="messageText" type="text" />
<button id="add">Add</button>
</div>
</form>
</div>
<script type="text/template" id="messageTemplate">
<a href="#" class="list autoWidth <% if(has_been_read) { %> selected <% } %>">
<div class="list-content">
<img src="//www.polyvore.com/cgi/img-thing?.out=jpg&size=l&tid=20774792" class="icon">
<div class="data">
<span class="item-title-secondary fg-gray"><b><%= sender %></b></span>
</div>
<span class="tertiary-text">
<%= message %>
</span>
</div>
</a>
</script>
You are setting the id as 3 for all new models as your defaults hash contains id: 3. The collection thinks it is the same model as it already has a model with that id.
So first you need to change your defaults with id:null:
var MessageModel = Backbone.Model.extend({
defaults:{
id: null,
message: "This is the message",
sender: "gabriel",
receiver: "gabriel",
has_been_read: false,
has_been_reported: false,
created_at: "2014-10-23T19:55:20+0200",
is_friend: false
}
});
Then you need to fix the code getting the formData. First of all you are using jQuery children() method which only looks at immediate childrens. That means you will never get the inputs inside the form as there is an intermediate div. You could use find.
Secondly, you need to make sure that formData has a property named message so it can override the default message: "This is the message". I would add a name attribute message to the input element and use it like formData[el.name] = $(el).val();. (You could later use one of the jquery serializeObject plugins to automatically serialize all input elements this way).
So the addMessage would look like this:
addMessage: function(e){
e.preventDefault();
var formData = {};
$("#addMessage").find("input").each(function (i, el) {
formData[el.name] = $(el).val();
});
messagesjson.push(formData);
this.collection.add(new MessageModel(formData));
}
You can try it in this fiddle
Related
I'm trying to learn Backbone and can't seem to match data from the fetch function into my Underscore template. How can can I get the children array in my JSON and match it to the template?
The Backbone.View looks like this:
var Projects = Backbone.Collection.extend({
url: '/tree/projects'
});
var Portfolio = Backbone.View.extend({
el: '.page',
render: function () {
var that = this;
var projects = new Projects();
projects.fetch({
success: function (projects) {
var template = _.template($('#projects-template').html());
that.$el.html(template({projects: projects.models}));
}
})
}
});
At the url: http://localhost:3000/portfolio/api/tree/projects
The JSON returned looks like this:
{
id:"projects",
url:"http://localhost:8888/portfolio/projects",
uid:"projects",
title:"Projects",
text:"",
files:[
],
children:[
{
id:"projects/example-1",
url:"http://localhost:8888/portfolio/projects/example-1",
uid:"example-1",
title:"Example 1",
images:"",
year:"2017",
tags:"Website",
files:[
],
children:[
]
},
{
id:"projects/example-2",
url:"http://localhost:8888/portfolio/projects/example-2",
uid:"example-2",
title:"Example #"2
text:"Example 2's text",
year:"2016",
tags:"Website",
files:[
{
url:"http://localhost:8888/portfolio/content/1-projects/4-example-2/example_ss.png",
name:"example_ss",
extension:"png",
size:244845,
niceSize:"239.11 kB",
mime:"image/png",
type:"image"
}
],
children:[
]
},
]
}
My Underscore file looks like this:
<script type="text/template" id="projects-template">
<h4>tester</h4>
<div>
<% _.each(projects.children, function (project) { %>
<div>
<div><%= project.get('year') %></div>
<div><%= project.get('title') %></div>
<div><%= project.get('tags') %></div>
</div>
<% }); %>
</div>
</script>
You can define a parse method on the collection:
var Projects = Backbone.Collection.extend({
url: '/tree/projects',
parse: function(response){
/* save other data from response directly to collection if needed.
for eg this.title = response.title; */
return response.children; // now models will be populated from children array
}
});
Do not use parse
While I usually agree with TJ, using parse on the collection is more like a hack than a definite solution. It would work only to get the children projects of a project and nothing more.
The parse function shouldn't have side-effects on the collection and with this approach, changing and saving fields on the parent project wouldn't be easily possible.
It also doesn't deal with the fact that it's a nested structure, it's not just a wrapped array.
This function works best when receiving wrapped data:
{
data: [{ /*...*/ }, { /*...*/ }]
}
Models and collections
What you have here are projects that have nested projects. A project should be a model. You also have files, so you should have a File model.
Take each resource and make a model and collection classes with it. But first, get the shared data out of the way.
var API_ROOT = 'http://localhost:8888/';
File
var FileModel = Backbone.Model.extend({
defaults: {
name: "",
extension: "png",
size: 0,
niceSize: "0 kB",
mime: "image/png",
type: "image"
}
});
var FileCollection = Backbone.Collection.extend({
model: FileModel
});
Project
var ProjectModel = Backbone.Model.extend({
defaults: function() {
return {
title: "",
text: "",
files: [],
children: []
};
},
getProjects: function() {
return this.get('children');
},
setProjects: function(projectArray, options) {
return this.set('children', projectArray, options);
},
getFiles: function() {
return this.get('files');
},
getSubProjectUrl: function() {
return this.get('url');
}
});
var ProjectCollection = Backbone.Collection.extend({
model: ProjectModel,
url: API_ROOT + '/tree/projects'
});
Project view
Then, make a view for a project. This is a simple example, see the additional information for tips on optimizing the rendering.
var ProjectView = Backbone.View.extend({
template: _.template($('#projects-template').html()),
initialize: function(options) {
this.options = _.extend({
depth: 0, // default option
}, options);
// Make a new collection instance with the array when necessary
this.collection = new ProjectCollection(this.model.getProjects(), {
url: this.model.getSubProjectUrl()
});
},
render: function() {
this.$el.html(this.template(this.model.toJSON()));
this.$projectList = this.$('.list');
// use the depth option to avoid rendering too much projects
if (this.depth > 0) this.collection.each(this.renderProject, this);
return this;
}
renderProject: function(model) {
this.$projectList.append(new ProjectView({
model: model,
depth: depth - 1
}).render().el);
}
});
With a template like this:
<script type="text/template" id="projects-template">
<h4><%= title %></h4>
<span><%= year %></span><span><%= tags %></span>
<p><%= text %></p>
<div class="list"></div>
</script>
Using the view:
var model = new ProjectModel({ id: "project" });
model.fetch({
success: function() {
var project = new ProjectView({
model: model,
depth: 2
});
}
});
Additional info
Nested models and collections
Efficiently rendering a list
I am struggling with displaying two models/collections in the same page.
<body>
<div id="mainContainer">
<div id="contentContainer"></div>
</div>
<div id="mainContainer2">
<div id="contentContainer2"></div>
</div>
<script id="list_container_tpl" type="text/template">
<div class="grid_5 listContainer">
<div class="box">
<h2 class="box_head grad_colour">Your tasks</h2>
<div class="sorting">Show: <select id="taskSorting"><option value="0">All Current</option><option value="1">Completed</option></select>
<input class="search round_all" id="searchTask" type="text" value="">
</div>
<div class="block">
<ul id="taskList" class="list"></ul>
</div>
</div>
</div>
</script>
<script id="list2_container_tpl" type="text/template">
<div class="grid_5 mylistContainer">
<div class="box">
<h2 class="box_head grad_colour">Your facets</h2>
<div class="sorting">
%{--Show: <select id="taskSorting"><option value="0">All Current</option><option value="1">Completed</option></select>--}%
<input class="search round_all" id="searchFacet" type="text" value="">
</div>
<div class="block">
<ul id="facetList" class="list"></ul>
</div>
</div>
</div>
</script>
<script id="task_item_tpl" type="text/template">
<li class="task">
<h4 class="name searchItem">{{ name }}</h4>
</li>
</script>
<script id="facet_item_tpl" type="text/template">
<li class="facet">
<h5 class="label searchItem">{{ label }}</h5>
</li>
</script>
<script>
var myapp = {
model: {},
view: {},
collection: {},
router: {}
};
var facetsSearch = {
model: {},
view: {},
collection: {},
router: {}
};
</script>
<script src="underscore-min.js"></script>
<script src="handlebars.min.js"></script>
<script src="backbone-min.js"></script>
<script>
/* avoid */
_.templateSettings = {
interpolate: /\{\{(.+?)\}\}/g
};
</script>
<script>
// model.tasks.js
myapp.model.Tasks = Backbone.Model.extend({
default:{
completed: 0,
name: ""
},
//url:"/js/libs/fixtures/task.json"
});
var tasks1 = new myapp.model.Tasks({
completed: 0,
name: "Clear dishes"
}
);
var tasks2 = new myapp.model.Tasks({
completed: 1,
name: "Get out the trash"
}
);
var tasks3 = new myapp.model.Tasks({
completed: 0,
name: "Do the laundry"
}
);
var tasks4 = new myapp.model.Tasks({
completed: 1,
name: "Vacuuming the carpet"
}
);
// collection.tasks.js
myapp.collection.Tasks = Backbone.Collection.extend({
currentStatus : function(status){
return _(this.filter(function(data) {
return data.get("completed") == status;
}));
},
search : function(letters){
if (letters == "") return this;
var pattern = new RegExp(letters,"gi");
return _(this.filter(function(data) {
return pattern.test(data.get("name"));
}));
}
});
myapp.collection.tasks = new myapp.collection.Tasks([tasks1, tasks2, tasks3, tasks4]);
// route.tasks.js
myapp.router.Tasks = Backbone.Router.extend({
routes: {
"": "list",
},
list: function(){
this.listContainerView = new myapp.view.TasksContainer({
collection: myapp.collection.tasks
});
$("#contentContainer").append(this.listContainerView.render().el);
this.listContainerView.sorts()
}
});
myapp.router.tasks = new myapp.router.Tasks;
<!-- render views -->
myapp.view.TasksContainer = Backbone.View.extend({
events: {
"keyup #searchTask" : "search",
"change #taskSorting" : "sorts"
},
render: function(data) {
$(this.el).html(this.template);
return this;
},
renderList : function(tasks){
$("#taskList").html("");
tasks.each(function(task){
var view = new myapp.view.TasksItem({
model: task,
collection: this.collection
});
$("#taskList").append(view.render().el);
});
return this;
},
initialize : function(){
this.template = _.template($("#list_container_tpl").html());
this.collection.bind("reset", this.render, this);
},
search: function(e){
var letters = $("#searchTask").val();
this.renderList(this.collection.search(letters));
},
sorts: function(e){
var status = $("#taskSorting").find("option:selected").val();
if (status == "") status = 0;
this.renderList(this.collection.currentStatus(status));
}
});
myapp.view.TasksItem = Backbone.View.extend({
events: {},
render: function(data) {
$(this.el).html(this.template(this.model.toJSON()));
console.log(this.model.toJSON(), "became", this.template(this.model.toJSON()));
return this;
},
initialize : function(){
this.template = _.template($("#task_item_tpl").html());
}
});
</script>
<script>
// model.facets.js
facetsSearch.model.Facets = Backbone.Model.extend({
default: {
id: 0,
label: "",
facetValues: []
}
});
var facet1 = new facetsSearch.model.Facets({
id: 1,
label: "Organism",
facetValues: ["Orga1", "Orga2"]
});
var facet2 = new facetsSearch.model.Facets({
id: 2,
label: "Omics",
facetValues: ["Omics1", "Omics2"]
});
var facet3 = new facetsSearch.model.Facets({
id: 3,
label: "Publication Date",
facetValues: ["2016-11-01", "2016-11-02"]
});
// collection.facets.js
facetsSearch.collection.Facets = Backbone.Collection.extend({
search : function(letters){
if (letters == "") return this;
/**
* the g modifier is used to perform a global match (find all matches rather than stopping after the first match).
* Tip: To perform a global, case-insensitive search, use this modifier together with the "i" modifier.
*/
var pattern = new RegExp(letters, "gi");
return _(this.filter(function(data) {
return pattern.test(data.get("label"));
}));
}
});
facetsSearch.collection.facets = new facetsSearch.collection.Facets([facet1, facet2, facet3]);
// route.facets.js
facetsSearch.router.Facets = Backbone.Router.extend({
routes: {
"": "list",
},
list: function(){
this.mylistContainerView = new facetsSearch.view.FacetsContainer({
collection: facetsSearch.collection.facets
});
console.log("Facet collection: ", facetsSearch.collection.facets);
$("#contentContainer2").append(this.mylistContainerView.render().el);
this.mylistContainerView.sorts()
}
});
facetsSearch.router.Facets = new facetsSearch.router.Facets;
facetsSearch.view.FacetsContainer = Backbone.View.extend({
events: {
"keyup #searchFacet" : "search",
"change #facetSorting": "sorts"
},
render: function(data) {
$(this.el).html(this.template);
return this;
},
renderList : function(facets){
$("#facetList").html("");
facets.each(function(facet){
var view2 = new facetsSearch.view.FacetsItem({
model: facet,
collection: this.collection
});
$("#facetList").append(view2.render().el);
});
return this;
},
initialize : function(){
this.template = _.template($("#list2_container_tpl").html());
this.collection.bind("reset", this.render, this);
},
search: function(e){
var letters = $("#searchFacet").val();
this.renderList(this.collection.search(letters));
},
sorts: function(e){
/*var status = $("#taskSorting").find("option:selected").val();
if (status == "") status = 0;
this.renderList(this.collection.currentStatus(status));*/
}
});
facetsSearch.view.FacetsItem = Backbone.View.extend({
events: {},
render: function(data) {
$(this.el).html(this.template(this.model.toJSON()));
console.log(this.model.toJSON(), "became", this.template(this.model.toJSON()));
return this;
},
initialize : function(){
this.template = _.template($("#facet_item_tpl").html());
}
});
</script>
<script>
Backbone.history.start();
</script>
</body>
The problem
To display Tasks above Your facets. I created two bunches of codes to render Tasks and Facets but modified the variable names respectively. Unfortunately, the former cannot be displayed.
As Emile mentioned in his detailed answer, your problem is that you're initializing multiple routers with the same route.
Since you seem to be beginning with Backbone, I'll give you a simpler answer than creating a complicated Layout (parent) view:
Just have a single handler for the specific route, and initialize both your views inside it.
It'll look something like:
myapp.router = Backbone.Router.extend({
routes: {
"": "list",
},
list: function() {
this.listContainerView = new myapp.view.TasksContainer({
collection: myapp.collection.tasks
});
$("#contentContainer").append(this.listContainerView.render().el);
this.listContainerView.sorts(); //this can be done inside the view
this.mylistContainerView = new facetsSearch.view.FacetsContainer({
collection: facetsSearch.collection.facets
});
$("#contentContainer2").append(this.mylistContainerView.render().el);
this.mylistContainerView.sorts(); //this can be done inside the view
}
});
You simply initialize 2 views in the same route.
You made 2 routers, both with an empty route. Each route is registered in Backbone.history, so when the facets router is initialized, its route overrides the tasks router route.
How to have multiple routers?
For the scope of your application, you should just start by making a single router, and handling the page with 2 lists within a parent view. Make a sort of Layout view for that page, which will handle the 2 lists:
var Layout = Backbone.View.extend({
template: _.template($('#layout-template').html()),
// keep the selector strings in a simple object
selectors: {
tasks: '.task-container',
facets: '.facet-container',
},
initialize: function() {
this.view = {
tasks: new TaskList(),
facets: new FacetList()
};
},
render: function() {
this.$el.html(this.template());
var views = this.views,
selectors = this.selectors;
this.$(selectors.tasks).append(views.tasks.render().el);
this.$(selectors.facets).append(views.facets.render().el);
return this;
}
});
Then, only one router:
var Router = Backbone.Router.extend({
routes: {
"": "list",
},
list: function() {
this.listContainerView = new Layout();
$("body").html(this.listContainerView.render().el);
}
});
This won't work with your code as-is, you'll have to incorporate the concepts yourself into your app.
Otherwise, if you really want multiple routers, you must understand that they can't share a route and that at any moment, only one route can be triggered.
When you have multiple routers, each manages the routes of a single module.
var TaskRouter = Backbone.Router.extend({
routes: {
'tasks': 'taskList',
'tasks/:id': 'taskDetails'
}
// ...snip...
});
var FacetsRouter = Backbone.Router.extend({
routes: {
'facets': 'facetList',
'facets/:id': 'facetDetails'
}
// ...snip...
});
Other improvements
Compile the template once
It's more efficient to compile the template once, when the view is being extended, than each time a new view is initialized.
myapp.view.TasksContainer = Backbone.View.extend({
// gets compiled once
template: _.template($("#list_container_tpl").html()),
initialize: function() {
// not here, as it gets compiled for each view
// this.template = _.template($("#list_container_tpl").html())
},
});
Avoid the global jQuery function
Avoid $(this.el) in favor of this.$el.
Avoid $('#divInTemplate') in favor of this.$('.divInTemplate'). It's a shortcut to this.$el.find.
See What is the difference between $el and el for additional informations.
Cache the jQuery objects
Whenever you want to select a child of the view's element, do it once and put the result in a variable.
render: function(data) {
this.$el.html(this.template);
// I like to namespace them inside an object.
this.elements = {
$list: this.$('.task-list'),
$search: this.$('.task-sorting')
};
// then werever you want to use them
this.elements.$list.toggleClass('active');
return this;
},
Use listenTo
Avoid bind/unbind and on/off/once (aliases) in favor of listenTo/stopListening/listenToOnce.
listenTo is an improved version of bind which solves problem with memory leaks.
this.collection.bind("reset", this.render, this);
// becomes
this.listenTo(this.collection, "reset", this.render);
Pass an array of objects to collection
myapp.collection.Tasks = Backbone.Collection.extend({
model: myapp.model.Tasks
// ...snip...
});
myapp.collection.tasks = new myapp.collection.Tasks([{
completed: 0,
name: "Clear dishes"
}, {
completed: 1,
name: "Get out the trash"
}, {
completed: 0,
name: "Do the laundry"
}, {
completed: 1,
name: "Vacuuming the carpet"
}]);
This would be enough, Backbone collection takes care of the rest.
I have a Model as
var Info = Backbone.Model.extend({
defaults: {
name: '',
email :''
},
initialize: function(){
console.log('Object created');
},
validate: function(attr){
if(!attr.name){
return 'Name cannot be empty';
},
if(!attr.email){
return 'Email cannot be empty';
}
}
});
var model = new Info();
model.set({
name: '',
email: ''
});
var viewClass = Backbone.View.extend({
_modelBind: undefined,
initialize: function(){
this._modelBind = new Backbone.ModelBinder();
this.render();
},
render: function(){
var template = _.template($('#App1').html());
this.$el.html(template);
var bindings = {
name: '[name=name]',
email: '[name=email]'
};
this._modelBind.bind(model, this.el, bindings);
},
'events': {
'click #btnSubmit': 'Submit'
},
Submit: function(){
var response = {
name: model.get('name'),
email: model.get('email')
};
this.model.save(response);
}
});
html is
<script type="text/template" id="App1">
Name: <input type="text" id="name" name="name"/><br />
Email: <input type="text" id="email" name="email" /><br />
<input type="button" id="btnSubmit" value="Submit" />
</script>
On save event, it goes to validate method and after that it goes to the value specified in the url attribute of the model.
I don't want to specify a url parameter. Can i still use validate method without url ?
Call validate directly:
var error = this.model.validate(this.model.attributes);
If you don't specify url parameter, you will get an error on this.model.save():
Uncaught Error: A "url" property or function must be specified
I have a view created with Backbone.js and inserted into a div element - it shows for about a second and then disappears.
Here is my code for the view:
var addPlayerView = Backbone.View.extend({
tagName: "div",
model: Player,
id: 'addPlayerDiv',
initialize: function() {
console.log('addPlayerView has been created');
},
render: function (){
this.$el.html('<p>show this puppy</p>');
return this;
}
});
here is the model:
var Player = Backbone.Model.extend({
defaults: {
ID: "",
firstName: '',
lastName: ''
},
idAttribute: "ID"
});
and here is the HTML:
<form onsubmit="addNewPlayer();">
<input type="submit" value="Add New Player New"/>
</form>
<p>
<div id="addPlayerDiv"></div>
</p>
<script>
function addNewPlayer() {
var player = new Player({});
var newPlayerView = new addPlayerView({el: $("#addPlayerDiv"), model: player});
newPlayerView.render();
};
</script>
The addNewPlayer() function is being called correctly and the newPlayerView is rendering on the page, but only for a second, then it disappears on the page.
No idea what to do. Anyone have this problem before?
You need cancel the default action (in our case onsubmit tries send data to server)
<form onsubmit="addNewPlayer(); return false;">
or
<form onsubmit="addNewPlayer(event);">
function addNewPlayer(e) {
e.preventDefault();
.....
}
Example
window.User = Backbone.Model.extend({
defaults: {
name: 'Jane',
friends: []
},
urlRoot: "users",
initialize: function(){
this.fetch();
}
});
var HomeView = Backbone.View.extend({
el: '#container',
template: _.template($("#home-template").html()),
render: function() {
$(this.el).html(this.template(this.model.toJSON()));
return this;
}
});
home: function() {
var user = new User({id: 1});
this.homeView = new HomeView({
model: user
});
this.homeView.render();
},
The model data is being queried and the root level attributes work fine, but the attribute that contains an array of other objects doesn't seem to show up.
Template:
<script id="home-template" type="text/template">
<div id="id">
<div class="name"><%= name %></div>
<br />
<h3> Single Friends</h3>
<br />
<ul data-role="listview" data-inset="true", data-filter="true">
<% _.each(friends, function(friend) { %>
<li>
<a href="/profile?id=<%= friend.id %>", data-ajax="false">
<div class="picture"><img src="http://graph.facebook.com/<%= friend.fb_user_id %>/picture"></div>
<div class="name"><%= friend.name %></div>
</a>
</li>
<% }); %>
</ul>
</div>
</script>
Return JSON:
{"name":"John Smith","friends":[{"id":"1234","name":"Joe Thompson","fb_user_id":"4564"},{"id":"1235","name":"Jane Doe","fb_user_id":"4564"}]}
It almost seems like it's not seeing the .friends attribute at all because it's taking the defaults of the model ([]).
Any suggestions?
You are calling render() before fetch() has returned the data from the server.
Try this?
window.User = Backbone.Model.extend({
defaults: {
name: 'Jane',
friends: []
},
urlRoot: "users"
});
var HomeView = Backbone.View.extend({
el: '#container',
template: _.template($("#home-template").html()),
initialize: function() {
this.model.fetch();
this.model.bind('change', this.render, this);
}
render: function() {
$(this.el).html(this.template(this.model.toJSON()));
return this;
}
});