Skip to content

Commit 5b869b5

Browse files
committed
Automatically manage binding state such that elements can be garbage 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.
1 parent 26e8b14 commit 5b869b5

13 files changed

+623
-32
lines changed

src/base.js

+43
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,49 @@
5353
unbindAll: function() {
5454
Polymer.unbindAll.apply(this, arguments);
5555
},
56+
/**
57+
* Ensures MDV bindings persist.
58+
*
59+
* Typically, it's not necessary to call this method. Polymer
60+
* automatically manages bindings when elements are inserted
61+
* and removed from the document.
62+
*
63+
* However, if an element is created and not inserted into the document,
64+
* cancelUnbindAll should be called to ensure bindings remain active.
65+
* Otherwise bindings will be removed so that the element
66+
* may be garbage collected, freeing the memory it uses. Please note that
67+
* if cancelUnbindAll is called and the element is not inserted
68+
* into the document, then unbindAll or asyncUnbindAll must be called
69+
* to dispose of the element.
70+
*
71+
* @method cancelUnbindAll
72+
* @param {Boolean} [preventCascade] If true, cancelUnbindAll will not
73+
* cascade to shadowRoot children. In the case described above,
74+
* and in general in application code, this should not be set to true.
75+
*/
76+
cancelUnbindAll: function(preventCascade) {
77+
Polymer.cancelUnbindAll.apply(this, arguments);
78+
},
79+
/**
80+
* Schedules MDV bindings to be removed asynchronously.
81+
*
82+
* Typically, it's not necessary to call this method. Polymer
83+
* automatically manages bindings when elements are inserted
84+
* and removed from the document.
85+
*
86+
* However, if an element is created and not inserted into the document,
87+
* cancelUnbindAll should be called to ensure bindings remain active.
88+
* Otherwise bindings will be removed so that the element
89+
* may be garbage collected, freeing the memory it uses. Please note that
90+
* if cancelUnbindAll is called and the element is not inserted
91+
* into the document, then unbindAll or asyncUnbindAll must be called
92+
* to dispose of the element.
93+
*
94+
* @method asyncUnbindAll
95+
*/
96+
asyncUnbindAll: function() {
97+
Polymer.asyncUnbindAll.apply(this, arguments);
98+
},
5699
/**
57100
* Schedules an async job with timeout and returns a handle.
58101
* @method job

src/bindMDV.js

+85-11
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515
HTMLTemplateElement.syntax['MDV'] = new MDVSyntax;
1616

1717
// bind tracking
18-
1918
var bindings = new SideTable();
2019

2120
function registerBinding(element, name, path) {
@@ -75,22 +74,93 @@
7574
}
7675
}
7776

78-
function unbindModel(node) {
79-
node.unbindAll();
80-
for (var child = node.firstChild; child; child = child.nextSibling) {
81-
unbindModel(child);
82-
}
83-
}
84-
8577
function unbind(name) {
8678
if (!Polymer.unregisterObserver(this, 'binding', name)) {
8779
HTMLElement.prototype.unbind.apply(this, arguments);
8880
}
8981
}
9082

9183
function unbindAll() {
92-
Polymer.unregisterObserversOfType(this, 'property');
93-
HTMLElement.prototype.unbindAll.apply(this, arguments);
84+
if (!isElementUnbound(this)) {
85+
Polymer.unregisterObserversOfType(this, 'property');
86+
HTMLElement.prototype.unbindAll.apply(this, arguments);
87+
// unbind shadowRoot, whee
88+
unbindNodeTree(this.webkitShadowRoot, true);
89+
markElementUnbound(this);
90+
}
91+
}
92+
93+
function unbindNodeTree(node, olderShadows) {
94+
forNodeTree(node, olderShadows, function(n) {
95+
if (n.unbindAll) {
96+
n.unbindAll();
97+
}
98+
});
99+
}
100+
101+
function forNodeTree(node, olderShadows, callback) {
102+
if (!node) {
103+
return;
104+
}
105+
callback(node);
106+
if (olderShadows && node.olderShadowRoot) {
107+
forNodeTree(node.olderShadowRoot, olderShadows, callback);
108+
}
109+
for (var child = node.firstChild; child; child = child.nextSibling) {
110+
forNodeTree(child, olderShadows, callback);
111+
}
112+
}
113+
114+
// binding state tracking
115+
var unboundTable = new SideTable();
116+
117+
function markElementUnbound(element) {
118+
unboundTable.set(element, true);
119+
}
120+
121+
function isElementUnbound(element) {
122+
return unboundTable.get(element);
123+
}
124+
125+
// asynchronous binding management
126+
var unbindAllJobTable = new SideTable();
127+
128+
function asyncUnbindAll() {
129+
if (!isElementUnbound(this)) {
130+
log.bind && console.log('asyncUnbindAll', this.localName);
131+
unbindAllJobTable.set(this, this.job(unbindAllJobTable.get(this),
132+
this.unbindAll));
133+
}
134+
}
135+
136+
function cancelUnbindAll(preventCascade) {
137+
if (isElementUnbound(this)) {
138+
log.bind && console.warn(this.localName,
139+
'is unbound, cannot cancel unbindAll');
140+
return;
141+
}
142+
log.bind && console.log('cancelUnbindAll', this.localName);
143+
var unbindJob = unbindAllJobTable.get(this);
144+
if (unbindJob) {
145+
unbindJob.stop();
146+
unbindAllJobTable.set(this, null);
147+
}
148+
// cancel unbinding our shadow tree iff we're not in the process of
149+
// cascading our tree (as we do, for example, when the element is inserted).
150+
if (!preventCascade) {
151+
forNodeTree(this.webkitShadowRoot, true, function(n) {
152+
if (n.cancelUnbindAll) {
153+
n.cancelUnbindAll();
154+
}
155+
});
156+
}
157+
}
158+
159+
// bind arbitrary html to a model
160+
function parseAndBindHTML(html, model) {
161+
var template = document.createElement('template');
162+
template.innerHTML = html;
163+
return template.createInstance(model);
94164
}
95165

96166
var mustachePattern = /\{\{([^{}]*)}}/;
@@ -101,7 +171,11 @@
101171
Polymer.unbind = unbind;
102172
Polymer.unbindAll = unbindAll;
103173
Polymer.getBinding = getBinding;
104-
Polymer.unbindModel = unbindModel;
174+
Polymer.asyncUnbindAll = asyncUnbindAll;
175+
Polymer.cancelUnbindAll = cancelUnbindAll;
176+
Polymer.isElementUnbound = isElementUnbound;
177+
Polymer.unbindNodeTree = unbindNodeTree;
178+
Polymer.parseAndBindHTML = parseAndBindHTML;
105179
Polymer.bindPattern = mustachePattern;
106180

107181
})();

src/register.js

+29-1
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,12 @@
4646
// hint the supercall mechanism
4747
// TODO(sjmiles): make prototype extension api that does this
4848
prototype.installTemplate.nom = 'installTemplate';
49-
// install readyCallback
49+
// install callbacks
5050
prototype.readyCallback = readyCallback;
51+
prototype.insertedCallback = insertedCallback;
52+
prototype.removedCallback = removedCallback;
53+
prototype.attributeChangedCallback = attributeChangedCallback;
54+
5155
// hint super call engine by tagging methods with names
5256
hintSuper(prototype);
5357
// parse declared on-* delegates into imperative form
@@ -123,11 +127,35 @@
123127
// add host-events...
124128
var hostEvents = scope.accumulateHostEvents.call(this);
125129
scope.bindAccumulatedHostEvents.call(this, hostEvents);
130+
// asynchronously unbindAll... will be cancelled if inserted
131+
this.asyncUnbindAll();
126132
// invoke user 'ready'
127133
if (this.ready) {
128134
this.ready();
129135
}
130136
};
137+
138+
function insertedCallback() {
139+
this.cancelUnbindAll(true);
140+
// invoke user 'inserted'
141+
if (this.inserted) {
142+
this.inserted();
143+
}
144+
}
145+
146+
function removedCallback() {
147+
this.asyncUnbindAll();
148+
// invoke user 'removed'
149+
if (this.removed) {
150+
this.removed();
151+
}
152+
}
153+
154+
function attributeChangedCallback() {
155+
if (this.attributeChanged) {
156+
this.attributeChanged.apply(this, arguments);
157+
}
158+
}
131159

132160
function hintSuper(prototype) {
133161
Object.getOwnPropertyNames(prototype).forEach(function(n) {

test/html/attr-mustache.html

+2-2
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,10 @@
1616
bind: function() {
1717
Element.prototype.bind.apply(this, arguments);
1818
},
19-
insertedCallback: function() {
19+
inserted: function() {
2020
this.testSrcForMustache();
2121
},
22-
attributeChangedCallback: function(name, oldValue) {
22+
attributeChanged: function(name, oldValue) {
2323
this.testSrcForMustache();
2424
if (this.getAttribute(name) === '../testSource') {
2525
done();

test/html/callbacks.html

+86
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
<!doctype html>
2+
<html>
3+
<head>
4+
<title>event path</title>
5+
<script src="../../polymer.js"></script>
6+
<script src="../../tools/test/htmltest.js"></script>
7+
<script src="../../node_modules/chai/chai.js"></script>
8+
</head>
9+
<body>
10+
11+
<x-base></x-base>
12+
13+
<x-extendor></x-extendor>
14+
15+
<element name="x-base">
16+
<script>
17+
Polymer.register(this, {
18+
ready: function() {
19+
this.isReadied = true;
20+
},
21+
inserted: function() {
22+
this.isInserted = true;
23+
},
24+
removed: function() {
25+
this.isRemoved = true;
26+
},
27+
attributeChanged: function() {
28+
this.hasAttributeChanged = true;
29+
}
30+
});
31+
</script>
32+
</element>
33+
34+
<element name="x-extendor" extends="x-base">
35+
<script>
36+
Polymer.register(this, {
37+
ready: function() {
38+
this.extendedIsReadied = true;
39+
this.super();
40+
},
41+
inserted: function() {
42+
this.extendedIsInserted = true;
43+
this.super();
44+
},
45+
removed: function() {
46+
this.extendedIsRemoved = true;
47+
this.super();
48+
},
49+
attributeChanged: function() {
50+
this.extendedHasAttributeChanged = true;
51+
this.super();
52+
}
53+
});
54+
</script>
55+
</element>
56+
57+
<script>
58+
document.addEventListener('WebComponentsReady', function() {
59+
var xBase = document.querySelector('x-base');
60+
chai.assert.equal(xBase.isReadied, true);
61+
chai.assert.equal(xBase.isInserted, true);
62+
xBase.setAttribute('foo', 'foo');
63+
chai.assert.equal(xBase.hasAttributeChanged, true);
64+
65+
var xExtendor = document.querySelector('x-extendor');
66+
chai.assert.equal(xExtendor.isReadied, true);
67+
chai.assert.equal(xExtendor.extendedIsReadied, true);
68+
chai.assert.equal(xExtendor.isInserted, true);
69+
chai.assert.equal(xExtendor.extendedIsInserted, true);
70+
xExtendor.setAttribute('foo', 'foo');
71+
chai.assert.equal(xExtendor.hasAttributeChanged, true);
72+
chai.assert.equal(xExtendor.extendedHasAttributeChanged, true);
73+
74+
xBase.parentNode.removeChild(xBase);
75+
xExtendor.parentNode.removeChild(xExtendor);
76+
77+
setTimeout(function() {
78+
chai.assert.equal(xBase.isRemoved, true);
79+
chai.assert.equal(xExtendor.isRemoved, true);
80+
chai.assert.equal(xExtendor.extendedIsRemoved, true);
81+
done();
82+
});
83+
});
84+
</script>
85+
</body>
86+
</html>

test/html/mdv-syntax.html

+1-1
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@
5757
</template>
5858
<script>
5959
Polymer.register(this, {
60-
insertedCallback: function() {
60+
inserted: function() {
6161
this.asyncMethod('runTests');
6262
},
6363
runTests: function() {

0 commit comments

Comments
 (0)