diff --git a/packages/ui/component.js b/packages/ui/component.js index d7d99a76f8..cbdfefad80 100644 --- a/packages/ui/component.js +++ b/packages/ui/component.js @@ -24,7 +24,9 @@ Component = function (args) { this._end = null; // last Component or Node this.isAttached = false; this._detachedContent = null; // DocumentFragment - this._buildComputation = null; + + this._buildUpdater = null; + this._childUpdaters = {}; this.constructed(); }; @@ -56,20 +58,22 @@ _.extend(Component.prototype, { build: function () { var self = this; self._requireStage(Component.ADDED); - self._buildComputation = + self._buildUpdater = Deps.autorun(function (c) { var isRebuild = (self.stage === Component.BUILT); var oldFirstNode, oldLastNode; if (isRebuild) { oldFirstNode = self.firstNode(); oldLastNode = self.lastNode(); - for (var k in self.children) { - if (self.children.hasOwnProperty(k)) { - var child = self.children[k]; - child.destroy(); - self.removeChild(child.key); + Deps.nonreactive(function () { + for (var k in self.children) { + if (self.children.hasOwnProperty(k)) { + var child = self.children[k]; + child.destroy(); + self.removeChild(child.key); + } } - } + }); self.stage = Component.ADDED; } var buf = new RenderBuffer(self); @@ -88,11 +92,13 @@ _.extend(Component.prototype, { self._end = buildResult.end; self.stage = Component.BUILT; - if (c.firstRun) { - self.built(); - } else { - self.rebuilt(); - } + Deps.nonreactive(function () { + if (c.firstRun) { + self.built(); + } else { + self.rebuilt(); + } + }); }); }, destroy: function () { @@ -107,8 +113,15 @@ _.extend(Component.prototype, { if (oldStage === Component.UNADDED) return; - if (this._buildComputation) - this._buildComputation.stop(); + if (this._buildUpdater) + this._buildUpdater.stop(); + + for (var k in this._childUpdaters) { + if (this._childUpdaters.hasOwnProperty(k)) { + this._childUpdaters[k].stop(); + delete this._childUpdaters[k]; + } + } // maybe GC sooner this._start = null; @@ -161,7 +174,7 @@ _.extend(Component.prototype, { self.attached(); }, - detach: function (_duringSwap) { + detach: function (_allowTransientEmpty) { var self = this; var parent = self.parent; @@ -174,7 +187,7 @@ _.extend(Component.prototype, { if (parent) { if (parent._start === comp) { if (parent._end === comp) { - if (_duringSwap) + if (_allowTransientEmpty) parent._start = parent._end = EMPTY; else throw new Error("Can't detach entire contents of " + @@ -313,42 +326,79 @@ _.extend(Component.prototype, { hasChild: function (key) { return this.children.hasOwnProperty(key); }, - addChild: function (key, childComponent, attachParentNode, + addChild: function (key, childComponentOrFunc, + attachParentNode, attachBeforeNode) { - if (key instanceof Component) { + if ((key instanceof Component) || + ((typeof key) === 'function')) { // omitted key arg - childComponent = key; + childComponentOrFunc = key; key = null; } + // omitted key, generate unique child key if (key === null || typeof key === 'undefined') key = "__child#" + (this._uniqueIdCounter++) + "__"; key = String(key); + var self = this; + if (self.stage === Component.DESTROYED) + throw new Error("parent Component already destroyed"); + if (self.stage === Component.UNADDED) + throw new Error("parent Component is unadded"); + + if (self.hasChild(key)) + throw new Error("Already have a child with key: " + key); + + var childComponent; + if (typeof childComponentOrFunc === 'function') { + var func = childComponentOrFunc; + this._childUpdaters[key] = + Deps.autorun(function (c) { + if (c.firstRun) { + childComponent = func(); + return; + } + var oldChild = self.children[key]; + if ((! (oldChild instanceof Component)) || + oldChild.stage === Component.DESTROYED) { + // child shouldn't be missing, but may be + // destroyed + c.stop(); + return; + } + var newChild = func(); + if (! (newChild instanceof Component)) + throw new Error("not a Component: " + newChild); + if (oldChild.constructor === newChild.constructor) { + oldChild.update(newChild._args); + } else { + self.replaceChild(key, newChild); + } + }); + } else { + childComponent = childComponentOrFunc; + } + if (! (childComponent instanceof Component)) throw new Error("not a Component: " + childComponent); - if (this.stage === Component.DESTROYED) - throw new Error("parent Component already destroyed"); - if (this.stage === Component.UNADDED) - throw new Error("parent Component is unadded"); childComponent._requireStage(Component.UNADDED); - if (this.hasChild(key)) - throw new Error("Already have a child with key: " + key); + self.children[key] = childComponent; - this.children[key] = childComponent; - - childComponent._added(key, this); + childComponent._added(key, self); if (attachParentNode) { - if (this.stage !== Component.BUILT) + if (self.stage !== Component.BUILT) throw new Error("Attaching new child requires built " + "parent Component"); childComponent.attach(attachParentNode, attachBeforeNode); } + + return childComponent; }, - removeChild: function (key) { + removeChild: function (key, _allowTransientEmpty) { // note: must work if child is destroyed key = String(key); @@ -364,12 +414,47 @@ _.extend(Component.prototype, { var childComponent = this.children[key]; if (childComponent.stage === Component.BUILT && childComponent.isAttached) - childComponent.detach(); + childComponent.detach(_allowTransientEmpty); delete this.children[key]; + + if (this._childUpdaters[key]) { + this._childUpdaters[key].stop(); + delete this._childUpdaters[key]; + } + childComponent.parent = null; childComponent.destroy(); + }, + replaceChild: function (key, newChild) { + if (this.stage === Component.DESTROYED) + throw new Error("parent Component already destroyed"); + if (this.stage === Component.UNADDED) + throw new Error("parent Component is unadded"); + + if (! this.hasChild(key)) + throw new Error("No such child component: " + key); + + if (! (newChild instanceof Component)) + throw new Error("Component required"); + + var oldChild = this.children[key]; + + if (oldChild.constructor === newChild.constructor) { + oldChild.update(newChild._args); + } else if (this.stage !== Component.BUILT || + oldChild !== Component.BUILT || + ! oldChild.isAttached) { + this.removeChild(key); + this.addChild(key, newChild); + } else { + // swap attached child + var parentNode = oldChild.parentNode(); + var beforeNode = oldChild.lastNode().nextSibling; + this.removeChild(key, true); + this.addChild(key, newChild, parentNode, beforeNode); + } } }); diff --git a/packages/ui/renderbuffer.js b/packages/ui/renderbuffer.js index 2acb8bc715..9ee6730e5c 100644 --- a/packages/ui/renderbuffer.js +++ b/packages/ui/renderbuffer.js @@ -127,18 +127,21 @@ _.extend(RenderBuffer.prototype, { }, component: function (componentOrFunction, options) { var self = this; - var comp = (typeof componentOrFunction === 'function' ? - componentOrFunction() : componentOrFunction); + + if (! ((componentOrFunction instanceof Component) || + (typeof componentOrFunction === 'function'))) + throw new Error("Component or function required"); var childKey = (options && options.childKey || null); - this._component.addChild(childKey, comp); + var childComp = self._component.addChild( + childKey, componentOrFunction); - var commentString = this.builderId + '_' + - (this._nextNum++); - this._htmlBuf.push(''); + var commentString = self.builderId + '_' + + (self._nextNum++); + self._htmlBuf.push(''); - this._childrenToAttach[commentString] = comp; + self._childrenToAttach[commentString] = childComp; }, build: function () { var html = this._htmlBuf.join('');