Skip to content

Commit

Permalink
Automatically manage binding state such that elements can be garbage …
Browse files Browse the repository at this point in the history
…collected normally.

 - when elements are inserted and removed normally, they will be cleaned up automatically.
 - when an element is not inserted into the dom, you must call cancelUnbindAll() on it for bindings to remain active and subsequently must call unbindAll to dispose of it.
 - WARNING: insertedCallback changed to inserted; removedCallback changed to removed; attributeChangedCallback changed to attributeChanged.
  • Loading branch information
sorvell committed Jun 12, 2013
1 parent 26e8b14 commit 5b869b5
Show file tree
Hide file tree
Showing 13 changed files with 623 additions and 32 deletions.
43 changes: 43 additions & 0 deletions src/base.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,49 @@
unbindAll: function() {
Polymer.unbindAll.apply(this, arguments);
},
/**
* Ensures MDV bindings persist.
*
* Typically, it's not necessary to call this method. Polymer
* automatically manages bindings when elements are inserted
* and removed from the document.
*
* However, if an element is created and not inserted into the document,
* cancelUnbindAll should be called to ensure bindings remain active.
* Otherwise bindings will be removed so that the element
* may be garbage collected, freeing the memory it uses. Please note that
* if cancelUnbindAll is called and the element is not inserted
* into the document, then unbindAll or asyncUnbindAll must be called
* to dispose of the element.
*
* @method cancelUnbindAll
* @param {Boolean} [preventCascade] If true, cancelUnbindAll will not
* cascade to shadowRoot children. In the case described above,
* and in general in application code, this should not be set to true.
*/
cancelUnbindAll: function(preventCascade) {
Polymer.cancelUnbindAll.apply(this, arguments);
},
/**
* Schedules MDV bindings to be removed asynchronously.
*
* Typically, it's not necessary to call this method. Polymer
* automatically manages bindings when elements are inserted
* and removed from the document.
*
* However, if an element is created and not inserted into the document,
* cancelUnbindAll should be called to ensure bindings remain active.
* Otherwise bindings will be removed so that the element
* may be garbage collected, freeing the memory it uses. Please note that
* if cancelUnbindAll is called and the element is not inserted
* into the document, then unbindAll or asyncUnbindAll must be called
* to dispose of the element.
*
* @method asyncUnbindAll
*/
asyncUnbindAll: function() {
Polymer.asyncUnbindAll.apply(this, arguments);
},
/**
* Schedules an async job with timeout and returns a handle.
* @method job
Expand Down
96 changes: 85 additions & 11 deletions src/bindMDV.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
HTMLTemplateElement.syntax['MDV'] = new MDVSyntax;

// bind tracking

var bindings = new SideTable();

function registerBinding(element, name, path) {
Expand Down Expand Up @@ -75,22 +74,93 @@
}
}

function unbindModel(node) {
node.unbindAll();
for (var child = node.firstChild; child; child = child.nextSibling) {
unbindModel(child);
}
}

function unbind(name) {
if (!Polymer.unregisterObserver(this, 'binding', name)) {
HTMLElement.prototype.unbind.apply(this, arguments);
}
}

function unbindAll() {
Polymer.unregisterObserversOfType(this, 'property');
HTMLElement.prototype.unbindAll.apply(this, arguments);
if (!isElementUnbound(this)) {
Polymer.unregisterObserversOfType(this, 'property');
HTMLElement.prototype.unbindAll.apply(this, arguments);
// unbind shadowRoot, whee
unbindNodeTree(this.webkitShadowRoot, true);
markElementUnbound(this);
}
}

function unbindNodeTree(node, olderShadows) {
forNodeTree(node, olderShadows, function(n) {
if (n.unbindAll) {
n.unbindAll();
}
});
}

function forNodeTree(node, olderShadows, callback) {
if (!node) {
return;
}
callback(node);
if (olderShadows && node.olderShadowRoot) {
forNodeTree(node.olderShadowRoot, olderShadows, callback);
}
for (var child = node.firstChild; child; child = child.nextSibling) {
forNodeTree(child, olderShadows, callback);
}
}

// binding state tracking
var unboundTable = new SideTable();

function markElementUnbound(element) {
unboundTable.set(element, true);
}

function isElementUnbound(element) {
return unboundTable.get(element);
}

// asynchronous binding management
var unbindAllJobTable = new SideTable();

function asyncUnbindAll() {
if (!isElementUnbound(this)) {
log.bind && console.log('asyncUnbindAll', this.localName);
unbindAllJobTable.set(this, this.job(unbindAllJobTable.get(this),
this.unbindAll));
}
}

function cancelUnbindAll(preventCascade) {
if (isElementUnbound(this)) {
log.bind && console.warn(this.localName,
'is unbound, cannot cancel unbindAll');
return;
}
log.bind && console.log('cancelUnbindAll', this.localName);
var unbindJob = unbindAllJobTable.get(this);
if (unbindJob) {
unbindJob.stop();
unbindAllJobTable.set(this, null);
}
// cancel unbinding our shadow tree iff we're not in the process of
// cascading our tree (as we do, for example, when the element is inserted).
if (!preventCascade) {
forNodeTree(this.webkitShadowRoot, true, function(n) {
if (n.cancelUnbindAll) {
n.cancelUnbindAll();
}
});
}
}

// bind arbitrary html to a model
function parseAndBindHTML(html, model) {
var template = document.createElement('template');
template.innerHTML = html;
return template.createInstance(model);
}

var mustachePattern = /\{\{([^{}]*)}}/;
Expand All @@ -101,7 +171,11 @@
Polymer.unbind = unbind;
Polymer.unbindAll = unbindAll;
Polymer.getBinding = getBinding;
Polymer.unbindModel = unbindModel;
Polymer.asyncUnbindAll = asyncUnbindAll;
Polymer.cancelUnbindAll = cancelUnbindAll;
Polymer.isElementUnbound = isElementUnbound;
Polymer.unbindNodeTree = unbindNodeTree;
Polymer.parseAndBindHTML = parseAndBindHTML;
Polymer.bindPattern = mustachePattern;

})();
Expand Down
30 changes: 29 additions & 1 deletion src/register.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,12 @@
// hint the supercall mechanism
// TODO(sjmiles): make prototype extension api that does this
prototype.installTemplate.nom = 'installTemplate';
// install readyCallback
// install callbacks
prototype.readyCallback = readyCallback;
prototype.insertedCallback = insertedCallback;
prototype.removedCallback = removedCallback;
prototype.attributeChangedCallback = attributeChangedCallback;

// hint super call engine by tagging methods with names
hintSuper(prototype);
// parse declared on-* delegates into imperative form
Expand Down Expand Up @@ -123,11 +127,35 @@
// add host-events...
var hostEvents = scope.accumulateHostEvents.call(this);
scope.bindAccumulatedHostEvents.call(this, hostEvents);
// asynchronously unbindAll... will be cancelled if inserted
this.asyncUnbindAll();
// invoke user 'ready'
if (this.ready) {
this.ready();
}
};

function insertedCallback() {
this.cancelUnbindAll(true);
// invoke user 'inserted'
if (this.inserted) {
this.inserted();
}
}

function removedCallback() {
this.asyncUnbindAll();
// invoke user 'removed'
if (this.removed) {
this.removed();
}
}

function attributeChangedCallback() {
if (this.attributeChanged) {
this.attributeChanged.apply(this, arguments);
}
}

function hintSuper(prototype) {
Object.getOwnPropertyNames(prototype).forEach(function(n) {
Expand Down
4 changes: 2 additions & 2 deletions test/html/attr-mustache.html
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,10 @@
bind: function() {
Element.prototype.bind.apply(this, arguments);
},
insertedCallback: function() {
inserted: function() {
this.testSrcForMustache();
},
attributeChangedCallback: function(name, oldValue) {
attributeChanged: function(name, oldValue) {
this.testSrcForMustache();
if (this.getAttribute(name) === '../testSource') {
done();
Expand Down
86 changes: 86 additions & 0 deletions test/html/callbacks.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
<!doctype html>
<html>
<head>
<title>event path</title>
<script src="../../polymer.js"></script>
<script src="../../tools/test/htmltest.js"></script>
<script src="../../node_modules/chai/chai.js"></script>
</head>
<body>

<x-base></x-base>

<x-extendor></x-extendor>

<element name="x-base">
<script>
Polymer.register(this, {
ready: function() {
this.isReadied = true;
},
inserted: function() {
this.isInserted = true;
},
removed: function() {
this.isRemoved = true;
},
attributeChanged: function() {
this.hasAttributeChanged = true;
}
});
</script>
</element>

<element name="x-extendor" extends="x-base">
<script>
Polymer.register(this, {
ready: function() {
this.extendedIsReadied = true;
this.super();
},
inserted: function() {
this.extendedIsInserted = true;
this.super();
},
removed: function() {
this.extendedIsRemoved = true;
this.super();
},
attributeChanged: function() {
this.extendedHasAttributeChanged = true;
this.super();
}
});
</script>
</element>

<script>
document.addEventListener('WebComponentsReady', function() {
var xBase = document.querySelector('x-base');
chai.assert.equal(xBase.isReadied, true);
chai.assert.equal(xBase.isInserted, true);
xBase.setAttribute('foo', 'foo');
chai.assert.equal(xBase.hasAttributeChanged, true);

var xExtendor = document.querySelector('x-extendor');
chai.assert.equal(xExtendor.isReadied, true);
chai.assert.equal(xExtendor.extendedIsReadied, true);
chai.assert.equal(xExtendor.isInserted, true);
chai.assert.equal(xExtendor.extendedIsInserted, true);
xExtendor.setAttribute('foo', 'foo');
chai.assert.equal(xExtendor.hasAttributeChanged, true);
chai.assert.equal(xExtendor.extendedHasAttributeChanged, true);

xBase.parentNode.removeChild(xBase);
xExtendor.parentNode.removeChild(xExtendor);

setTimeout(function() {
chai.assert.equal(xBase.isRemoved, true);
chai.assert.equal(xExtendor.isRemoved, true);
chai.assert.equal(xExtendor.extendedIsRemoved, true);
done();
});
});
</script>
</body>
</html>
2 changes: 1 addition & 1 deletion test/html/mdv-syntax.html
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@
</template>
<script>
Polymer.register(this, {
insertedCallback: function() {
inserted: function() {
this.asyncMethod('runTests');
},
runTests: function() {
Expand Down
Loading

0 comments on commit 5b869b5

Please sign in to comment.