I have a Backbone application where I trigger a View through an event-click and the Views HTML gets rendered with HandlebarsJS and displays data from a collection. So far it works except that my HTML gets repeated for each entry
My HTML looks like this:
<header>
<span class="blackdot" aria-hidden="true" />
<h1>Overview</h1>
</header>
<div>
<ul>
<li>
{{brand}}
<p>{{country}}</p>
</li>
</ul>
</div>
Right now, it duplicates the whole HTML code-block, including <header> and <h1>-tag for each entry but what I want to achieve is that my HTML would look like this:
<header>
<span class="blackdot" aria-hidden="true" />
<h1>Overview</h1>
</header>
<div>
<ul>
<li>
Audi
<p>Germany</p>
</li>
<li>
Hyundai
<p>South Korea</p>
</li>
<li>
Fiat
<p>Italy</p>
</li>
</ul>
</div>
My backbone View looks like this:
define(['backbone','handlebars', 'text!templates/Cars.html'],
function(Backbone,Handlebars, Template) {
'use strict';
var CarsView = Backbone.View.extend({
template: Handlebars.compile(Template),
events: {
},
initialize: function () {
_.bindAll(this, 'render');
},
render: function() {
var self = this;
self.collection.each(function(model){
self.$el.append(self.template({
brand:model.get('brand'),
country:model.get('country')
})
);
});
return this;
}
});
return CarsView;
}
);
Of course i could define the most of the HTML on the index page, wrap it in a DIV and do display:none and only write the <li>-tag in the Handlebars HTML template, but I want to avoid that and since the data what is returned are strings, I cant do the {{#each}}-thing... so, is there any solution for this?
To add onto what guzmonne said, the reason you are seeing this is because you are looping over the entire template. What you should be doing is taking the <li>'s and making a new template specifically for them. I've modified your existing code to show how something like what you are trying to do can be accomplished.
The CarsView Handlebars template:
<header>
<span class="blackdot" aria-hidden="true" />
<h1>Overview</h1>
</header>
<div>
<ul id="cars_list" />
</div>
The CarsView Backbone View:
var CarsView = Backbone.View.extend({
template: Handlebars.compile(Template),
events: {},
initialize: function () {
_.bindAll(this, 'render');
},
render: function() {
this.$el.html(this.template()); // this now only renders once
this.addAll();
return this;
},
addOne: function(car) {
var view = new CarView({model: car});
this.$("#cars_list").append(view.render().el);
},
addAll: function() {
this.collection.each(this.addOne, this);
}
});
The main differences between what I have done and your original CarsView is that firstly, the template itself doesn't contain any <li>'s. Instead, there is a placeholder ID which I have titled "cars_list". This container will give us an entry point to loop through the cars collection and add each item. Secondly, we aren't looping through the collection and re-rendering the CarsView. Instead, we take what CarsView dropped into the DOM and manually apppend to it from there.
Normally when dealing with collections, you can leverage Backbone's this.listenTo() function, which can take an event such as "reset" or "add" and tie it to a function. Since it would appear that the collection has already been fetched, we simply do this.addAll() after the CarsView template has been rendered. It is in here that the collection is looped and added.
To accomplish this, you will need another Backbone view, and another template...
The CarView Handlebars template:
{{brand}}
<p>{{country}}</p>
The CarView Backbone View:
var CarView = Backbone.View.extend({
tagName: "li",
template: Handlebars.compile(Template),
events: {},
initialize: function () {
// model event listeners (in case these list items can be edited/removed)
this.listenTo(this.model, 'change', this.render);
this.listenTo(this.model, 'destroy', this.remove);
},
render: function() {
this.$el.html(this.template(this.model.toJSON()));
return this;
}
});
The CarView view is short and simple, but takes care of a lot for you. As you can see, this view will generate an <li> tag and take the contents of the model and send it to the handlebars template. No more having to deal with manually fetching model attributes, which is an added bonus.
Related
I'm working with backbone.js and have following listView for <ul> element and a separate tabView for dynamic <li> element. In the render method of listView, I'm creating a new tabView and appending that el to listView el.
var listView = Backbone.View.extend({
//<ul> element for tabs
el: '.nav-tabs',
render: function(model) {
var tabView = new TabView({ model: model });
tabView.render();
$(this.el).append(tabView.el);
}
var TabView = Backbone.View.extend({
//create <li> element to hold each tab
tagName: "li",
className: "currentTab ",
render() {
var html = this.template(this.model.attributes);
$(this.el).append(html);
//creates new div for tab content
var tabContent = new TabContentView({ model: this.model });
tabContent.render();
}
This is fine and works as expected.
To add a new tab dynamically, I have a single li item at the start, so when the user clicks on that li item only new tab creation happens.
Now what I need is to add the newly creating tab before li + element. Currently all the new tabs are getting added only after this + element.
Following is the html of the <ul> tag for reference.
<div id="test">
<ul class="nav nav-tabs ">
<li>+</li>
</ul>
</div>
I tried out changing listView render method like below, but that doesn't work. Rather it just adds the new tab on top of (+) li element itself.
tabView.render();
$(this.el).find(".add-newTab").before(tabView.el);
Any idea how this can be done?
jQuery offers prepend or before methods depending on what you really want.
prepend
<ul class="nav nav-tabs ">
<li>prepending adds element here</li>
<li></li>
<li class="plus">+</li>
</ul>
before
<ul class="nav nav-tabs ">
<li></li>
<li>before adds element here when used on $('.plus')</li>
<li class="plus">+</li>
</ul>
Here's a simplified implementation of your list and tabs:
var TabView = Backbone.View.extend({
//create <li> element to hold each tab
tagName: "li",
className: "currentTab", // why? all tabs will have "currentTab"
initialize: function() {
//creates new div for tab content
this.tabContent = new TabContentView({
model: this.model
});
},
// render should only renders, and should be idempotent.
render: function() {
this.$el.empty().append(tabContent.render().el);
// returning "this" is the default in Backbone, which enables
// chaining method calls.
return this;
}
});
var ListView = Backbone.View.extend({
//<ul> element for tabs
el: '.nav-tabs',
template: '<li class="plus">+</li>',
events: {
"click .add-newTab": "onAddTab",
},
render: function() {
this.$el.empty().append(this.template);
// cache the '+' li element.
this.$plus = this.$('.plus');
return this;
},
onAddTab: function(e) {
var tabView = new TabView({ model: this.model });
// the magic happens here.
// if you want the tab at the beginning of the ul:
this.$el.prepend(tabView.render().el);
// or if you want it at the end, but before the + :
this.$plus.before(tabView.render().el);
},
});
You don't need to use the global jQuery to select elements, Backbone views have their own element pre-scoped and cached accessible through this.$el.
If you really need to find an element inside the view's el, you can select it using this.$('.element-you-want') which is a shortcut for:
$(this.el).find('.element-you-want')
This question already has an answer here:
Backbone click event not firing in template View
(1 answer)
Closed 6 years ago.
I am getting the entire html code using jquery get() method and setting it on the el of backbone view.The view gets rendered perfectly but the click events i added are not firing. As i am a newbie to backbone i am not able to find the issue. Any help would be appreciated.
The currentTabID is contain the div id on which i want this html to be rendered.
view.js
var MyFirstView = Backbone.View.extend({
currentTabID:'',
initialize:function(){
this.render();
},
render: function (){
var self = this;
self.el = self.options.currentTabID;
$.get('resources/html/myBB.html', function(data) {
$(self.el).html(_.template(data));
});
return this;
},
events: {
'click .savebtnBB': 'invokeME'
},
invokeME: function (){
console.log('Fired');
}
});
Html looks something like below
myBB.html
<div id="sample_tab">
<div class="sub-main">
<form>
..
</form>
</div>
<div class="button">
<button class="savebtnBB">click me</button>
</div>
</div>
view.el is an actual dom element holding the event listeners for your view. You're replacing view's reference to that element with some number and appending the template to some other element.
Your view should act like an isolated unit as much as possible. Your code for appending it to something else should be outside the view, where you're creating it. Your code should look something like the following:
var MyFirstView = Backbone.View.extend({
initialize: function() {
var self = this;
$.get('resources/html/myBB.html', function(html) {
self.template = _.template(html);
this.render();
});
},
events: {
'click .savebtnBB': 'invokeME'
},
render: function() {
this.$el.html(this.template({ /*some data for template*/ }));
},
invokeME: function() {
console.log('Fired');
}
});
var viewInstance = new MyFirstView();
/*append to whatever you want*/
$(currentTabID).append(viewInstance.el);
I'm trying to setup a little app in backbone where I can add items to a list and, when I click them, they'll be deleted. I've managed to add items to the list but when using model.destroy() nothing happens.
When I console.log the click event on the list models I get:
child {cid: "c0", attributes: Object, _changing: false, _previousAttributes: Object, changed: Object…}
for any item I click.
Code is below:
Html:
<h1>INDEX!</h1>
<form class="add-form">
<input type="text" name="name"/>
<hr />
<button type="submit" class="btn">Submit</button>
</form>
<h2>LIST STUFF</h2>
<ul class="blah">
{{#each indexCollection}}
<li class="li-class">{{name}}</li>
{{/each}}
</ul>
Javascript:
//Local Storage
App.Storage.Local = new Backbone.LocalStorage('localIndexList1-backbone');
//Index Model
App.Models.IndexModel = Backbone.Model.extend({
localStorage: App.Storage.Local,
defualts:{
name:''
},
urlRoot: '/'
});
//Index Collection
App.Collections.IndexCollection = Backbone.Collection.extend({
localStorage: App.Storage.Local,
model: App.Models.IndexModel,
initialize: function(){
console.log('Collection initialised');
},
url: '/'
});
//View for H1 and input form
App.Views.IndexView = Backbone.View.extend({
el: '.page',
events:{
'submit .add-form' : 'addNew',
'click' : 'deleteMe'
},
initialize: function(){
console.log('IndexView initialised');
},
addNew: function(ev){
// ev.preventDefault();
var submitEntry = $(ev.currentTarget).serializeObject();
var newEntry = new App.Models.IndexModel();
newEntry.save(submitEntry, {
success: function(newEntry){
// router.navigate('', {trigger: true});
console.log('SUCESSS!!!!!!!!!');
}
});
},
deleteMe: function(){
console.log(this.model);
//Whatever I put here will not work
}
});
//View for list
App.Views.ListView = Backbone.View.extend({
el: '.page',
initialize: function(){
console.log('ListView initialised');
},
template: Handlebars.compile($('#list').html()),
render: function(){
this.$el.html(this.template);
var that = this;
var indexCollection = new App.Collections.IndexCollection();
indexCollection.fetch({
success:function(indexCollection){
that.$el.html(that.template({indexCollection: indexCollection.toJSON()}));
}
});
}
});
Would anyone be able to help letting me know where I am going wrong?
Thanks!
Where are you creating one IndexView for each of your collection models? You should have an item view, configure its model to be one IndexModel, and move your delete code to that particular view. When you do that, you should also call remove in this item view.
This is why something like Backbone.Marionette helps a lot. Just throw in a CollectionView and you're done.
Think of it like this:
"list view" -> has a collection
"item view" -> has a single model
Anything you need to on the collection level (like adding a new one, re-loading, whatever), do it on your list view. Anything you need on model level (editing, saving, deleting), do it on your item view.
I have a text input which I use to collect a keyword from the user, and then send an ajax request (which is getting successful results). I would like to append the <ul> with <li> items, each representing a result.
Here is the which includes the search input and should get the appended <li> results.
<div class="search_group pull-right">
<h5>Find Other Groups<span class="caret"></span></h5>
<ul>
<li class="search-input">
<input type="text" class="search" name="search">
</li>
</ul>
</div>
I have the following underscore template which would ideally represent each result in the collection.
<script type="text/template" class="template" id="template-search-result">
<% _.each(results,function(result){ %>
<li class="result">
<a href="/surnames/<%=URL%>">
<%=result.name%>
<span><%=result.members%> – Members</span>
<span><%=result.ancestors%> – Ancestors</span>
</a>
</li>
<% }); %>
</script>
And now, my Backbone code.
I fetch my Results collection after the user enters something in the input field, and when that collection returns results from my server (which I can see successfully in the console), I would expect the ResultView to append the ul with additional li results.
window.Results = Backbone.Collection.extend({
url: function() {
return '/ajax/groups/search?q=' + query;
}
});
var results = new Results();
window.SearchInput = Backbone.View.extend({
el: ".search-input input",
events: {
"change": "getResults",
"keyup": "getResults",
"keydown": "getResults",
"paste": "getResults"
},
getResults: function() {
query = $('input').val();
results.fetch();
}
});
var searchInput = new SearchInput({ collection: results });
window.ResultView = Backbone.View.extend({
el: ".search-group ul",
initialize: function() {
_.bindAll(this, 'render');
this.collection.bind('change', this.render);
this.template = _.template($("#template-search-result").html());
},
render: function() {
this.$el.append( this.template( {results: this.collection.toJSON()} ));
return this;
}
});
var resultView = new ResultView({ collection: results });
You're either looking for the add or reset events, depending how you're loading your collection.
If you're doing a fetch, you'll want to listen to reset. If you are adding models to the collection manually, or calling fetch({update:true}), you'll get the add events.
The collection change event only fires when the one of the collection's models fires a change of its own.
Here's a list of all the built-in Backbone events for reference.
Instead of defining the "el" in my ResultView as:
el: ".search-group ul",
I changed it to an id and modified my markup accordingly:
el: "#result-list",
and:
<ul id="result-list">
<li class="search-input">
<input type="text" class="search" name="search">
</li>
</ul>
Take a HTML tabbar as example. Usually you have a ul and a list of div's. All the Backbone examples that I have found, link the View with only one node by the 'el', 'tagName', etc...
HTML TabBar:
<div class=".tabbar">
<ul class=".tabbar-header">
<li>Cars</li>
<li>Houses</li>
</ul>
<div id="tab-cars" class=".tabbar-item">...</div>
<div id="tab-houses" class=".tabbar-item">...</div>
</div>
Backbone Code:
window.TabBarView = Backbone.View.extend({
el: ???,
tabs: [],
render:function (eventName) {
// Render all tabs in this.tabs
_.each(this.tabs, function (item, position) {
// Render each tab with item.render()
}, this);
return this;
}
});
window.TabBarItemView = Backbone.View.extend({
el: ???,
initialize:function () {
this.model.bind("change", this.render, this);
this.model.bind("destroy", this.close, this);
},
render:function (eventName) {
// Render the tab header and tab content
return this;
}
});
I wish to add several TabBarItemView's to the TabBarView and each one creates itself the li node inside the ul.tabbar-header and the div.tabbar-item as content.
I've written an article that addresses this issue: http://lostechies.com/derickbailey/2011/10/11/backbone-js-getting-the-model-for-a-clicked-element/
It will show you how you can either use a single view to do what you want, or a parent/child setup with a collection view and item view like you're showing in your sample code
you can go as far as to make a separate navigation view, and have the navigation add an item through the render method of your tab-item-view.
when you render the tab item view, you do something like navigation.add(new nav item);
and also add a way to remove the navigation item.
or you can keep the navigation in pure html and append a <li> item with jquery / javascript when you are rendering a tab below.
can't give you a fully working example though, if you really need it i can probably make one tonight,.