mirror of
https://github.com/meteor/meteor.git
synced 2026-05-02 03:01:46 -04:00
reactive attributes
This commit is contained in:
@@ -50,13 +50,25 @@ Template.item({
|
||||
|
||||
Span = UIComponent.extend({
|
||||
render: function (buf) {
|
||||
buf("<span style='background:red;margin:5px'>Hello</span>");
|
||||
buf("<span ",
|
||||
{ attrs: { style: function () {
|
||||
return ['background: ' + (Session.get('bg') || 'red') + ';',
|
||||
'margin: 5px;']; },
|
||||
foo: function () { return 'bar'; } }},
|
||||
"><br ", { attrs: { 'class': function () { return 'brrr'; } }},
|
||||
">Hello</span>");
|
||||
}
|
||||
});
|
||||
|
||||
Div = UIComponent.extend({
|
||||
render: function (buf) {
|
||||
buf("<div style='background:blue;margin:5px'>World</div>");
|
||||
buf("<div style='background:cyan;margin:5px'>World",
|
||||
"<input type=checkbox ",
|
||||
{attrs: {checked: function () {
|
||||
return Session.get('checked') ? 'checked' : null;
|
||||
}}},
|
||||
">",
|
||||
"</div>");
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -67,10 +67,11 @@ var Component = function Component() {
|
||||
return constrImpl(this, arguments, Component);
|
||||
};
|
||||
|
||||
var _extend = function (tgt, src) {
|
||||
_extend = function (tgt, src) {
|
||||
for (var k in src)
|
||||
if (src.hasOwnProperty(k))
|
||||
tgt[k] = src[k];
|
||||
return tgt;
|
||||
};
|
||||
|
||||
var setSuperType = function (subType, superType) {
|
||||
@@ -136,7 +137,8 @@ _extend(Component, {
|
||||
}
|
||||
},
|
||||
extendHooks: function (hooks) {
|
||||
_extend(this._extendHooks, hooks);
|
||||
this._extendHooks =
|
||||
_extend(_extend({}, this._extendHooks), hooks);
|
||||
},
|
||||
// make typeName count as a special option for when `create`
|
||||
// checks for special options, even though it's not
|
||||
|
||||
@@ -46,6 +46,30 @@ var compareElementIndex = function (a, b) {
|
||||
}
|
||||
};
|
||||
|
||||
// Returns true if element a contains node b and is not node b.
|
||||
var elementContains = function (a, b) {
|
||||
if (a.nodeType !== 1) /* ELEMENT */
|
||||
return false;
|
||||
if (a === b)
|
||||
return false;
|
||||
|
||||
if (a.compareDocumentPosition) {
|
||||
return a.compareDocumentPosition(b) & 0x10;
|
||||
} else {
|
||||
// Should be only old IE and maybe other old browsers here.
|
||||
// Modern Safari has both functions but seems to get contains() wrong.
|
||||
// IE can't handle b being a text node. We work around this
|
||||
// by doing a direct parent test now.
|
||||
b = b.parentNode;
|
||||
if (! (b && b.nodeType === 1)) /* ELEMENT */
|
||||
return false;
|
||||
if (a === b)
|
||||
return true;
|
||||
|
||||
return a.contains(b);
|
||||
}
|
||||
};
|
||||
|
||||
var insertNodesBefore = function (nodes, parentNode, beforeNode) {
|
||||
if (beforeNode) {
|
||||
$(nodes).insertBefore(beforeNode);
|
||||
@@ -100,51 +124,9 @@ Component({
|
||||
var html = buf.getHtml();
|
||||
|
||||
$(div).append(html);
|
||||
var start = div.firstChild;
|
||||
var end = div.lastChild;
|
||||
|
||||
var componentsToAttach = buf.componentsToAttach;
|
||||
// walk div and replace comments with Components
|
||||
|
||||
var wireUpDOM = function (parent) {
|
||||
var n = parent.firstChild;
|
||||
while (n) {
|
||||
var next = n.nextSibling;
|
||||
if (n.nodeType === 8) { // COMMENT
|
||||
var comp = componentsToAttach[n.nodeValue];
|
||||
if (comp) {
|
||||
if (parent === div) {
|
||||
if (n === div.firstChild)
|
||||
start = comp;
|
||||
if (n === div.lastChild)
|
||||
end = comp;
|
||||
}
|
||||
comp.attach(parent, n);
|
||||
parent.removeChild(n);
|
||||
delete componentsToAttach[n.nodeValue];
|
||||
}
|
||||
} else if (n.nodeType === 1) { // ELEMENT
|
||||
// recurse through DOM
|
||||
wireUpDOM(n);
|
||||
}
|
||||
n = next;
|
||||
}
|
||||
};
|
||||
|
||||
wireUpDOM(div);
|
||||
|
||||
// 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.
|
||||
for (var k in componentsToAttach)
|
||||
componentsToAttach[k].destroy();
|
||||
|
||||
return {
|
||||
// start and end will both be null if div is empty
|
||||
start: start,
|
||||
end: end
|
||||
};
|
||||
// returns info object with {start, end}
|
||||
return buf.wireUpDOM(div);
|
||||
},
|
||||
|
||||
build: function () {
|
||||
@@ -186,8 +168,15 @@ Component({
|
||||
|
||||
c.builtChildren = newChildren;
|
||||
|
||||
Deps.nonreactive(function () {
|
||||
self._built();
|
||||
// don't capture dependencies, but provide a
|
||||
// parent autorun (so that any autoruns created
|
||||
// from a built callback are stopped on rebuild)
|
||||
var x = Deps.autorun(function (c) {
|
||||
if (c.firstRun)
|
||||
self._built();
|
||||
});
|
||||
Deps.onInvalidate(function () {
|
||||
x.stop();
|
||||
});
|
||||
});
|
||||
},
|
||||
@@ -516,6 +505,26 @@ Component({
|
||||
this.insertBefore(childOrDom, after.nextSibling, parentNode);
|
||||
},
|
||||
|
||||
containsElement: function (elem) {
|
||||
var self = this;
|
||||
self._requireBuilt();
|
||||
|
||||
var firstNode = self.firstNode();
|
||||
var prevNode = firstNode.previousSibling;
|
||||
var nextNode = self.lastNode().nextSibling;
|
||||
|
||||
// element must not be "above" this component
|
||||
if (elementContains(elem, firstNode))
|
||||
return false;
|
||||
// element must not be "at or before" prevNode
|
||||
if (prevNode && compareElementIndex(prevNode, elem) >= 0)
|
||||
return false;
|
||||
// element must not be "at or after" nextNode
|
||||
if (nextNode && compareElementIndex(elem, nextNode) >= 0)
|
||||
return false;
|
||||
return true;
|
||||
},
|
||||
|
||||
$: function (selector) {
|
||||
var self = this;
|
||||
|
||||
@@ -610,6 +619,24 @@ Component({
|
||||
|
||||
oldChild.detach();
|
||||
self.insertBefore(newChild, nextNode, parentNode);
|
||||
},
|
||||
|
||||
built: function () {
|
||||
var self = this;
|
||||
var cbs = self._builtCallbacks;
|
||||
if (cbs) {
|
||||
for (var i = 0, N = cbs.length; i < N; i++)
|
||||
cbs[i](self);
|
||||
self._builtCallbacks.length = 0;
|
||||
}
|
||||
},
|
||||
|
||||
_onNextBuilt: function (cb) {
|
||||
var self = this;
|
||||
var cbs = self._builtCallbacks;
|
||||
if (! cbs)
|
||||
cbs = self._builtCallbacks = [];
|
||||
cbs.push(cb);
|
||||
}
|
||||
|
||||
// If Component is ever emptied, it gets an empty comment node.
|
||||
@@ -633,7 +660,7 @@ Component({
|
||||
|
||||
// Next up:
|
||||
//
|
||||
// - reactive attributes
|
||||
// - reactive *dynamic* attributes
|
||||
// - content()
|
||||
// - Spacebars compiler
|
||||
// - event maps
|
||||
|
||||
@@ -26,6 +26,19 @@ _UI.encodeSpecialEntities = function (text, isQuoted) {
|
||||
|
||||
var ATTRIBUTE_NAME_REGEX = /^[^\s"'>/=/]+$/;
|
||||
|
||||
// takes a known-to-be non-function, asserts it is
|
||||
// a string or an array, and produces a string
|
||||
var stringifyAttrValue = function (v) {
|
||||
if (typeof v === 'string')
|
||||
return v;
|
||||
else if (typeof v.length === 'number')
|
||||
return Array.prototype.join.call(v, ' ');
|
||||
else
|
||||
throw new Error("Expected string or array for attr value");
|
||||
};
|
||||
|
||||
var GT_OR_QUOTE = /[>'"]/;
|
||||
|
||||
makeRenderBuffer = function (component, options) {
|
||||
var isPreview = !! options && options.preview;
|
||||
|
||||
@@ -33,15 +46,60 @@ makeRenderBuffer = function (component, options) {
|
||||
var componentsToAttach = {};
|
||||
var randomString = Random.id();
|
||||
var commentUid = 1;
|
||||
var elementUid = 1;
|
||||
// Problem: In the template `<span {{foo}} {{bar}}>`, how do
|
||||
// we make foo and bar insert some HTML in the stream that
|
||||
// will allow us to find the element later? Since we don't
|
||||
// tokenize the HTML here, we can't even be sure whether
|
||||
// they are in the same tag. We can't emit a duplicate
|
||||
// extra attribute. We can emit different attributes,
|
||||
// but if every attr tag emits a different attribute, it
|
||||
// won't be efficient to find them.
|
||||
//
|
||||
// Solution: Emit different attributes, data-meteorui-id1
|
||||
// and data-meteorui-id2, not knowing if they are on the
|
||||
// same element or not. Reset the number, which is
|
||||
// `curDataAttrNumber`, if we can be absolutely sure a tag
|
||||
// has ended. To detect if a tag has definitely ended,
|
||||
// we set `greaterThanEndsTag` to true after an attr tag,
|
||||
// and set it to false if we see a quote character. If we
|
||||
// a greater-than (`>`) between the attrs and the next quote
|
||||
// character, we know the tag has ended and we can reset
|
||||
// `curDataAttrNumber` to 1. When we look for these
|
||||
// attributes, we look for attribute names with numbers
|
||||
// between 1 and `maxDataAttrNumber` inclusive.
|
||||
var curDataAttrNumber = 1;
|
||||
var maxDataAttrNumber = 0;
|
||||
var dataAttrs = [];
|
||||
var greaterThanEndsTag = false;
|
||||
|
||||
var elementsToWire = {};
|
||||
|
||||
var push = function (/*stringsToPush*/) {
|
||||
for (var i = 0, N = arguments.length;
|
||||
greaterThanEndsTag && i < N;
|
||||
i++) {
|
||||
// find first greater-than or quote
|
||||
var match = arguments[i].match(GT_OR_QUOTE);
|
||||
if (match) {
|
||||
if (match[0] == '>')
|
||||
curDataAttrNumber = 1;
|
||||
// if it's a quote, missed our chance to
|
||||
// reset the count. either way, stop looking.
|
||||
greaterThanEndsTag = false;
|
||||
}
|
||||
}
|
||||
strs.push.apply(strs, arguments);
|
||||
};
|
||||
|
||||
var handle = function (arg) {
|
||||
if (typeof arg === 'string') {
|
||||
// "HTML"
|
||||
strs.push(arg);
|
||||
push(arg);
|
||||
} else if (arg instanceof Component) {
|
||||
// Component
|
||||
var commentString = randomString + '_' + (commentUid++);
|
||||
strs.push('<!--', commentString, '-->');
|
||||
push('<!--', commentString, '-->');
|
||||
component.add(arg);
|
||||
componentsToAttach[commentString] = arg;
|
||||
} else if (arg.type) {
|
||||
@@ -51,7 +109,7 @@ makeRenderBuffer = function (component, options) {
|
||||
} else if (typeof arg.type === 'function') {
|
||||
var curType;
|
||||
component.autorun(function (c) {
|
||||
// capture dependencies on this line:
|
||||
// capture dependencies of this line:
|
||||
var type = arg.type();
|
||||
if (c.firstRun) {
|
||||
curType = type;
|
||||
@@ -82,8 +140,50 @@ makeRenderBuffer = function (component, options) {
|
||||
for (var attrName in arg.attrs) {
|
||||
if (! ATTRIBUTE_NAME_REGEX.test(attrName))
|
||||
throw new Error("Illegal HTML attribute name: " + attrName);
|
||||
// XXX push initial HTML into strs
|
||||
// XXX set up an autorun
|
||||
// the declared property of `attrs`, which may
|
||||
// be a string or array, or a function that returns
|
||||
// one.
|
||||
var attrValue = arg.attrs[attrName];
|
||||
// the current value, which may be an array or a string.
|
||||
var initialValue;
|
||||
|
||||
if (typeof attrValue === 'function') {
|
||||
// calculate the initial value without reactivity.
|
||||
// once the element exists, recalculate it with
|
||||
// an autorun.
|
||||
Deps.nonreactive(function () {
|
||||
initialValue = attrValue();
|
||||
});
|
||||
|
||||
var elemId = elementUid++;
|
||||
strs.push('data-meteorui-id', curDataAttrNumber,
|
||||
'="', elemId, '" ');
|
||||
if (curDataAttrNumber > maxDataAttrNumber) {
|
||||
dataAttrs[curDataAttrNumber-1] =
|
||||
'data-meteorui-id' + curDataAttrNumber;
|
||||
maxDataAttrNumber = curDataAttrNumber;
|
||||
}
|
||||
curDataAttrNumber++;
|
||||
greaterThanEndsTag = true;
|
||||
|
||||
elementsToWire[elemId] = {
|
||||
attrName: attrName,
|
||||
attrValueFunc: attrValue,
|
||||
initialValue: initialValue
|
||||
};
|
||||
} else {
|
||||
initialValue = attrValue;
|
||||
}
|
||||
|
||||
if (initialValue != null) {
|
||||
var stringValue = stringifyAttrValue(initialValue);
|
||||
|
||||
// don't call the `push` helper, go around it
|
||||
strs.push(' ', attrName, '="',
|
||||
_UI.encodeSpecialEntities(stringValue, true),
|
||||
'" ');
|
||||
}
|
||||
|
||||
// XXX make attr update hookable
|
||||
}
|
||||
} else {
|
||||
@@ -100,7 +200,120 @@ makeRenderBuffer = function (component, options) {
|
||||
return strs.join('');
|
||||
};
|
||||
|
||||
buf.componentsToAttach = componentsToAttach;
|
||||
buf.wireUpDOM = function (root) {
|
||||
var start = root.firstChild;
|
||||
var end = root.lastChild;
|
||||
|
||||
// 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
|
||||
var comp = componentsToAttach[n.nodeValue];
|
||||
if (comp) {
|
||||
if (parent === root) {
|
||||
if (n === root.firstChild)
|
||||
start = comp;
|
||||
if (n === root.lastChild)
|
||||
end = comp;
|
||||
}
|
||||
comp.attach(parent, n);
|
||||
parent.removeChild(n);
|
||||
delete componentsToAttach[n.nodeValue];
|
||||
}
|
||||
} else if (n.nodeType === 1) { // ELEMENT
|
||||
var elemId, callback;
|
||||
// detect elements with reactive attributes
|
||||
for (var i = 0; i < maxDataAttrNumber; i++) {
|
||||
var attrName = dataAttrs[i];
|
||||
var elemId = n.getAttribute(attrName);
|
||||
if (elemId) {
|
||||
var info = elementsToWire[elemId];
|
||||
if (info)
|
||||
info.element = n;
|
||||
n.removeAttribute(attrName);
|
||||
}
|
||||
}
|
||||
|
||||
// recurse through DOM
|
||||
recurse(n);
|
||||
}
|
||||
n = next;
|
||||
}
|
||||
};
|
||||
|
||||
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.
|
||||
for (var k in componentsToAttach)
|
||||
componentsToAttach[k].destroy();
|
||||
|
||||
// aid GC
|
||||
componentsToAttach = null;
|
||||
|
||||
// onNextBuilt callbacks run within the build
|
||||
// computation and are stopped on rebuild.
|
||||
component._onNextBuilt(function () {
|
||||
for (var k in elementsToWire) {
|
||||
var infoObj = elementsToWire[k];
|
||||
if (infoObj.element) {
|
||||
// element found during DOM traversal
|
||||
component.autorun(function (c) {
|
||||
// bring infoObj into our closure as `info`.
|
||||
// `infoObj` is not safe to close over because
|
||||
// it's in a for loop, but it is safe during
|
||||
// the first autorun which is inline.
|
||||
if (c.firstRun) {
|
||||
c.info = infoObj;
|
||||
c.curValue = infoObj.initialValue;
|
||||
}
|
||||
var info = c.info;
|
||||
if (component.stage !== Component.BUILT ||
|
||||
! component.containsElement(info.element)) {
|
||||
c.stop();
|
||||
return;
|
||||
}
|
||||
// capture dependencies of this line:
|
||||
var newValue = info.attrValueFunc();
|
||||
|
||||
var oldValue = c.curValue;
|
||||
if (newValue == null) {
|
||||
if (oldValue != null)
|
||||
info.element.removeAttribute(info.attrName);
|
||||
} else {
|
||||
var newStringValue = stringifyAttrValue(newValue);
|
||||
if (oldValue == null) {
|
||||
info.element.setAttribute(
|
||||
info.attrName, newStringValue);
|
||||
} else {
|
||||
var oldStringValue =
|
||||
stringifyAttrValue(oldValue);
|
||||
if (newStringValue !== oldStringValue) {
|
||||
info.element.setAttribute(
|
||||
info.attrName, newStringValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
c.curValue = newValue;
|
||||
});
|
||||
}
|
||||
}
|
||||
elementsToWire = null;
|
||||
});
|
||||
|
||||
return {
|
||||
// start and end will both be null if div is empty
|
||||
start: start,
|
||||
end: end
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
return buf;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user