Change event is not getting trigger when updating model in backbone js - backbone.js

I have a view called layerPanel that is using screenData model. Now on model.set i get update event from model itself, but its not working on view.
MODEL
var screenData = Backbone.Model.extend({
initialize : function() {
_.bindAll(this,"update");
this.bind('change:vdata', this.update);
},
update: function() {
var obj = this.vdata;
alert("update");
},
vdata:[{id : 0, title : "Welcome to Persieve 0"}]
});
VIEW
var layerPanel = Backbone.View.extend({
el: "#allLayers",
model: new screenData(),
initialize: function() {
this.render();
this.model.bind('change:vdata', this.render);
},
render: function() {
this.template = _.template(LayersTpl, {splitData: this.model.vdata});
this.$el.html(this.template);
return this;
}
});
Here is how I set values in Model.
screendata = new screenData;
var obj = screendata.vdata;
obj[obj.length] = {id : $(".bullet").length, title : "Welcome to Persieve"};
var tempData = [];
for ( var index=0; index<obj.length; index++ ) {
if ( obj[index]) {
tempData.push( obj );
}
}
obj = tempData;
screendata.set({vdata:[obj]});

The event should fire. But your render wont work as the 'this' context needs setting.
try:
this.model.bind('change:vdata', this.render, this);
or even better, use listenTo and the context is implicit (+ you can clean up easily this.remove())
Edit. From the edit you made above, I can see that you are creating a new screendata instance. The binding you created is for a different instance model: new screenData() .
You must reference the binded object and set it if you want the event to trigger.
If all the model setting happens in the actual model. Call this.set({vdata:[obj]});

Related

Event handling between views

Ok I have a layout like the one in this pic:
The table in the upper part of the screen is made by:
MessageListView
define(['backbone','collections/messages','views/message'], function(Backbone, MessageCollection, MessageView) {
var MessageListView = Backbone.View.extend({
el: '#messagesContainer',
initialize: function() {
this.collection = new MessageCollection();
this.collection.fetch({reset:true});
this.listenTo( this.collection, 'reset', this.render );
this.table = this.$el.find("table tbody");
this.render();
},
render: function() {
this.collection.each( function(message, index) {
this.renderMessage(message, index);
}, this);
},
renderMessage: function(message, index) {
var view = new MessageView({
model:message,
className: (index % 2 == 0) ? "even" : "odd"
});
this.table.append( view.render().el );
}
});
return MessageListView;
});
MessageView
define(['backbone','models/message'], function(Backbone, MessageCollection, MessageView) {
var MessageView = Backbone.View.extend({
template: _.template( $("#messageTemplate").html() ),
render: function() {
this.setElement( this.template(this.model.toJSON()) );
return this;
},
events:{
'click':'select'
},
select: function() {
// WHAT TO DO HERE?
}
});
return MessageView;
});
AppView
define(['backbone','views/messages'], function(Backbone, MessageList) {
var App = Backbone.View.extend({
initialize: function() {
new MessageList();
}
});
return App;
});
I will soon add a new view (maybe "PreviewView") in the lower part of the screen.
I want to make something happen inside the "PreviewView" when user clicks a row.
For example, it could be interesting to display other model's attributes (details, e.g.) inside the PreviewView.
What is the best practice?
holding a reference to PreviewView inside each MessageView ?
triggering events inside select method, and listening to them using on() inside the preview view.
using a transient "selected" attribute in my model, and make PreviewView listen to collection "change" events?
Thank you, if you need more details tell me please, I'll edit the question.
Not sure about the best practice but I found this solution trivial to implement. I created a global messaging object, bus, whatever:
window.App = {};
window.App.vent = _.extend({}, Backbone.Events);
You have to register the "triggerable" functions of PreviewView on the previously created event bus (according to your example, this should be in the PreviewView):
initialize: function () {
App.vent.on('PreviewView.show', this.show, this);
}
Now you should be able to trigger any of registered events from anywhere within your application by calling: App.vent.trigger. For example when the user click on a row you will have something similar:
App.vent.trigger('PreviewView.show');
in case if you have to send and object along with the triggered event use:
App.vent.trigger('PreviewView.show', data);

Additional Model is undefined

I am having problems including an additional model into my view which is based on a collection. I have a list of comments which is created by a parent view. Its need that I have the current user name when rendering the comments to show delete button and to highlight if its his own comment. The problem is now that I cant access in CommentListView the model session, so this.session in initialize or a call from a method like addAllCommentTo list is undefinied. What I am doing wrong here? I thought its easily possible to add another object to an view appart from the model.
CommentListView:
window.CommentListView = Backbone.View.extend({
el: $("#comments"),
initialize: function () {
this.model.bind('reset', this.addAllCommentToList, this);
this.model.bind('add', this.refresh, this);
this.model.bind('remove', this.refresh, this);
},
refresh: function(){
this.model.fetch();
},
addCommentToList : function(comment) {
console.log("comment added to dom");
//need to check why el reference is not working
$("#comments").append(new CommentView({model:comment, sessionModel: this.session}).render().el);
},
addAllCommentToList: function() {
$("#comments").empty();
this.model.each(this.addCommentToList);
}
});
Call from parent list in initialize method:
window.UserDetailView = Backbone.View.extend({
events: {
"click #newComment" : "newComment"
},
initialize: function () {
this.commentText = $("#commentText", this.el);
new CommentListView({ model: this.model.comments, session: this.model.session });
new LikeView({ model: this.model.like });
this.model.comments.fetch();
},
newComment : function() {
console.log("new comment");
this.model.comments.create(
new Comment({text: this.commentText.val()}), {wait: true}
);
this.commentText.val('');
}
});
Model:
window.UserDetail = Backbone.Model.extend({
urlRoot:'/api/details',
initialize:function () {
this.comments = new Comments();
this.comments.url = "/api/details/" + this.id + "/comments";
this.like = new Like();
this.like.url = "/api/details/" + this.id + "/likes";
this.session = new Session();
},
...
});
I see one problem, but can there be others.
You are initializing the View like this:
new CommentListView({ model: this.model.comments, session: this.model.session });
And you are expecting into your View to have a reference like this this.session.
This is not gonna happen. All the hash you send to the View constructor will be stored into this.options, from Backbone View constructor docs:
When creating a new View, the options you pass are attached to the view as this.options, for future reference.
So you can start changing this line:
$("#comments").append(new CommentView({model:comment, sessionModel: this.session}).render().el);
by this other:
$("#comments").append(new CommentView({model:comment, sessionModel: this.options.session}).render().el);
Try and tell us.
Updated
Also change this line:
this.model.each(this.addCommentToList);
by this:
this.model.each(this.addCommentToList, this);
The second argument is the context, in other words: what you want to be this in the called handler.

How can I bind the model to the view?

When the view is initialized, how can I bind the model to the specific View that is created? The view is current initialized at the start of the application. Also, how can I bind the model to the collection?
(function ($) { //loads at the dom everything
//Creation, Edit, Deletion, Date
var Note = Backbone.Model.extend({
defaults: {
text: "write here...",
done: false
},
initialize: function (){
if(!this.get("text")){
this.set({"text": this.default.text});
}
},
edit: function (){
this.save({done: !this.get("done")});
},
clear: function (){
this.destroy();
}
});
var NoteList = Backbone.Collection.extend({
model:Note
});
var NoteView = Backbone.View.extend ({
el: "body",
initialize: function(){
alert("initialized");
var list = new NoteList;
return list;
},
events: {
"click #lol" : "createNote"
},
createNote : function(){
var note = new Note;
this.push(note);
alert("noted");
}
});
var ninja = new NoteView;
})(jQuery);
Update
I just took a look at #James Woodruff's answer, and that prompted me to take another look at your code. I didn't look closely enough the first time, but I'm still not sure what you're asking. If you're asking how to have a model or view listen for and handle events triggered on the other, then check out James's example of calling bind() to have the view listen for change (or change:attr) events on the model (although I'd recommend using on() instead of bind(), depending what version of Backbone you're using).
But based on looking at your code again, I've revised my answer, because I see some things you're trying to do in ways that don't make sense, so maybe that's what you're asking about.
New Answer
Here's the code from your question, with comments added by me:
var NoteView = Backbone.View.extend ({
// JMM: This doesn't make sense. You wouldn't normally pass `el`
// to extend(). I think what you really mean here is
// passing el : $( "body" )[0] to your constructor when you
// instantiate the view, as there can only be one BODY element.
el: "body",
initialize: function(){
alert("initialized");
// JMM: the next 2 lines of code won't accomplish anything.
// Your NoteList object will just disappear into thin air.
// Probably what you want is one of the following:
// this.collection = new NoteList;
// this.list = new NoteList;
// this.options.list = new NoteList;
var list = new NoteList;
// Returning something from initialize() won't normally
// have any effect.
return list;
},
events: {
"click #lol" : "createNote"
},
createNote : function(){
var note = new Note;
// JMM: the way you have your code setup, `this` will be
// your view object when createNote() is called. Depending
// what variable you store the NoteList object in (see above),
// you want something here like:
// this.collection.push( note ).
this.push(note);
alert("noted");
}
});
Here is a revised version of your code incorporating changes to the things I commented on:
var NoteView = Backbone.View.extend( {
initialize : function () {
this.collection = new NoteList;
},
// initialize
events : {
"click #lol" : "createNote"
},
// events
createNote : function () {
this.collection.push( new Note );
// Or, because you've set the `model` property of your
// collection class, you can just pass in attrs.
this.collection.push( {} );
}
// createNote
} );
var note = new NoteView( { el : $( "body" )[0] } );
You have to bind views to models so when a model updates [triggers an event], all of the corresponding views that are bound to the model update as well. A collection is a container for like models... for example: Comments Collection holds models of type Comment.
In order to bind a view to a model they both have to be instantiated. Example:
var Note = Backbone.Model.extend({
defaults: {
text: "write here..."
},
initialize: function(){
},
// More code here...
});
var NoteView = Backbone.View.extend({
initialize: function(){
// Listen for a change in the model's text attribute
// and render the change in the DOM.
this.model.bind("change:text", this.render, this);
},
render: function(){
// Render the note in the DOM
// This is called anytime a 'Change' event
// from the model is fired.
return this;
},
// More code here...
});
Now comes the Collection.
var NoteList = Backbone.Collection.extend({
model: Note,
// More code here...
});
Now it is time to instantiate everything.
var Collection_NoteList = new NoteList();
var Model_Note = new Note();
var View_Note = new NoteView({el: $("Some Element"), model: Model_Note});
// Now add the model to the collection
Collection_NoteList.add(Model_Note);
I hope this answers your question(s) and or leads you in the right direction.

Backbone view event atacched to all views

I'm doing my first application in backbone and i get a strange thing happening trying to attach an event.
I got this code so far:
//View for #girl, EDIT action
GirlEditView = Backbone.View.extend({
initialize: function(el, attr) {
this.variables = attr;
console.log(attr);
this.render();
},
render: function() {
var template = _.template( $("#girl_edit").html(), this.variables );
$(this.el).html( template );
$("#edit_girl").modal('show');
}
});
//View for #girl
GirlView = Backbone.View.extend({
initialize: function(el, attr) {
this.variables = attr;
this.render();
},
render: function() {
var template = _.template( $("#girl_template").html(), this.variables );
$(this.el).html( $(this.el).html() + template );
},
events: {
"click p.modify": "modify"
},
modify: function() {
//calls to modify view
new GirlEditView({el : $("#edit_girl")}, this.variables);
}
});
//One girl from the list
Girl = Backbone.Model.extend({
initialize: function() {
this.view = new GirlView({el : $("#content")}, this.attributes );
}
});
//all the girls
Girls = Backbone.Collection.extend({
model: Girl,
});
//do magic!
$(document).ready(function() {
//Underscore template modification
_.templateSettings = {
escape : /\{\[([\s\S]+?)\]\}/g,
evaluate : /\{\[([\s\S]+?)\]\}/g,
interpolate : /\{\{([\s\S]+?)\}\}/g
}
//get initial data and fill the index
var list = [];
$.getJSON('girls.json', function(data) {
list = [];
$.each(data, function(key, val) {
list.push( new Girl(val) );
});
var myGirls = new Girls(list);
console.log( myGirls.models);
});
});
As you can see.
I'm using a collection to store all the girls and the data comes from a REST api in ruby.
Each girls create a new model instance and inside i attached a view instance.
I don't know if it's a good practice but i can't think a better way to do it.
Each view makes a content with a unique id. girl-1 girl-2 and go on.
Now, the template have a edit button.
My original idea is to attack the onclick event and trigger the edit view to get rendered.
That is working as expected.
The proble so far is:
When the events triggers, all the collection (girls) fire the edit view, not the one that "owns" the rendered view.
My question is what i'm doing wrong?
Thanks a lot
All the edit-views come up because all the GirlViews are using the same el:
this.view = new GirlView({el : $("#content")}, this.attributes );
and then you render be appending more HTML:
render: function() {
var template = _.template( $("#girl_template").html(), this.variables );
$(this.el).html( $(this.el).html() + template );
}
Backbone events are bound using delegate on the view's el. So, if multiple views share the same el, you'll have multiple delegates attached to the same DOM element and your events will be a mess of infighting.
You have things a little backwards: models do not own views, views watch models and collections and respond to their events. You'll see this right in the documentation:
constructor / initialize new View([options])
[...] There are several special options that, if passed, will be attached directly to the view: model, collection, [...]
Generally, you create a collection, c, and then create the view by handing it that collection:
var v = new View({ collection: c })
or you create a model, m, and then create a view wrapped around that model:
var v = new View({ model: m })
Then the view binds to events on the collection or model so that it can update its display as the underlying data changes. The view also acts as a controller in Backbone and forwards user actions to the model or collection.
Your initialization should look more like this:
$.getJSON('girls.json', function(data) {
$.each(data, function(key, val) {
list.push(new Girl(val));
});
var myGirls = new Girls(list);
var v = new GirlsView({ collection: myGirls });
});
and then GirlsView would spin through the collection and create separate GirlViews for each model:
var _this = this;
this.collection.each(function(girl) {
var v = new GirlView({ model: girl });
_this.$el.append(v.render().el);
});
Then, GirlView would render like this:
// This could go in initialize() if you're not certain that the
// DOM will be ready when the view is created.
template: _.template($('#girl_template').html()),
render: function() {
this.$el.html(this.template(this.model.toJSON());
return this;
}
The result is that each per-model view will have its own distinct el to localize the events. This also makes adding and removing a GirlView quite easy as everything is nicely wrapped up in its own el.

backbone.js - accessing a model from a click event

I have a BoardView containing a CellCollection of CellModels. I fetch the collection from the db and then create the CellViews.
This all works swimmingly until I try to access a CellModel via a click event on the BoardView. I can't get to the underlying models at all... only the views. Is there a way to do this?
I've attempted to include the relevant code below:
CellModel = Backbone.Model.extend({});
CellCollection = Backbone.Collection.extend({
model : CellModel
});
CellView = Backbone.View.extend({
className : 'cell',
});
BoardView = Backbone.View.extend({
this.model.cells = new CellCollection();
render : function() {
this.cellList = this.$('.cells');
return this;
},
allCells : function(cells) {
this.cellList.html('');
this.model.cells.each(this.addCell);
return this;
},
addCell : function(cell) {
var view = new Views.CellView({
model : cell
}).render();
this.cellList.append(view.el);
},
events : {
'click .cell' : 'analyzeCellClick',
},
analyzeCellClick : function(e) {
// ?????????
}
});
I need the click to "happen" on the BoardView, not the CellView, because it involves board-specific logic.
Good question! I think the best solution would be to implement an
EventBus aka EventDispatcher
to coordinate all events among the different areas of your application.
Going that route seems clean, loosely coupled, easy to implement, extendable and it is actually suggested by the backbone documentation, see Backbone Docs
Please also read more on the topic here and here because (even though I tried hard) my own explanation seems kind of mediocre to me.
Five step explanation:
Create an EventBus in your main or somewhere else as a util and include/require it
var dispatcher = _.clone(Backbone.Events); // or _.extends
Add one or more callback hanlder(s) to it
dispatcher.CELL_CLICK = 'cellClicked'
Add a trigger to the Eventlistener of your childView (here: the CellView)
dispatcher.trigger(dispatcher.CELL_CLICK , this.model);
Add a Listener to the Initialize function of your parentView (here: the BoardView)
eventBus.on(eventBus.CARD_CLICK, this.cardClick);
Define the corresponding Callback within of your parentView (and add it to your _.bindAll)
cellClicked: function(model) {
// do what you want with your data here
console.log(model.get('someFnOrAttribute')
}
I can think of at least two approaches you might use here:
Pass the BoardView to the CellView at initialization, and then handle the event in the CellView:
var CellView = Backbone.View.extend({
className : 'cell',
initialize: function(opts) {
this.parent = opts.parent
},
events : {
'click' : 'analyzeCellClick',
},
analyzeCellClick : function() {
// pass the relevant CellModel to the BoardView
this.parent.analyzeCellClick(this.model);
}
});
var BoardView = Backbone.View.extend({
// ...
addCell : function(cell) {
var view = new Views.CellView({
model : cell,
parent : this
}).render();
this.cellList.append(view.el);
},
analyzeCellClick : function(cell) {
// do something with cell
}
});
This would work, but I prefer to not have views call each other's methods, as it makes them more tightly coupled.
Attach the CellModel id to the DOM when you render it:
var CellView = Backbone.View.extend({
className : 'cell',
render: function() {
$(this.el).data('cellId', this.model.id)
// I assume you're doing other render stuff here as well
}
});
var BoardView = Backbone.View.extend({
// ...
analyzeCellClick : function(evt) {
var cellId = $(evt.target).data('cellId'),
cell = this.model.cells.get(cellId);
// do something with cell
}
});
This is probably a little cleaner, in that it avoids the tight coupling mentioned above, but I think either way would work.
I would let the CellView handle the click event, but it will just trigger a Backbone event:
var CellView = Backbone.View.extend({
className : 'cell',
initialize: function() {
_.bindAll(this, 'analyzeCellClick');
}
events : {
'click' : 'analyzeCellClick',
},
analyzeCellClick : function() {
this.trigger('cellClicked', this.model);
}
});
var BoardView = Backbone.View.extend({
// ...
addCell : function(cell) {
var view = new Views.CellView({
model : cell
}).render();
this.cellList.append(view.el);
view.bind('cellClicked', function(cell) {
this.analyzeCellClick(cell);
};
},
analyzeCellClick : function(cell) {
// do something with cell
}
});

Resources