Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(components) $dispose not always called #306

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 30 additions & 17 deletions hsp/rt/$root.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@ var CPT_TYPES={
var DOCUMENT_FRAGMENT_NODE = 11;

/**
* Root node - created at the root of each template
* Contains the listeners requested by the child nodes
* Root node - created at the root of each template
* Contains the listeners requested by the child nodes
* Is replaced by the $CptNode (child class) when the template is inserted in another template
*/
var $RootNode = klass({
Expand All @@ -51,7 +51,7 @@ var $RootNode = klass({
* @param {Map} ctlInitAtts the init value of the controller attributes (optional) - e.g.
* {value:'123',mandatory:true}
*/
$constructor : function (vscope, nodedefs, argnames, ctlWrapper, ctlInitAtts) {
$constructor : function(vscope, nodedefs, argnames, ctlWrapper, ctlInitAtts) {
if (this.isInsertNode) {
TNode.$constructor.call(this, this.exps);
} else {
Expand Down Expand Up @@ -125,12 +125,16 @@ var $RootNode = klass({
o.$dispose();
}
this.propObs=null;
this.$disposeCtlWrapper();
TNode.$dispose.call(this);
},

$disposeCtlWrapper: function() {
if (this.ctlWrapper) {
this.ctlWrapper.$dispose();
this.ctlWrapper = null;
this.controller = null;
}
TNode.$dispose.call(this);
},

/**
Expand Down Expand Up @@ -318,6 +322,7 @@ var getObject = exports.getObject = function (path, scope) {
o = o[path[i]];
}
}

return o;
};

Expand All @@ -338,7 +343,7 @@ var $CptNode = klass({
* expression index associated to the event hanlder callback
* @param {Array} children list of child node generators - correponding to pseudo components and attribute content
*/
$constructor : function (tplPath, exps, attcfg, ehcfg, children) {
$constructor : function(tplPath, exps, attcfg, ehcfg, children) {
this.pathInfo=tplPath.slice(1).join("."); // debugging info
this.info="[Component: #"+this.pathInfo+"]"; // debug info
this.isCptNode = true;
Expand Down Expand Up @@ -411,7 +416,7 @@ var $CptNode = klass({
ni.node2=node2;

if (p.cptType==="$CptAttInsert") {
// this cpt is used to an insert another component passed as attribute
// this cpt is used to an insert another component passed as attribute
ni.initCpt(po);
} else {
// we are in a template or component cpt
Expand All @@ -425,20 +430,20 @@ var $CptNode = klass({
// create an element to avoid generating other errors
ni=this.createCptInstance("$CptAttInsert",parent);
}

return ni;
},

/**
* Calculates the object referenced by the path and the component type
* @return {Object} object with the following properties:
* pathObject: {Object} the object referenced by the path
* cptType: {String} one of the following option: "$CptComponent",
* cptType: {String} one of the following option: "$CptComponent",
* "$CptTemplate", "$CptAttInsert" or "InvalidComponent"
*/
getPathData:function(path, vscope) {
// determine the type of this component:
// - either a template - e.g. <#mytemplate foo="bar"/>
// determine the type of this component:
// - either a template - e.g. <#mytemplate foo="bar"/>
// -> instance will extend $CptTemplate
// - a component with controller - e.g. <#mycpt foo="bar"/>
// -> instance will extend $CptComponent
Expand Down Expand Up @@ -513,7 +518,7 @@ var $CptNode = klass({
nd.appendChild(this.node2);
}
},

/**
* Callback called when a controller attribute or a template attribute has changed
*/
Expand Down Expand Up @@ -599,12 +604,21 @@ var $CptNode = klass({

if (tplChanged) {
// check if component nature changed from template to component or opposite

// Change might also be a different component ref, ie cpt1 -> cpt2
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is not enough as apparently the $dispose method is not called on the template root node (cf. $root.js line 120). So we should keep a reference on the template root node (which is returned when the template function is called) and call $dispose() on this reference. This should both dispose the node chain and the controller
(sorry I just realize this now!)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is somehow valid, but could be done in another PR.

Nevertheless, the $RootNode.$dispose method as far as I get it is only called when we don't want to keep localProperties on the node itself.

/**
 * Default dispose method
 * @param {Boolean} localPropOnly if true only local properties will be deleted (optional)
 *        must be used when a new instance is created to adapt to a path change
 */
$dispose:function(localPropOnly) {
    this.cleanObjectProperties(localPropOnly);
},

/**
 * Removes object properties - helper for $dispose methods
 * @param {Boolean} localPropOnly if true only local properties will be deleted (optional)
 *        must be used when a new instance is created to adapt to a path change
 */
cleanObjectProperties : function (localPropOnly) {
    this.removePathObservers();
    if (this._scopeChgeCb) {
        json.unobserve(this.vscope, this._scopeChgeCb);
        this._scopeChgeCb = null;
    }
    if (localPropOnly!==true) {
        $RootNode.$dispose.call(this);
    }
    this.exps = null;
    this.controller = null;
    this.ctlAttributes = null;
    this.template = null;
    if (this.node1) {
        this.node1=null;
        this.node2=null;
    }
}

// let's dispose anything related to previous template
if (this.template.$dispose) {
this.template.$dispose(true);
} else {
// this.template might be only a reference to template closure function, not what it returns
this.$disposeCtlWrapper();
}
this.template=tpl;
this.createChildNodeInstances();
} else {
if (this.refreshAttributes) {
this.refreshAttributes();
// for component and sub-templates the original vscope is substituted
// for component and sub-templates the original vscope is substituted
// to the one of the component- or sub-template
// so we need to revert to the parent scope to observe the correct objects
var vs=this.vscope;
Expand Down Expand Up @@ -668,7 +682,7 @@ var $CptNode = klass({
var sz=pos.length;

this._pathChgeCb = this.onPathChange.bind(this);

for (var i=0;sz>i;i++) {
json.observe(pos[i], this._pathChgeCb);
}
Expand Down Expand Up @@ -747,9 +761,9 @@ var $CptAttElement = klass({
isCptAttElement : true,

/**
* $CptAttElement generator
* $CptAttElement generator
*/
$constructor : function (name, exps, attcfg, ehcfg, children) {
$constructor : function(name, exps, attcfg, ehcfg, children) {
this.name = name;
this.info = "[Component attribute element: @"+this.name+"]";
this.tagName = "@"+name;
Expand Down Expand Up @@ -789,7 +803,7 @@ var $CptAttElement = klass({
if (p.ctlAttributes) {
attDef=p.ctlAttributes[this.name];
}

if (!eltDef && !attDef) {
// invalid elt
log.error(this.info+" Element not supported by its parent component");
Expand Down Expand Up @@ -846,4 +860,3 @@ cptComponent.setDependency("$CptAttElement",$CptAttElement);
exports.$RootNode = $RootNode;
exports.$CptNode = $CptNode;
exports.$CptAttElement = $CptAttElement;

8 changes: 2 additions & 6 deletions hsp/rt/cptcomponent.js
Original file line number Diff line number Diff line change
Expand Up @@ -138,11 +138,7 @@ exports.$CptComponent = {
* must be used when a new instance is created to adapt to a path change
*/
$dispose:function(localPropOnly) {
if (this.ctlWrapper) {
this.ctlWrapper.$dispose();
this.ctlWrapper=null;
this.controller=null;
}
this.$disposeCtlWrapper();
this.ctlAttributes=null;
this.cleanObjectProperties(localPropOnly);
this.ctlConstuctor=null;
Expand Down Expand Up @@ -219,7 +215,7 @@ exports.$CptComponent = {
// nm is a template attribute passed as text attribute
if (this.tplAttributes && this.tplAttributes[nm]) {
// already defined: raise an error

log.error(this+" Component attribute '" + nm + "' is defined multiple times - please check");
} else {
// create new tpl Attribute Text Node and add it to the tplAttributes collection
Expand Down
99 changes: 99 additions & 0 deletions test/rt/cptlifecyle.spec.hsp
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
<script>
/*
* Copyright 2013 Amadeus s.a.s.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

var klass=require("hsp/klass"),
ht=require("hsp/utils/hashtester");

var constructor_counter = 0;
var dispose_counter = 0;

var LifeCycleController = klass({
$constructor: function() {
constructor_counter++;
},
$dispose: function() {
dispose_counter++;
}
});
</script>

<template lifecycle1 using ctrl:LifeCycleController>
<div class="content">I am component 1</div>
</template>

<template lifecycle2 using ctrl:LifeCycleController>
<div class="content">I am component 2</div>
</template>

<template wrapper(data)>
{if data.visible}
<#lifecycle1 />
{/if}
</template>

<template dynamicWrapper(data)>
<#data.cpt />
</template>

<script>
var data = {
visible: true,
cpt: lifecycle1
};
</script>

<script>
describe("Component lifecycle", function () {

it("should invoke $contructor and $dispose anytime a new instance is needed", function() {
var h=ht.newTestContext();
constructor_counter = dispose_counter = 0;

wrapper(data).render(h.container);
expect(h(".content").text()).to.equal('I am component 1');
expect(constructor_counter).to.equal(1);
h.$set(data, "visible", false);
h.$set(data, "visible", true);
expect(constructor_counter).to.equal(2);

expect(dispose_counter).to.equal(1);

h.$dispose();
});
});

describe("Component used as dynamic <#ref >", function() {

it("should invoke $constructor at instance creation and $dispose when not needed anymore", function() {
var h=ht.newTestContext();
constructor_counter = dispose_counter = 0;

dynamicWrapper(data).render(h.container);
expect(h(".content").text()).to.equal('I am component 1');
expect(constructor_counter).to.equal(1);

h.$set(data, "cpt", lifecycle2);
expect(h(".content").text()).to.equal('I am component 2');
expect(constructor_counter).to.equal(2);

h.$set(data, "cpt", lifecycle1);
expect(dispose_counter).to.equal(2);

h.$dispose();
});

});
</script>