Backbone View not appending on Collection change - backbone.js

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>

Related

Adding dynamic bootstrap tab before the existing 'addition' tab

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')

Backbone - Cannot get model on click using localstorage

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.

Backbone + Handlebars: template not getting the data

I'm sending data from my backbone view to a handlebars template (js fiddle: http://jsfiddle.net/8NhjD/) like this:
this.$el.html(this.template({
users: that.users.toJSON(),
audiences: that.audiences.toJSON()
}));
and I'm trying to access the list of users and audiences like this:
<select name="user" class = "form-control">
{{#each users}}
<option value="{{name}}">{{name}}</option>
{{/each}}
</select>
But the dropdown menus for the users and audiences are empty. What am I doing wrong here?
Your problem is that you are passing in the models, which do not expose their attributes directly. Try something like this:
this.$el.html(this.template({
users: that.users.toJSON(),
audiences: that.audiences.toJSON()
}));
UPDATED:
Without a complete fiddle, it is hard to see where you went wrong. Here is a working fiddle: http://jsfiddle.net/moderndegree/qW7Tz/
HTML:
<script id="thing-template" type="text/x-handlebars-template">
<ul>
{{#each things}}
<li>{{this.thing}}</li>
{{/each}}
</ul>
</script>
<div id="thing-view"></div>
JS:
var ThingModel = Backbone.Model.extend({}),
ThingCollection = Backbone.Collection.extend({
model: ThingModel
}),
ThingView = Backbone.View.extend({
el: '#thing-view',
template: Handlebars.compile($("#thing-template").html()),
initialize: function(){
this.things = new ThingCollection([{thing: 'purple'}, {thing: 'monkey'}, {thing: 'dishwasher'}]);
},
render: function(){
console.log(this.things.toJSON());
this.$el.html(this.template({
things: this.things.toJSON()
}));
return this;
}
});
var view = new ThingView().render();
Moving the collection-fetching from the view's initialize method to the route-handler resolved the issue.

BackboneJS + HandlebarsJS - How to avoid HTML to be rendered multiple times

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.

Backbone View not creating "itself"

I just started learning Backbone and from what I've seen so far when you create a view and you define a tagName and a className the view you create is created inside that element but it doesn't that it works on the code below, could someone please explain to me why? I've spend wayyy too much time on this and my head is spinning.
var app = {};
(function ($) {
app.Todo = Backbone.Model.extend({
defaults : {
name : '',
priority: '',
description: ''
}
});
app.TodoList = Backbone.Collection.extend({
model: app.Todo,
url: '#todolist'
});
app.TodoListView = Backbone.View.extend({
tagName: 'ul',
className: 'todolist',
initialize: function() {
this.template = _.template($('#todolist-template').html());
this.render();
},
render: function() {
this.$el.empty();
this.$el.append(this.template({items: this.collection.toJSON()}));
return this;
}
});
app.todoList = new app.TodoList([
new app.Todo({
name: 'unclog the sink',
priority: '10',
description: 'FIX THE SINK!!!'
}),
new app.Todo({
name: 'get bread',
priority: '0',
description: 'We are out of bread, go get some'
}),
new app.Todo({
name: 'get milk',
priority: '2',
description: 'We are out of milk, go get some'
})
]);
new app.TodoListView({el: $('#container'), collection: app.todoList});
})(jQuery);
template:
<script type="text/template" id="todolist-template">
<% _.each(items, function(item){ %>
<li>
<%= item.name %>
<%= item.description %>
<%= item.priority %>
</li>
<%}); %>
</script>
result:
<div id="container">
<li>unclog the sink FIX THE SINK!!! 10</li>
<li>get bread We are out of bread, go get some 0</li>
<li>get milk We are out of milk, go get some 2</li>
</div>
I am not expert in BackboneJS but trying to resolve the issue. As per backboneJS doc about el property, its always there in to render backbone view. The default element is <div> if you don't specify. You have understood right, that element would be render inside it.
In your code you are providing <div> element as an el, so its overriding your tagName property when creating new object of view. And another thing is you are calling render before creating object which may cause a problem. So as per my opinion, you code should looks like this :
$("$container").html((new app.TodoListView({collection: app.todoList})).render())
You should read about the el property of View, here. When you specify tagName and className it creates the DOM element <ul class="todolist"></ul> but doesn’t append it to the DOM. If the element already exists in the DOM, you can set el as a CSS selector that matches the element.
So in your case your template is getting created in an ul element, but the ul element itself is not added in the DOM.
Try doing following:
NOTE : div with an id as container should already be present in DOM.
View definition:
app.TodoListView = Backbone.View.extend({
el: '#container',
initialize: function() {
this.template = _.template($('#todolist-template').html());
this.render();
},
render: function() {
this.$el.html(this.template({items: this.collection.toJSON()}));
return this;
}
});
Template:
<script type="text/template" id="todolist-template">
<ul class="todolist">
<% _.each(items, function(item){ %>
<li>
<%= item.name %>
<%= item.description %>
<%= item.priority %>
</li>
<%}); %>
</ul>
</script>
View creation :
new app.TodoListView({collection: app.todoList});

Resources