Passing control back to a Backbone Router from a view? - javascript

I have a loginview and its loginmodel. loginmodel is created from the initialize of the mainrouter. this way according to the loginmodel's loginp attribute mainrouter's render_home decides whether to render homeview or navigate to login route which calls render_login. render_login have a method to save() the loginmodel populated from the loginview's input fields. So if the data is correct save() responded and loginmodel changes, its loginp attribute becomes true. so now the homeview should be rendered. but I cannot success this.
I think at this point the control should be passed back to the mainrouter's route that invokes render_home above again. So it tests loginView.model.get('loginp') == true and results in true so this time creates homeview and renders it via homeView.render() defined in the homeview already.
I put a listenTo in the loginview for when its model changes or syncs to call route in mainrouter to call render_home. but this ends in endless back and forths between these two view without any render. this.listenTo( this.loginView.model, 'sync', Backbone.history.navigate("",{trigger:true}));
route "" is assigned to render_home by the way.
What am I missing. Is it not possible to pass control back to a router?
main router:
define([
'jquery',
'ratchet',
'underscore',
'backbone',
'login/loginmodel',
'login/loginview',
'register/registerview',
'home/homeview',
],
function($, Ratchet, _, Backbone, LoginModel, LoginView, RegisterView, HomeView){
var MainRouter = Backbone.Router.extend({
routes: {
"": "render_home",
"login": "render_login",
"register": "render_register"
},
initialize: function(){
this.loginView = new LoginView;
},
render_home: function(){
this.loginView.model.fetch({
success: function(model){
if( model.get('loginp') == true ){ //show you app view for logged in users!
this.homeView = new HomeView();
this.homeView.render();
}
else { //not logged in!
Backbone.history.navigate("/login", {trigger:true})
}
},
});
},
render_login: function(){ //display your login view
this.loginView.render();
},
render_register: function(){ //display your register view
this.registerView = new RegisterView;
this.registerView.render();
},
});
return MainRouter;
});
loginview:
define([
'jquery',
'ratchet',
'underscore',
'backbone',
'login/loginmodel',
'text!login/logintemplate.html',
],
function($, Ratchet, _, Backbone, LoginModel, LoginTemplate){
var LoginView = Backbone.View.extend({
el: $('body'),
initialize: function(){
this.model = new LoginModel;
},
template: _.template( LoginTemplate ),
render: function(){ //display your login view
this.$el.html( this.template( this.model.attributes ) );
},
events: {
'keyup input' : 'updateform',
'click #loginbutton' : 'login',
'click #renderregisterbutton' : 'render_register',
},
updateform: function(e){
var el = e.target;
var formData = {};
formData[ el.id ] = $(el).val();
this.model.set( formData );
},
login: function(e){
e.preventDefault();
this.model.save();
console.log( JSON.stringify( this.model ) );
},
render_register: function(){
Backbone.history.navigate("/register", {trigger:true});
},
});
return LoginView;
});
loginmodel:
define([
'underscore',
'backbone',
],
function(_, Backbone) {
var LoginModel = Backbone.Model.extend({
urlRoot: '/log_in',
defaults: {
username: null,
},
});
return LoginModel;
});

You absolutely can "pass control" from a Route to a View and back; your render_login function does exactly that:
render_login: function(){ //display your login view
// 1) router has control
this.loginView.render(); // 2) control passes to loginView
// 3) control passes back to the router
},
But that only works with synchronous logic. I think the problem you're having is because you are using asynchronous logic (this.loginView.model.fetch()) inside your render_home method. Asynchronous logic splits off from the main execution path, so the path for render_home is more like:
// 0) Router has control, and calls render_home
render_home: function(){
// 1) LoginView has control
this.loginView.model.fetch({
success: function(model){
// 4) LoginView has control once again, but now routing is done
if( model.get('loginp') == true ){ //show you app view for logged in users!
this.homeView = new HomeView();
this.homeView.render();
}
else { //not logged in!
Backbone.history.navigate("/login", {trigger:true})
}
},
});
// 2) LoginView still has control
},
// 3) Control passes back to the Router, which finishes routing
So, by the time the fetch returns, there is no routing logic to come back to.
However, this is easy enough to fix: just call Backbone.history.navigate (with trigger: true) from your fetch success handler. This will start a new routing process, and by that point (since the fetch has completed) the LoginModel will now be populated and your routing logic should work.

Related

Cannot read property '_listenId' of undefined when using listenTo in a view

I am creating a view but it isn't rendering in my page.
I am building a SPA with backbone, and I need that my template can open inside of div in my body, but I don't know what is my problem here.
What can be?
Show this error:
Uncaught TypeError: Cannot read property '_listenId' of undefined
at child.Events.(anonymous function) [as listenTo] (http://localhost:9000/bower_components/backbone/backbone.js:222:19)
at child.initialize (http://localhost:9000/scripts/views/RepositoriesView.js:21:12)
at child.Backbone.View (http://localhost:9000/bower_components/backbone/backbone.js:1001:21)
at new child (http://localhost:9000/bower_components/backbone/backbone.js:1566:41)
at child.repositories (http://localhost:9000/scripts/routes/AppRouter.js:46:7)
at child.execute (http://localhost:9000/bower_components/backbone/backbone.js:1265:30)
at Object.callback (http://localhost:9000/bower_components/backbone/backbone.js:1254:16)
at http://localhost:9000/bower_components/backbone/backbone.js:1481:19
at Function.some (http://localhost:9000/bower_components/lodash/dist/lodash.compat.js:4304:25)
at Backbone.History.loadUrl (http://localhost:9000/bower_components/backbone/backbone.js:1479:16)
My AppRouter is:
/*global Sice, Backbone*/
Sice.Routers = Sice.Routers || {};
Sice.Views = Sice.Views || {};
(function() {
'use strict';
Sice.Routers.AppRouter = Backbone.Router.extend({
//map url routes to contained methods
routes: {
"": "repositories",
"repositories": "repositories",
"search": "search",
"starreds": "starreds"
},
deselectPills: function() {
//deselect all navigation pills
$('ul.pills li').removeClass('active');
},
selectPill: function(pill) {
//deselect all navigation pills
this.deselectPills();
//select passed navigation pill by selector
$(pill).addClass('active');
},
hidePages: function() {
//hide all pages with 'pages' class
$('div#content').hide();
},
showPage: function(page) {
//hide all pages
this.hidePages();
//show passed page by selector
$(page).show();
},
repositories: function() {
this.showPage('div#content');
this.selectPill('li.repositories-pill');
new Sice.Views.RepositoriesView();
},
search: function() {
this.showPage('div#content');
this.selectPill('li.search-pill');
},
starreds: function() {
this.showPage('div#content');
this.selectPill('li.starreds-pill');
}
});
Sice.Views.AppView = Backbone.View.extend({
//bind view to body element (all views should be bound to DOM elements)
el: $('body'),
//observe navigation click events and map to contained methods
events: {
'click ul.pills li.repositories-pill a': 'displayRepositories',
'click ul.pills li.search-pill a': 'displaySearch',
'click ul.pills li.starreds-pill a': 'displayStarreds'
},
//called on instantiation
initialize: function() {
//set dependency on Sice.Routers.AppRouter
this.router = new Sice.Routers.AppRouter();
//call to begin monitoring uri and route changes
Backbone.history.start();
},
displayRepositories: function() {
//update url and pass true to execute route method
this.router.navigate("repositories", true);
},
displaySearch: function() {
//update url and pass true to execute route method
this.router.navigate("search", true);
},
displayStarreds: function() {
//update url and pass true to execute route method
this.router.navigate("starreds", true);
}
});
//load application
new Sice.Views.AppView();
})();
My View is:
/*global Sice, Backbone, JST*/
Sice.Views = Sice.Views || {};
(function() {
'use strict';
Sice.Views.RepositoriesView = Backbone.View.extend({
template: JST['app/scripts/templates/RepositoriesView.ejs'],
tagName: 'div',
id: 'repositoriesView',
className: 'page-repositories',
events: {},
initialize: function() {
this.listenTo(this.model, 'change', this.render);
},
render: function() {
this.$el.html(this.template());
}
});
})();
What's happening?
In the following router function, you're instantiating a new View instance.
repositories: function() {
this.showPage('div#content');
this.selectPill('li.repositories-pill');
new Sice.Views.RepositoriesView(); // <-- here
},
But you're not passing a model object which the view listens to.
initialize: function() {
this.listenTo(this.model, 'change', this.render);
},
So it's calling this.listenTo with this.model as undefined.
What's the solution?
Pass a model instance
new Sice.Views.RepositoriesView({ model: new MyModel() });
Create a model instance in the view
initialize: function() {
this.model = new MyModel();
this.listenTo(this.model, 'change', this.render);
},

Single model doesn't show with router

I can not understand how to show a single product. When I want to show product page (model view - app.ProductItemView in productPageShow) I get "Uncaught TypeError: Cannot read property 'get' of undefined at child.productPageShow (some.js:58)" >> this.prod = this.prodList.get(id);
Here is my code:
// Models
var app = app || {};
app.Product = Backbone.Model.extend({
defaults: {
coverImage: 'img/placeholder.png',
id: '1',
name: 'Unknown',
price: '100'
}
});
app.ProductList = Backbone.Collection.extend({
model: app.Product,
url: 'php/listProducts.php'
});
// Views
app.ProductListView = Backbone.View.extend({
el: '#product-list',
initialize: function() {
this.collection = new app.ProductList();
this.collection.fetch({ reset: true });
this.render();
this.listenTo(this.collection, 'reset', this.render);
},
render: function() {
this.collection.each(function(item) {
this.renderProduct(item);
}, this);
},
renderProduct: function(item) {
app.productView = new app.ProductView({
model: item
});
this.$el.append(app.productView.render().el);
}
});
app.ProductItemView = Backbone.View.extend({
tagName: 'div',
template: _.template($('#productPage').html()),
render: function(eventName) {
$(this.el).html(this.template(this.model.toJSON()));
return this;
}
});
app.ProductView = Backbone.View.extend({
tagName: 'div',
template: _.template($('#productTemplate').html()),
render: function() {
this.$el.html(this.template(this.model.attributes));
return this;
}
});
// Router
app.Router = Backbone.Router.extend({
routes: {
"list": 'list',
"product/:id": "productPageShow"
},
initialize: function() {
this.$content = $("#product-list");
},
list: function() {
this.prodList = new app.ProductList();
this.productListView = new app.ProductListView({ model: this.prodList });
this.prodList.fetch();
this.$content.html(app.productListView.el);
},
productPageShow: function(id) {
this.prod = this.prodList.get(id);
this.prodItView = new app.ProductItemView({ model: this.prod });
this.$content.html(this.prodItView.el);
}
});
$(function() {
new app.Router();
Backbone.history.start();
});
There are some conceptual problems with the code, without getting into too much details, there are a lot of things happening in the Router that don't belong there, but for a (currently) non-complex application that's manageable.
I'll focus on the app.Router file because that's the culprit of your problems most probably.
routes: {
"list": 'list',
"product/:id": "productPageShow"
}
Let's start with the basics, when you define a list of routes in Backbone Router( or any other Router in other frameworks ) you give a route a key that will correspond to something in the URL that the Router will recognize and call a callback method.
If you navigate your browser to:
http://your-url#list
Backbone will call the list callback
Similarly:
http://your-url#product/1
Backbone will call productPageShow callback
Thing to know: ONLY ONE ROUTE CALLBACK CAN EVER BE CALLED! The first time a Backbone Router finds a matching route it will call that callback and skip all others.
In your code you're relying on the fact that this.prodList will exist in productPageShow method but that will only happen if you first go to list route and then to product/{id} route.
Another thing .. in your listcallback in the Router you set a model on the ProductListView instance .. but that model is neither user, nor is it a model since this.productList is a Backbone.Collection
Additionally, you need to know that fetch is an asynchronous action, and you're not using any callbacks to guarantee that you'll have the data when you need it ( other than relying on the 'reset' event ).
So this would be my attempt into making this workable:
// Models
var app = app || {};
app.Product = Backbone.Model.extend({
defaults: {
coverImage: 'img/placeholder.png',
id: '1',
name: 'Unknown',
price: '100'
}
});
app.ProductList = Backbone.Collection.extend({
model: app.Product,
url: 'php/listProducts.php'
});
// Views
app.ProductListView = Backbone.View.extend({
el: '#product-list',
initialize: function() {
this.render();
this.listenTo(this.collection, 'reset', this.render);
},
render: function() {
this.collection.each(function(item) {
this.renderProduct(item);
}, this);
},
renderProduct: function(item) {
app.productView = new app.ProductView({
model: item
});
this.$el.append(app.productView.render().el);
}
});
app.ProductItemView = Backbone.View.extend({
tagName: 'div',
template: _.template($('#productPage').html()),
render: function(eventName) {
$(this.el).html(this.template(this.model.toJSON()));
return this;
}
});
app.ProductView = Backbone.View.extend({
tagName: 'div',
template: _.template($('#productTemplate').html()),
render: function() {
this.$el.html(this.template(this.model.attributes));
return this;
}
});
// Router
app.Router = Backbone.Router.extend({
routes: {
"": "list",
"product/:id": "productPageShow"
},
initialize: function() {
this.$content = $("#product-list");
},
list: function() {
this.prodList = new app.ProductList();
this.productListView = new app.ProductListView({ collection: this.prodList });
this.prodList.fetch({reset:true});
this.$content.html(app.productListView.el);
},
productPageShow: function(id) {
try {
this.prod = this.prodList.get(id);
this.prodItView = new app.ProductItemView({ model: this.prod });
this.$content.html(this.prodItView.el);
} catch (e) {
// Navigate back to '' route that will show the list
app.Router.navigate("", {trigger:'true'})
}
}
});
$(function() {
app.Router = new app.Router();
Backbone.history.start();
});
So with a bit of shooting in the dark without the complete picture, this is what changed:
ProductListView is no longer instantiating ProductList collection that is done in the list callback in Router
Changed the route from 'list' to '', that will guarantee that the list is shown immediately
In case there are no product data available in productPageShow navigate back to list

First view undesireably renders from second view when an input field event triggered in Backbone app?

I have views each with a model and a template and one form model instance to render them from their respective templates. Form model instance is accessible from both view to render their templates. It is needed to update this form model instance on every character typed, and set the model of the current view of the two views with this form model instance. For this I have an keyup event on input elements in both views, which has different number of input views. Second view has all the same typed, named, id, input fields with the first view and some different ones.
When I am on the second view with more input fields and click and type a character in the input field both views have, the first view renders. Calling this.unbind() from one of the view before it routes to the other view did not work.
What is the reason for this? I do not have an event to render it when this is done.
mainrouter:
define([
'jquery',
'ratchet',
'underscore',
'backbone',
'forms/formsdatamodel',
'login/loginview',
'register/registerview',
'home/homeview',
],
function($, Ratchet, _, Backbone, FormsDataModelInst, LoginView, RegisterView, HomeView){
var MainRouter = Backbone.Router.extend({
routes: {
"": "render_home",
"login": "render_login",
"register": "render_register"
},
initialize: function(){
this.model = FormsDataModelInst;
this.listenTo( this.model, 'change', this.render_home );//this solved render_home problem.
},
render_home: function(){
if( this.model.get('loginp') == true ){ //show you app view for logged in users!
this.homeView = new HomeView();
this.homeView.render();
}
else { //not logged in!
Backbone.history.navigate("/login", {trigger:true});
}
},
render_login: function(){ //display your login view
this.loginView = new LoginView;
//this.loginView.delegateEvents();
this.loginView.render();
},
render_register: function(){ //display your register view
this.registerView = new RegisterView;
//this.registerView.delegateEvents();
this.registerView.render();
},
});
return MainRouter;
});
loginview:
define([
'jquery',
'ratchet',
'underscore',
'backbone',
'login/loginmodel',
'text!login/logintemplate.html',
'forms/formsdatamodel',
],
function($, Ratchet, _, Backbone, LoginModel, LoginTemplate, FormsDataModelInst){
var LoginView = Backbone.View.extend({
el: $('body'),
initialize: function(){
this.model = new LoginModel();
},
template: _.template( LoginTemplate ),
render: function(){ //display your login view
this.$el.html( this.template( FormsDataModelInst.attributes ) );
},
events: {
'keyup input#username' : 'updateModel',
'click #loginbutton' : 'login',
'click #renderregisterbutton' : 'render_register',
},
updateModel: function(e){
e.preventDefault();
var elm = e.target;
FormsDataModelInst.set( elm.id, $( elm ).val() );
this.model.set( FormsDataModelInst );
console.log( "1-FormsDataModelInst:" + JSON.stringify( FormsDataModelInst.attributes ) );
console.log( "2-this.model.set(...):" + JSON.stringify(this.model.attributes) );
},
login: function(e){
e.preventDefault();
this.model.save();
console.log( "3-this.model.save():" + JSON.stringify(this.model.attributes) );
},
render_register: function(e){
e.preventDefault();
this.undelegateEvents();
Backbone.history.navigate("/register", {trigger:true});
},
});
return LoginView;
});
registerview:
define([
'jquery',
'ratchet',
'underscore',
'backbone',
'register/registermodel',
'text!register/registertemplate.html',
'forms/formsdatamodel',
],
function($, Ratchet, _, Backbone, RegisterModel, RegisterTemplate, FormsDataModelInst){
var RegisterView = Backbone.View.extend({
el: $('body'),
initialize: function(){
this.model = new RegisterModel();
},
template: _.template( RegisterTemplate ),
render: function(){ //display your login view
this.$el.html( this.template( FormsDataModelInst.attributes ) );
},
events: {
'keyup input#username' : 'updateModel',
'keyup input#phone' : 'updateModel',
'keyup input#email' : 'updateModel',
'click #registerbutton' : 'register',
'click #renderloginbutton' : 'render_login',
},
updateModel: function(e){
e.preventDefault();
var elm = e.target;
FormsDataModelInst.set( elm.id, $( elm ).val() );
this.model.set( FormsDataModelInst );
console.log( "FormsDataModel:" + JSON.stringify( FormsDataModelInst ) );
console.log( "this.model.set(...):" + JSON.stringify(this.model.attributes) );
},
login: function(e){
e.preventDefault();
this.model.save();
console.log( "this.model.save():" + JSON.stringify(this.model) );
},
render_login: function(e){
e.preventDefault();
this.undelegateEvents();
Backbone.history.navigate("/login", {trigger:true});
},
});
return RegisterView;
});
When you instantiate a View you setup event bindings on the el of that View. It doesn't matter if a different View also uses the same inputs; as long as the first View is still around its event handlers will continue to listen for and respond to events.
You had the right idea by trying to call this.unbind() ... except that Views don't have an unbind method. They do however have a undelegateEvents method, which I think is what you're looking for.
In mainRouter the listener:
this.listenTo( this.formsDataModelInst, 'change', this.render_home );
causes the problem of rendering loginview undesirebly on typing in registerview. Because on registerview keyup is bound to updateModels and this changes both registermodel and formsDataModelInst. this change triggers above written event which routes to the loginview and renders it.
Above listener is needed when loginp property of the formsDataModelInst changes, to decide which View to render as implemented in mainRouter render, but not changes in the other properties like the ones updated on keyup events on form.
Change it to:
this.listenTo( this.formsDataModelInst, 'change:loginp', this.render_home );

Cannot unbind and close events in my Backbone router

I want to clean up my views in Backbone, before loading new views.
For that, I have searched and added a prototype property to the Backbone object called close:
Backbone.View.prototype.close = function () {
this.$el.empty();
//this.$el.unbind();
this.undelegateEvents();
console.log('close');
};
P.S. the empty() does not seem to work, neither does unbind -- undelegateEvents works.
Now, I cannot seem to figure out hot to call the close function on my router:
define([
'jquery',
'underscore',
'backbone',
'views/userList',
'views/addUser'
], function ($, _, Backbone, UserList, AddUser) {
var Router = Backbone.Router.extend({
routes: {
'' : 'home',
'new' : 'addUser'
}
});
var initialize = function () {
var router = new Router();
router.on('route:home', function () {
userList = new UserList();
userList.render();
});
router.on('route:addUser', function () {
addUser = new AddUser();
addUser.render();
});
Backbone.history.start();
}
return {
initialize : initialize
}
});
Any ideas?
First of all, don't use close, just override the standard remove method:
var SomeBaseView = Backbone.View.extend({
remove: function () {
this.$el.empty(); // Instead of this.$el.remove();
this.stopListening();
this.undelegateEvents();
return this;
}
});
and then derive all your views from SomeBaseView. Messing around with Backbone.View.prototype should be avoided.
Then, stop using global variables like this:
userList = new UserList();
in your route handlers.
Then, start keeping track of which view is currently open so that you can call remove on it before throwing up the next view. A router more like this perhaps:
var Router = Backbone.Router.extend({
routes: {
'' : 'home',
'new' : 'addUser'
},
home: function() {
if(this.currentView)
this.currentView.remove();
this.currentView = UserList();
this.currentView.render();
},
addUser: function() {
if(this.currentView)
this.currentView.remove();
this.currentView = AddUser();
this.currentView.render();
}
});
You'd drop the router.on calls since you don't need them with this approach.

How do I render a Backbone Collection in a List and Item View?

I am working on a contact bar which renders all contacts of a user in a html list.
What I have:
UserModel - This is a simple Backbone.Model with username and email
UserCollection - This is used as the contact list
ContactsView - This is the ul contact list
ContactView - This is a single contact model rendered as li
I am currently breaking my head about a solution how (and where) I can fetch my UserCollection and how I pass the single models down to a single ContactView item.
Specific hurdles are:
Where should I fetch, store the UserCollection
How do I render the contact list
How do I render the contact items
How do I prevent fetch({ success: callback }) from breaking my code structure
My current code is this:
entrance point:
// create a new instance of the contact list view
var view = new ContactsView();
// insert the rendered element of the contact list view in to the dom
$('div.contacts-body').html(view.render().el);
view.fetch({ success: view.loadContacts });
ContactsView:
define(
['jquery', 'underscore', 'backbone', 'text!templates/conversations/contacts.html', 'collections/users', 'views/conversations/contact'],
function($, _, Backbone, ContactsTemplate, UserCollection, ContactView) {
var ContactsView = Backbone.View.extend({
tagName: "ul",
className: "contacts unstyled",
attributes: "",
// I am feeling uneasy hardcoding the collection into the view
initialize: function() {
this.collection = new UserCollection();
},
// this renders our contact list
// we don't need any template because we just have <ul class="contacts"></ul>
render: function() {
this.$el.html();
return this;
},
// this should render the contact list
// really crappy and unflexible
loadContacts: function() {
this.collection.each(function(contact) {
// create a new contact item, insert the model
var view = new ContactView({ model: contact });
// append it to our list
this.$el.append(view.render().el);
});
}
});
return ContactsView;
});
ContactView
define(
['jquery', 'underscore', 'backbone', 'text!templates/conversations/contact.html'],
function($, _, Backbone, ContactTemplate) {
var ContactView = Backbone.View.extend({
tagName: "li",
className: "contact",
attributes: "",
template:_.template(ContactTemplate),
initialize: function() {
this.model.bind('change', this.render, this);
this.model.bind('destroy', this.remove, this);
},
render: function() {
this.$el.html(this.template(this.model.toJSON()));
return this;
}
});
return ContactView;
});
Could somebody help me about my four hurdles.
Good example links are welcome. I oriented my code style at the todos list unfortunatly the todos list isn't that advanced...
UPDATED CODE:
define(
['jquery', 'underscore', 'backbone', 'text!templates/conversations/contacts.html', 'collections/users', 'views/conversations/contact'],
function($, _, Backbone, ContactsTemplate, UserCollection, ContactView) {
var ContactsView = Backbone.View.extend({
tagName: "ul",
className: "contacts unstyled",
attributes: "",
events: {
},
initialize: function() {
this.collection = new UserCollection();
this.collection.on('reset', this.render);
this.collection.fetch();
},
render: function() {
// in chromium console
console.log(this.el); // first: html, second: undefined
console.log(this.$el); // first: html in array, second: undefined
this.$el.empty(); // error on the called that this.$el is undefined
this.collection.each(function(contact) {
var view = new ContactView({ model: contact });
this.$el.append(view.el);
}.bind(this));
return this;
}
});
return ContactsView;
Can it be that reset is triggering this.render twice?
First of all: why do you fetch the view? Backbone views do not have a fetch method..
1 The correct place to fetch your UserCollection would be inside the view's initialize method:
initialize: function() { // ContactsView
_.bindAll(this, 'render', 'otherMethodName', ...); // Bind this to all view functions
...
this.collection.on('reset', this.render); // bind the collection reset event to render this view
this.collection.fetch();
...
}
Now you fetch the contacts exactly when you need them. Next step is to render the collection.
2 Binding to the reset event makes your loadContacts method obsolete and we can do that in the render function:
render: function() {
this.$el.empty(); // clear the element to make sure you don't double your contact view
var self = this; // so you can use this inside the each function
this.collection.each(function(contact) { // iterate through the collection
var contactView = new ContactView({model: contact});
self.$el.append(contactView.el);
});
return this;
}
Now you render your contactlist inside the render method, where it should be done.
3 The ContactView actually looks good.
Just make the item to render itself in the initialize method, so you don't have to make useless calls in the ContactsView's render method and clutter up your code. Also bindAll here as well.
initialize: function() { // ContactView
_.bindAll(this, 'render', 'otherMethodName', ...);
...
this.render(); // Render in the end of initialize
}
I have no idea what you are asking in here, but I think the best way is not to use success callbacks. The collections and models trigger events whenever something is done to them, so tapping onto them is much more robust and reliable than success callbacks. Check out the catalog of events to learn more. The Wine Cellar tutorial by Christophe Coenraets is has an excellent example of this kind of listview-listitemview arrangement.
Hope this helps!
UPDATE: Added _.bindAlls to fix the problem with this in a event bound render call. Some info on binding this.
NOTE: all the code is simplified and no tested
When I have all the elements structure defined, as you have, with all the Models, Collections and Views implemented then I implement a Loader which is in charge of trigger the fetching and rendering actions.
First of all I need to expose the classes definition from the outside something like this:
// App.js
var App = {}
// ContactsCollection.js
$(function(){
var App.ContactsCollection = Backbone.Collection.extend({ ... });
});
// ContactsView.js
$(function(){
var App.ContactsView = Backbone.View.extend({ ... });
});
// and so on...
And then I implement what I call the Loader:
// AppLoad.js
$(function(){
// instantiate the collection
var App.contactsCollection = new App.ContactsCollection();
// instantiate the CollectionView and assign the collection to it
var App.contactsView = new App.ContactsView({
el: "div.contacts-body ul",
collection: App.contactsCollection
});
// fetch the collection the contactsView will
// render the content authomatically
App.contactsCollection.fetch();
});
Another changes you have to do is configure the ContactsView in a way that respond to the changes in the App.contactsCollection because as the fetch() is asynchronous you can call render() when the collection is still not loaded, so you have to tell to the CollectionView to render it self when the Collection is ready:
var ContactsView = Backbone.View.extend({
initialize: function( opts ){
this.collection.on( 'reset', this.addAll, this );
this.collection.on( 'add', this.addOne, this );
// ... same with 'remove'
},
addOne: function( model ){
var view = new App.ContactView({ model: contact });
this.$el.append( view.render().el );
},
addAll: function(){
this.collection.each( $.proxy( this.addOne, this ) );
}
});
You have to require your js files in the proper order:
App.js
Your Models, Collections, Views
AppLoad.js
With this system you obtain:
External access to your collection in case you need to access it from another place.
External control of the CollectionView.el with is better for decoupling and testing.
The CollectionView will respond to changes in the Collection authomatically
Note: If you use Router you can move the AppLoad.js logic to there.

Categories