I've extended the Backbone version of todomvc and am finding the views very fickle. Hard getting listeners and events disambiguated. Cannot see why this calls app.AppView.render() render twice, and on the second call the items in local storage are dropped. Anyone had issues with local Backbone storage?
A search input stores the first item, then something reloads and the collection is empty.
The Html View:
app.AppView = Backbone.View.extend({
el: '#searchapp',
events: {
'keypress #new-search': 'createOnEnter',
'click #clear-unstarred': 'clearUnStarred',
},
initialize: function () {
this.$input = this.$('#new-search');
this.$list = $('#search-list');
this.$results = this.$('#search-grids');
this.$stats = this.$('#search-stats');
this.listenTo(app.searches, 'add', this.addOne);
this.listenTo(app.searches, 'reset', this.addAll);
this.listenTo(app.searches, 'change:starred', this.filterOne);
this.listenTo(app.searches, 'filter', this.filterAll);
this.listenTo(app.searches, 'all', this.render);
app.searches.fetch(); //{reset: true}
},
render: function () {
//app.SearchFilter = app.SearchFilter || 'starred';
var starred = app.searches.starred().length;
var remaining = app.searches.remaining().length;
if (app.searches.length) {
var h = $('#stats-template').html(), t = _.template(h),
d = {
count: (starred + remaining),
starred: starred,
remaining: remaining
}, s = t(d);
this.$stats.html(s);
this.$('#filters a').removeClass('selected').filter('[href="#/' + (app.SearchFilter || '') + '"]').addClass('selected');
app.searches.last().trigger('runsearch');
} else {
//this.$results.hide();
//this.$stats.hide();
}
}, ...............
The View for Items:
app.SearchView = Backbone.View.extend({
tagName: 'div',
events: {
'click .do-search': 'runSearch',
'click .do-destroy': 'clear',
'click .do-toggle-star': 'togglestar',
'dblclick label': 'edit',
'keypress input.title': 'updateOnEnter',
'keydown input.title': 'revertOnEscape',
'blur input.title': 'close'
},
initialize: function () {
this.$results = $('#search-grids');
this.listenTo(this.model, 'change', this.render);
this.listenTo(this.model, 'destroy', this.remove);
this.listenTo(this.model, 'visible', this.toggleVisible);
this.listenTo(this.model, 'runsearch', this.runSearch);
},
render: function () {
if (this.model.changed.id !== undefined) {
return;
}
var h = $('#search-item-template').html(), t = _.template(h),
d = this.model.toJSON(), s = t(d);
this.$el.html(s);
this.$el.toggleClass('starred', this.model.get('starred'));
this.$input = this.$('input.title');
this.toggleVisible();
return this;
},
`
Chrome developer tools wasn't showing "Storage" during issue. Cleanup did help. Haven't made significant changes. Essentially made sure to do the collection add() and model save() here --
http://www.mostlystatic.com/2013/01/26/minimal-backbone-localstorage-example.html
Related
I am running the following view:
app.OrganisationTab = Backbone.View.extend({
el : "#organisations",
template : _.template( $("#tpl-groups-list").html() ),
events : {
"click .js-edit-group" : "editGroup"
},
initialize: function() {
this.listenTo(this.collection, 'change', this.change);
var that = this;
this.collection.fetch({
success: function() {
that.render();
}
})
},
change: function() {
//this.$el.empty();
console.log("collection has changed");
},
render:function() {
this.$el.empty();
this.addAll();
return this;
},
addAll: function() {
this.collection.each(this.addOne, this);
},
addOne: function(model) {
var view = new app.GroupEntry({
model: model
});
this.$el.append(view.render().el);
},
editGroup: function(e) {
e.preventDefault();
var elm = $(e.currentTarget),
that = this;
$('#myModal').on('hidden.bs.modal', function () {
$('.modal-body').remove();
});
var organisation = this.collection.findWhere({ id : String(elm.data('groupid')) });
var members = organisation.get('users');
organisation.set('members', new app.UserCollection(members));
var projects = organisation.get('projects');
organisation.set('projects', new ProjectCollection(projects));
var orgForm = new app.createOrganisationForm({
model : organisation,
});
$('#myModal').modal({
backdrop: 'static',
keyboard: false
});
}
});
This view triggers a new view, and in that I can change a model save it (sends a PUT) and I can get in my console, collection has changed. If I console.log this collection I can see that the collection has changed. If I try and re-render the page all I see are the models as they were without the edits.
Why would this be happening, when clearly the collection is getting changes as it fires the events and I can see it when I log the collection?
After reading your comment:
No sorry on collection change I try to run render() which should empty
the container, and add all the models...but it seems to render the old
collection again.
You're getting this problem because you are overriding the success handler for the fetch call. That success callback is triggered before the models are placed in the collection. You need to listen to the sync event if you want render after the collection has been synchronized with the server (models are updated after fetch).
Update initialize to:
initialize: function() {
this.listenTo(this.collection, 'change', this.change);
this.listenTo(this.collection, 'sync', this.render);
this.collection.fetch();
},
I've been following Addy Osmani's book Developing Backbone.js Applications, the version available online for free. As such, the first exercise is the much touted sample "Todo app", which can also be lifted from Backbone's original documentation samples.
While the app updates the view when a new entry is added to the list of things to do, I've hit an odd snafu: When I edit a list entry and press the enter button, nothing happens. The input doesn't go out of focus and the label in the entry doesn't update automatically. However, the Model does save the edited entry, because when I refresh the page, the edited entry features the change I made it to it.
I have no idea what is going on, as the view does listen to model changes.
Here's the code:
app.TodoView = Backbone.View.extend({
tagName: "li",
template: _.template($("#item-template").html()),
events:{
"dblclick label":"edit",
"keypress .edit": "updateOnEnter",
"blur .edit": "close"
},
initalize: function(){
this.listenTo(this.model, "change", this.render);
},
render: function(){
this.$el.html(this.template(this.model.toJSON()));
this.myInput = this.$(".edit");
return this;
},
edit: function(){
this.$el.addClass("editing");
this.myInput.focus();
},
close: function(){
var value = this.myInput.val().trim();
if(value) this.model.save({ title: value});
this.$el.removeClass("editing");
},
updateOnEnter: function(x){
if(x.keyCode === 13)
this.close();
}});
app.AppView = Backbone.View.extend({
el: $("#app"),
statsTemplate: _.template($("#stat-template").html()),
events:{
"keypress #newentry": "createOnEnter",
"click #clear-completed": "clearCompleted",
"click #toggle-all": "toggleAllComplete"
},
initialize: function(){
this.allCheck = this.$("#toggle-all")[0];
this.newJob = this.$("#newentry");
this.main = this.$("#main");
this.foot = this.$("#foot");
this.listenTo(app.TodoCol, "add", this.addOne);
this.listenTo(app.TodoCol, "reset", this.addAll);
this.listenTo(app.TodoCol, "change:completed", this.filterOne);
this.listenTo(app.TodoCol, "filter", this.filterAll);
this.listenTo(app.TodoCol, "all", this.render);
app.TodoCol.fetch();
console.log(app.TodoCol.toJSON());
},
addOne: function(todo){
var view = new app.TodoView({model: todo});
$("#items").append(view.render().el);
},
addAll: function(todo){
this.$("items").html("");
app.TodoCol.each(this.addOne, this);
},
filterOne: function(todo){
todo.trigger("visible");
},
filterAll: function(){
app.TodoCol.each(this.filterOne, this);
},
render: function(){
var comp = app.TodoCol.getCompleted().length;
var open = app.TodoCol.getOpen().length;
if(app.TodoCol.length){
this.main.show();
this.foot.show();
this.foot.html(this.statsTemplate({
completed: comp,
remain: open
}));
this.$("#filters li").removeClass("active")
.filter('[href="#/' + ( app.TodoFilter || '' ) + '"]')
.addClass('selected');
}else{
this.main.hide();
this.foot.hide();
}
this.allCheck.checked = !open;
},
newEntry: function() {
return {
title: this.newJob.val().trim(),
order: app.TodoCol.nextOrder(),
completed: false
};
},
createOnEnter: function(event){
if(event.which !== 13 || !this.newJob.val().trim())
return;
app.TodoCol.create(this.newEntry());
this.newJob.val("");
},
clearCompleted: function(){
_.invoke(app.TodoCol.getCompleted(), "destroy");
return false;
},
toggleAllComplete: function(){
var comp = this.allCheck.checked;
app.TodoCol.each(function(t){
t.save({ completed: comp});
});
}});
Here are the models for good measure:
app.Todo = Backbone.Model.extend({
defaults:{
title: "",
completed: false
},
toggle: function(){
this.save({completed: !this.get("completed")});
}});
app.List = Backbone.Collection.extend({
model: app.Todo,
localStorage: new Backbone.LocalStorage("todo-backbone"),
getCompleted: function(){
return this.where({completed: true});
},
getOpen: function(){
return this.where({completed: false});
},
nextOrder: function(){
if(!this.length) return 1;
return this.last().get("order") + 1;
},
comparator: function(todo){
return todo.get("order");
}});
Please help me!
i am trying to make my first backbone app, and have run into a problem that i just cant solve..
I have a list of links, each link has a counter next to it,
when i click on a link i want the counter to increment by 1. (i have made this, and it is working)
Next i want the link i clicked to move up in the list IF the counter value is higher than the link above.
like this.
first link (4)
second link (3)
third link (3) <-- if i click on this link i want it to move up above second link.
I have tried using comparator and sortBy, but each time i try something i just cant seem to re-render the view and also have the link move up one spot.
I did manage to sort the list initially, when the main view is initialized.
But updating the view and list placement after i click one of the links i cant figure out how to accomplish.
my code:
(function() {
window.App = {
Models: {},
Collections: {},
Views: {}
};
window.template = function(id) {
return _.template( $('#' + id).html() );
};
//Modellen
App.Models.Task = Backbone.Model.extend({
defaults: {
name: 'Foo Bar Baz',
uri: 'http://www.google.com',
counter: 0
},
validate: function(attr) {
if ( ! $.trim(attr.name) ) {
return 'En opgave kræver en title.';
};
}
});
//Collection
App.Collections.Tasks = Backbone.Collection.extend({
model: App.Models.Task,
comparator: function(task) {
return task.get('counter');
},
});
//Singel view
App.Views.TaskView = Backbone.View.extend({
tagName: 'li',
template: template('Tasks'),
initialize: function() {
this.model.on('change', this.render, this);
this.model.on('destroy', this.remove, this);
},
events: {
'click .edit' : 'retTask',
'click .delete' : 'destroy',
'click .uriLink' : 'addCounter'
},
retTask: function() {
var newTaskNavn = prompt('Hvad skal det nye navn være', this.model.get('name'));
if ( !newTaskNavn ) return;
this.model.set('name', newTaskNavn);
},
destroy: function() {
this.model.destroy();
},
addCounter: function(e) {
e.preventDefault();
var newCounter = this.model.get('counter');
this.model.set('counter', newCounter + 1);
},
remove: function() {
this.$el.remove();
},
render: function() {
this.$el.html(this.template(this.model.toJSON()) );
return this;
}
});
//Collection View
App.Views.TasksView = Backbone.View.extend({
tagName: 'ul',
initialize: function() {
this.collection.on('add', this.addOne, this);
this.render();
},
render: function() {
this.collection.each(this.addOne, this);
return this;
},
addOne: function(task) {
var taskView = new App.Views.TaskView({ model: task });
this.$el.append(taskView.render().el);
}
});
App.Views.AddTask = Backbone.View.extend({
el: '#addTask',
initialize: function() {
},
events: {
'submit' : 'submit'
},
submit: function(e) {
e.preventDefault();
var taskNavn = $(e.currentTarget).find('.navnClass').val(),
uriNum = $(e.currentTarget).find('.uriClass').val();
if ( ! $.trim(taskNavn)) {
var test = prompt('opgaven skal have et navn', '');
if ( ! $.trim(test)) return false;
taskNavn = test;
}
if( uriNum.indexOf( "http://" ) == -1 ) {
addedValue = 'http://',
uriNum = addedValue + uriNum;
}
$(e.currentTarget).find('input[type=text]').val('').focus();
//var task = new App.Models.Task({ name: taskNavn, uri: uriNum });
this.collection.add({ name: taskNavn, uri: uriNum });
}
});
// new tasks collection
var tasks = new App.Collections.Tasks([
{
name: 'Foo',
uri: 'www.google.com',
counter: 3
},
{
name: 'Bar',
uri: 'http://google.com',
counter: 2
},
{
name: 'Baz',
uri: 'http://www.google.com',
counter: 1
}
]);
// tasks.comparator = function(task) {
// return task.get("counter");
// };
tasks.sort();
// new collection view (add)
var addTaskView = new App.Views.AddTask({ collection: tasks});
// new collection view
var tasksView = new App.Views.TasksView({ collection: tasks });
$('.tasks').html(tasksView.el);
})();
My HTML: (if someone wanna try to replicate the scenario :)
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>LinkList</title>
</head>
<body>
<h1>Mine opgaver</h1>
<form action="" id="addTask">
<input class="navnClass" type="text" placeholder="Link name"><input clas s="uriClass" type="text" placeholder="www.url-here.com">
<button class="nyOpgave">Ny opgave</button><br />
</form>
<div class="tasks">
<script type="text/template" id="Tasks">
<span class="linkNavn"><%= name %></span> - <%= uri %> : [<span class="counterClass"><%= counter %></span>] <button class="edit">Edit</button> <button class="delete">Delete</button>
</script>
</div>
<script src="js/underscore.js"></script>
<script src="http://ajax.cdnjs.com/ajax/libs/json2/20110223/json2.js"></script>
<script src="js/jquery.js"></script>
<script src="js/backbone.js"></script>
<script src="main.js"></script>
</body>
</html>
can anyone please help me figure this one out ?
/Cheers
Marcel
Okay , i have created the application for you , as you have intended it to run.I'm going to try and explain you the entire code , what i have written and why i have written.
First , take a look at the JSfiddle : here
Next , let me explain :
1.This is my model that stores the name of the link , href , the id(not used in my example but its just good practise to assign a unique id to each model) and finally the number of clicks to a link(model).
var myModel = Backbone.Model.extend({
defaults:{
'id' : 0,
'name' : null,
'link' : '#',
'clicks' : 0
}
});
2.This the collection , that stores all my models , i have added a comparator function so that when ever you add a model to a collection , it will sort the collection.
Note : i have added a - sign to sort the collection in descending order of clicks (link with maximum click to appear first)
var myCollection = Backbone.Collection.extend({
model: myModel,
comparator: function(item) {
return -item.get('clicks');
}
});
3.Now this is my main view , what do i mean main view ? This view does the main rendering of the list , that you want to show.Pretty self explanatory code here.One thing , the this.coll.bind('add change',this.render,this) , i have added a 'change' because whenever any of the models in this collection change , we want to re-render the entire list , this happens when i change the count of any link , on clicking it , i want to re-render the entire list.
var myView = Backbone.View.extend({
el: $("#someElement"),
tagName : 'ul',
initialize: function() {
this.coll = new myCollection();
this.coll.bind('add change',this.render,this);
},
events: {
"click #add": "add"
},
add: function(e){
e.preventDefault();
var mod = new myModel();
var name = $('#name').val();
var link = $('#link').val();
mod.set({'id':mod.cid, 'name':name,'link':link});
this.coll.add(mod);
},
render : function(){
$('#list').empty();
this.coll.sort();
this.coll.forEach(function(model){
var listItem = new printView({ model: model});
$('#list').append(listItem.render().el);
});
}
});
4.This is my sub-view , why do i ever make a second view , why isnt 1 view sufficient ?
Well this consider a scenario, with every link you have a delete button (for instance) when i click the delete button (and i have just 1 view) how do i identify which model to destroy(remove from collection ? ) , 1 possible way would be to associate a cid with each model and then on click i can do a this.coll.getByCid() , but this isnt such a good way to do it , IMHO , so i created a separate view for each model.This View renders each model and returns nothing more.
var printView = Backbone.View.extend({
tagName: 'li',
initialize : function(options) {
_.bindAll(this, "render");
},
events:{
"click a": "count"
},
render:function(){
var linkName = this.model.get("name");
var link= this.model.get("link");
var clicks = this.model.get("clicks");
this.$el.append("<a class='link' href='"+link+"'>"+linkName+"</a> ("+clicks+")");
return this;
},
count:function(e){
e.preventDefault();
e.stopPropagation();
var clicks = this.model.get("clicks");
clicks++;
this.model.set({'clicks':clicks});
}
});
5.Initializing my (main) myView
new myView();
Note: I do believe that this application/code can be written in much better way , with several improvements but with my calibre and with the fact that it works ( :p ) i think it can help you.
The collection comparator is only executed when new models are added to the collection: it doesn't update the collection order when properties change. In order to achieve this, you need to call collection.sort():
App.Collections.Tasks = Backbone.Collection.extend({
model: App.Models.Task,
initialize: function() {
this.on('change:counter', this.sort);
},
comparator: function(task) {
return task.get('counter');
}
});
In the list view you can listen to the collection's sort event, and re-render your view:
App.Views.TasksView = Backbone.View.extend({
tagName: 'ul',
initialize: function() {
this.collection.on('add', this.addOne, this);
this.collection.on('sort', this.render, this);
this.render();
},
render: function() {
//if there are existing child views, remove them
if(this.taskViews) {
_.each(this.taskViews, function(view) {
view.remove();
});
}
this.taskViews = [];
this.collection.each(this.addOne, this);
return this;
},
addOne: function(task) {
var taskView = new App.Views.TaskView({ model: task });
this.$el.append(taskView.render().el);
//keep track of child views
this.taskViews.push(taskView);
}
});
I have a filter working on my backbone collection. Type a search in the search box and the list live filters. Works great, or so I thought. When I looked at the memory heap snapshot in chrome, I can see the memory leaking with each search... 6 megs 8 megs... before long the heap snapshots are 100+ megs.
I have isolated the problem in the view below. If I comment out the this.listenTo in the initialize function I no longer seem to leak memory.
So my question is how do I keep these event listeners and the live filtering on the collection without leaking.
var View = Backbone.View.extend({
tagName: 'tr',
initialize: function() {
this.listenTo(this.model, 'change', this.render);
this.listenTo(this.model, 'destroy', this.remove);
},
events: {
'click .edit': 'edit',
'click .delete': 'delete',
},
edit: function() { /* EDIT */ },
delete: function() {
this.model.destroy(); //backbone
},
render: function () {
var template = _.template( ProductTemplate )
this.$el.html( template({ this.model.toJSON() }) )
return this;
}
})
var ListView = Backbone.View.extend({
initialize: function()
{
this.collection = new Collection( Products ) //products are bootstrapped on load
},
render: function (terms)
{
this.$el.html( ListTemplate );
var filtered = Shop.products.collection.search(terms)
_.each(filtered, this.addOne, this)
//append list to table
$('#products').html( this.el )
return this
},
addOne: function (product)
{
this.$el.find('tbody').append(
new View({ model: product }).render().el
)
return this
},
});
var Collection = Backbone.Collection.extend({
model: Model,
search : function(letters){
//set up a RegEx pattern
var pattern = new RegExp(letters,"gi")
//filter the collection
return this.filter(function(model)
{
if(letters == "") return true //if search string is empty return true
return pattern.test(model.attributes['Product']['name'])
});
}
});
SOLVED:
This is my new search method. I am no longer filtering the collection and re-rendering. I simply loop over the collection, and if a model matches the search we trigger a 'show' event, if it is not in the search we trigger a 'hide' event. Then we subscribe to these events in the view and act accordingly.
search function from the collection:
search : function(query){
//set up a RegEx pattern
var pattern = new RegExp(query,"gi")
//filter the collection
this.each(function(model){
if ( pattern.test(model.attributes['Product']['name']) ){
model.trigger('show')
}
else{
model.trigger('hide')
}
});
}
The new view:
var ProductView = Backbone.View.extend({
tagName: 'tr',
initialize: function() {
this.listenTo(this.model, 'show', this.show);
this.listenTo(this.model, 'hide', this.hide);
},
hide: function()
{
this.$el.addClass('hide')
},
show: function()
{
this.$el.removeClass('hide')
},
render: function ()
{
var template = _.template( ProductTemplate )
this.$el.html( template( {data: this.model.toJSON(), Utils: Shop.utils} ) )
return this;
}
});
To expand on what #mu already commented on, you're not removing views that you've created. They're not in the DOM, but they're still hanging around in memory because they have a reference to your models (therefore, the garbage collector will not remove them for you).
You have a couple options:
Keep track of all the views that are being instantiated by addOne and remove them each time render is called.
Make your code show/hide views rather than instantiate/destroy each time the filter criteria is changed. This is more work, but is certainly the more optimal solution.
Just beginning with backbone and after few hours can't seem to get even a view render working correctly. I've included all appropriate JavaScript files in HTML. Here is my script:
(function($) {
// MODELS
var Paper = Backbone.Model.extend ({
defaults : {
title : null,
author: null,
}
});
// COLLECTIONS
var PaperCollection = Backbone.Collection.extend({
model : Paper,
initialize : function() {
console.log("We've created our collection");
}
});
// VIEWS
var PaperView = Backbone.View.extend({
tagName:'li',
className: 'resultTable',
events: {
'click .ptitle':'handleClick'
},
initialize: function() {
_.bindAll(this, 'render', 'handleClick');
},
render: function() {
$(this.el).html('<td>'+this.model.get('title')+'</td>');
return this; // for chainable calls
},
handleClick: function() {
alert('Been clicked');
}
});
var ListView = Backbone.View.extend({
events: {
//"keypress #new-todo": "createOnEnter",
},
initialize : function() {
console.log('Created my app view');
_.bindAll(this, 'render', 'addOne', 'appendOne');
this.collection = new PaperCollection();
this.collection.bind('add', this.appendOne); // collection event binder
this.counter = 0;
this.render();
},
render : function() {
console.log('Render app view');
$(this.el).append("<button id='add'>Add list item</button>");
$(this.el).append("<p>More text</p>");
// $(this.el).append("<ul></ul>");
/*
_(this.collection.models).each(function(item){ // in case collection is not empty
appendOne(item);
}, this); */
},
addOne: function() {
this.counter++;
var p = new Paper();
p.set({
title: "My title: " + this.counter // modify item defaults
});
this.collection.add(p);
},
appendOne: function(p) {
var paperView = new PaperView({
model: p
});
$('ul', this.el).append(paperView.render().el);
}
});
var App = new ListView({el: $('paper_list') });
// App.addOne();
})(jQuery);
Note not getting any errors in console on FF - but still not displaying any of the render outputs in AppView). Appreciate any help. Simple HTML:
<body>
<div class="container_16">
<div class="grid_16">
<div id="paper_list">
Text...
<ul class="thelist"></ul>
</div>
</div>
<div class="clear"></div>
</div>
</body>
This will at least get you rendering the ListView...
// MODELS
var Paper = Backbone.Model.extend ({
defaults : {
title : null,
author: null,
}
});
// COLLECTIONS
var PaperCollection = Backbone.Collection.extend({
model : Paper,
initialize : function() {
console.log("We've created our collection");
}
});
// VIEWS
var PaperView = Backbone.View.extend({
tagName:'li',
className: 'resultTable',
events: {
'click .ptitle':'handleClick'
},
initialize: function() {
_.bindAll(this, 'render', 'handleClick');
},
render: function() {
$(this.el).html('<td>'+this.model.get('title')+'</td>');
return this; // for chainable calls
},
handleClick: function() {
alert('Been clicked');
}
});
var ListView = Backbone.View.extend({
el: '#paper_list',
events: {
"click #add": "createOnEnter",
},
initialize : function() {
console.log('Created my app view');
_.bindAll(this, 'render', 'addOne', 'appendOne');
this.collection = new PaperCollection();
this.collection.bind('add', this.appendOne); // collection event binder
this.counter = 0;
this.render();
},
render : function() {
console.log(this);
$(this.el).append("<button id='add'>Add list item</button>");
$(this.el).append("<p>More text</p>");
// $(this.el).append("<ul></ul>");
/*
_(this.collection.models).each(function(item){ // in case collection is not empty
appendOne(item);
}, this); */
},
addOne: function() {
this.counter++;
var p = new Paper();
p.set({
title: "My title: " + this.counter // modify item defaults
});
this.collection.add(p);
},
appendOne: function(p) {
var paperView = new PaperView({
model: p
});
$('ul', this.el).append(paperView.render().el);
}
});
$(function(){
var App = new ListView();
});
A couple of things...First, I initialized your ListView inside of a document.ready to make sure that the DOM was ready to go, second, I made the el in the listview simply #paper_list then you can do $(this.el) later.
I at least got the button and "more text" to show up...Let me know if that helps!