diff --git a/examples/unfinished/shark/client/shark.js b/examples/unfinished/shark/client/shark.js index 626cece5a6..d7638c2494 100644 --- a/examples/unfinished/shark/client/shark.js +++ b/examples/unfinished/shark/client/shark.js @@ -139,17 +139,23 @@ Meteor.startup(function () { }); Meteor.startup(function () { - var c = function (n) { return UIComponent.create({render:function(buf) { buf(String(n)); }}); }; + var c = UIComponent.extend({ + render: function(buf) { + buf(String(this.data())); + } + }); - L = _UI.List({elseContent: function () { return c('else'); }}); + L = _UI.List({elseContent: function () { + return c(function () { return 'else'; }); + }}); - L.addItemBefore('a', c(1)); - L.addItemBefore('b', c(2)); - L.addItemBefore('c', c(3)); + L.addItemBefore('a', c, 1); + L.addItemBefore('b', c, 2); + L.addItemBefore('c', c, 3); L.makeRoot(); L.attach(document.body); - L.addItemBefore('d', c(4)); - L.addItemBefore('e', c(5), 'b'); + L.addItemBefore('d', c, 4); + L.addItemBefore('e', c, 5, 'b'); L.moveItemBefore('d', 'c'); L.moveItemBefore('a'); L.removeItem('b'); @@ -157,8 +163,8 @@ Meteor.startup(function () { L.removeItem('c'); L.removeItem('d'); L.removeItem('e'); - L.addItemBefore('a', c(1)); - L.addItemBefore('b', c(2), 'a'); - L.addItemBefore('c', c(3)); + L.addItemBefore('a', c, 1); + L.addItemBefore('b', c, 2, 'a'); + L.addItemBefore('c', c, 3); L.moveItemBefore('c', 'a'); }); \ No newline at end of file diff --git a/packages/ui/dom.js b/packages/ui/dom.js index 194b06b9ef..a28a9d410e 100644 --- a/packages/ui/dom.js +++ b/packages/ui/dom.js @@ -287,9 +287,14 @@ Component.include({ // We could be a root (and have no parent). Parent could // theoretically be destroyed, or not yet built (if we // are currently building). + // // We use a falsy `parent.start` as a cue that this is a // rebuild, another case where we skip the start/end adjustment // logic. + // + // `attach` is special in that it is used during building + // and rebuilding; it is not required that the parent is + // completely built. if (parent && parent.stage === Component.BUILT && parent.start) { if (parent.isEmpty()) { diff --git a/packages/ui/each.js b/packages/ui/each.js index 670525044b..05b67fa539 100644 --- a/packages/ui/each.js +++ b/packages/ui/each.js @@ -7,17 +7,26 @@ _UI.List = Component.extend({ constructed: function () { this._items = new OrderedDict; }, - addItemBefore: function (id, comp, beforeId) { - this._items.putBefore(id, comp, beforeId); + addItemBefore: function (id, compType, data, beforeId) { + var self = this; - if (this.stage === Component.BUILT) { - if (this._else) { - this._else.remove(); - this._else = null; + var comp = compType(function () { + this.dataDep.depend(); + return this._data; + }, { + _data: data, + dataDep: new Deps.Dependency + }); + self._items.putBefore(id, comp, beforeId); + + if (self.stage === Component.BUILT) { + if (self._else) { + self._else.remove(); + self._else = null; } - this.insertBefore( - comp, beforeId ? this._items.get(beforeId) : null); + self.insertBefore( + comp, beforeId ? self._items.get(beforeId) : null); } }, removeItem: function (id) { @@ -45,7 +54,24 @@ _UI.List = Component.extend({ getItem: function (id) { return this._items.get(id) || null; }, + setItemData: function (id, newData) { + var comp = this.getItem(id); + if (! comp) + throw new Error("No such item: " + id); + // Do a `===` check even though it's weak + if (newData !== comp._data) { + comp._data = newData; + comp.dataDep.changed(); + } + }, render: function (buf) { + // This component reactively rebuilds when any dependencies + // here are invalidated. + // + // The "item" methods cannot be called from here; they assume + // they are not operating during the build, but either + // before or after it. + var self = this; if (self._items.empty()) { buf(self._else = self.elseContent()); @@ -66,49 +92,54 @@ _UI.List = Component.extend({ _UI.Each = Component.extend({ typeName: 'Each', List: _UI.List, + _oldData: null, + init: function () { + var self = this; + self._list = self.List({ + elseContent: function (/**/) { + return self.elseContent.apply(self, arguments); + } + }); + // add outside of the rebuild cycle + self.add(self._list); + }, render: function (buf) { var self = this; + var list = self._list; // XXX support arrays too. // XXX and objects. // For now, we assume the data is a database cursor. - var cursor = self.data(); - if (! cursor) - return; + var newData = self.data(); + // Do a `===` check even though it's weak + if (newData !== self._oldData) { + self._oldData = newData; - var list = new self.List({ - elseContent: self.elseContent - }); + if (newData && newData.observe) { + var cursor = newData; - cursor.observe({ - _no_indices: true, - addedAt: function (doc, i, beforeId) { - var comp = self.content(function () { - this.dataDep.depend(); - return this._data; - }, { - _data: doc, - dataDep: new Deps.Dependency + cursor.observe({ + _no_indices: true, + addedAt: function (doc, i, beforeId) { + list.addItemBefore(Meteor.idStringify(doc._id), + self.content, doc, + beforeId && Meteor.idStringify(beforeId)); + }, + removed: function (doc) { + list.removeItem(Meteor.idStringify(doc._id)); + }, + movedTo: function (doc, i, j, beforeId) { + list.moveItemBefore(Meteor.idStringify(doc._id), + beforeId && Meteor.idStringify(beforeId)); + }, + changed: function (newDoc) { + list.setItemData(Meteor.idStringify(newDoc._id), newDoc); + } }); - // XXX could `before` be a falsy ID? Technically - // idStringify seems to allow for them -- though - // OrderedDict won't call stringify on a falsy arg. - list.addItemBefore(Meteor.idStringify(doc._id), comp, - beforeId && Meteor.idStringify(beforeId)); - }, - removed: function (doc) { - list.removeItem(Meteor.idStringify(doc._id)); - }, - movedTo: function (doc, i, j, beforeId) { - list.moveItemBefore(Meteor.idStringify(doc._id), - beforeId && Meteor.idStringify(beforeId)); - }, - changed: function (newDoc) { - var comp = list.getItem(Meteor.idStringify(newDoc._id)); - comp._data = newDoc; - comp.dataDep.changed(); } - }); + } + // XXX we fail on switching to empty; should use + // patching replace for that. buf(list); } diff --git a/packages/ui/render.js b/packages/ui/render.js index 8d4f4bfbc7..cdd0dd504e 100644 --- a/packages/ui/render.js +++ b/packages/ui/render.js @@ -91,7 +91,6 @@ makeRenderBuffer = function (component, options) { randomString = randomString || Random.id(); var commentString = randomString + '_' + (commentUid++); push(''); - component.add(arg); componentsToAttach = componentsToAttach || {}; componentsToAttach[commentString] = arg; } else if (arg.type) { @@ -192,6 +191,12 @@ makeRenderBuffer = function (component, options) { if (n === root.lastChild) end = comp; } + if (comp.stage === Component.INITIAL) { + component.add(comp); + } else if (comp.parent !== component) { + throw new Error("Component used in render must be a child " + + "(or addable as one)"); + } comp.attach(parent, n); parent.removeChild(n); delete componentsToAttach[n.nodeValue];