Files
meteor/packages/ui/render.js
2014-01-27 14:44:19 -08:00

387 lines
12 KiB
JavaScript

UI.Component.instantiate = function (parent) {
var kind = this;
// check arguments
if (UI.isComponent(kind)) {
if (kind.isInited)
throw new Error("A component kind is required, not an instance");
} else {
throw new Error("Expected Component kind");
}
var inst = kind.extend(); // XXX args go here
inst.isInited = true;
// XXX messy to define this here
inst.templateInstance = {
findAll: function (selector) {
// XXX check that `.dom` exists here?
return inst.dom.$(selector);
},
find: function (selector) {
var result = this.findAll(selector);
return result[0] || null;
},
firstNode: null,
lastNode: null,
data: null,
__component__: inst
};
inst.parent = (parent || null);
if (inst.init)
inst.init();
if (inst.created) {
updateTemplateInstance(inst);
inst.created.call(inst.templateInstance);
}
return inst;
};
UI.Component.render = function () {
return null;
};
// Takes a reactive function (call it `inner`) and returns a reactive function
// `outer` which is equivalent except in its reactive behavior. Specifically,
// `outer` has the following two special properties:
//
// 1. Isolation: An invocation of `outer()` only invalidates its context
// when the value of `inner()` changes. For example, `inner` may be a
// function that gets one or more Session variables and calculates a
// true/false value. `outer` blocks invalidation signals caused by the
// Session variables changing and sends a signal out only when the value
// changes between true and false (in this example). The value can be
// of any type, and it is compared with `===` unless an `equals` function
// is provided.
//
// 2. Value Sharing: The `outer` function returned by `emboxValue` can be
// shared between different contexts, for example by assigning it to an
// object as a method that can be accessed at any time, such as by
// different templates or different parts of a template. No matter
// how many times `outer` is called, `inner` is only called once until
// it changes. The most recent value is stored internally.
//
// Conceptually, an emboxed value is much like a Session variable which is
// kept up to date by an autorun. Session variables provide storage
// (value sharing) and they don't notify their listeners unless a value
// actually changes (isolation). The biggest difference is that such an
// autorun would never be stopped, and the Session variable would never be
// deleted even if it wasn't used any more. An emboxed value, on the other
// hand, automatically stops computing when it's not being used, and starts
// again when called from a reactive context. This means that when it stops
// being used, it can be completely garbage-collected.
//
// If a non-function value is supplied to `emboxValue` instead of a reactive
// function, then `outer` is still a function but it simply returns the value.
//
UI.emboxValue = function (funcOrValue, equals) {
if (typeof funcOrValue === 'function') {
var func = funcOrValue;
var curResult = null;
// There's one shared Dependency and Computation for all callers of
// our box function. It gets kicked off if necessary, and when
// there are no more dependents, it gets stopped to avoid leaking
// memory.
var resultDep = null;
var computation = null;
return function () {
if (! computation) {
if (! Deps.active) {
// Not in a reactive context. Just call func, and don't start a
// computation if there isn't one running already.
return func();
}
// No running computation, so kick one off. Since this computation
// will be shared, avoid any association with the current computation
// by using `Deps.nonreactive`.
resultDep = new Deps.Dependency;
computation = Deps.nonreactive(function () {
return Deps.autorun(function (c) {
var oldResult = curResult;
curResult = func();
if (! c.firstRun) {
if (! (equals ? equals(curResult, oldResult) :
curResult === oldResult))
resultDep.changed();
}
});
});
}
if (Deps.active) {
var isNew = resultDep.depend();
if (isNew) {
// For each new dependent, schedule a task for after that dependent's
// invalidation time and the subsequent flush. The task checks
// whether the computation should be torn down.
Deps.onInvalidate(function () {
if (resultDep && ! resultDep.hasDependents()) {
Deps.afterFlush(function () {
// use a second afterFlush to bump ourselves to the END of the
// flush, after computation re-runs have had a chance to
// re-establish their connections to our computation.
Deps.afterFlush(function () {
if (resultDep && ! resultDep.hasDependents()) {
computation.stop();
computation = null;
resultDep = null;
}
});
});
}
});
}
}
return curResult;
};
} else {
var value = funcOrValue;
var result = function () {
return value;
};
result._isEmboxedConstant = true;
return result;
}
};
////////////////////////////////////////
// Insert a DOM node or DomRange into a DOM element or DomRange.
//
// One of three things happens depending on what needs to be inserted into what:
// - `range.add` (anything into DomRange)
// - `UI.DomRange.insert` (DomRange into element)
// - `elem.insertBefore` (node into element)
//
// The optional `before` argument is an existing node or id to insert before in
// the parent element or DomRange.
var insert = function (nodeOrRange, parent, before) {
if (! parent)
throw new Error("Materialization parent required");
if (parent instanceof UI.DomRange) {
parent.add(nodeOrRange, before);
} else if (nodeOrRange instanceof UI.DomRange) {
// parent is an element; inserting a range
UI.DomRange.insert(nodeOrRange, parent, before);
} else {
// parent is an element; inserting an element
parent.insertBefore(nodeOrRange, before || null); // `null` for IE
}
};
// 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, newAttrs, handlers) {
if (handlers) {
for (var k in 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;
handler.update(elem, oldValue, null);
delete handlers[k];
}
}
}
for (var k in newAttrs) {
var handler = null;
var oldValue;
var value = newAttrs[k];
if ((! handlers) || (! handlers.hasOwnProperty(k))) {
if (value !== null) {
// make new handler
handler = makeAttributeHandler(elem, k, value);
if (handlers)
handlers[k] = handler;
oldValue = null;
}
} else {
handler = handlers[k];
oldValue = handler.value;
}
if (handler && oldValue !== value) {
handler.value = value;
handler.update(elem, oldValue, value);
if (value === null)
delete handlers[k];
}
}
};
UI.render = function (kind, parentComponent) {
if (kind.isInited)
throw new Error("Can't render component instance, only component kind");
var inst = kind.instantiate(parentComponent);
var content = (inst.render && inst.render());
var range = new UI.DomRange;
inst.dom = range;
range.component = inst;
materialize(content, range, null, inst);
range.removed = function () {
inst.isDestroyed = true;
if (inst.destroyed) {
updateTemplateInstance(inst);
inst.destroyed.call(inst.templateInstance);
}
};
return inst;
};
// 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, parentComponent) {
// XXX should do more error-checking for the case where user is supplying the tags.
// For example, check that CharRef has `html` and `str` properties and no content.
// Check that Comment has a single string child and no attributes. Etc.
if (node == null) {
// null or undefined.
// do nothinge.
} else if ((typeof node === 'string') || (typeof node === 'boolean') || (typeof node === 'number')) {
node = String(node);
insert(document.createTextNode(node), parent, before);
} else if (node instanceof Array) {
for (var i = 0; i < node.length; i++)
materialize(node[i], parent, before, parentComponent);
} else if (typeof node === 'function') {
var range = new UI.DomRange;
var rangeUpdater = Deps.autorun(function (c) {
if (! c.firstRun)
range.removeAll();
var content = node();
Deps.nonreactive(function () {
materialize(content, range, null, parentComponent);
});
});
range.removed = function () {
rangeUpdater.stop();
};
insert(range, parent, before);
} else if (node instanceof HTML.Tag) {
var tagName = HTML.properCaseTagName(node.tagName);
var elem;
if (HTML.isKnownSVGElement(tagName) && (! HTML.isKnownElement(tagName)) &&
document.createElementNS) {
elem = document.createElementNS('http://www.w3.org/2000/svg', tagName);
} else {
elem = document.createElement(node.tagName);
}
var rawAttrs = node.attrs;
var children = node.children;
if (node.tagName === 'TEXTAREA') {
rawAttrs = (rawAttrs || {});
rawAttrs.value = children;
children = [];
};
if (rawAttrs) {
var attrUpdater = Deps.autorun(function (c) {
if (! c.handlers)
c.handlers = {};
try {
var attrs = HTML.evaluateAttributes(rawAttrs, parentComponent);
var stringAttrs = {};
if (attrs) {
for (var k in attrs) {
stringAttrs[k] = HTML.toText(attrs[k], HTML.TEXTMODE.STRING,
parentComponent);
}
updateAttributes(elem, stringAttrs, c.handlers);
}
} catch (e) {
reportUIException(e);
}
});
UI.DomBackend.onRemoveElement(elem, function () {
attrUpdater.stop();
});
}
materialize(children, elem, null, parentComponent);
insert(elem, parent, before);
} else if (typeof node.instantiate === 'function') {
// component
var instance = UI.render(node, parentComponent);
insert(instance.dom, parent, before);
} else if (node instanceof HTML.CharRef) {
insert(document.createTextNode(node.str), parent, before);
} else if (node instanceof HTML.Comment) {
insert(document.createComment(node.sanitizedValue), parent, before);
} else if (node instanceof HTML.Raw) {
// Get an array of DOM nodes by using the browser's HTML parser
// (like innerHTML).
var htmlNodes = UI.DomBackend.parseHTML(node.value);
for (var i = 0; i < htmlNodes.length; i++)
insert(htmlNodes[i], parent, before);
} else if (node instanceof HTML.Special) {
throw new Error("Can't materialize Special tag, it's just an intermediate rep");
} else {
// can't get here
throw new Error("Unexpected node in htmljs: " + node);
}
};
// XXX figure out the right names, and namespace, for these.
// for example, maybe some of them go in the HTML package.
UI.materialize = materialize;
UI.body = UI.Component.extend({
kind: 'body',
contentParts: [],
render: function () {
return this.contentParts;
},
// XXX revisit how body works.
INSTANTIATED: false
});
UI.block = function (renderFunc) {
return UI.Component.extend({ render: renderFunc });
};
UI.toHTML = function (content, parentComponent) {
return HTML.toHTML(content, parentComponent);
};
UI.toRawText = function (content, parentComponent) {
return HTML.toText(content, HTML.TEXTMODE.STRING, parentComponent);
};