cleaner reactive attributes; error reporting

This commit is contained in:
David Greenspan
2013-11-01 16:41:50 -07:00
parent bfc0ce6798
commit 09fa915fb0
4 changed files with 179 additions and 8 deletions

86
packages/ui/attrs2.js Normal file
View File

@@ -0,0 +1,86 @@
// An AttributeHandler object is responsible for updating a particular attribute
// of a particular element. AttributeHandler subclasses implement
// browser-specific logic for dealing with particular attributes across
// different browsers.
//
// To define a new type of AttributeHandler, use
// `var FooHandler = AttributeHandler.extend({ update: function ... })`
// where the `update` function takes arguments `(element, oldValue, value)`.
// The `element` argument is always the same between calls to `update` on
// the same instance. `oldValue` and `value` are each either `null` or
// a Unicode string of the type that might be passed to the value argument
// of `setAttribute` (i.e. not an HTML string with character references).
// When an AttributeHandler is installed, an initial call to `update` is
// always made with `oldValue = null`. The `update` method can access
// `this.name` if the AttributeHandler class is a generic one that applies
// to multiple attribute names.
//
// AttributeHandlers can store custom properties on `this`, as long as they
// don't use the names `element`, `name`, `value`, and `oldValue`.
//
// AttributeHandlers can't influence how attributes appear in rendered HTML,
// only how they are updated after materialization as DOM.
AttributeHandler2 = function (name, value) {
this.name = name;
this.value = value;
};
_extend(AttributeHandler2.prototype, {
update: function (element, oldValue, value) {
if (value === null) {
if (oldValue !== null)
element.removeAttribute(this.name);
} else {
element.setAttribute(this.name, this.value);
}
}
});
AttributeHandler2.extend = function (options) {
var curType = this;
var subType = function AttributeHandlerSubtype(/*arguments*/) {
AttributeHandler2.apply(this, arguments);
};
subType.prototype = new curType;
subType.extend = curType.extend;
if (options)
_.extend(subType.prototype, options);
return subType;
};
// Value of a ClassHandler is either a string or an array.
var ClassHandler = AttributeHandler2.extend({
update: function (element, oldValue, value) {
var oldClasses = oldValue ? _.compact(oldValue.split(' ')) : [];
var newClasses = value ? _.compact(value.split(' ')) : [];
// the current classes on the element, which we will mutate.
var classes = _.compact(element.className.split(' '));
// optimize this later (to be asymptotically faster) if necessary
_.each(oldClasses, function (c) {
if (_.indexOf(newClasses, c) < 0)
classes = _.without(classes, c);
});
_.each(newClasses, function (c) {
if (_.indexOf(oldClasses, c) < 0 &&
_.indexOf(classes, c) < 0)
classes.push(c);
});
element.className = classes.join(' ');
}
});
// XXX make it possible for users to register attribute handlers!
makeAttributeHandler2 = function (name, value) {
// XXX will need one for 'style' on IE, though modern browsers
// seem to handle setAttribute ok.
if (name === 'class') {
return new ClassHandler(name, value);
} else {
return new AttributeHandler2(name, value);
}
};

34
packages/ui/exceptions.js Normal file
View File

@@ -0,0 +1,34 @@
var debugFunc;
// Meteor UI calls into user code in many places, and it's nice to catch exceptions
// propagated from user code immediately so that the whole system doesn't just
// break. Catching exceptions is easy; reporting them is hard. This helper
// reports exceptions.
//
// Usage:
//
// ```
// try {
// // ... someStuff ...
// } catch (e) {
// reportUIException(e);
// }
// ```
//
// An optional second argument overrides the default message.
reportUIException = function (e, msg) {
if (! debugFunc)
// adapted from Deps
debugFunc = function () {
return (typeof Meteor !== "undefined" ? Meteor._debug :
((typeof console !== "undefined") && console.log ? console.log :
function () {}));
};
// In Chrome, `e.stack` is a multiline string that starts with the message
// and contains a stack trace. Furthermore, `console.log` makes it clickable.
// `console.log` supplies the space between the two arguments.
debugFunc(msg || 'Exception in Meteor UI:', e.stack || e.message);
};

View File

@@ -13,13 +13,14 @@ Package.on_use(function (api) {
api.use('minimongo'); // for idStringify
api.use('observe-sequence');
api.add_files(['base.js']);
api.add_files(['exceptions.js', 'base.js']);
api.add_files(['dombackend.js',
'dombackend2.js',
'domrange.js'], 'client');
api.add_files(['attrs.js',
'attrs2.js',
'render.js',
'render2.js',
'components.js',

View File

@@ -106,6 +106,46 @@ var insert = function (nodeOrRange, parent, before) {
}
};
// Update attributes on `elem` to the dictionary `attrs`, using the
// dictionary of existing `handlers` if provided.
//
// 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) {
if (handlers) {
for (var k in handlers) {
if (! attrs.hasOwnProperty(k)) {
// remove old attributes (and handlers)
var handler = handlers[k];
var oldValue = handler.value;
handler.value = null;
handler.update(elem, oldValue, null);
delete handlers[k];
}
}
}
for (var k in attrs) {
var handler;
var oldValue;
var value = attributeValueToString(attrs[k]);
if ((! handlers) || (! handlers.hasOwnProperty(k))) {
// 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);
}
};
// Convert the pseudoDOM `node` into reactive DOM nodes and insert them
// into the element or DomRange `parent`, before the node or id `before`.
var materialize = function (node, parent, before) {
@@ -129,13 +169,23 @@ var materialize = function (node, parent, before) {
var elem = document.createElement(node.tagName);
if (node.attrs) {
var attrs = node.attrs;
// XXX make attributes reactive!
if (typeof attrs === 'function')
attrs = attrs();
_.each(attrs, function (v, k) {
checkAttributeName(k);
elem.setAttribute(k, attributeValueToString(v));
});
if (typeof attrs === 'function') {
var attrUpdater = Deps.autorun(function (c) {
if (! c.handlers)
c.handlers = {};
try {
updateAttributes(elem, attrs(), c.handlers);
} catch (e) {
reportUIException(e);
}
});
UI.DomBackend2.onRemoveElement(elem, function () {
attrUpdater.stop();
});
} else {
updateAttributes(elem, attrs);
}
}
_.each(node, function (child) {
materialize(child, elem);