From 08822cb2d6538ee03d0dd735135db3e236d5bfda Mon Sep 17 00:00:00 2001 From: David Greenspan Date: Sun, 25 Aug 2013 20:26:31 -0700 Subject: [PATCH] towards working DomRange rendering next stages: - spacebars calls make something happen - appropriate reactivity then: - can we get the test driver? - DomRange improvements (tests?) - proper lifecycle / stop autoruns --- packages/spacebars/spacebars.js | 2 +- packages/ui/attrs.js | 25 ++-- packages/ui/each.js | 24 ++++ packages/ui/render.js | 218 +++++++++++++++----------------- 4 files changed, 133 insertions(+), 136 deletions(-) diff --git a/packages/spacebars/spacebars.js b/packages/spacebars/spacebars.js index b1823317ec..11bcfce17c 100644 --- a/packages/spacebars/spacebars.js +++ b/packages/spacebars/spacebars.js @@ -708,7 +708,7 @@ Spacebars.compile = function (inputString, options) { if (path.length === 1) compFunc = 'Template[' + toJSLiteral(path[0]) + '] || ' + compFunc; - return '{child: ' + compFunc + (argCode ? ', props: ' + argCode : '') + + return '{kind: ' + compFunc + (argCode ? ', props: ' + argCode : '') + '}'; }; diff --git a/packages/ui/attrs.js b/packages/ui/attrs.js index 3626fd52e0..f4fc83ca5f 100644 --- a/packages/ui/attrs.js +++ b/packages/ui/attrs.js @@ -5,9 +5,8 @@ var isValidAttributeName = function (str) { return ATTRIBUTE_NAME_REGEX.test(str); }; -AttributeManager = function (component, dictOrFunc) { +AttributeManager = function (dictOrFunc) { var self = this; - self.component = component; var dict, func; @@ -39,7 +38,7 @@ AttributeManager = function (component, dictOrFunc) { throw new Error("Illegal HTML attribute name: " + attrName); handlers[attrName] = makeAttributeHandler( - component, attrName, dict[attrName]); + attrName, dict[attrName]); } }; @@ -66,17 +65,11 @@ _extend(AttributeManager.prototype, { if (! self.isReactive()) throw new Error("Can't start a non-reactive AttributeManager"); - var component = self.component; var element = self.element; var handlers = self.handlers; - component.autorun(function (c) { - if ((! component.isBuilt) || - component.isDestroyed || - ! component.containsElement(element)) { - c.stop(); - return; - } + // XXX make this be stopped at the right time + Deps.autorun(function (c) { // capture dependencies of this line: var newDict = self.func(); @@ -101,7 +94,7 @@ _extend(AttributeManager.prototype, { throw new Error("Illegal HTML attribute name: " + attrName); var h = makeAttributeHandler( - component, attrName, newDict[attrName]); + attrName, newDict[attrName]); handlers[attrName] = h; h.add(element); @@ -188,12 +181,10 @@ var ClassHandler = AttributeHandler.extend({ } }); -var makeAttributeHandler = function (component, name, value) { - // return new (component.constructor._attributeHandlers[name] || - // AttributeHandler)(name, value); - - // will need one for 'style' on IE, though modern browsers +var makeAttributeHandler = function (name, value) { + // XXX will need one for 'style' on IE, though modern browsers // seem to handle setAttribute ok. + // XXX components should be able to hook into this if (name === 'class') { return new ClassHandler(name, value); } else { diff --git a/packages/ui/each.js b/packages/ui/each.js index 08ee680b51..90a65c318c 100644 --- a/packages/ui/each.js +++ b/packages/ui/each.js @@ -31,6 +31,23 @@ var moveNode = function (n, parent, next) { parent.insertBefore(n, next || null); }; +// not a UI hook; uses jQuery's html-to-DOM functionality +// and calls UI hooks +var insertHtml = function (html, parent, next) { + var prev = (next ? next.previousSibling : parent.lastChild); + // jQuery does some fancy compatibility stuff here to + // insert the HTML as DOM: + $(next).before(html); + for (var n = (prev ? prev.nextSibling : parent.firstChild); + n && (n !== next); + n = n.nextSibling) + // Call insert on all the nodes, even though they are + // already inserted. We assume the "insertElement" hook + // is required to execute immediately, and since it's + // idempotent, this works out fine. + insertNode(n, parent, n.nextSibling); +}; + var newFragment = function (nodeArray) { // jQuery fragments are built specially in // IE<9 so that they can safely hold HTML5 @@ -347,6 +364,13 @@ _extend(DomRange.prototype, { if (children.hasOwnProperty(id)) return children[id]; return null; + }, + getFirstNode: function () { + // does not refresh. + return this.start; + }, + getLastNode: function () { + return this.end; } }); diff --git a/packages/ui/render.js b/packages/ui/render.js index a5c959593b..87b7a5af58 100644 --- a/packages/ui/render.js +++ b/packages/ui/render.js @@ -2,7 +2,7 @@ UI.renderTo = function (kind, props, parentNode, beforeNode, parentComp) { if (kind === null) - return; + return null; if (! UI.isComponent(kind)) throw new Error("Expected Component, function, or null"); if (kind.isInited) @@ -24,10 +24,12 @@ UI.renderTo = function (kind, props, comp.init(); if (comp.render) { - var buf = makeRenderBuffer(dom); + var buf = makeRenderBuffer(); comp.render(buf); - buf.wireUp(); + buf.build(comp); } + + return comp; }; var ESCAPED_CHARS_UNQUOTED_REGEX = /[&<>]/g; @@ -57,11 +59,11 @@ UI.encodeSpecialEntities = function (text, isQuoted) { var GT_OR_QUOTE = /[>'"]/; -makeRenderBuffer = function (range, options) { +makeRenderBuffer = function (options) { var isPreview = !! options && options.preview; var strs = []; - var componentsToAttach = null; // {} + var componentsToRender = null; // {} var randomString = null; // Random.id() var commentUid = 1; var elementUid = 1; @@ -110,99 +112,20 @@ makeRenderBuffer = function (range, options) { strs.push.apply(strs, arguments); }; - var handleComponent = function (comp) { - randomString = randomString || Random.id(); - var commentString = randomString + '_' + (commentUid++); - push(''); - componentsToAttach = componentsToAttach || {}; - componentsToAttach[commentString] = comp; - return comp; - }; - var handle = function (arg) { if (arg == null) { // nothing to do } else if (typeof arg === 'string') { // "HTML" push(arg); - } else if (UI.isComponent(arg)) { - // Component - if (! arg.isInited) - arg = arg.extend(); + } else if (UI.isComponent(arg) || + (typeof arg) === 'function') { - return handleComponent(arg); - } else if ((typeof arg === 'function') || arg.child) { - // `componentFunction`, or - // `{child: componentOrFunction, props: object}` - - // In `{child: comp}` with no `props`, it's ok - // for `comp` to be already inited. This lets - // you write `{{> foo}}` to insert already-inited - // `foo`, with cooperation from the template - // compiler (i.e. not emitting an empty props object). - - // `curComp` holds the latest value of the `child` - // function if `componentOrFunction` is a function, - // or else the value itself if it is not. - // In the case where `curComp` - // is uninited and we instantiate a copy, `curComp` - // is not that copy, it's the original component used - // as a prototype. - var curComp, props; - if (typeof arg === 'function') { - curComp = arg; - props = null; - } else { - curComp = arg.child; - props = arg.props; - } - - if (typeof curComp === 'function') { - var compFunc = curComp; - // use `Deps.autorun`, not `component.autorun`, - // because we *do* want to be stopped when the - // enclosing computation is invalidated (i.e. - // the rebuilder computation). - Deps.autorun(function (c) { - // capture dependencies of this line: - var comp = compFunc(); - if (c.firstRun) { - // right away - curComp = comp; - } else { - // later (on subsequent runs)... - if (! component.isBuilt || - component.isDestroyed || - ! component.hasChild(curChild)) { - c.stop(); - } if (comp !== curComp) { - var oldChild = curChild; - var oldComp = curComp; - curComp = comp; - // don't capture any dependencies here - Deps.nonreactive(function () { - curChild = constructify(curComp, props); - if (oldChild === oldComp) - // didn't create the oldChild, just - // used it! So detach it, don't destroy it. - component.swapChild(oldChild, curChild); - else - component.replaceChild(oldChild, curChild); - }); - } - } - }); - } else if (! UI.isComponent(curComp)) { - throw new Error("Expected function or Component"); - } - // the autorun above closes down over this var: - var curChild = constructify(curComp, props); - // return something the caller of `buf.write` can't get - // any other way if `arg` involved a componentFunction: - // the actual component created. If the arg we are - // handling is the last arg to `buf.write`, it will return - // this value. - return handleComponent(curChild); + randomString = randomString || Random.id(); + var commentString = randomString + '_' + (commentUid++); + push(''); + componentsToRender = componentsToRender || {}; + componentsToRender[commentString] = arg; } else if (arg.attrs) { // `{attrs: functionOrDictionary }` // attrs object inserts zero or more `name="value"` items @@ -213,7 +136,7 @@ makeRenderBuffer = function (range, options) { // they won't cooperate). var elemId = null; - var manager = new AttributeManager(component, arg.attrs); + var manager = new AttributeManager(arg.attrs); if (manager.isReactive()) { var elemId = elementUid++; @@ -239,7 +162,7 @@ makeRenderBuffer = function (range, options) { strs.push(' ', manager.getInitialHTML(), ' '); } else { - throw new Error("Expected HTML string, Component, component spec or attrs spec, found: " + arg); + throw new Error("Expected HTML string, Component, function, or attrs spec, found: " + arg); } }; @@ -255,26 +178,90 @@ makeRenderBuffer = function (range, options) { return strs.join(''); }; - buf.wireUpDOM = function (root) { - var start = root.firstChild; - var end = root.lastChild; + buf.build = function (component) { + var html = buf.getHtml(); + + var range = component.dom; + // assert: range is empty. + var start = range.getFirstNode(); + var nextNode = start.nextSibling; + // jQuery does fancy html-to-DOM compat stuff here: + $(start).after(html); + // now the DOM elements are physically inside the DomRange, + // but they haven't been added yet (so they aren't tracked + // and UI hooks haven't been called; they are foreign + // matter). + + var wire = function (n) { + // returns what ended up in the place of `n`: + // component, node, or null + if (n.nodeType === 8) { // COMMENT + if (componentsToRender) { + var kind = componentsToRender[n.nodeValue]; + if (kind || kind === null) { + var comp = UI.renderTo( + kind, null, + n.parentNode, n, component); + n.parentNode.removeChild(n); + delete componentsToRender[n.nodeValue]; + return comp; // may be null + } + } + } else if (n.nodeType === 1) { // ELEMENT + if (attrManagersToWire) { + // detect elements with reactive attributes + for (var i = 0; i < maxDataAttrNumber; i++) { + var attrName = dataAttrs[i]; + var elemId = n.getAttribute(attrName); + if (elemId) { + var mgr = attrManagersToWire[elemId]; + if (mgr) { + mgr.wire(n); + // XXX bad to do this immediately for + // some reason? we used to delay it using + // `onNextBuilt` + mgr.start(); + } + n.removeAttribute(attrName); + } + } + } + } + return n; + }; + + // walk nodes and replace comments with Components + var walk = function (parentNode) { + // TODO -- this is `recurse` except it just calls `wire` + // for the hard stuff. + }; + + // top level + for (var n = start.nextSibling, m; + n && n !== nextNode; + n = m) { + m = n.nextSibling; + var result = wire(n); + if (result) { + if (result.dom) + // XXX won't be necessary when DomRange takes + // components in: + result = result.dom; + range.add(result); + if (result.firstChild) + walk(result); + } + } - // walk div and replace comments with Components var recurse = function (parent) { var n = parent.firstChild; while (n) { var next = n.nextSibling; if (n.nodeType === 8) { // COMMENT - if (componentsToAttach) { - var comp = componentsToAttach[n.nodeValue]; + if (componentsToRender) { + var comp = componentsToRender[n.nodeValue]; if (comp) { - if (parent === root) { - if (n === root.firstChild) - start = comp; - if (n === root.lastChild) - end = comp; - } if (! comp.isInited) { component.add(comp); } else if (comp.parent !== component) { @@ -283,7 +270,7 @@ makeRenderBuffer = function (range, options) { } comp._attach(parent, n); parent.removeChild(n); - delete componentsToAttach[n.nodeValue]; + delete componentsToRender[n.nodeValue]; } } } else if (n.nodeType === 1) { // ELEMENT @@ -315,27 +302,22 @@ makeRenderBuffer = function (range, options) { } }; - if (componentsToAttach || attrManagersToWire) + if (componentsToRender || attrManagersToWire) recurse(root); // We should have attached all specified components, but // if the comments we generated somehow didn't turn into // comments (due to bad HTML) we won't have found them, // in which case we clean them up here just to be safe. - if (componentsToAttach) - for (var k in componentsToAttach) - componentsToAttach[k].destroy(); + // + // XXXX revisit when there's "destroy" again +// if (componentsToRender) +// for (var k in componentsToRender) +// componentsToRender[k].destroy(); // aid GC - componentsToAttach = null; + componentsToRender = null; attrManagersToWire = null; - - return { - // start and end will both be null if div is empty - start: start, - end: end - }; - }; return buf;