Backbone.js adding sub-view elements to parent-view's element - javascript

I have a collection view and a model view, like so:
EventListView
|-- EventView
EventListView must display many EventViews in a one-to-many relationship. I am using the underscore _.template() function to build my views templates.
Here is my EventView template:
<h1>
<span class="date"><%= prefix %><%= dateString %></span>
<span class="title"><%= title %></span>
</h1>
<div class="caption"><%= caption %></div>
My EventView render method:
render: function () {
this.$el.html(this.template(this.model.attributes));
return this;
}
And here is my EventListView template:
<h1>
<% if(typeof(title) != "undefined") { print(title) } %>
</h1>
<%= events %>
And it's render method:
// this._EventViews is an array of eventView objects
render: function() {
var templateData = {
events: _.reduce(this._EventViews, function(memo, eventView) { return memo + eventView.$el.html(); }, "")
}
this.$el.html(this.template(templateData));
return this;
}
The problem I am having is that eventView.$el.html() contains only the HTML in my template, but I need to take advantage of the tagName, className and id attributes that Backbone views support.
Consider if I set up EventView like so:
return Backbone.View.extend({
model: EventModel
, tagName: 'article'
, className: 'event'
, template: _.template(templateText)
, render: function () {
this.$el.html(this.template(this.model.attributes));
return this;
}
});
I want to insert:
<article class="event" id="someID342">
<h1>
<span class="date">01/02/2010</span>
<span class="title"></span>
<div class="caption></div>
</h1>
</article>
but eventView.$el returns:
<h1>
<span class="date">01/02/2010</span>
<span class="title"></span>
<div class="caption></div>
</h1>
How do I insert the entire eventView element? Not just it's innerHTML.

Just reserve placeholder in your EvenListView's template
<h1><%- title %></h1>
<div class="js-events"></div>
And then render and append child views
render: function() {
this.$el.html(this.template({title: 'Title'}));
this.$events = this.$('.js-events');
_.each(this._EventViews, function (eventView) {
this.$events.append(eventView.render().$el);
}, this);
return this;
}

The render() function shouldn't be responsible for handling the setup of the view.el. This is done by Backbone in the _ensureElement function that is called when you initialize the view.
Also, the $.fn.html() function is only supposed to return the contents of the selected element.

You have many options but I think the most flexible and sustainable approach is to get each sub view to define its own template. The parent view simply appends the child elements .el property.
The advantages of this approach, your template is only compiled once. And updates to children do not require re-rendering parent and neighbouring elements.
Here is a JSBin
Example:
var ContainerView = Backbone.View.extend({
tagName: "article",
className: "event",
id: "someID342",
initialize: function(options){
//the template will now be rendered
this.childView = new ChildView()
//the rendered child will now appear within the parent view
this.el.appendChild( this.childView.el )
}
})
var ChildView = Backbone.View.extend({
tagName: "h1",
dateString:"01/02/2010",
prefix: "Date: ",
caption: "What a wonderful date!:",
title: "I am a title",
template: _.template([
'<h1>',
'<span class="date"><%= prefix %><%= dateString %></span>',
'<span class="title"><%= title %></span>',
'</h1>',
'<div class="caption"><%= caption %></div>'
].join("")),
initialize: function(){
this.render()
},
render: function(){
// because you are only altering innerHTML
// you do not need to reappend the child in the parent view
this.el.innerHTML = this.template(this)
}
})
I'd personally caution against using templates in Backbone at all. I've found that simply having a Backbone view for every component of your app becomes a lot easier to edit later. Sharing templates is a lot harder than sharing views. Of course it depends on the requirements of your project.

Related

Binding events to views - Firing uneven number of times

I have UserModel: UserView and UserCollection: UserCollectionView. With this, I am trying to bind a click event to the UserView (I am doing this in UserView). So, this is the code I have:
var root = 'http://localhost:5000/api/v1';
var app = {};
// Backbone Model
app.UserModel = Backbone.Model.extend({
defaults: {
// urlRoot: root + '/users',
name: 'Default Name',
email: '30',
username: 'default_username'
},
initialize: function() {
this.set({
id: this.get('username')
});
console.log('User model \'' + this.id + '\' has been initialized.');
},
// parse: function(data) {
// console.log('Model parse funciton called');
// return data;
// }
});
// Backbone Model View
app.UserView = Backbone.View.extend({
// el: '#users-list',
// tagName: 'div',
el: '.user-box-wrapper',
events: {
'click .user-data': 'userClicked'
},
userClicked: function(ev) {
console.log("User selected");
// console.log(ev.currentTarget);
},
template: _.template($('#connections-user-template').html()),
initialize: function() {
this.render();
},
render: function() {
$('#users-list').append(this.template(this.model.toJSON()));
// this.$el.append( this.template( this.model.toJSON()));
console.log('User view is rendered');
}
});
// Backbone Collection
app.UserCollection = Backbone.Collection.extend({
model: app.UserModel,
url: root + '/users',
initialize: function() {
// this.fetch();
},
parse: function(data) {
// console.log(data.data);
return data.data;
}
});
// Backbone Collection View
app.UserCollectionView = Backbone.View.extend({
el: '#users-list',
template: _.template($('#connections-template').html()),
initialize: function() {
this.connections = new app.UserCollection();
var self = this;
this.connections.fetch().done(function() {
self.render();
});
},
render: function() {
console.log('User collection view is rendered');
this.$el.html(this.template());
// this.$el.append( this.template( this.model.toJSON()));
this.connections.each(function(user) {
console.log('User : ' + user.get('id'));
var userView = new app.UserView({
model: user
});
// userView.model.fetch();
// userView.render();
});
}
});
var connectionsView = new app.UserCollectionView();
The JSON data actually returns 14 objects (or UserModels in this case). The problem is, if I click the first user view, it is triggered 13 times, and the second view click is triggered 12 times and so on, the last view click event not being triggered at all when clicked.
The individual UserViews are rendered once each, however (that's what I think at least). Can someone please explain what the problem here is and what exactly is happening here?
P.S. - I am aware of the workaround of binding the events in the CollectionView.
Edit 1
This is the DOM structure:
<!doctype html>
<html>
<head>
<title>Hey there</title>
<link rel="stylesheet" href="../static/css/normalize.css">
<link rel="stylesheet" href="../static/css/index.css">
<script src="/static/js/jquery-2.2.0.js"></script>
<script src="/static/js/underscore-1.8.3.js"></script>
<script src="/static/js/backbone-1.2.3.js"></script>
</head>
<body>
<header id="top">
<div id="logo-wrapper">
<img src="../static/img/logo.png" alt="Logo" id="logo">
</div>
<div id="top-links">
<div id="top-profile-box" class="toplink">
<div id="top-profile-data-box">
<div id="top-profile-data-name">Kevin Isaac</div>
<div id="top-profile-data-passion">Writer</div>
</div>
<img id="top-profile-image" src="../static/img/user1.jpg" alt="">
</div>
<div id="notification-icon" class="toplink"></div>
<div id="top-message-icon" class="toplink"></div>
<div id="logout-icon" class="toplink"></div>
</div>
</header>
<div id="middle">
<nav id="side-nav">
<div id="side-nav-top">
<div class="side-nav-link" id="side-nav-home-link">
<div class="side-nav-link-img"></div>
<div class="side-nav-link-title">Home</div>
</div>
<div class="side-nav-link" id="side-nav-profile-link">
<div class="side-nav-link-img"></div>
<div class="side-nav-link-title">Profile</div>
</div>
<div class="side-nav-link" id="side-nav-messages-link">
<div class="side-nav-link-img"></div>
<div class="side-nav-link-title">Message</div>
</div>
<div class="side-nav-link" id="side-nav-account-link">
<div class="side-nav-link-img"></div>
<div class="side-nav-link-title">Account</div>
</div>
</div>
</nav>
<div id="main-content">
<!-- Start of page specific HTML -->
<div id="content-title">
<div class="content-subtitle" id="connections">Connections</div>
<div class="content-subtitle" id="followers">Followers</div>
<div class="content-subtitle" id="followings">Followings</div>
</div>
<div id="content-body">
<div id="users-box">
<div id="users-list">No connection</div>
<!-- Backbone Template Starts -->
<script type="text/template" id="connections-template"></script>
<script type="text/template" id="connections-user-template">
<div class="user-box-wrapper">
<div class="user-box">
<div class="user-pic-wrapper">
<img src="/static/img/user1.jpg" alt="">
</div>
<div class="user-data" id="boox">
<div class="user-name"><%= name %></div>
<div class="user-passion"><%= username %></div>
<div class="user-city"><%= email %></div>
</div>
</div>
</div>
</script>
<!-- Backbone Template Ends -->
<div id="users-side-box">
<div id="users-box-search">
<input id="user-search" type="text" name="">
</div>
<div id="user-metadata">
<div id="metadata-user-top-box">
<div id="metadata-user-image-wrapper">
<img src="/static/img/user1.jpg" alt="">
</div>
<div id="metadata-user-name-box">
<div id="metadata-name">Name's Bond</div>
<div id="metadata-passion">Assassin</div>
</div>
</div>
<div id="metadata-user-bottom-box">
<div class="metadata-user-attribute">
<span class="metadata-property">Studied at: </span>
<span class="metadata-value">Karunya University </span>
</div>
<div class="metadata-user-attribute">
<span class="metadata-property">Native City: </span>
<span class="metadata-value">London</span>
</div>
<div class="metadata-user-attribute">
<span class="metadata-property">Website: </span>
<span class="metadata-value">www.007.com</span>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- End of page specific HTML -->
</div>
<aside id="main-aside">
Aside one two therr
</aside>
</div>
<script src="../static/js/index.min.js"></script>
</body>
</html>
There are many issues with your code.
Main problem is that, all your userView's are pointing to the same selector, and the element matching this selector .user-box-wrapper is inside the view template - So whenever you create a new userView, it'll add a new event listener to .user-box-wrapper present in all existing userViews, but not to itself. So your first userView will have n-1 events handlers registered to it while last one has none (n being total number of userViews). View element is not supposed to be part of template, template is supposed to be added to the view element.
When your view will have multiple copies of itself, don't define el option, let backbone create a new element for each instance of the view. You can customize the properties of this element.
Another issue is that you are not appending the userView template to your userView element, but something else outside it ($('#users-list'). So clicking the template won't trigger the event handler (in your case this is why you're last userView doesn't fire it's event. It was bound to all other existing views because common selector was provided by el)
You should try to avoid global selectors like $('#users-list') from within a view. In this case you can append the userView to #users-list from within userCollectionView who's el points to #users-list.
Your code should be:
var root = 'http://localhost:5000/api/v1';
var app = {};
// Backbone Model
app.UserModel = Backbone.Model.extend({
defaults: {
// urlRoot: root + '/users',
name: 'Default Name',
email: '30',
username: 'default_username'
},
initialize: function() {
this.set({
id: this.get('username')
});
console.log('User model \'' + this.id + '\' has been initialized.');
}
});
// Backbone Model View
app.UserView = Backbone.View.extend({
className: 'user-box-wrapper',
/*--^-- creates a new div with this class for each user*/
template: _.template($('#connections-user-template').html()),
events: {
'click .user-data': 'userClicked'
},
initialize: function() {
this.render();
},
render: function() {
this.$el.append( this.template( this.model.toJSON()));
console.log('User view is rendered');
},
userClicked: function(ev) {
console.log("User selected");
// console.log(ev.currentTarget);
}
});
// Backbone Collection
app.UserCollection = Backbone.Collection.extend({
model: app.UserModel,
url: root + '/users',
initialize: function() {
// this.fetch();
},
parse: function(data) {
// console.log(data.data);
return data.data;
}
});
// Backbone Collection View
app.UserCollectionView = Backbone.View.extend({
el: '#users-list',
template: _.template($('#connections-template').html()),
initialize: function() {
this.connections = new app.UserCollection();
var self = this;
this.connections.fetch().done(function() {
self.render();
});
},
render: function() {
this.$el.html(this.template());
console.log('User collection view is rendered');
this.connections.each(function(user) {
console.log('User : ' + user.get('id'));
var userView = new app.UserView({
model: user
});
this.$el.append(userView.el); /*append child view here*/
},this);
}
});
var connectionsView = new app.UserCollectionView();
The main problem is that you are rendering all UserView views in the same container:
$('#users-list').append(this.template(this.model.toJSON()));
This is why the click event selector('click .user-data') is selecting the other UserView buttons and firing it's clicks too.
You didn't ask but... here it goes some suggestions:
Do not call render from the initialize function;
Avoid defining the el;
Avoid adding the view content outside the view using $. Use
this.$el instead;
NEVER forget to return this; at you render functions.
Here is a jsfiddle with what I think you want:
https://jsfiddle.net/Neviton/n52j873u/

BackboneJS avoid re-rendering

I have a Backbone App with a large router. I use the Backbone Layout manager to load different layouts depending on what subpage I'm on. My problem is, that my top navigation gets rendered once again, each time the subpage gets rendered. So how can I avoid this?
My router:
routes: {
'': 'index',
'home': 'home',
':name' : 'artistchannel',
':name/' : 'artistchannel',
':name/videoes': 'artist_videos',
':name/videoes/': 'artist_videos',
':name/videoes?w=:videoid' : 'artist_videos',
':name/releases': 'artist_discography',
':name/releases/': 'artist_discography',
':name/merchandise' : 'artist_merchandise',
':name/concerts': 'artist_concerts'
},
artistchannel: function (params) {
artistController.initArtist(params.name);
},
artist_discography: function(params){
artistController.initDiscography(params.name);
},
and so on...
then I have a controller for each route (here artist and discography page):
ArtistController.prototype.initArtist = function(name) {
this.artistModel = new ArtistModule.Model({slug: name});
this.artistModel.fetch();
this.artistModel.on('sync', function(){
this.artistView = new ArtistModule.View({model: this.artistModel});
App.useLayout('artistchannel', 'artistchannel').setViews({
'.userMenu': this.acuserNav,
'.artistChannelDiv': this.artistView
}).render();
}, this);
window.scrollTo(0,0);
};
ArtistController.prototype.initDiscography = function(name) {
this.artistdiscographyModel = new ArtistDiscographyModule.ArtistDiscographyModel({slug: name});
this.artistdiscographyModel.fetch();
this.artistdiscographyModel.on('sync', function() {
this.artistDiscographyView = new ArtistDiscographyModule.View({model: this.artistdiscographyModel});
App.useLayout('artistDiscography', 'artistDiscography').setViews({
'.userMenu': this.acuserNav,
'.releasesDiv' : this.artistDiscographyView
}).render();
}, this);
window.scrollTo(0,0);
};
The same goes for concerts, merchandise etc.
All subpages (in this case artistchannel.html and artistDiscography.html) have the same menu in the HTML, which I want to avoid, so basically, its repeated code which looks like:
<ul>
<li>
Releasepage
</li>
<li>
Concertpage
</li>
etc. etc.
</ul>
So what I want that the topmenu not gets rerendered all the time. Is it possible to include all inside one single controller?
Have a Layout & ShellView/MenuView. Don't append $el of every view to body, instead use a container for each specific view. One approach can be :
<body>
<div id='menu'></div>
<div id='content'></div>
</body>
new Backbone.Router.extend({
initialize:function(){
new MenuView({el:"#menu"}).render(); //this one creates menu
},
routes:{...},
routeHandler:function(){
new ArtistVideosView({el:'#content'}).render();
}
});

Hello World - Backbone + Firebase + Backfire

I'm trying to create a simple hello world with backbone and firebase(using backfire). The code is working to insert data to firebase, but when I try to get data and fill the template, it says "Uncaught ReferenceError: firstName is not defined". On debug I can see the object with the data but I don't know how to provide the template with this object.
Here is the code:
$(document).ready(function(){
var registerModel = Backbone.Model.extend({
defaults: {
firstName: '',
lastName: ''
}
});
var registerColletion = Backbone.Firebase.Collection.extend({
model:registerModel,
firebase: new Firebase("https://XXXXXXXX.firebaseio.com/")
});
var registerView = Backbone.View.extend({
el: $("#myTest"),
itemTemplate: _.template($('#item-template').html()),
events: {
"click #btnSave": "saveToFirebase"
},
initialize: function () {
this.listenTo(registerList, 'add', this.render);
},
render: function(){
$('#divContent').html(this.itemTemplate(this.model.toJSON()));
},
saveToFirebase: function () {
registerList.add({firstName: $("#txtFirstName").val(), lastName: $("#txtLastName").val()});
}
});
var registerList = new registerColletion;
var app = new registerView({model:registerList});
});
The exact point of the exception is on render function:
render: function(){
$('#divContent').html(this.itemTemplate(this.model.toJSON()));
},
The template:
<script type="text/template" id="item-template">
<div class="view">
<p>
<%- firstName %> <%- lastName %>
</p>
</div>
</script>
Can anyone please help me? I think I'm missing something (probably obvious) but I can't see it.
Thank you!
When you create the registerView you're telling it that the underlying model is a registerList (not a registerModel)
var app = new registerView({model:registerList});
Therefore, when the render function is called, it's looking for the firstName property of a registerList, and that property doesn't exist.
Seems like you've got Models and Collections mixed up

Backbone.js loading a template by parameter

I would like to load a View and passing a parameter named : layout_version
I'm using JST as my template engine.
Views.BottomBarView = Backbone.View.extend({
el: ".l-bottom-bar",
template: JST['templates/bottom_bar'],
render: function(options) {
this.model.set("layoutVersion", options.layoutVersion);
this.$el.html( this.template(this.model.toJSON()) );
return this;
}
});
The .jst file is as follows:
{{ if (layoutVersion == 1) { }}
<div class="bottom-bar-s-version">Other</div>
{{ } else if { layoutVersion == 2 }}
<div class="bottom-bar-s-version">Show more</div>
{{ } }}
Since I'm not passing any model to the view when creating it, just an object { layoutVersion: 2 } I get this.model is undefined
I'm using the bottom_bar file to hold two different HTMLs inside one file and rendering it depending on the parameter inside the model.
How can I achieve my goal ?
You don't need to pass a model, you can just pass an object to the template.
Views.BottomBarView = Backbone.View.extend({
el: ".l-bottom-bar",
template: JST['templates/bottom_bar'],
render: function(options) {
this.$el.html(this.template( { "layoutVersion": options.layoutVersion }));
return this;
}
});
Just replace this this.model.toJSON() by :
{ "layoutVersion": options.layoutVersion }

JSON item to Backbone Model

I'm using backbone.js. I get a json like this:
{
first_name: 'David',
last_name: 'Smith',
family: [{father: 'David', mother: 'Rose', brother: 'Max'}]
}
first_name and last_name shows in through a PersonView (extending Backbone.View) and family data I want to show in a DetailsView.
So, I was trying like this. First:
personView = new PersonView(model: person)//person it's the json above
PersonView shows well. Then I want to pass the model to DetailsView like this:
detailsView = new DetailsView(model: JSON.parse(person.get('family'));
Well, when I try to pass the model to a template in DetailsView implementation, like this:
DetailsView = Backbone.View.extend({
className: 'tab-pane',
template: _.template($('#detail-tpl').html()),
render: function(){
this.$el.html(this.template(this.model.toJSON()));
return this;
},
});
I get this message:
Uncaught TypeError: Object [object Object] has no method 'toJSON'
I don't know how to get or pass the model to solved this.
I'm trying several ways but I can't make it go.
Hope you can help me.
I think the problem is is because of this line.
model: JSON.parse(person.get('family')
It expects model to be an instance of backbone Model . But I don't think that is the case here.. try defining the Model for family or otherwise change the name of the key
Instead try this approach
familyMembers : JSON.parse(person.get('family')
In your view you can access this as
(this.options.familyMembers.toJSON())
The issue is that you model you are passing in is just an array. Therefore doesn't have the .toJSON method. As grant suggested you could use new Backbone.Model when creating the view but I would recommend using a collection and 2 new views for the family. It would look something like this.
var PersonModel = Backbone.Model.extend({
initialize: function(attributes, options) {
if(attributes.family) {
this.family = new FamilyCollection(attributes.family, options);
}
}
});
var FamilyCollection = Backbone.Collection.extend({
model: FamilyMember,
initialize: function(models, options) {
this.view = new FamilyView(options);
}
});
var FamilyMember = Backbone.Model.extend({
initialize: function(attributes, options) {
this.view = new DetailedView({
model: this
});
}
});
Then you would use a view structure something like this..
<div class="person">
<span class="name-first">David</span> <span class="name-last">Smith</span>
<div class="family-members>
<div class="family-member">
<span class="name-first">Rose</span> <span class="name-last">Smith</span>
</div>
<div class="family-member">
<span class="name-first">David</span> <span class="name-last">Smith</span>
</div>
<div class="family-member">
<span class="name-first">Max</span> <span class="name-last">Smith</span>
</div>
</div>
</div>
The "family" property is an array, you could do one of the following...
var familyArray = model.get('family');
new DetailsView({model: new Backbone.Model(familyArray[0])});
...or add a getFamily function to the person model...
var PersonModel = Backbone.Model.extend({
getFamily: function() {
var familyArray = this.get('family');
return new Backbone.Model(familyArray[0]);
}
});
...
new DetailsView({model: person.getFamily()});

Categories

Resources