I wanted my Marionette views to close in an animated fashion and wait for the animation to finish before removing them from the DOM.
(Insights will be highly appreciated)
I've rewritten Marionette.View close method in the following way:
close: function(){
if (this.isClosed) { return; }
// allow the close to be stopped by returning `false`
// from the `onBeforeClose` method
var beforeCloseReturnValue = this.triggerMethod("before:close");
if (beforeCloseReturnValue === false){
return;
}
if (IsObjectNullOrUndefined(beforeCloseReturnValue))
{
this.closeTheView();
return;
}
if (beforeCloseReturnValue.promise)
{
var _this = this;
$.when(beforeCloseReturnValue).done(function(){
console.log("view close done!");
_this.closeTheView();
});
}
},
closeTheView: function(){
// mark as closed before doing the actual close, to
// prevent infinite loops within "close" event handlers
// that are trying to close other views
this.isClosed = true;
this.triggerMethod("close");
// unbind UI elements
this.unbindUIElements();
// remove the view from the DOM
this.remove();
}
And the view instance contains the following implementation for the onBeforeClose method:
onBeforeClose: function(){
_this = this;
return $.Deferred(function(){
_this.$el.fadeOut(2000, dfd.resolve);
}).promise();
}
Works quite well so far.
Wanted to hear if you have any thought about it.
Thanks.
I needed something like this and came up with more or less the same solution.
It works, but I don't like 2 things:
1) It's geared specially towards closing of views. A more general solution to animations would be nice.
2) If one uses CSS animations (by swapping classes) and they change the timing in css, then they need to manually update the promises (which can get messy on large applications).
I think the way AngularJS deals with animations works well and effectively deals with above 2 problems. But maybe it doesn't fit the Marionette/Backbone unopinionated style...
Related
I have found this solution from Aviv Ben-Yosef in his blog post:
angular.module('app').controller('TheCtrl', function($scope, NotifyingService) {
// ... stuff ...
NotifyingService.subscribe($scope, function somethingChanged() {
// Handle notification
});
});
angular.module('app').factory('NotifyingService', function($rootScope) {
return {
subscribe: function(scope, callback) {
var handler = $rootScope.$on('notifying-service-event', callback);
scope.$on('$destroy', handler);
},
notify: function() {
$rootScope.$emit('notifying-service-event');
}
};
});
I am thinking to do this: jsfiddle
angular.module('app').factory('NotifyingService', function() {
var handlers = {
list: []
};
return {
subscribe: function(callback) {
handlers.list.push(callback)
},
notify: function() {
handlers.list.forEach(function(handler) {
handler();
});
}
};
});
Edited: My goal is send changes to controllers in the same page. For example, there is a controller is doing something with the user and then it will notify the results to other controllers on the same page every time. I don't need to have the following abilities: unsubscribe the handlers, differentiate which handler to fire each time.
My solution seems very simple and I have not found anyone using it on internet, so I am feeling there should be something wrong with it (memory issues?).
Please point out the flaws in my solution (with resources if complicated. Ex. Memory). All I want to know is if this solution is okay to go with.
If my solution is good, please tell me, or compare pros and cons of both solutions.
Updated
Thanks to #Daniel Beck. He pointed out that my solution does need a way/function to remove handler once the controllers are out of scopes, otherwise memory leaks. I believe this is the major flaw in my solution.
Maybe I'm missing something obvious -- I'm sure someone will come along to point that out if so -- but it seems to me that the sole advantage to the first one (from the blog post) is that you don't have to remember to destroy the $on watcher when you're done with it. Its major disadvantage is that the service uses a single event name for all its $emits -- if you used this service in two different controllers, both would be triggered every time; there's no way to differentiate which handler you actually intended to trigger.
Your version shares that flaw, and additionally provides no way to remove unneeded handlers -- it appears to support only running each of a list of callbacks when its notify is called. Which is exactly what $rootScope.$emit does already.
I think you're both kinda reinventing the wheel, here.
I have a backbone view that contains project data for various research projects.
In this view, I have a button, that when clicked, it executes a method called 'toggleChildView'. This method inserts a child view into the main page.
In the child view, I'm listening for an event where the user clicks anywhere on the page except for the element that contains the research child review.
The problem is, if I close the child view, the child view is actually still hanging around somewhere because the event is still firing, and will fire multiple times if I had opened and closed the child view.
So for example, if I opened and closed the childview 5 times, after the final close, the event will still fire 5 times.
But it shouldn't fire at all if closed, and only once when opened.
I think my question would best be phrased like this:
Is there anyway to get rid of "orphaned" child views and ensure that only one child view is open at a time?
thanks!
Parent View:
toggleChildView: function (e) {
this.$('.displayresearchdata').toggle();
this.$('.editdata').toggle();
//initialize the research view into our main view
var researchView = new researchView({ parent: this });
self.$('.research').append(researchView.render().$el);
},
saveresearchdata: function (e) {
this.model.save('researchData', this.$(".researchData").html());
},
Child render method:
initialize: function (options) {
this.parent = options.parent;
},
render: function () {
var self = this;
this.$el.append(this.template());
$("body").on("click", function (event) {
if (!$(event.target).closest('.editdata').length) {
if (self.parent.$('.editdata').is(":visible")) {
self.parent.saveresearchdata();
}
}
});
return this;
},
As #mu is too short points out, you need to explictly remove() any view you add.
If that view adds some custom event listeners, you should remove them too. If you use the view.listenTo(target, "eventname", this.functionName) flavor of event listening, those event handlers will be removed automatically when you call view.remove() because of the stopListening() method being called.
In your code, the problem is that you're not keeping a reference to the child view(s) you're adding, so you can't call remove on it. Keep a internal reference from the parent to the child like:
//initialize the research view into our main view
if(this._researchView) {
this._researchView.remove();
}
this._researchView = new researchView(...)
this.$(".research").empty().append(this._researchView.render().$el);
Take note of the use of empty before appending, if you don't want to have many researchViews added, only one at a time. If you indeed want many views, then you can remove that, and keep the internal reference as an array.
Handling so-called 'zombie' views is one of the trickiest parts of using Backbone, and if you have a lot of sub-views, it can become a real problem if you do not manage the views correctly. The seminal post on the subject is this one by Derrik Bailey although note that some of the methods he references, such as bind are now deprecated in favor of listenTo
#CharlieBrown's answer will do trick. However, if you plan on creating other views and/or subviews, here's one way you can set things up on a larger scale:
1) Create a Baseview, from which all other views will be extended.
var BaseView = Backbone.View.extend({
//typical initialize and render functions
//...
//Create the close method
close: function () {
if (this.onClose) this.onClose(); //looks for an onClose method in your subviews
this.undelegateEvents();
this.$el.off();
this.$el.children().remove();
this.$el.empty();
this.stopListening();
}
});
2) Now in your Backbone router you can create a trackView function which will call the the close() method from the base view
//within router
trackView: function (next) {
console.log('now closing ' + next.cid);
if (this.current) this.current.close();
this.current = next;
},
3) All other views in the router should now be called from within trackview like this:
//within router
someView: function () {
console.log('loading create user page');
this.trackView(new someView()); //use trackView from step 2
},
4) Finally, within any sub-views, be sure to add an 'onClose()' method where you can close down any potential zombies using the close method inherited from the Baseview:
//inside a view
onClose: function() {
if (this.couldBeAZombieView) this.couldBeAZombieView.close();
}
Now you are setup for a more complex site. There are other ways to set this up, but this is the one I'm familiar with.
I have a webapp written in AngularJS which basically polls an API to two endpoints. So, every minute it polls to see if there is anything new.
I discovered that it has a small memory leak and I've done my best to find it but I'm not able to do it. In the process I've managed to reduce the memory usage of my app, which is great.
Without doing anything else, every poll you can see a spike in the memory usage (that's normal) and then it should drop, but it's always increasing. I've changed the cleaning of the arrays from [] to array.length = 0 and I think I'm sure that references don't persist so it shouldn't be retaining any of this.
I've also tried this: https://github.com/angular/angular.js/issues/1522
But without any luck...
So, this is a comparison between two heaps:
Most of the leak seems to come from (array) which, if I open, are the arrays returned by the parsing of the API call but I'm sure they're not being stored:
This is basically the structure:
poll: function(service) {
var self = this;
log('Polling for %s', service);
this[service].get().then(function(response) {
if (!response) {
return;
}
var interval = response.headers ? (parseInt(response.headers('X-Poll-Interval'), 10) || 60) : 60;
services[service].timeout = setTimeout(function(){
$rootScope.$apply(function(){
self.poll(service);
});
}, interval * 1000);
services[service].lastRead = new Date();
$rootScope.$broadcast('api.'+service, response.data);
});
}
Basically, let's say I have a sellings service so, that would be the value of the service variable.
Then, in the main view:
$scope.$on('api.sellings', function(event, data) {
$scope.sellings.length = 0;
$scope.sellings = data;
});
And the view does have an ngRepeat which renders this as needed. I spent a lot of time trying to figure this out by myself and I couldn't. I know this is a hard issue but, do anyone have any idea on how to track this down?
Edit 1 - Adding Promise showcase:
This is makeRequest which is the function used by the two services:
return $http(options).then(function(response) {
if (response.data.message) {
log('api.error', response.data);
}
if (response.data.message == 'Server Error') {
return $q.reject();
}
if (response.data.message == 'Bad credentials' || response.data.message == 'Maximum number of login attempts exceeded') {
$rootScope.$broadcast('api.unauthorized');
return $q.reject();
}
return response;
}, function(response) {
if (response.status == 401 || response.status == 403) {
$rootScope.$broadcast('api.unauthorized');
}
});
If I comment out the $scope.$on('api.sellings') part, the leakage still exists but drops to 1%.
PS: I'm using latest Angular version to date
Edit 2 - Opening (array) tree in an image
It's everything like that so it's quite useless imho :(
Also, here are 4 heap reports so you can play yourself:
https://www.dropbox.com/s/ys3fxyewgdanw5c/Heap.zip
Edit 3 - In response to #zeroflagL
Editing the directive, didn't have any impact on the leak although the closure part seems to be better since it's not showing jQuery cache things?
The directive now looks like this
var destroy = function(){
if (cls){
stopObserving();
cls.destroy();
cls = null;
}
};
el.on('$destroy', destroy);
scope.$on('$destroy', destroy);
To me, it seems that what's happening is on the (array) part. There is also 3 new heaps in between pollings.
And the answer is cache.
I don't know what it is, but this thing grows. It seems to be related to jQuery. Maybe it's the jQuery element cache. Do you by any chance apply a jQuery plugin on one or more elements after every service call?
Update
The problem is that HTML elements are added, processed with jQuery (e.g. via the popbox plugin), but either never removed at all or not removed with jQuery. To process in this case means stuff like adding event handlers. The entries in the cache object (whatever it is for) do only get removed if jQuery knows that the elements have been removed. That is the elements have to be removed with jQuery.
Update 2
It's not quite clear why these entries in the cache haven't been removed, as angular is supposed to use jQuery, when it's included. But they have been added through the plugin mentioned in the comments and contained event handlers and data. AFAIK Antonio has changed the plugin code to unbind the event handlers and remove the data in the plugin's destroy() method. That eventually removed the memory leak.
The standard browser way to fix memory leaks is to refresh the page. And JavaScript garbage collection is kind of lazy, likely banking on this. And since Angular is typically a SPA, the browser never gets a chance to refresh.
But we have 1 thing to our advantage: Javascript is primarily a top-down hierarchial language. Instead of searching for memory leaks from the bottom up, we may be able to clear them from the top down.
Therefore I came up with this solution, which works, but may or may not be 100% effective depending on your app.
The Home Page
The typical Angular app home page consists of some Controller and ng-view. Like this:
<div ng-controller="MainController as vm">
<div id="main-content-app" ng-view></div>
</div>
The Controller
Then to "refresh" the app in the controller, which would be MainController from the code above, we redundantly call jQuery's .empty() and Angular's .empty() just to make sure that any cross-library references are cleared.
function refreshApp() {
var host = document.getElementById('main-content-app');
if(host) {
var mainDiv = $("#main-content-app");
mainDiv.empty();
angular.element(host).empty();
}
}
and to call the above before routing begins, simulating a page refresh:
$rootScope.$on('$routeChangeStart',
function (event, next, current) {
refreshApp();
}
);
Result
This is kind of a hacky method for "refreshing the browser type behavior", clearing the DOM and hopefully any leaks. Hope it helps.
I have been looking at some code for a while now and can't decide on the best practice to apply in this situation:
Let's say that we have a view that has n-subviews. I have come across two practices for initializing them
1 Inside intitialize
initialize: function() {
this.subViews = [];
this.subViewModelCollection.each(function(model) {
var view = new SubView({model: model});
this.subViews.push(view);
this.$el.append(view.el);
}, this);
},
render: function() {
_.invoke(this.subViews, 'render');
}
2 Inside render
initialize: function() {
... // render handles the subviews
},
render: function() {
this.subViews = [];
this.subViewModelCollection.each(function(model) {
var view = new SubView({model: model}).render(); // render optional
this.subViews.push(view);
this.$el.append(view.el);
}, this);
}
now these are just crude samples, but they demonstrate my dilemma. Should the initialize or the render -function be responsible for initializing the subviews? Personally I have been a strong advocate for the latter version, but some code I saw made me sway towards the former.
So, WHERE do you initialize your subviews, WHY do you do it there and WHY is it better than the other option?
You should maximize the amount of work you do in initialize, as that will only be done once, and minimize the amount of work you do in your render function which may typically be called many times (for instance in a single page application or responsive webpage).
If you know your render method for your subviews will never change the generated html, then you can probably also call the render method for the subviews in the initialize method as well, and simply appending the rendered element in the "main view".
If the render methods for both your subviews and main view is only called once anyway (as part of loading a page or whatever) it probably does not matter how you do it, but generally minimizing the amount of work to be done in the render functions is probably good advice.
I would put the actual sub view rendering functionality into a new function completely, say renderSubViews().
This will allow for greater flexibility.
You can call this function from initialize or render, and change it easily as your application grows/changes.
You can bind this new function to events, i.e. the reset event for the view.
This is more logical. Does it make sense to auto-render sub-views when you initialize or render the main view? Should one be directly tied to the other?
I'm just getting started with Backbone. I went through the first two PeepCode screencasts which were great and now I'm digging in on a quick detached (no server side) mock-up of a future app.
Here's what I'm looking to build (roughly). A series of five text boxes - lets call these Widgets. Each Widget input, when selected, will display a pane that shows Tasks associated with the Widget and allow the user to create a new Task or destroy existing Tasks.
At this point, I'm thinking I have the following models:
Widget
Task
The following collections:
Tasks
Widgets
The following views (this is where it gets hairy!)
WidgetListView
- Presents a collection of Widgets
WidgetView
- sub-view of WidgetListView to render a specific Widget
TaskPaneView
- Presented when the user selects a Widget input
TaskCreateView
- Ability to create a new Task associated with selected Widget
TaskListView
- Presents a collection of Tasks for the given widget
TaskView
- Displays Task detail - sub-view of TaskListView
Assuming that's reasonable, the trick becomes how to display a TaskPaneView when a WidgetView is selected. And futhermore, how that TaskPaneView should in turn render TaskCreateViews and TaskListViews.
The real question here is: Does one cascade render events across Views? Is it permissible for a Root view to know of sub-views and render them explicitly? Should this be event-driven?
Apologies if this is an open-ended question, just hoping someone will have seen something similar before and be able to point me in the right direction.
Thanks!
Definitely make it event driven. Also, try not to create views that are closely coupled. Loose coupling will make your code more maintainable as well as flexible.
Check out this post on the event aggregator model and backbone:
http://lostechies.com/derickbailey/2011/07/19/references-routing-and-the-event-aggregator-coordinating-views-in-backbone-js/
The short version is you can do this:
var vent = _.extend({}, Backbone.Events);
and use vent.trigger and vent.bind to control your app.
Pre p.s.: I have made a gist for you with the code that I wrote below:
https://gist.github.com/2863979
I agree with the pub/sub ('Observer pattern') that is suggested by the other answers. I would however also use the power of Require.js along with Backbone.
Addy Osmani has written a few GREAT! resources about Javascript design patterns and about building Backbone applications:
http://addyosmani.com/resources/essentialjsdesignpatterns/book/
http://addyosmani.com/writing-modular-js/
http://addyosmani.github.com/backbone-fundamentals/
The cool thing about using AMD (implemented in Require.js) along with Backbone is that you solve a few problems that you'd normally have.
Normally you will define classes and store these in some sort of namespaced way, e.g.:
MyApp.controllers.Tasks = Backbone.Controller.extend({})
This is fine, as long as you define most things in one file, when you start adding more and more different files to the mix it gets less robust and you have to start paying attention to how you load in different files, controllers\Tasks.js after models\Task.js etc. You could of course compile all the files in proper order, etc, but it is far from perfect.
On top of this, the problem with the non AMD way is that you have to nest Views inside of each other more tightly. Lets say:
MyApp.classes.views.TaskList = Backbone.View.extend({
// do stuff
});
MyApp.views.App = Backbone.View.extend({
el: '#app',
initialize: function(){
_.bindAll(this, 'render');
this.task_list = new MyApp.classes.views.TaskList();
},
render: function(){
this.task_list.render();
}
});
window.app = new MyApp.views.App();
All good and well, but this can become a nightmare.
With AMD you can define a module and give it a few dependencies, if you are interested in how this works read the above links, but the above example would look like this:
// file: views/TaskList.js
define([], function(){
var TaskList = Backbone.View.extend({
//do stuff
});
return new TaskList();
});
// file: views/App.js
define(['views/TaskList'], function(TaskListView){
var App = Backbone.View.extend({
el: '#app',
initialize: function(){
_.bindAll(this, 'render');
},
render: function(){
TaskListView.render();
}
});
return new App();
});
// called in index.html
Require(['views/App'], function(AppView){
window.app = AppView;
});
Notice that in the case of views you'd return instances, I do this for collections too, but for models I'd return classes:
// file: models/Task.js
define([], function(){
var Task = Backbone.Model.extend({
//do stuff
});
return Task;
});
This may seem a bit much at first, and people may think 'wow this is overkill'. But the true power becomes clear when you have to use the same objects in many different modules, for example collections:
// file: models/Task.js
define([], function(){
var Task = Backbone.Model.extend({
//do stuff
});
return Task;
});
// file: collections/Tasks.js
define(['models/Task'], function(TaskModel){
var Tasks = Backbone.Collection.extend({
model: TaskModel
});
return new Tasks();
});
// file: views/TaskList.js
define(['collections/Tasks'], function(Tasks){
var TaskList = Backbone.View.extend({
render: function(){
_.each(Tasks.models, function(task, index){
// do something with each task
});
}
});
return new TaskList();
});
// file: views/statistics.js
define(['collections/Tasks'], function(Tasks){
var TaskStats = Backbone.View.extend({
el: document.createElement('div'),
// Note that you'd have this function in your collection normally (demo)
getStats: function(){
totals = {
all: Tasks.models.length
done: _.filter(Tasks, function(task){ return task.get('done'); });
};
return totals;
},
render: function(){
var stats = this.getStats();
// do something in a view with the stats.
}
});
return new TaskStats();
});
Note that the 'Tasks' object is exactly the same in both views, so the same models, state, etc. This is a lot nicer than having to create instances of the Tasks collection at one point and then reference it through the whole application all the time.
At least for me using Require.js with Backbone has taken away a gigantic piece of the puzzling with where to instantiate what. Using modules for this is very very helpful. I hope this is applicable to your question as well.
p.s. please also note that you'd include Backbone, Underscore and jQuery as modules to your app too, although you don't have to, you can just load them in using the normal script tags.
For something with this kind of complexity, I might recommend using Backbone Aura, which has not yet had a stable release version. Aura essentially allows you to have multiple fully self-contained Backbone apps, called "widgets," running on a single page, which might help disentangle and smooth over some of your model/view logic.
From a classical MVC perspective, your views respond to changes in their associated models.
//initialize for view
initialize : function() {
this.model.on("change", this.render(), this);
}
The idea here is that anytime a view's model is changed, it'll render itself.
Alternatively or additionally, if you change something on a view, you can trigger an event that the controller listens to. If the controller also created the other models, it can modify them in some meaningful way, then if you're listening for changes to the models the views will change as well.
Similar answer to Chris Biscardi's. Here's what I have:
You create a global var Dispatcher (doesn't have to be global as long as it can be accessed from the scope of Backbone app):
Dispatcher = _.extend({}, Backbone.Events);
Dispatcher will help you execute subscribed callbacks with events that are not particularly triggered by changes in models or collections. And cool thing is that Dispatcher can execute any function, inside or outside Backbone app.
You subscribe to events using bind() in a view or any part of the app:
Dispatcher.bind('editor_keypress', this.validate_summary);
Then in another view or part of the app you trigger new event using trigger():
Dispatcher.trigger('redactor_keypress');
The beauty of using a dispatcher is its simplicity and ability to subscribe multiple listeners (e.g. callbacks in different Backbone views) to the same event.