Use Backbone Events To Toggle Collection View Properties Or jQuery.siblings()? - javascript

Imagine the following simple model and view:
app.Messages = Backbone.Model.extend({
defaults: function() {
return {
message: "an empty messageā€¦",
messageActive: false
};
}
});
app.MessageView = Backbone.View.extend({
tagName: "li",
template: _.template($("#template").html()),
events: {
"click": "open"
},
initialize: function() {
this.listenTo(this.model, "change", this.render);
this.listenTo(this.model, "message:hide", this.hide);
},
render: function() {
this.$el.html(this.template(this.model.toJSON()));
return this;
},
open: function() {},
hide: function() {
this.model.set("messageActive", false);
this.$el.removeClass("show-message");
}
});
The model is part of a collection and everything is held together by a parent view (simplified for this question):
app.AppView = Backbone.View.extend({
el: "#messages",
initialize: function() {
this.listenTo(app.messages, "message:show", this.foo);
},
foo: function(model) {
var open = app.messages.where({messageActive: true});
open.forEach(function(e, i) {
if (model != e) {
e.trigger("message:hide");
}
});
},
Only one message of the collection can be active at any given time. This means that as soon as a closed message is clicked, any open message will be closed. The question now concerns the functionality of app.MessageView.open(). It could imho either use jQuery.siblings() to hide the message of all its siblings and show its own:
open: function() {
this.$el
.addClass("show-message")
.siblings(".show-message")
.removeClass("show-message");
}
Personally I think this solution quite elegant, I only dislike that it kind of needs to know that it has siblings. My other implementation would use a custom event that gets intercepted by the parent view (app.AppView.foo()) that then calls app.MessageView.close() on every open message:
open: function() {
this.model.set("messageActive", true).trigger("message:show", this.model);
this.$el.addClass("show-message");
}
This feels a little more "backbone" to me, but seems a little over-engineered. ;-)
I could event think of a third possibilty where the parent view simply keeps track of the last opened model itself and triggers any close event that is intercepted by the models view (seems manageable but not very backbone-like to me ;-) ). I've already covered quite some blog posts and questions here on SO, but came to no real satisfactory conclusion. Maybe someone can shed some light this specific question.

EDIT: Completely missed the click event on the messageview so revamped my answer.
You could trigger a click event on the model whenever the view is clicked, then wait for the parentview to trigger show and hide events on the model.
app.MessageView = Backbone.View.extend({
tagName: "li",
template: _.template($("#template").html()),
events: {
"click": "isClicked"
},
initialize: function() {
this.listenTo(this.model, "change", this.render);
this.listenTo(this.model, "message:show", this.open);
this.listenTo(this.model, "message:hide", this.hide);
},
render: function() {
this.$el.html(this.template(this.model.toJSON()));
return this;
},
isClicked: function(){
this.model.trigger("message:clicked");
},
open: function() {
this.model.set("messageActive", true);
this.$el.addClass("show-message");
},
hide: function() {
this.model.set("messageActive", false);
this.$el.removeClass("show-message");
}
});
Your parentview is now responsible of sending the right events to the models whenever one of it's childviews is clicked.
app.AppView = Backbone.View.extend({
el: "#messages",
initialize: function() {
this.listenTo(app.messages, "message:clicked", this.foo);
},
foo: function(model) {
var open = app.messages.where({messageActive: true});
open.forEach(function(e, i) {
if (model != e) {
e.trigger("message:hide");
}
});
// Because we retrieved the currently active models before sending this event,
// there will be no racing condition:
model.trigger("message:show");
},
Perhaps not the shortest way to achieve this sorta thing. But it is completely event driven and customizable.

As a heads-up, I ended up with a slight variation of the accepted answer. Instead of triggering custom events, the views now listen to the change of the models messageActive attribute and act accordingly.
app.MessageView = Backbone.View.extend({
tagName: "li",
template: _.template($("#template").html()),
events: {
"click": "isClicked"
},
initialize: function() {
this.listenTo(this.model, "change:messageActive", this.showMessage);
},
render: function() {
this.$el.html(this.template(this.model.toJSON())).addClass("sprite");
return this;
},
isClicked: function() {
this.model.set("messageActive", true);
},
showMessage: function(model, value) {
this.$el.toggleClass("show-message", value);
}
});
app.AppView = Backbone.View.extend({
el: "#messages",
initialize: function() {
this.listenTo(app.messages, "change:messageActive", this.hideMessages);
},
hideMessages: function(model, value) {
if (value) {
var open = app.messages.where({messageActive: true});
open.forEach(function(e, i) {
if (model != e) {
e.set("messageActive", false);
}
});
}
},

Related

Backbone.js collection doesn't listen model change event

I've just started using backbone.js to create a spa. I'm trying to make a books collection listen to model change but nothing happens. This is the model and collection:
var Book = Backbone.Model.extend({
idAttribute: 'id',
defaults: {
id: null,
title: '',
author: '',
completed: false
}
});
var BooksCollection = Backbone.Collection.extend({
model: Book,
url: 'books',
initialize: function() {
this.listenTo(this.model, 'change', this.debug);
},
debug: function () {
console.log('something happened !!!');
}
});
This is the view:
var BookView = Backbone.View.extend({
template: _.template($('#viewBook').html()),
events: {
"click #save": "bookSave"
},
initialize: function() {
this.listenTo(this.model, 'change', this.render);
},
render: function() {
$(this.el).html(this.template(this.model.toJSON()));
return this;
},
bookSave: function() {
this.model.set({author: "nobody"});
}
});
When I click on the #save button the author is changed but nothing is logged. Where am I wrong?
Events from models automatically bubbles up to the collection, so you can listen to model changes directly like this:
// in the collection
initialize: function() {
this.listenTo(this, 'change', this.debug);
},

How can I prevent Backbones save method from trying to update every model?

I am creating a crud web app with backbone. I am writing the functionality to update a resource (PUT). I am trying to achieve this by fetching a models properties from the server (see the SubscriberView) and on successfully fetching the resource to instantiate a SubscriberEditView whereby the newly fetched model is passed.
So far this works as expected; SubscriberEditView renders an html form which is populated with the model instance properties.
When I enter a new login value into the form I can trigger the update function which successfully makes a PUT request to the server resource and updates the model instance as expected.
However, the problem is that when I then repeat this process with another model instance the PUT request is made against the curent model AND the previously instantiated model.
Is the reason for this because I now have two instances of SubscriberEditView? Or is it something else that I have missed/misunderstood.
Please see below the described code.
// The view for a single subscriber
var SubscriberView = Backbone.View.extend({
tagName: 'tr',
template: _.template($('#subscribers-tmpl').html()),
initialize: function() {
this.listenTo(this.model, 'destroy', this.remove);
},
render: function() {
var html = this.template(this.model.toJSON());
this.$el.html(html);
return this;
},
events: {
'click .remove': 'onRemove',
'click .edit-subscriber': 'editSubscriber',
},
editSubscriber: function() {
var getSubscriberModel = this.model.set('id', this.model.attributes.id, {silent:true})
getSubscriberModel.fetch({
success: function (model, response) {
$('#addSubscriber').fadeOut();
new SubscriberEditView({model:model});
},
error: function (response) {
console.log('There was an error');
}
});
},
onRemove: function() {
this.model.destroy();
}
});
// The edit view
var SubscriberEditView = Backbone.View.extend({
tagName: 'div',
el: '#updateSubscriber',
template: _.template($('#subscriberEdit-tmpl').html()),
initialize: function() {
this.model.on('sync', this.render, this);
},
events: {
'click #close': 'cancel',
'click .save-subscriber': 'update'
},
update: function() {
var $login = this.$('#login');
this.model.save({
login: $login.val(),
},
{
dataType: 'text',
success: function (model, response, options) {
console.log('success');
},
error: function (model, response, options) {
console.log('error');
}
});
},
cancel: function() {
$('#addSubscriber').fadeIn();
$('#editInner').fadeOut();
},
render: function() {
var html = this.template(this.model.toJSON());
this.$el.html(html);
},
});
If anyone could help then that would be greatly appreciated.
Cheers
The issue is el: '#updateSubscriber',. All your view instances are pointing to same element to which events are delegated. So clicking on any of the .save-subscriber will trigger update for all the view instances. You should not specify el for a view that is going to have more than one instance.

Collection not firing listen events

In my backbone collection I am working with a collection view at the moment, the view is generated "on the fly".
I am struggling to get the view to update when something is added to the collection, on debugging I have noticed that the collection does not have a _listenid which in my limited knowledge I assume means it cannot listen for the events bound to it?
What would this be happening?
Here is my view,
Pops.Views.ProjectManagers = Backbone.View.extend({
className: 'ui-user-list ui-user-list--single',
template: _.template($("#tpl-project-managers").html()),
events: {
"click .js-add-user": "addUser"
},
initialize: function () {
// this.collection.on('all', function() {
// console.log(arguments);
// });
// this.collection.on('reset', this.render, this);
console.log(this.collection);
this.collection.on('add', this.addOneProjectManager, this);
},
render: function () {
//alert("!!!!!");
this.$el.html(this.template());
this.addAllProjectManagers();
return this;
},
addAllProjectManagers: function () {
this.collection.each(this.addOneProjectManager, this);
},
addOneProjectManager: function (model) {
console.log(model);
if(this.model.get('is_admin') == true) {
model.set('admin', true);
}
var teamMember = new App.Views.SingleTeamMember({
model: model,
project: this.model
});
this.$('.ui-member-list').prepend(teamMember.render().el);
},
});
If I physically refresh the page, the collection then has a _listenid
I initialise this view like this,
var projectManagerList = new Pops.Views.ProjectManagers({
model : this.model,
collection : this.model.get('project_manager')
});
and this is the model I pass through,

Backbone: bind event happen multiple times

I make a simple todo app:
var Todo = Backbone.Model.extend({
});
var Todos = Backbone.Collection.extend({
model: Todo
});
var todos = new Todos();
var ItemView = Backbone.View.extend({
tagName: "li",
template: _.template($("#item-template").html()),
render: function () {
this.$el.html(this.template(this.model.toJSON()));
return this;
},
initialize: function () {
this.listenTo(todos, 'remove', this.remove);
},
events: {
"click .delete": "clear"
},
clear: function () {
todos.remove(this.model);
}
});
var AppView = Backbone.View.extend({
el: $("body"),
initialize: function () {
this.listenTo(todos, 'add', this.addOne);
},
addOne: function(todo) {
var view = new ItemView({
model: todo
});
this.$("#list").append(view.render().el);
},
events: {
"click #create": "create"
},
create: function () {
var model = new Todo({
title: this.$("#input").val()
});
todos.add(model);
}
})
var app = new AppView();
and DEMO online is here: http://jsfiddle.net/JPL94/1/
I can add item correctly, but when I want delete some item, all of them been removed;
I found it related to the bind event in ItemView, when I click one delete button, all of them are triggered.
But how can I solve this problem?
You are listening to remove events from the collection, and if my memory serves me right a collection will dispatch a remove event whenever a model is removed, so when you remove a model from the collection, all the views will see the event.
I changed your initialize in the view to
initialize: function () {
this.listenTo(this.model, 'remove', this.remove);
},
And it seems to work.
http://jsfiddle.net/JPL94/5/

Getting the attribute from a View's Model when the view is clicked (backbone.js)

When a user clicks on a div with class .photo_container which is part of the view PhotoListView, there is a function sendSelectedPhotoId that will be triggered. This function has to get the attribute photo_id from the Photo model that belongs to this view whose div .photo_container element has been clicked, and send it to the serverside via fetch().
Problem: So far I managed to get the function sendSelectedPhotoId to be triggered when the div is clicked, but I cant figure out how to get the photo_id attribute of the view's Photo model. How should I achieve this?
On a side note, I'm not sure whether the correct photo_id will be send.
Code
$('#button').click( function() {
// Retrieve photos
this.photoList = new PhotoCollection();
var self = this;
this.photoList.fetch({
success: function() {
self.photoListView = new PhotoListView({ model: self.photoList });
$('#photo_list').html(self.photoListView.render().el);
}
});
});
Model & Collection
// Models
Photo = Backbone.Model.extend({
defaults: {
photo_id: ''
}
});
// Collections
PhotoCollection = Backbone.Collection.extend({
model: Photo,
url: 'splash/process_profiling_img'
});
Views
// Views
PhotoListView = Backbone.View.extend({
tagName: 'div',
events: {
'click .photo_container': 'sendSelectedPhotoId'
},
initialize: function() {
this.model.bind('reset', this.render, this);
this.model.bind('add', function(photo) {
$(this.el).append(new PhotoListItemView({ model: photo }).render().el);
}, this);
},
render: function() {
_.each(this.model.models, function(photo) {
$(this.el).append(new PhotoListItemView({ model: photo }).render().el);
}, this);
return this;
},
sendSelectedPhotoId: function() {
var self = this;
console.log(self.model.get('photo_id'));
self.model.fetch({
data: { chosen_photo: self.model.get('photo_id')},
processData: true,
success: function() {
}});
}
});
PhotoListItemView = Backbone.View.extend({
tagName: 'div',
className: 'photo_box',
template: _.template($('#tpl-PhotoListItemView').html()),
initialize: function() {
this.model.bind('change', this.render, this);
this.model.bind('destroy', this.close, this);
},
render: function() {
$(this.el).html(this.template( this.model.toJSON() ));
return this;
},
close: function() {
$(this.el).unbind();
$(this.el).remove();
}
});
SECOND ATTEMPT
I also tried placing the event handler and sendSelectedPhotoId in the PhotoListItemView where I managed to get the Model's attribute properly, but I can't figure out how to trigger the reset event when the PhotoList collection did a fetch().
View
PhotoListItemView = Backbone.View.extend({
tagName: 'div',
className: 'photo_box',
events: {
'click .photo_container': 'sendSelectedPhotoId'
},
template: _.template($('#tpl-PhotoListItemView').html()),
initialize: function() {
this.model.bind('change', this.render, this);
this.model.bind('destroy', this.close, this);
},
render: function() {
$(this.el).html(this.template( this.model.toJSON() ));
return this;
},
close: function() {
$(this.el).unbind();
$(this.el).remove();
},
sendSelectedPhotoId: function() {
console.log('clicked!');
var self = this;
console.log(self.model.get('photo_id'));
self.model.fetch({
data: { chosen_photo: self.model.get('photo_id')},
processData: true,
success: function() {
$(this.el).html('');
}});
}
});
Problem: With this, I cant seem to fire the reset event of the model after doing the fetch() in function sendSelectedPhotoId, which means I cant get it to re-render using PhotoListView's render().
In the screenshot below from Chrome's javascript console, I printed out the collection after sendSelectedPhotoId did its fetch(), and it seems like the fetched added the new data to the existing model, instead of creating 2 new models and removing all existing model!
You already have child views for each model, so I would put the click event handler in the child view. In the handler in the child, trigger an event passing this.model, and listen for that event in your parent.
Update based on update:
Try changing
this.model.bind('reset', this.render, this); to
this.model.bind('remove', this.render, this); // model is a collection right?
and then remove the model from the collection after the view is clicked. Also, I don't think using Model.fetch is what you really want to do. Maybe a .save or a custom method on the model?
Update based on author's comment showing sample base from blog
I would not follow that blog's advice. If you are using backbone professionally I can't recommend the Thoughtbot ebook enough.
It's $50 for a work in progress, and it's worth every penny
It has a simple sample application that lays out how to organize a backbone app. This is why I bought the book.
It uses Rails in the examples for the backend, but I have used Rails, Node, and C# MVC and all work no problem.

Categories