I am trying to trace all of the zombies in my application and get better understanding of how the event binding happens.
I have a view that binds its collection "add" event to its render function.
_.bindAll(this, "render");
this.collection.bind("add", this.render);
So if I log something in my render function I can see in console that rendering happened twice right after user adds new model through UI. The console output looks like this:
rendering index.js?body=1 (line 88)
POST http://localhost:3000/tasks jquery.js?body=1 (line 8103)
rendering index.js?body=1 (line 88)
I wonder why is this happening. I know for a fact that model was added only once to the collection so that makes me think that event should only be fired once. Then I don't understand why render was executed twice.
I have looked in similar question here but it is different because I am using add event instead of change.
Did you instantiate your view twice? It might be 2 different views.
I think that you are calling render two times.
Yoy are doing something like this:
var yourView = new YourDefinedView();
yourView.render(); // this is your manual call to render
//here you call to the server and when data arrives is the second render
this.collection.fetch();
I do not think that render is the best method to bind when a collection receives a new item.
Check this example how we bind a specific event for add items from the Collection to the View:
http://danielarandaochoa.com/backboneexamples/blog/2012/02/22/real-world-usage-of-a-backbone-collection/
It turned out that there was an additional binding to render through Event Aggregator. It was added by another developer.
Related
I've tried to prepare data from an OData source to show it in a bar graph in my fiori app. For this, I setup the OData model in the manifest.json. A test with a list, simply using
items="{path : 'modelname>/dataset'}
works fine and shows the content.
To prepare data for a diagram (VizFrame), I used the onInit() function in the controller of the view (mvc:XMLView). The data preparation is similar to the one discussed in question.
At first I obtain the ODataModel:
var oODataModel = this.getOwnerComponent().getModel("modelname");
Next I do the binding:
var oBindings = oODataModel.bindList("/dataset");
Unfortunately, the oBindings().getContexts() array is always empty, and also oBindings.getLength() is zero. As a consequence, the VizFrame shows only "No Data".
May it be that the data model is not fully loaded during the onInit() function, or do I misunderstand the way to access data?
Thanks in advance
Update
I temporary solved the problem by using the automatically created bind from the view displaying the data as list. I grep the "dataReceived" event from the binding getView().byId("myList").getBindings("items") and do my calculation there. The model for the diagram (since it is used in a different view) is created in the Component.js, and registered in the Core sap.ui.getCore().setModel("graphModel").
I think this solution is dirty, because the graph data depends on the list data from a different view, which causes problems, e.g. when you use a growing list (because the data in the binding gets updated and a different range is selected from the odata model).
Any suggestions, how I can get the odata model entries without depending on a different list?
The following image outlines the lifecycle of your UI5 application.
Important are the steps which are highlighted with a red circle. Basically, in your onInit you don't have full access to your model via this.getView().getModel().
That's probably why you tried using this.getOwnerComponent().getModel(). This gives you access to the model, but it's not bound to the view yet so you don't get any contexts.
Similarly metadataLoaded() returns a Promise that is fullfilled a little too early: Right after the metadata has been loaded, which might be before any view binding has been done.
What I usually do is
use onBeforeRendering
This is the lifecycle hook that gets called right after onInit. The view and its models exist, but they are not yet shown to the user. Good possibility to do stuff with your model.
use onRouteMatched
This is not really a lifecycle hook but an event handler which can be bound to the router object of your app. Since you define the event handler in your onInit it will be called later (but not too late) and you can then do your desired stuff. This obviously works only if you've set up routing.
You'll have to wait until the models metadata has been loaded. Try this:
onInit: function() {
var oBindings;
var oODataModel = this.getComponent().getModel("modelname");
oODataModel.metadataLoaded().then(function() {
oBindings = oODataModel.bindList("/dataset");
}.bind(this));
},
May it be that the data model is not fully loaded during the onInit()
function, or do I misunderstand the way to access data?
You could test if your model is fully loaded by console log it before you do the list binding
console.log(oODataModel);
var oBindings = oODataModel.bindList("/dataset");
If your model contains no data, then that's the problem.
My basic misunderstanding was to force the use of the bindings. This seems to work only with UI elements, which organize the data handling. I switched to
oODataModel.read("/dataset", {success: function(oEvent) {
// do all my calculations on the oEvent.results array
// write result into graphModel
}
});
This whole calculation is in a function attached to the requestSent event of the graphModel, which is set as model for the VizFrame in the onBeforeRendering part of the view/controller.
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 rarely get stuck in JS but this time I have a feeling I did something wrong somewhere - Enlightened people, here you go :
in view1, I have :
listView = new ListView({collection: listElements});
listView.render();
Which is called each time listElements changes.
in ListView, the collection gets parsed to a template in the render method then an event is fired on click :
//ListView.js
events: {
"click .listEl": "doStuff"
...
},
doStuff: function(e) {
// if this is when the problem arises : this.collection at this place isn't the
// same collection passed to ListView in view1 (or the collection in
// the initialize() ).
// It's actually the first value ever to be rendered with ListElements.
Any thoughts ?
Thanks #muistooshort for the head's up.
It was due to Zombie events/views; to 'quickly' fix it, run this after you need to get rid of the view and it's bound to events.
this.listView.undelegateEvents();
$(this.listView.el).removeData().unbind();
this.listView.$el.html('');
A more elegant version would be to extend Backbone.View and add a remove() method to it.
I want to remove a model from a collection:
var item = new Backbone.Model({
id: "01",
someValue: "blabla",
someOtherValue: "boa"
});
var list = new Backbone.Collection([item]);
list.get("01").destroy();
Result:
item is still in the backbone array ....
I have reservations about the accepted answer. When a model is destroyed, the "destroy" event bubbles through any collection the model is in. Thus, when you destroy a model you should not have to also remove the model from the list.
model.destroy();
Should be enough.
Looking at your code it looks correct: (If the intent is to destroy + remove, not just remove)
list.get('01').destroy();
Are you sure that your resource is getting properly destroyed? Have you tried placing a success and error callback in your destroy() call to ensure the destroy was executed? For example, if your model URL is incorrect and it can't access the resource, the destroy call would return an error and your collection will still have the model. This would exhibit the symptoms you outline in your question.
By placing the remove() after the destroy call, your collection will definitely remove the model. But that model will still be floating around (still persisted). This may be what you want, but since you're calling destroy() I'm assuming you want to obliterate it completely. If this is the case, while remove seems to work, what it's really doing is masking that your model has been destroyed when in fact it may not.
Thus, this is what I have a feeling is actually happening. Something is preventing the model from being destroyed. That's why when you call destroy(), then check your collection - the model is still there.
I could be wrong though. Could you check this and update your findings?
You should also remove the model from the collection.
var model = list.get('01');
model.destroy();
list.remove(model);
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.