When I use Backbone's model.destroy(), it seems to automatically remove that view from the DOM.
Is there a way for me to use destroy() to send the DELETE request, but remove the view from the DOM myself?
Something like:
this.model.destroy({
wait: true,
success: function(){
$('#myElement').animate({
"height" : "0",
1000,
function(){$('#myElement').remove()}
});
}
});
You need to override _onCollectionRemove() in whichever Collection view contains the item views (documentation). This is the function which is called when your model is removed from the collection, and it's also what's destroying your view. Specifically how you choose to override it is up to you, but it might be easiest to override it with your animation function, maybe along the following lines...
_onCollectionRemove: function(model) {
var view = this.children.findByModel(model);
var that = this;
view.$('#myElement').animate({
"height" : "0",
1000,
function(){
that.removeChildView(view);
that.checkEmpty();
}
});
}
If you prefer to handle the removal of the view manually in your destroy callback, just override _onCollectionRemove() to contain an empty function and do whatever you'd like in the callback of your delete request. I'd recommend the approach I describe above rather than doing it in your destroy callback, though. Completely eliminating the function and then handling it's responsibilities elsewhere in your code interferes with Marionette's intended event flow. Simply overriding the function with a different UI effect preserves the natural flow.
EDIT: Another user's previous answer (now deleted due to downvoting) suggested that it might be wise to call destroy after the UI effect was completed. This is not a good approach for the reason OP pointed out - if something goes wrong with the destroy method, (for example, if the remote server goes down) it appears to the user as if the model was deleted (the UI effect had already completed) even though the server was unreachable and the model remains.
the mentioned onBeforeDestroy method does not work for me. It throws an error in backbone (remove method missing)
My solution has the same aproach and is working very well in itemView
remove: function(){
this.$el.animate({"height" : "0"},500, function(){
$(this).remove();
});
},
Instead of focusing in the model event, we can focus on the view life cycle. For that purpose, Marionette makes the onBeforeDestroy callback available on Marionette.View (which is extended by all Marionette views). In your ItemView you'd define the callback like this
onBeforeDestroy: function () {
$('#myElement').animate({ "height" : "0", 1000 });
}
They're is an important caveat here. Since $.animate is an asynchronous function, it is possible that the view may be removed before $.animate finishes the transition. So, we have to make a modification to our onBeforeDestroy.
onBeforeDestroy: function () {
var animationDelay = 1000ms;
this.remove = _.delay(_.bind(this.remove, this), animationDelay );
this.$el.animate({ "height" : "0", animationDelay });
}
Essentially, what we did here is set the View.remove() method to fire after the animation has run through, ensuring that when this.remove is called, it's called asynchronously, after the animation has run through. You could also do this with Promises, I suppose, but that requires a bit more overhead.
You need to use one of:
collection.remove model
collection.reset collection.model
Every of this methods will re-render your collection or composite view.
It is not Good practice to remove element from collection/composite view directly by using js or jQuery;
Related
I understand that when a view is removed through .remove(), .stopListening() is called on that view to remove any event listeners associated with that view in Backbone. From the Backbone docs:
remove view.remove()
Removes a view from the DOM, and calls stopListening to remove any bound events that the view has listenTo'd.
I have views that are appended to a container that only have events related to dom actions on themselves through Backbone's events hook.
var View = Backbone.View.extend({
events : {
'input keyup' : 'searchDropdown'
},
searchDropdown: function () {
$('dropdown').empty();
//Appends views based on search
}
});
My question is really whether or not I'm leaking any memory (significant or not) when calling $.empty() on a container that effectively removes the view(s) appended inside of it. And if I am, is there any good convention for accessing and calling .remove() on those views?
You don't need any special framework for this but it's a good idea to implement removal properly and not depend on the browser being smart enough to do this. Sometimes in a large app you will find you specifically need to override the remove method to do some special cleanup - for instance you are using a library in that view which has a destroy method.
A modern browser tends to have a GC which is smart enough for most cases but I still prefer not to rely on that. Recently I came on to a project in Backbone which had no concept of subviews and I reduced the leaking nodes by 50% by changing to remove from empty (in Chrome 43). It's very hard to have a large javascript app not leak memory, my advice is to monitor it early on: If a DOM Element is removed, are its listeners also removed from memory?
Watch out for things which leak a lot of memory - like images. I had some code on a project that did something like this:
var image = new Image();
image.onLoad(.. reference `image` ..)
image.src = ...
Basically a pre-loader. And because we weren't explicitly doing image = null the GC never kicked in because the callback was referencing the image variable. On an image heavy site we were leaking 1-2mb with every page transition which was crashing phones. Setting the variable to null in a remove override fixed this.
Calling remove on subviews is as easy as doing something like this:
remove: function() {
this.removeSubviews();
Backbone.View.prototype.remove.call(this);
},
removeSubviews: function() {
if (!_.isEmpty(this.subViews)) {
_.invoke(this.subViews, 'remove');
this.subViews = [];
}
}
You just need to add your subview instances to an array. For example when you create a subview you could have an option like parentView: this and add it to the array of the parent. I have done more intricate subview systems in the past but that would work fine. On initialize of the views you could do something like:
var parentView = this.options.parentView;
if (parentView) {
(parentView.subViews = parentView.subViews || []).push(this);
}
I am coding in ASP.NET MVC 5.2, and using jQuery as my primary script library. I am having a bit of a problem though, with the disparity between _Layout and views that use that layout.
Essentially, it goes like this
_Layout has some script that needs to run (initial wiring, progress bar, splash screen, etc)
Inheriting View has some script that needs to run (unique to that view)
_Layout has additional scripts that need to run after the view's unique scripts.
I have been trying a lot of ways to solve this, but it is actually proving to be a big problem. I have been frequently told that I should not create objects on the global namespace, so I am wondering if there are any other options to creating a script object that I can access in both views that isn't as damaging as global objects.
I have tried promises, and that is getting frustrating. I have tried events, and that doesn't really help because I cannot figure out what to attach the events to. I am told not to attach them to $(document), but that is really one of the only things that will be shared between the view and the layout.
I understand that global objects are not considered good in javascript, but at this point I'm not sure what other options I have to make sure things execute in the right order.
Update
The issue is more about "tooling" than it is about run time. It is true that when the actual view loads and runs, it is all pressed into one big happy page, and would work just fine. The issue is mostly that I have to split up the logic in the tooling (Visual Studio) to keep it from throwing errors and getting confused.
So I suppose it is more accurate to say it is a pseudo-problem.
I have attempted to split up the logic like this, but I think this is just another way of declaring a global object. I got the idea from the Q.js library.
Tasks.js
(function(definition) {
// assign the task system
tasks = definition();
})(function() {
var list = [];
function tasks() {
};
tasks.start = start;
tasks.enqueue = enqueue;
/*
* start the task queue.
*/
function start() {
// make sure to raise a started event for things that need
// to monitor it.
$(this).trigger("started");
};
function enqueue(f) {
// add the potential function to the queue to be processed later.
list.push(f);
$(this).trigger("enqueue", { item: f });
};
return tasks;
});
example usage
$(function(){
$(tasks).on("started", function(){
console.log("event called");
});
console.log("tasks", tasks);
tasks.start();
});
There are a number of ways you could go about this:
Use RequireJs to define Tasks as a module, then:
require(['tasks'], function(tasks){
$(tasks).on("started", function(){
console.log("event called");
});
console.log("tasks", tasks);
tasks.start();
});
Use a global object, but namespace it:
Ciel = Ciel || {};
Ciel.tasks = Ciel.tasks || function(){
var list = [];
...
};
Tie your data to a specific dom element:
<div class="ciel-tasks"></div>
...
$(function() { $('.ciel-tasks').each(function() {
var tasks = $(this);
...
});
It's not really clear what you're describing. From JavaScript's perspective there's no such thing as "_Layout" and "Inheriting View." There's only the resulting DOM delivered to the browser. Any JavaScript code within that DOM can operate on anything else in that DOM. So I'm not sure what any of this has to do with global namespace, events, $(document), etc. Perhaps you're overcomplicating the issue by assuming disparity between your views when, client side, no such disparity exists?
_Layout has additional scripts that need to run after the view's unique scripts.
This sounds like it's just a matter of providing callbacks for operations so that they internally execute in the correct order. For example, if the desired order is:
Layout executes initializeLayout()
View executes initializeView()
Layout executes completeLayout()
Then you can pass these to one another as callbacks and the functions can internally execute those callbacks. So in your Layout you might have something like this at the very top (such as in the header, as long as it's before the view is rendered):
<script type="text/javascript">
function initializeView(){} // placeholder for view-specific initialization
</script>
Then at the bottom with the rest of your scripts:
initializeLayout();
initializeView(completeLayout);
What this does is provide your views with an opportunity to overwrite that initializeView function. If the view defines its own function called initializeView then that one will be executed instead of the placeholder one defined in the layout (remembering that the layout and the view are all one page to JavaScript).
(This also assumes you've elsewhere defined a completeLayout function, since that's what you want to execute after the view is initialized.)
Then in your view you can define that overwriting function:
function initializeView(callback) {
// do some stuff...
if (typeof callback == 'function') {
callback();
}
}
That will execute your view initialization code and then when it's complete will invoke the callback which was provided by the layout, so the layout will then execute its post-view-initialization code. (Naturally, if any of this "initialization" code is asynchronous, you'll want to invoke callbacks in response to those asynchronous callbacks, etc.)
I'm trying to write a plugin that will select multiple elements and then apply some private methods to them (see code below). Then I also want to give the user the ability to trigger the activation of the plugin's methods manually with a .activate() function.
Here is my code :
MARKUP : https://github.com/simonwalsh/jquery.imagepox/blob/master/demo/index.html
JS : https://github.com/simonwalsh/jquery.imagepox/blob/master/dist/jquery.imagepox.js
Basically, when I select multiple items and then try to use the manual activation like so :
$(".pox-wrapper").imagepox({ // NOTE: selects two elements
manualActivation: true
});
var manual = $(".pox-wrapper").data('imagepox');
setTimeout(function(){
manual.activate();
}, 5000);
It will only apply the activate() method to the first element in the query...
This is my first jQuery plugin and I've been able to handle everything so far but I'm not sure about this one or even if it is the right way to effectively call a public method. I also tried using a custom event with an event listener in the plugin but it still only applies the methods on the first element in the page.
Thanks in advance :)
its not your plugin's fault. data does not work like that, it doesnt know how to return data from a collection of elements. Because think about it, each element in the collection contains its own data object!
So when you call data on a collection, it returns the data from the first one. The quick solution would be to change the innards of the setTimeout into a loop over all the elements in the set and call activate on them.
setTimeout(function(){
$(".pox-wrapper").each(function(){
$(this).data('imagepox').activate();
})
}, 5000);
It seems to me that you want to add functions to collections of jquery objects. This is the usecase of a jquery plugin. You can create a lightweight one like this:
$.fn.imagepox.activate = function(){ //do this after you create your plugin!
return this.each(function(){
var $this = $(this);
var data = $this.data('imagepox');
if(data){
data.activate();
}
});
};
now you can call it like this:
$(".pox-wrapper").imagepox.activate()
You can implement a custom delegateEvents() and undelegateEvents() in a Backbone view.
The Backbone.View constructor calls delegateEvents automatically. I thought that undelegateEvents was called when you remove the view with Backbone.View.prototype.remove, but it is not true.
So, which is the best way to do this manually? I have overridden the remove() view method with this code:
Backbone.View.prototype.remove = function() {
var remove = Backbone.View.prototype.remove;
if (this.undelegateEvents) {
this.undelegateEvents();
}
return remove.apply(this, arguments);
};
It works, but I don't know if is the best option. How should I do this?
As mu is too short suggested, the real answer to the question
which is the best way to do this manually?
is don't. Events are bound to DOM elements, and if those elements go away so do the bindings. undelegateEvents is designed to be used in situations where you aren't removing the DOM element, but still want to take the event bindings off of it (eg. when you set a new element for the view).
When my "chartModel" changes I want to update the "globalModel".
chartModel.bind("change", updateGlobalModel);
updateGlobalModel(){
globalModel.set(obj)
}
And vice versa, I want my chartModel to update when the globalModel changes.
globalModel.bind("change", updateChartModel);
updateChartModel(){
chartModel.set(obj)
}
This results in a feedback loop when setting the globalModel. I could prevent this by setting {silent:true}.
But here comes the problem. I have another Model that is dependent on the change event:
globalModel.bind("change", updateOtherModel);
How can I alert this model of the change but not the former one (to avoid the feedback loop)?
UPDATE:
For now, I decided to generate a specific ID for each set call:
set : function(attrs, options) {
if(!("setID" in attrs)){
attrs.setID = myApp.utils.uniqueID(); //newDate.getTime();
}
Backbone.Model.prototype.set.call(this, attrs, options);
},
This way, I can always generate a "setID" attribute from anywhere in my application. If the setID is still the same when fetching something from the model, I know there could be risk for a feedback loop.
Better late than never..
The easiest way to do this is by using a flag. For example, when setting something in globalModel, you could also change a property on the model to indicate that you've changed something. You can then verify the value of this flag in updateChartModel. For example:
chartModel.bind("change", updateGlobalModel);
function updateGlobalModel() {
if (!flag) {
globalModel.set(obj);
flag = true;
}
}
Probably very similar to what you've ended up doing with your setID. As an aside, underscore has a uniqueId function built in.
Another thing that you can do, which is much cleaner, is to pass an option with your sets calls.
chartModel.set(obj, { notify : false });
Yes, you can pass any options you want, you're not just limited to { silent : true }. See this discussion on github for more. Then, you check for the existence of this property where you handle change events like so:
function updateGlobalModel(model, options){
// explicitly check for false since it will otherwise be undefined and falsy
// you could reverse it.. but I find this simpler
if (options.notify !== false) {
globalModel.set(obj)
}
}
and in your third (and other models), you can just forego this check.
The final option is of course to look at your design. If these two models are so closely related that they must be kept in sync with each other, maybe it makes sense to merge their functionality. Alternatively, you could split the common functionality out. This all depends heavily on your particular situation.
My knowledge is limited, so maybe I shouldn't be answering, but I would try to pass a reference to chartModel when it's created that refers to the "other" model that you want updated. Then trigger an event on updateChartModel() and make sure your "other" model is bound on that event.
The question I have is: does the silent object mute all events? Or just model related ones? This obviously wouldn't work if all events are muted.