mirror of
https://github.com/meteor/meteor.git
synced 2026-05-02 03:01:46 -04:00
reactive attribute tests and clean-up
in particular, fully support nested arrays in attribute values and null and undefined attribute values (which indicate no attribute)
This commit is contained in:
@@ -18,7 +18,11 @@ canonicalizeHtml = function(html) {
|
||||
attrs = attrs.replace(/^\s+/g, '');
|
||||
attrs = attrs.replace(/\s+$/g, '');
|
||||
attrs = attrs.replace(/\s+/g, ' ');
|
||||
var attrList = attrs.split(' ');
|
||||
// for the purpose of splitting attributes in a string like
|
||||
// 'a="b" c="d"', assume they are separated by a single space
|
||||
// and values are double-quoted, but allow for spaces inside
|
||||
// the quotes. Split on space following quote.
|
||||
var attrList = attrs.replace(/" /g, '"\u0000').split('\u0000');
|
||||
// put attributes in alphabetical order
|
||||
attrList.sort();
|
||||
var tagContents = [tagName];
|
||||
|
||||
@@ -39,7 +39,7 @@ var makeTagFunc = function (name) {
|
||||
var attrsGiven = (optAttrs && (typeof optAttrs === 'object') &&
|
||||
(typeof optAttrs.splice !== 'function'));
|
||||
var attrs = (attrsGiven ? optAttrs : null);
|
||||
if (attrsGiven && (typeof attrs.$attrs === 'funciton'))
|
||||
if (attrsGiven && (typeof attrs.$attrs === 'function'))
|
||||
attrs = attrs.$attrs;
|
||||
|
||||
var tag = new Tag(attrs);
|
||||
@@ -112,11 +112,16 @@ var insert = function (nodeOrRange, parent, before) {
|
||||
// Values in the `attrs` dictionary are in pseudo-DOM form -- a string,
|
||||
// CharRef, or array of strings and CharRefs -- but they are passed to
|
||||
// the AttributeHandler in string form.
|
||||
var updateAttributes = function(elem, attrs, handlers) {
|
||||
var updateAttributes = function(elem, newAttrs, handlers) {
|
||||
|
||||
if (handlers) {
|
||||
for (var k in handlers) {
|
||||
if (! attrs.hasOwnProperty(k)) {
|
||||
// remove old attributes (and handlers)
|
||||
if (! newAttrs.hasOwnProperty(k)) {
|
||||
// remove attributes (and handlers) for attribute names
|
||||
// that don't exist as keys of `newAttrs` and so won't
|
||||
// be visited when traversing it. (Attributes that
|
||||
// exist in the `newAttrs` object but are `null`
|
||||
// are handled later.)
|
||||
var handler = handlers[k];
|
||||
var oldValue = handler.value;
|
||||
handler.value = null;
|
||||
@@ -126,23 +131,29 @@ var updateAttributes = function(elem, attrs, handlers) {
|
||||
}
|
||||
}
|
||||
|
||||
for (var k in attrs) {
|
||||
var handler;
|
||||
for (var k in newAttrs) {
|
||||
var handler = null;
|
||||
var oldValue;
|
||||
var value = attributeValueToString(attrs[k]);
|
||||
var value = attributeValueToString(newAttrs[k]);
|
||||
if ((! handlers) || (! handlers.hasOwnProperty(k))) {
|
||||
// make new handler
|
||||
checkAttributeName(k);
|
||||
handler = makeAttributeHandler2(k, value);
|
||||
if (handlers)
|
||||
handlers[k] = handler;
|
||||
oldValue = null;
|
||||
if (value !== null) {
|
||||
// make new handler
|
||||
checkAttributeName(k);
|
||||
handler = makeAttributeHandler2(k, value);
|
||||
if (handlers)
|
||||
handlers[k] = handler;
|
||||
oldValue = null;
|
||||
}
|
||||
} else {
|
||||
handler = handlers[k];
|
||||
oldValue = handler.value;
|
||||
}
|
||||
handler.value = value;
|
||||
handler.update(elem, oldValue, value);
|
||||
if (handler) {
|
||||
handler.value = value;
|
||||
handler.update(elem, oldValue, value);
|
||||
if (value === null)
|
||||
delete handlers[k];
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -202,12 +213,17 @@ var materialize = function (node, parent, before) {
|
||||
insert(document.createTextNode(node), parent, before);
|
||||
} else if (typeof node === 'function') {
|
||||
var range = new UI.DomRange;
|
||||
Deps.autorun(function (c) {
|
||||
var rangeUpdater = Deps.autorun(function (c) {
|
||||
if (! c.firstRun)
|
||||
range.removeAll();
|
||||
|
||||
materialize(node(), range);
|
||||
});
|
||||
range.parented = function () {
|
||||
UI.DomBackend2.onRemoveElement(range.parentNode(), function () {
|
||||
rangeUpdater.stop();
|
||||
});
|
||||
};
|
||||
insert(range, parent, before);
|
||||
} else if (node == null) {
|
||||
// null or undefined.
|
||||
@@ -233,67 +249,88 @@ var properCaseTagName = function (name) {
|
||||
};
|
||||
|
||||
// Takes an attribute value -- i.e. a string, CharRef, or array of strings and
|
||||
// CharRefs -- and renders it as a double-quoted string literal suitable for an
|
||||
// HTML attribute value.
|
||||
var attributeValueToQuotedString = (function () {
|
||||
|
||||
var attributeValuePartToQuotedStringPart = function (v) {
|
||||
if (typeof v === 'string') {
|
||||
return v.replace(/"/g, '"').replace(/&/g, '&');
|
||||
} else if (v.tagName === 'CharRef') {
|
||||
return v.attrs.html;
|
||||
}
|
||||
};
|
||||
|
||||
return function (v) {
|
||||
var result = '"';
|
||||
if (typeof v === 'object' && (typeof v.length === 'number') && ! v.tagName) {
|
||||
// array
|
||||
for (var i = 0; i < v.length; i++)
|
||||
result += attributeValuePartToQuotedStringPart(v[i]);
|
||||
} else {
|
||||
result += attributeValuePartToQuotedStringPart(v);
|
||||
}
|
||||
result += '"';
|
||||
return result;
|
||||
};
|
||||
})();
|
||||
|
||||
// Takes an attribute value -- i.e. a string, CharRef, or array of strings and
|
||||
// CharRefs -- and converts it to a string suitable for passing to `setAttribute`.
|
||||
var attributeValueToString = (function () {
|
||||
var attributeValuePartToString = function (v) {
|
||||
if (typeof v === 'string') {
|
||||
return v;
|
||||
} else if (v.tagName === 'CharRef') {
|
||||
return v.attrs.str;
|
||||
}
|
||||
};
|
||||
|
||||
return function (v) {
|
||||
if (typeof v === 'object' && (typeof v.length === 'number') && ! v.tagName) {
|
||||
// array
|
||||
var result = '';
|
||||
for (var i = 0; i < v.length; i++)
|
||||
result += attributeValuePartToString(v[i]);
|
||||
return result;
|
||||
} else {
|
||||
return attributeValuePartToString(v);
|
||||
}
|
||||
};
|
||||
})();
|
||||
|
||||
// Takes an attribute value -- i.e. a string, CharRef, or array of strings and
|
||||
// CharRefs -- and converts it to JavaScript code.
|
||||
var attributeValueToCode = function (v) {
|
||||
if (typeof v === 'object' && (typeof v.length === 'number') && ! v.tagName) {
|
||||
// CharRefs (and arrays) -- and renders it as a double-quoted string literal
|
||||
// suitable for an HTML attribute value (without the quotes). Returns `null`
|
||||
// if there's no attribute value (`null`, `undefined`, or empty array).
|
||||
var attributeValueToQuotedContents = function (v) {
|
||||
if (v == null) {
|
||||
// null or undefined
|
||||
return null;
|
||||
} else if (typeof v === 'string') {
|
||||
return v.replace(/"/g, '"').replace(/&/g, '&');
|
||||
} else if (v.tagName === 'CharRef') {
|
||||
return v.attrs.html;
|
||||
} else if (typeof v === 'object' && (typeof v.length === 'number')) {
|
||||
// array or tag
|
||||
if (v.tagName)
|
||||
throw new Error("Unexpected tag in attribute value: " + v.tagName);
|
||||
// array
|
||||
var partStrs = [];
|
||||
for (var i = 0; i < v.length; i++)
|
||||
partStrs.push(toCode(v[i]));
|
||||
return '[' + partStrs.join(', ') + ']';
|
||||
var parts = [];
|
||||
for (var i = 0; i < v.length; i++) {
|
||||
var part = attributeValueToQuotedContents(v[i]);
|
||||
if (part !== null)
|
||||
parts.push(part);
|
||||
}
|
||||
return parts.length ? parts.join('') : null;
|
||||
} else {
|
||||
throw new Error("Unexpected node in attribute value: " + v);
|
||||
}
|
||||
};
|
||||
|
||||
// Takes an attribute value -- i.e. a string, CharRef, or array of strings and
|
||||
// CharRefs (and arrays) -- and converts it to a string suitable for passing
|
||||
// to `setAttribute`. May return `null` to mean no attribute.
|
||||
var attributeValueToString = function (v) {
|
||||
if (v == null) {
|
||||
// null or undefined
|
||||
return null;
|
||||
} else if (typeof v === 'string') {
|
||||
return v;
|
||||
} else if (v.tagName === 'CharRef') {
|
||||
return v.attrs.str;
|
||||
} else if (typeof v === 'object' && (typeof v.length === 'number')) {
|
||||
// array or tag
|
||||
if (v.tagName)
|
||||
throw new Error("Unexpected tag in attribute value: " + v.tagName);
|
||||
// array
|
||||
var parts = [];
|
||||
for (var i = 0; i < v.length; i++) {
|
||||
var part = attributeValueToString(v[i]);
|
||||
if (part !== null)
|
||||
parts.push(part);
|
||||
}
|
||||
return parts.length ? parts.join('') : null;
|
||||
} else {
|
||||
throw new Error("Unexpected node in attribute value: " + v);
|
||||
}
|
||||
};
|
||||
|
||||
// Takes an attribute value -- i.e. a string, CharRef, or array of strings and
|
||||
// CharRefs (and arrays) -- and converts it to JavaScript code. May also return
|
||||
// `null` to indicate that the attribute should not be included because it has
|
||||
// an identically "nully" value (`null`, `undefined`, `[]`, `[[]]`, etc.).
|
||||
var attributeValueToCode = function (v) {
|
||||
if (v == null) {
|
||||
// null or undefined
|
||||
return null;
|
||||
} else if (typeof v === 'string') {
|
||||
return toJSLiteral(v);
|
||||
} else if (v.tagName === 'CharRef') {
|
||||
return toCode(v);
|
||||
} else if (typeof v === 'object' && (typeof v.length === 'number')) {
|
||||
// array or tag
|
||||
if (v.tagName)
|
||||
throw new Error("Unexpected tag in attribute value: " + v.tagName);
|
||||
// array
|
||||
var parts = [];
|
||||
for (var i = 0; i < v.length; i++) {
|
||||
var part = attributeValueToCode(v[i]);
|
||||
if (part !== null)
|
||||
parts.push(part);
|
||||
}
|
||||
return parts.length ? ('[' + parts.join(', ') + ']') : null;
|
||||
} else {
|
||||
throw new Error("Unexpected node in attribute value: " + v);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -362,9 +399,12 @@ var toHTML = function (node) {
|
||||
var attrs = node.attrs;
|
||||
if (typeof attrs === 'function')
|
||||
attrs = attrs();
|
||||
|
||||
_.each(attrs, function (v, k) {
|
||||
checkAttributeName(k);
|
||||
result += ' ' + k + '=' + attributeValueToQuotedString(v);
|
||||
v = attributeValueToQuotedContents(v);
|
||||
if (v !== null)
|
||||
result += ' ' + k + '="' + v + '"';
|
||||
});
|
||||
}
|
||||
result += '>';
|
||||
@@ -446,7 +486,9 @@ var toCode = function (node) {
|
||||
var kvStrs = [];
|
||||
_.each(node.attrs, function (v, k) {
|
||||
checkAttributeName(k);
|
||||
kvStrs.push(toObjectLiteralKey(k) + ': ' + attributeValueToCode(v));
|
||||
v = attributeValueToCode(v);
|
||||
if (v !== null)
|
||||
kvStrs.push(toObjectLiteralKey(k) + ': ' + v);
|
||||
});
|
||||
argStrs.push('{' + kvStrs.join(', ') + '}');
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ var BR = UI.Tag.BR;
|
||||
var A = UI.Tag.A;
|
||||
var UL = UI.Tag.UL;
|
||||
var LI = UI.Tag.LI;
|
||||
var SPAN = UI.Tag.SPAN;
|
||||
|
||||
Tinytest.add("ui - render2 - basic", function (test) {
|
||||
var run = function (input, expectedInnerHTML, expectedHTML, expectedCode) {
|
||||
@@ -101,3 +102,109 @@ Tinytest.add("ui - render2 - closures", function (test) {
|
||||
})();
|
||||
|
||||
});
|
||||
|
||||
Tinytest.add("ui - render2 - closure GC", function (test) {
|
||||
// test that removing parent element removes listeners and stops autoruns.
|
||||
(function () {
|
||||
var R = ReactiveVar('Hello');
|
||||
var test1 = P(function () { return R.get(); });
|
||||
|
||||
var div = document.createElement("DIV");
|
||||
materialize(test1, div);
|
||||
test.equal(canonicalizeHtml(div.innerHTML), "<p>Hello</p>");
|
||||
|
||||
R.set('World');
|
||||
Deps.flush();
|
||||
test.equal(canonicalizeHtml(div.innerHTML), "<p>World</p>");
|
||||
|
||||
test.equal(R.numListeners(), 1);
|
||||
|
||||
$(div).remove();
|
||||
|
||||
test.equal(R.numListeners(), 0);
|
||||
|
||||
R.set('Steve');
|
||||
Deps.flush();
|
||||
// should not have changed:
|
||||
test.equal(canonicalizeHtml(div.innerHTML), "<p>World</p>");
|
||||
})();
|
||||
|
||||
});
|
||||
|
||||
Tinytest.add("ui - render2 - reactive attributes", function (test) {
|
||||
(function () {
|
||||
var R = ReactiveVar({'class': ['david gre', CharRef({html: 'ë', str: '\u00eb'}), 'nspan'],
|
||||
id: 'foo'});
|
||||
|
||||
var spanCode = SPAN({$attrs: function () { return R.get(); }});
|
||||
test.equal(typeof spanCode.attrs, 'function');
|
||||
|
||||
test.equal(toHTML(spanCode), '<span class="david greënspan" id="foo"></span>');
|
||||
|
||||
test.equal(R.numListeners(), 0);
|
||||
|
||||
var div = document.createElement("DIV");
|
||||
materialize(spanCode, div);
|
||||
test.equal(canonicalizeHtml(div.innerHTML), '<span class="david gre\u00ebnspan" id="foo"></span>');
|
||||
|
||||
test.equal(R.numListeners(), 1);
|
||||
|
||||
var span = div.firstChild;
|
||||
test.equal(span.nodeName, 'SPAN');
|
||||
span.className += ' blah';
|
||||
|
||||
R.set({'class': 'david smith', id: 'bar'});
|
||||
Deps.flush();
|
||||
test.equal(canonicalizeHtml(div.innerHTML), '<span class="david blah smith" id="bar"></span>');
|
||||
test.equal(R.numListeners(), 1);
|
||||
|
||||
R.set({});
|
||||
Deps.flush();
|
||||
test.equal(canonicalizeHtml(div.innerHTML), '<span class="blah"></span>');
|
||||
test.equal(R.numListeners(), 1);
|
||||
|
||||
$(div).remove();
|
||||
|
||||
test.equal(R.numListeners(), 0);
|
||||
})();
|
||||
|
||||
// Test `null`, `undefined`, and `[]` attributes
|
||||
(function () {
|
||||
var R = ReactiveVar({id: 'foo',
|
||||
aaa: null,
|
||||
bbb: undefined,
|
||||
ccc: [],
|
||||
ddd: [null],
|
||||
eee: [undefined],
|
||||
fff: [[]],
|
||||
ggg: ['x', ['y', ['z']]]});
|
||||
|
||||
var spanCode = SPAN({$attrs: function () { return R.get(); }});
|
||||
|
||||
test.equal(toHTML(spanCode), '<span id="foo" ggg="xyz"></span>');
|
||||
test.equal(toCode(SPAN(R.get())),
|
||||
'UI.Tag.SPAN({id: "foo", ggg: ["x", ["y", ["z"]]]})');
|
||||
|
||||
var div = document.createElement("DIV");
|
||||
materialize(spanCode, div);
|
||||
var span = div.firstChild;
|
||||
test.equal(span.nodeName, 'SPAN');
|
||||
|
||||
test.equal(canonicalizeHtml(div.innerHTML), '<span ggg="xyz" id="foo"></span>');
|
||||
R.set({id: 'foo', ggg: [[], [], []]});
|
||||
Deps.flush();
|
||||
test.equal(canonicalizeHtml(div.innerHTML), '<span id="foo"></span>');
|
||||
|
||||
R.set({id: 'foo', ggg: null});
|
||||
Deps.flush();
|
||||
test.equal(canonicalizeHtml(div.innerHTML), '<span id="foo"></span>');
|
||||
|
||||
R.set({id: 'foo', ggg: ''});
|
||||
Deps.flush();
|
||||
test.equal(canonicalizeHtml(div.innerHTML), '<span ggg="" id="foo"></span>');
|
||||
|
||||
$(span).remove();
|
||||
|
||||
test.equal(R.numListeners(), 0);
|
||||
})();
|
||||
});
|
||||
Reference in New Issue
Block a user