Analyzing this code I am not sure what is actually happening.I keep falling into this trap with JS especially with callbacks. Here is an example taken from backbone's documentation.
//creates a new constructor function with a promptColor function as an attribute.
var Sidebar = Backbone.Model.extend({
promptColor: function() {
var cssColor = prompt("Please enter a CSS color:");
this.set({color: cssColor});
}
});
// creates a property on the global window object called sidebar
window.sidebar = new Sidebar;
// .on is an event listener and passed a callback function taking the parameters of model and color. Here is my confusion, what does it do with the model parameter?
sidebar.on('change:color', function(model, color) {
$('#sidebar').css({background: color});
});
sidebar.set({color: 'white'});
sidebar.promptColor();
My main question is what does it do with the model parameter? What is it actually doing with the model parameter?
Thanks!
In your particular case the model parameter is of no real use since their is a 1-to-1 relationship between the change event and the model.
However, there are times when this is not the case. For example, imagine you have a backbone collection of models. You can attach a "change" event listener to the collection which will get called every time any model in the collection changes. In cases like this, it's helpful to know which model originated the "change" event.
Related
I am new to Backbone.js
I must be doing something wrong here. I'm trying to make a little demo to see what I can do with Backbone and basing it off some sample code.
I an get "Uncaught TypeError: Cannot call method toJSON of undefined".
I see why it is doing this, because the .bind("change", taskView.render) call is setting the context to the model (which the alert confirms) but it seems like there should at least be an argument to the render function to get access to the view. Maybe I am just going about it the wrong way? (see the sample code below).
task.bind("change", _.bind(taskView.render, taskView));
On Backbone Views and Models, the 'this' context for 'bind' is the calling object, so for model.bind('change', ...) this with be the model. For view.bind(... this will be the view.
You are getting the error because task.bind("change", _.bind(taskView.render, taskView)); sets this to be task when render runs, but this actually needs to be taskView.
Since you want to bind the model event to the view, as irvani suggests, the best way to do this is to use the .listenTo method (see: http://backbonejs.org/#Events-listenTo)
taskView.listenTo(task, 'change', taskView.render);
Depending on how you want the view lifecycle to work, you could also put the code in the initialize method of the view, which executes when the view is created. Then use stopListening in the remove method, to clear up the listener when the view is no longer in use.
As the task model is passed into the view, you get a fairly neat:
AskView = Backbone.Model.extend({
initialize: function(){
this.listenTo(this.model, 'change', this.render);
},
...
remove: function(){
this.stopListening(this.model);
...
}
});
var askView = new AskView({ model: task });
However, the one line solution to your problem is:
task.on("change", taskView.render, taskView);
bind is just an alias of on (see: http://backbonejs.org/#Events-on). The first argument is the event you are listening to, the second is the method to fire, and the third argument is the context to bind to, in this case, your taskView.
task.listenTo(model, 'change', taskView.render);
This says that listen to the model change and render the taskView on every change.
As irvani suggested, use listenTo instead.
object.listenTo(other, event, callback) Tell an object to listen to a particular event on an other object. The advantage of using this
form, instead of other.on(event, callback, object), is that listenTo
allows the object to keep track of the events, and they can be removed
all at once later on. The callback will always be called with object
as context.
In your case,
taskView.listenTo(task,"change",taskView.render);
assuming taskView is Backbone.View and task is a Backbone.Model.
The chances of memory leaks will be less when you use listenTo compared to using on.
If you must use on, you can specify the context as a third argument as and mu is too short suggested:
To supply a context value for this when the callback is invoked, pass the optional last argument: model.on('change', this.render, this) or model.on({change: this.render}, this).
In your case:
task.on("change", taskView.render, taskView);
I have a model that has collections within it. Within my web app I have a project model, and in that I have an attribute called "collaborators" this attribute is actually a collection of users.
In the initialize of my view I have the following,
this.model.get('collaborators').on( 'change', alert("!!"), this);
When the view firsts loads I get an alert, firther into the view I have an event, that fires the following,
var newModel = new app.User({ id: this.clickedElement.data('userId') });
console.log(this.model.get(this.formSection)); // shows that the collection has 4 models
newModel.fetch({
success: function() {
that.model.get(that.formSection).add(newModel);
console.log(that.model.get(that.formSection)); //shows that the collection has 5 models
}
});
As you can see in the code above, I am logging my collaborators collection and it shows a length of 4, after the fetch in the success method, am I adding the model I just fetched to that collection, and then logging the collection again, this time it returns a length of 5.
This to me means that the add is successful, so is the event listener for "add" not firing after the initial page view?
The trouble is that you should pass some function as a second argument of on method. But you're firing alert right at the event binding moment and passing alert's result instead of function.
This should work correctly:
this.model.get('collaborators').on('change', function(){
alert("!!");
});
Update: But you should clarify which events you want to catch. You can find all events fired during fetching (or any other process) by launching search for trigger method calls across backbone.js file.
Update: Also you can use this code to catch all of events fired:
collaborators.on('all', function(){
/**
* `arguments` object contains all of arguments passed to the function.
* #see http://backbonejs.org/#Events-on for more details about `all`
* catching.
*/
console.log(arguments);
});
When you binding to the collaborators' change event, you should use Backbone's listenTo event.
this.listenTo(this.model.get('collaborators'), "change", function(){alert("!!"), this);
http://backbonejs.org/#Events-listenTo
The advantage of using this.listenTo is when the view gets closed/removed, Backbone will automatically invoke this.stopListening causing your view to automatically unbind the event. This will help you clean up events and prevent phantom views.
http://backbonejs.org/#Events-stopListening
when my page opens, I call the collection and populate the view:
var pagColl = new pgCollection(e.models);
var pagView = new pgView({collection: pagColl});
Separately (via a Datepicker), I wish to want to populate the same collection with different models and instantiate the view again.
The problem I have is how to close the original pagView and empty the pagColl before I open the new one, as this "ghost view" is creating problems for me. The variables referred to above are local variables? is it that I need to create a global pagColl and reset() this?
well there has been many discussion on this topic actually,
backbone does nothing for you, you will have to do it yourself and this is what you have to take care of:
removing the view (this delegates to jQuery, and jquery removes it from the DOM)
// to be called from inside your view... otherwise its `view.remove();`
this.remove();
this removes the view from the DOM and removes all DOM events bound to it.
removing all backbone events
// to be called from inside the view... otherwise it's `view.unbind();`
this.unbind();
this removes all events bound to the view, if you have a certain event in your view (a button) which delegates to a function that calls this.trigger('myCustomEvent', params);
if you want some idea's on how to implement a system I suggest you read up on Derrick Bailey's blogpost on zombie views: http://lostechies.com/derickbailey/2011/09/15/zombies-run-managing-page-transitions-in-backbone-apps/.
another option
another option would be to reuse your current view, and have it re-render or append certain items in the view, bound to the collection's reset event
I was facing the same issue. I call the view.undelegateEvents() method.
Removes all of the view's delegated events. Useful if you want to disable or remove a view from the DOM temporarily.
I use the stopListening method to solve the problem, usually I don't want to remove the entire view from the DOM.
view.stopListening();
Tell an object to stop listening to events. Either call stopListening
with no arguments to have the object remove all of its registered
callbacks ... or be more precise by telling it to remove just the
events it's listening to on a specific object, or a specific event, or
just a specific callback.
http://backbonejs.org/#Events-stopListening
Here's one alternative I would suggest to use, by using Pub/Sub pattern.
You can set up the events bound to the View, and choose a condition for such events.
For example, PubSub.subscribe("EVENT NAME", EVENT ACTIONS, CONDITION); in the condition function, you can check if the view is still in the DOM.
i.e.
var unsubscribe = function() {
return (this.$el.closest("body").length === 0);
};
PubSub.subscribe("addSomething",_.bind(this.addSomething, this), unsubscribe);
Then, you can invoke pub/sub via PubSub.pub("addSomething"); in other places and not to worry about duplicating actions.
Of course, there are trade-offs, but this way not seems to be that difficult.
I'm pretty new to Backbone so excuse me if this question is a little obvious.
I am having problems with a collection inside of a model. When the collection changes it doesn't register as a change in the model (and doesn't save).
I have set up my model like so:
var Article = Backbone.Model.extend({
defaults: {
"emsID" : $('html').attr('id')
},
initialize: function() {
this.tags = new App.Collections.Tags();
},
url: '/editorial_dev.php/api/1/article/persist.json'
});
This works fine if I update the tags collection and manually save the model:
this.model.tags.add({p : "a"});
this.model.save();
But if the model is not saved the view doesn't notice the change. Can anyone see what I am doing wrong?
initialize: function() {
this.tags = new App.Collections.Tags();
var model = this;
this.tags.bind("change", function() {
model.save();
});
},
Bind to the change event on the inner collection and just manually call .save on your outer model.
This is actually an addendum to #Raynos answer, but it's long enough that I need answer-formatting instead of comment-formatting.
Clearly OP wants to bind to change and add here, but other people may want to bind to destroy as well. There may be other events (I'm not 100% familiar with all of them yet), so binding to all would cover all your bases.
remove also works instead of destroy. Note that both remove and destroy fire when a model is deleted--first destroy, then remove. This propagates up to the collection and reverses order: remove first, then destroy. E.g.
model event : destroy
model event : remove
collection event : destroy
collection event : remove
There's a gotcha with custom event handlers described in this blog post.
Summary: Model-level events should propagate up to their collection, but can be prevented if something handles the event first and calls event.stopPropagation. If the event handler returns false, this is a jQuery shortcut for event.stopPropagation();
event.preventDefault();
Calling this.bind in a model or collection refers to Underscore.js's bind, NOT jQuery/Zepto's. What's the difference? The biggest one I noticed is that you cannot specify multiple events in a single string with space-separation. E.g.
this.bind('event1 event2 ...', ...)
This code looks for the event called event1 event2 .... In jQuery, this would bind the callback to event1, event2, ... If you want to bind a function to multiple events, bind it to all or call bind once for each event. There is an issue filed on github for this, so this one will hopefully change. For now (11/17/2011), be wary of this.
This seems possible as http://www.knockoutjs.com appears to be doing it. I haven't been able to make enough sense of their code-base to get a similar pattern working though.
Effectively I have a MVVM style application with the UI based on jQuery tabs. Each tab is represented by a view model that I want to be able to validate and fire events based on changes in the model.
I create a representation of my data similar to the following on page load:
$(document).ready(function(){
thisTab = new ThisTab();
});
function ThisTab(){
Load: {Load from my model}
Save: {Save/Persist model to the db (via web service call)}
Validate: {
this.Item1 = function(){Validate item 1, do work, refresh fields, whatever.}
}
}
The model itself is a complex global object and changes to the DOM (inputs, etc.) immediately update the object. Changes to some of those properties should call their associated validate items thisTab.Validate.Item1. I have no issue raising events from the changes. If I bind that event listener to a random DOM element I can call my routines without issue and everything works beautifully. It does seem strange, however, to attach the event to a non-related DOM object.
So the question is: how can I do something like thisTab.addEventListner("someEvent") or $(thisTab).bind("someEvent"), where thisTab is not a DOM element, but instead is a native object. Trying to do it, I always get an error that "this method is not supported".
Attaching an event to a standard object does not use the same methods; basically, you would implement your own eventing like so:
function ThisTab()
{
listeners: [],
addListener: function(callback) { this.listeners.push(callback); },
load: { // Finds DOM elements and such, and attaches to their events. The callback from the DOM event should be a method on your object },
yourDomEventCallback: function()
{
for(var j = 0; j < this.listeners.length; j++)
this.listeners[j]();
}
}
The above code should be used as a starting point, since I just cobbled it together and there are likely syntax errors. Basically, you have taken your object and mapped onto events you want to capture, and then expose methods to attach callback methods that you will call when the hidden DOM events occur. You wont be able to use jQuery's abstractions for DOM events, because such events have no meaning on your custom object.
Bind the event to your regular JS object as you would do for a DOM object.
$(thisTab).bind("someEvent", function() {
// handler's code here
});
See this example. Using this has one side-effect that jQuery will add a housekeeping identifier as a property on the object - it looks something like
jQuery1510587397349299863.
This property named jQuery<timestamp> is added to all DOM objects that have events or data associated with them, and regular objects behave similarly. If you are uncomfortable with your model objects being modified, then use your own callback mechanism which should be fairly easy to add.