Widgets within Widgets
At the day-job we do a lot of Widget work. We have Container widgets that hold Panel widgets, which hold Box widgets, which hold other widgets. The widgets themselves create other widgets and place them in their own ownership. Our full-page/no-refresh/Ajax-app with the long lived page views creates and destroys all these widgets based on the various events published around the page, but we ran into a problem along the way: a lot of the widgets weren't being destroyed. Ever.
The whole system is quite sound, though lacked in this one regard. To be fair, Dijit cleans up after itself. Everything that is created when a Dijit widget instance is new'd up is removed when that instance is destroyed. Everything that is created declaratively in a template is cleaned up automatically for you when a widget is destroyed. The problem only exists when one widget creates another widget programatically, either for permanent placement or temporary use. Let me illustrate with a basic Widget example:
// file is my/Thinger.js dojo.provide("my.Thinger"); dojo.require("dijit._Widget"); dojo.require("dijit._Templated"); dojo.declare("my.Thinger", [dijit._Widget, dijit._Templated], { // the listItem in the template is "tracked" and cleaned widgetsInTemplate:true, templateString:" <div> <ul><" + "li dojoType='my.ListItem' dojoAttachPoint="li">${foo}</" + "li></ul> </div> ", postCreate: function(){ // some self-cleaning event connections. These are disconnected upon destruction. this.connect(this.domNode, "onclick", "_clicker"); // this item is leaked when Thinger is destroyed. new my.ListItem().placeAt(this.li, "after"); } });
Here, we're creating new my.ListItem instances and adding them to our own DOM, but not keeping track of them in any way. This is an exceptionally common pattern of code compartmentalization. The my.Thinger scopes all DOM references and event connections to itself. It contains other widgets which handle some part of the overall functionality. By making a whole ListItem Class, we compartmentalize that bit of logic into little pieces.
We can see the leak pretty easily:
console.log(dijit.registry.length); // 0 var x = new my.Thinger().placeAt(dojo.body()); console.log(dijit.registry.length); // 3, A Thinger, and Two ListItem instances x.destroy(); console.log(dijit.registry.length); // 1. A ListItem
So I'll show my solution to this little dilemma in hopes you can adopt a similar method to suite your own needs. My solution requires imposing a style guideline on how new widgets are bound and added to other widgets, but I feel it to be well worth the extra efforts.
The first thing we do, because all Dijit widgets clean up properly after themselves already, is make our own Base Widget class, extending dijit._Widget. Here we'll add a few functions to add new APIs to the lifecycle so we can track our own widgets.
dojo.provide("my.Widget"); dojo.require("dijit._Widget"); dojo.delcare("my.Widget", [ dijit._Widget, dijit._Templated ], { // summary: Our custom dijit._Widget base class. All our widgets should inherit from this. adopt: function(cls, props, srcNode){ // summary: Take ownership of a new widget // returns: The added widget }, orphan: function(widget, destroy){ // summary: relinquish ownership of an owned widget. optionally destroy. // returns: nothing. }, destroy: function(preserveDom){ // summary: an overridden destroy method. call the parent method, but we'll add stuff here too. this.inherited(arguments); }, _kill: function(widget){ // summary: destroy properly this widget instance // returns: nothing. }, _addItem: function(/* Widget... */){ // summary: Add any number of widgets to this instance // returns: nothing. } });
So we've stubbed out the API with two new public functions, a single overridden destroy method, and two private helper functions. _addItem will do most of the work, so we'll show it first:
_addItem: function(/* Widget... */){
this._addedItems = this._addedItems || [];
this._addedItems.push.apply(this._addedItems, arguments);
},
The code will create or reuse an instance member (array) called _addedItems. This contains the list of widgets we've created programatically. It accepts any number of arguments, so you can add multiple items in the same line of code.
this._addItem(
new dijit.form.Button().placeAt(this.fooNode),
new dijit.form.Button().placeAt(this.barNode)
);
When the widget calling addItem is destroyed, the two anonymously created Button instances will go with it. Well, not quite yet, but we only have to add a line to our destroy method to handle that action. We'll also add the code for the _kill function, which we'll reuse in orphan in a moment. _kill is a simple function which will destroy a single widget instance properly. In destroy we call _kill by passing it to the forEach call:
destroy: function(){
dojo.forEach(this._addedItems, this._kill);
this.inherited(arguments);
},
_kill: function(widget){
if(widget.destroyRecursive){
widget.destroyRecursive();
}else if(widget.destroy){
widget.destroy();
}
}
So we now have a way to add tracked widgets to ourself and to destroy them cleanly when we go away. Admittedly, I don't like the API for _addItem ... Not only is it _private, I still have a lot of boilerplate stuff repeated, now being wrapped with _addItem(). This is where the adopt API. It takes the repetition down one notch, and wraps the creation and adding into a single function call:
adopt: function(cls, props, srcNode){
var x = new cls(pros, srcNode);
this._addItem(x);
return x;
}
All Dijit _Widget derivatives accept two constructor arguments: an object hash of properties, and an optional source-node reference (from which to create the instance). We simply relay those two arguments into a single 'new' call, add the item to ourselves, and returning the widget instance otherwise unmodified. To use it is simple:
postCreate: function(){
this.adopt(my.ListItem).placeAt(this.li, "before");
}
The ListItem in this example doesn't need any constructor args, so they are omitted. The return from adopt is the widget instance, so you can chain placeAt calls on it, or save the return value to reuse in other scoped functions. Here's a small example of everything working "together":
postCreate: function(){
this.dialog = this.adopt(my.Dialog, { onClick: dojo.hitch(this, "_click" )});
setTimeout(dojo.hitch(this.dialog, "open"), 2000);
this.connect(this.destroyButton, "onclick", "_close");
},
_click: function(e){
e && e.preventDefault();
this.close();
},
_close: function(){
this.dialog && this.dialog.close();
}
The last function is wildy useful, but not as mind-blowing as having tracked programmatic instances. In the above example we tracked an instance of my.Dialog locally as this.dialog. We're checking the presence of this.dialog before calling close() on it. Sometimes we only need a widget for a short while ... where we want to destroy it upon closing, or destroy a single reused variable holding a widget. That is what orphan is for:
orphan: function(widget, destroy){
this._addedItems = this._addedItems || [];
var i = dojo.indexOf(this._addedItems, widget);
if(i >= 0) this._addedItems.splice(i, 1);
destroy && this._kill(widget);
}
The code loaded the local list of added children, searches for a passed reference, and removes it from the tracking list. Optionally, you can destroy the widget while being orphaned by passing any truthy value in the second argument. Now, the code pattern which looks like:
_click: function(){
this.dialog && this.dialog.destroy();
this.dialog = new my.Dialog();
}
can be reduced to:
_click: function(){
this.dialog && this.orphan(this.dialog, true);
this.dialog = this.adopt(my.Dialog);
}
The latter example is slightly more verbose, but it has the benefit of not leaking any widgets. Memory costs more than keystrokes methinks.
The full, documented code is as follows. Feel free to use it in your own app: the migration is painless:
dojo.declare("my.Widget", [ dijit._Widget, dijit._Templated ], { // summary: // The Foundation widget for our things. Includes _Widget and _Templated with some custom addin methods. adopt: function(/* Function */cls, /* Object? */props, /* DomNode */node){ // summary: Instantiate some new item from a passed Class, with props with an optional srcNode (node) // reference. Also tracks this widget as if it were a child to be destroyed when this parent widget // is removed. // // cls: Function // The class to instantiate. Cannot be a string. Use dojo.getObject to get a full class object if you // must. // // props: Object? // An optional object mixed into the constructor of said cls. // // node: DomNode? // An optional srcNodeRef to use with dijit._Widget. This thinger will be instantiated using // this passed node as the target if passed. Otherwise a new node is created and you must placeAt() your // instance somewhere in the dom for it to be useful. // // example: // | this.adopt(my.ui.Button, { onClick: function(){} }).placeAt(this.domNode); // // example: // | var x = this.adopt(my.ui.Button).placeAt(this.domNode); // | x.connect(this.domNode, "onclick", "fooBar"); // // example: // If you *must* new up a thinger and only want to adopt it once, use _addItem instead: // | var t; // | if(4 > 5){ t = new my.ui.Button(); }else{ t = new my.ui.OtherButton() } // | this._addItem(t); var x = new cls(props, node); this._addItem(x); return x; // my.Widget }, _addItem: function(/* dijit._Widget... */){ // summary: Add any number of programatically created children to this instance for later cleanup. // private, use `adopt` directly. this._addedItems = this._addedItems || []; this._addedItems.push.apply(this._addedItems, arguments); }, orphan: function(/* dijit._Widget */widget, /* Boolean? */destroy){ // summary: remove a single item from this instance when we destroy it. It is the parent widget's job // to properly destroy an orphaned child. // // widget: // A widget reference to remove from this parent. // // destroy: // An optional boolean used to force immediate destruction of the child. Pass any truthy value here // and the child will be orphaned and killed. // // example: // Clear out all the children in an array, but do not destroy them. // | dojo.forEach(this._thumbs, this.orphan, this); // // example: // Create and destroy a button cleanly: // | var x = this.adopt(my.ui.Button, {}); // | this.orphan(x, true); // this._addedItems = this._addedItems || []; var i = dojo.indexOf(this._addedItems, widget); if(i >= 0) this._addedItems.splice(i, 1); destroy && this._kill(widget); }, _kill: function(w){ // summary: Private helper function to properly destroy a widget instance. if(w && w.destroyRecursive){ w.destroyRecursive(); }else if(w && w.destroy){ w.destroy() } }, destroy: function(){ // summary: override the default destroy function to account for programatically added children. dojo.forEach(this._addedItems, this._kill) this.inherited(arguments); } });
Does anyone have any alternate solutions they've found to combat this problem?
