mirror of
https://github.com/meteor/meteor.git
synced 2026-05-02 03:01:46 -04:00
If you Blaze.remove a View that is a template rendered by Blaze.renderWithData, or included with an implicit “with” as in `{{> myTemplate someData}}`, Blaze will now remove the DOM of the template, and also remove the implicit “with” (in both cases).
As background, Blaze.remove only works on Views that were attached directly under a DOM element, not inside another View. Blaze.render always attaches the resulting View directly under a DOM element, but Blaze.renderWithData creates a “with” View around the template View. Previously, you could Blaze.remove the “with” View (which is returned by renderWithData), but if you got access to the template’s View some other way and tried to remove it directly, nothing would happen. Now, the correct thing happens (the View is destroyed and the DOM is removed).
In the future, we should consider whether Blaze.remove should work on arbitrary Views, not just Views attached under a DOM element.
777 lines
26 KiB
JavaScript
777 lines
26 KiB
JavaScript
/// [new] Blaze.View([name], renderMethod)
|
|
///
|
|
/// Blaze.View is the building block of reactive DOM. Views have
|
|
/// the following features:
|
|
///
|
|
/// * lifecycle callbacks - Views are created, rendered, and destroyed,
|
|
/// and callbacks can be registered to fire when these things happen.
|
|
///
|
|
/// * parent pointer - A View points to its parentView, which is the
|
|
/// View that caused it to be rendered. These pointers form a
|
|
/// hierarchy or tree of Views.
|
|
///
|
|
/// * render() method - A View's render() method specifies the DOM
|
|
/// (or HTML) content of the View. If the method establishes
|
|
/// reactive dependencies, it may be re-run.
|
|
///
|
|
/// * a DOMRange - If a View is rendered to DOM, its position and
|
|
/// extent in the DOM are tracked using a DOMRange object.
|
|
///
|
|
/// When a View is constructed by calling Blaze.View, the View is
|
|
/// not yet considered "created." It doesn't have a parentView yet,
|
|
/// and no logic has been run to initialize the View. All real
|
|
/// work is deferred until at least creation time, when the onViewCreated
|
|
/// callbacks are fired, which happens when the View is "used" in
|
|
/// some way that requires it to be rendered.
|
|
///
|
|
/// ...more lifecycle stuff
|
|
///
|
|
/// `name` is an optional string tag identifying the View. The only
|
|
/// time it's used is when looking in the View tree for a View of a
|
|
/// particular name; for example, data contexts are stored on Views
|
|
/// of name "with". Names are also useful when debugging, so in
|
|
/// general it's good for functions that create Views to set the name.
|
|
/// Views associated with templates have names of the form "Template.foo".
|
|
|
|
/**
|
|
* @class
|
|
* @summary Constructor for a View, which represents a reactive region of DOM.
|
|
* @locus Client
|
|
* @param {String} [name] Optional. A name for this type of View. See [`view.name`](#view_name).
|
|
* @param {Function} renderFunction A function that returns [*renderable content*](#renderable_content). In this function, `this` is bound to the View.
|
|
*/
|
|
Blaze.View = function (name, render) {
|
|
if (! (this instanceof Blaze.View))
|
|
// called without `new`
|
|
return new Blaze.View(name, render);
|
|
|
|
if (typeof name === 'function') {
|
|
// omitted "name" argument
|
|
render = name;
|
|
name = '';
|
|
}
|
|
this.name = name;
|
|
this._render = render;
|
|
|
|
this._callbacks = {
|
|
created: null,
|
|
rendered: null,
|
|
destroyed: null
|
|
};
|
|
|
|
// Setting all properties here is good for readability,
|
|
// and also may help Chrome optimize the code by keeping
|
|
// the View object from changing shape too much.
|
|
this.isCreated = false;
|
|
this._isCreatedForExpansion = false;
|
|
this.isRendered = false;
|
|
this._isAttached = false;
|
|
this.isDestroyed = false;
|
|
this._isInRender = false;
|
|
this.parentView = null;
|
|
this._domrange = null;
|
|
// This flag is normally set to false except for the cases when view's parent
|
|
// was generated as part of expanding some syntactic sugar expressions or
|
|
// methods.
|
|
// Ex.: Blaze.renderWithData is an equivalent to creating a view with regular
|
|
// Blaze.render and wrapping it into {{#with data}}{{/with}} view. Since the
|
|
// users don't know anything about these generated parent views, Blaze needs
|
|
// this information to be available on views to make smarter decisions. For
|
|
// example: removing the generated parent view with the view on Blaze.remove.
|
|
this._hasGeneratedParent = false;
|
|
|
|
this.renderCount = 0;
|
|
};
|
|
|
|
Blaze.View.prototype._render = function () { return null; };
|
|
|
|
Blaze.View.prototype.onViewCreated = function (cb) {
|
|
this._callbacks.created = this._callbacks.created || [];
|
|
this._callbacks.created.push(cb);
|
|
};
|
|
|
|
Blaze.View.prototype._onViewRendered = function (cb) {
|
|
this._callbacks.rendered = this._callbacks.rendered || [];
|
|
this._callbacks.rendered.push(cb);
|
|
};
|
|
|
|
Blaze.View.prototype.onViewReady = function (cb) {
|
|
var self = this;
|
|
var fire = function () {
|
|
Tracker.afterFlush(function () {
|
|
if (! self.isDestroyed) {
|
|
Blaze._withCurrentView(self, function () {
|
|
cb.call(self);
|
|
});
|
|
}
|
|
});
|
|
};
|
|
self._onViewRendered(function onViewRendered() {
|
|
if (self.isDestroyed)
|
|
return;
|
|
if (! self._domrange.attached)
|
|
self._domrange.onAttached(fire);
|
|
else
|
|
fire();
|
|
});
|
|
};
|
|
|
|
Blaze.View.prototype.onViewDestroyed = function (cb) {
|
|
this._callbacks.destroyed = this._callbacks.destroyed || [];
|
|
this._callbacks.destroyed.push(cb);
|
|
};
|
|
|
|
/// View#autorun(func)
|
|
///
|
|
/// Sets up a Tracker autorun that is "scoped" to this View in two
|
|
/// important ways: 1) Blaze.currentView is automatically set
|
|
/// on every re-run, and 2) the autorun is stopped when the
|
|
/// View is destroyed. As with Tracker.autorun, the first run of
|
|
/// the function is immediate, and a Computation object that can
|
|
/// be used to stop the autorun is returned.
|
|
///
|
|
/// View#autorun is meant to be called from View callbacks like
|
|
/// onViewCreated, or from outside the rendering process. It may not
|
|
/// be called before the onViewCreated callbacks are fired (too early),
|
|
/// or from a render() method (too confusing).
|
|
///
|
|
/// Typically, autoruns that update the state
|
|
/// of the View (as in Blaze.With) should be started from an onViewCreated
|
|
/// callback. Autoruns that update the DOM should be started
|
|
/// from either onViewCreated (guarded against the absence of
|
|
/// view._domrange), or onViewReady.
|
|
Blaze.View.prototype.autorun = function (f, _inViewScope) {
|
|
var self = this;
|
|
|
|
// The restrictions on when View#autorun can be called are in order
|
|
// to avoid bad patterns, like creating a Blaze.View and immediately
|
|
// calling autorun on it. A freshly created View is not ready to
|
|
// have logic run on it; it doesn't have a parentView, for example.
|
|
// It's when the View is materialized or expanded that the onViewCreated
|
|
// handlers are fired and the View starts up.
|
|
//
|
|
// Letting the render() method call `this.autorun()` is problematic
|
|
// because of re-render. The best we can do is to stop the old
|
|
// autorun and start a new one for each render, but that's a pattern
|
|
// we try to avoid internally because it leads to helpers being
|
|
// called extra times, in the case where the autorun causes the
|
|
// view to re-render (and thus the autorun to be torn down and a
|
|
// new one established).
|
|
//
|
|
// We could lift these restrictions in various ways. One interesting
|
|
// idea is to allow you to call `view.autorun` after instantiating
|
|
// `view`, and automatically wrap it in `view.onViewCreated`, deferring
|
|
// the autorun so that it starts at an appropriate time. However,
|
|
// then we can't return the Computation object to the caller, because
|
|
// it doesn't exist yet.
|
|
if (! self.isCreated) {
|
|
throw new Error("View#autorun must be called from the created callback at the earliest");
|
|
}
|
|
if (this._isInRender) {
|
|
throw new Error("Can't call View#autorun from inside render(); try calling it from the created or rendered callback");
|
|
}
|
|
if (Tracker.active) {
|
|
throw new Error("Can't call View#autorun from a Tracker Computation; try calling it from the created or rendered callback");
|
|
}
|
|
|
|
var c = Tracker.autorun(function viewAutorun(c) {
|
|
return Blaze._withCurrentView(_inViewScope || self, function () {
|
|
return f.call(self, c);
|
|
});
|
|
});
|
|
self.onViewDestroyed(function () { c.stop(); });
|
|
|
|
return c;
|
|
};
|
|
|
|
Blaze.View.prototype.firstNode = function () {
|
|
if (! this._isAttached)
|
|
throw new Error("View must be attached before accessing its DOM");
|
|
|
|
return this._domrange.firstNode();
|
|
};
|
|
|
|
Blaze.View.prototype.lastNode = function () {
|
|
if (! this._isAttached)
|
|
throw new Error("View must be attached before accessing its DOM");
|
|
|
|
return this._domrange.lastNode();
|
|
};
|
|
|
|
Blaze._fireCallbacks = function (view, which) {
|
|
Blaze._withCurrentView(view, function () {
|
|
Tracker.nonreactive(function fireCallbacks() {
|
|
var cbs = view._callbacks[which];
|
|
for (var i = 0, N = (cbs && cbs.length); i < N; i++)
|
|
cbs[i].call(view);
|
|
});
|
|
});
|
|
};
|
|
|
|
Blaze._createView = function (view, parentView, forExpansion) {
|
|
if (view.isCreated)
|
|
throw new Error("Can't render the same View twice");
|
|
|
|
view.parentView = (parentView || null);
|
|
view.isCreated = true;
|
|
if (forExpansion)
|
|
view._isCreatedForExpansion = true;
|
|
|
|
Blaze._fireCallbacks(view, 'created');
|
|
};
|
|
|
|
Blaze._materializeView = function (view, parentView) {
|
|
Blaze._createView(view, parentView);
|
|
|
|
var domrange;
|
|
var lastHtmljs;
|
|
// We don't expect to be called in a Computation, but just in case,
|
|
// wrap in Tracker.nonreactive.
|
|
Tracker.nonreactive(function () {
|
|
view.autorun(function doRender(c) {
|
|
// `view.autorun` sets the current view.
|
|
view.renderCount++;
|
|
view._isInRender = true;
|
|
// Any dependencies that should invalidate this Computation come
|
|
// from this line:
|
|
var htmljs = view._render();
|
|
view._isInRender = false;
|
|
|
|
Tracker.nonreactive(function doMaterialize() {
|
|
var materializer = new Blaze._DOMMaterializer({parentView: view});
|
|
var rangesAndNodes = materializer.visit(htmljs, []);
|
|
if (c.firstRun || ! Blaze._isContentEqual(lastHtmljs, htmljs)) {
|
|
if (c.firstRun) {
|
|
domrange = new Blaze._DOMRange(rangesAndNodes);
|
|
view._domrange = domrange;
|
|
domrange.view = view;
|
|
view.isRendered = true;
|
|
} else {
|
|
domrange.setMembers(rangesAndNodes);
|
|
}
|
|
Blaze._fireCallbacks(view, 'rendered');
|
|
}
|
|
});
|
|
lastHtmljs = htmljs;
|
|
|
|
// Causes any nested views to stop immediately, not when we call
|
|
// `setMembers` the next time around the autorun. Otherwise,
|
|
// helpers in the DOM tree to be replaced might be scheduled
|
|
// to re-run before we have a chance to stop them.
|
|
Tracker.onInvalidate(function () {
|
|
domrange.destroyMembers();
|
|
});
|
|
});
|
|
|
|
var teardownHook = null;
|
|
|
|
domrange.onAttached(function attached(range, element) {
|
|
view._isAttached = true;
|
|
|
|
teardownHook = Blaze._DOMBackend.Teardown.onElementTeardown(
|
|
element, function teardown() {
|
|
Blaze._destroyView(view, true /* _skipNodes */);
|
|
});
|
|
});
|
|
|
|
// tear down the teardown hook
|
|
view.onViewDestroyed(function () {
|
|
teardownHook && teardownHook.stop();
|
|
teardownHook = null;
|
|
});
|
|
});
|
|
|
|
return domrange;
|
|
};
|
|
|
|
// Expands a View to HTMLjs, calling `render` recursively on all
|
|
// Views and evaluating any dynamic attributes. Calls the `created`
|
|
// callback, but not the `materialized` or `rendered` callbacks.
|
|
// Destroys the view immediately, unless called in a Tracker Computation,
|
|
// in which case the view will be destroyed when the Computation is
|
|
// invalidated. If called in a Tracker Computation, the result is a
|
|
// reactive string; that is, the Computation will be invalidated
|
|
// if any changes are made to the view or subviews that might affect
|
|
// the HTML.
|
|
Blaze._expandView = function (view, parentView) {
|
|
Blaze._createView(view, parentView, true /*forExpansion*/);
|
|
|
|
view._isInRender = true;
|
|
var htmljs = Blaze._withCurrentView(view, function () {
|
|
return view._render();
|
|
});
|
|
view._isInRender = false;
|
|
|
|
var result = Blaze._expand(htmljs, view);
|
|
|
|
if (Tracker.active) {
|
|
Tracker.onInvalidate(function () {
|
|
Blaze._destroyView(view);
|
|
});
|
|
} else {
|
|
Blaze._destroyView(view);
|
|
}
|
|
|
|
return result;
|
|
};
|
|
|
|
// Options: `parentView`
|
|
Blaze._HTMLJSExpander = HTML.TransformingVisitor.extend();
|
|
Blaze._HTMLJSExpander.def({
|
|
visitObject: function (x) {
|
|
if (x instanceof Blaze.Template)
|
|
x = x.constructView();
|
|
if (x instanceof Blaze.View)
|
|
return Blaze._expandView(x, this.parentView);
|
|
|
|
// this will throw an error; other objects are not allowed!
|
|
return HTML.TransformingVisitor.prototype.visitObject.call(this, x);
|
|
},
|
|
visitAttributes: function (attrs) {
|
|
// expand dynamic attributes
|
|
if (typeof attrs === 'function')
|
|
attrs = Blaze._withCurrentView(this.parentView, attrs);
|
|
|
|
// call super (e.g. for case where `attrs` is an array)
|
|
return HTML.TransformingVisitor.prototype.visitAttributes.call(this, attrs);
|
|
},
|
|
visitAttribute: function (name, value, tag) {
|
|
// expand attribute values that are functions. Any attribute value
|
|
// that contains Views must be wrapped in a function.
|
|
if (typeof value === 'function')
|
|
value = Blaze._withCurrentView(this.parentView, value);
|
|
|
|
return HTML.TransformingVisitor.prototype.visitAttribute.call(
|
|
this, name, value, tag);
|
|
}
|
|
});
|
|
|
|
// Return Blaze.currentView, but only if it is being rendered
|
|
// (i.e. we are in its render() method).
|
|
var currentViewIfRendering = function () {
|
|
var view = Blaze.currentView;
|
|
return (view && view._isInRender) ? view : null;
|
|
};
|
|
|
|
Blaze._expand = function (htmljs, parentView) {
|
|
parentView = parentView || currentViewIfRendering();
|
|
return (new Blaze._HTMLJSExpander(
|
|
{parentView: parentView})).visit(htmljs);
|
|
};
|
|
|
|
Blaze._expandAttributes = function (attrs, parentView) {
|
|
parentView = parentView || currentViewIfRendering();
|
|
return (new Blaze._HTMLJSExpander(
|
|
{parentView: parentView})).visitAttributes(attrs);
|
|
};
|
|
|
|
Blaze._destroyView = function (view, _skipNodes) {
|
|
if (view.isDestroyed)
|
|
return;
|
|
view.isDestroyed = true;
|
|
|
|
Blaze._fireCallbacks(view, 'destroyed');
|
|
|
|
// Destroy views and elements recursively. If _skipNodes,
|
|
// only recurse up to views, not elements, for the case where
|
|
// the backend (jQuery) is recursing over the elements already.
|
|
|
|
if (view._domrange)
|
|
view._domrange.destroyMembers(_skipNodes);
|
|
};
|
|
|
|
Blaze._destroyNode = function (node) {
|
|
if (node.nodeType === 1)
|
|
Blaze._DOMBackend.Teardown.tearDownElement(node);
|
|
};
|
|
|
|
// Are the HTMLjs entities `a` and `b` the same? We could be
|
|
// more elaborate here but the point is to catch the most basic
|
|
// cases.
|
|
Blaze._isContentEqual = function (a, b) {
|
|
if (a instanceof HTML.Raw) {
|
|
return (b instanceof HTML.Raw) && (a.value === b.value);
|
|
} else if (a == null) {
|
|
return (b == null);
|
|
} else {
|
|
return (a === b) &&
|
|
((typeof a === 'number') || (typeof a === 'boolean') ||
|
|
(typeof a === 'string'));
|
|
}
|
|
};
|
|
|
|
/**
|
|
* @summary The View corresponding to the current template helper, event handler, callback, or autorun. If there isn't one, `null`.
|
|
* @locus Client
|
|
* @type {Blaze.View}
|
|
*/
|
|
Blaze.currentView = null;
|
|
|
|
Blaze._withCurrentView = function (view, func) {
|
|
var oldView = Blaze.currentView;
|
|
try {
|
|
Blaze.currentView = view;
|
|
return func();
|
|
} finally {
|
|
Blaze.currentView = oldView;
|
|
}
|
|
};
|
|
|
|
// Blaze.render publicly takes a View or a Template.
|
|
// Privately, it takes any HTMLJS (extended with Views and Templates)
|
|
// except null or undefined, or a function that returns any extended
|
|
// HTMLJS.
|
|
var checkRenderContent = function (content) {
|
|
if (content === null)
|
|
throw new Error("Can't render null");
|
|
if (typeof content === 'undefined')
|
|
throw new Error("Can't render undefined");
|
|
|
|
if ((content instanceof Blaze.View) ||
|
|
(content instanceof Blaze.Template) ||
|
|
(typeof content === 'function'))
|
|
return;
|
|
|
|
try {
|
|
// Throw if content doesn't look like HTMLJS at the top level
|
|
// (i.e. verify that this is an HTML.Tag, or an array,
|
|
// or a primitive, etc.)
|
|
(new HTML.Visitor).visit(content);
|
|
} catch (e) {
|
|
// Make error message suitable for public API
|
|
throw new Error("Expected Template or View");
|
|
}
|
|
};
|
|
|
|
// For Blaze.render and Blaze.toHTML, take content and
|
|
// wrap it in a View, unless it's a single View or
|
|
// Template already.
|
|
var contentAsView = function (content) {
|
|
checkRenderContent(content);
|
|
|
|
if (content instanceof Blaze.Template) {
|
|
return content.constructView();
|
|
} else if (content instanceof Blaze.View) {
|
|
return content;
|
|
} else {
|
|
var func = content;
|
|
if (typeof func !== 'function') {
|
|
func = function () {
|
|
return content;
|
|
};
|
|
}
|
|
return Blaze.View('render', func);
|
|
}
|
|
};
|
|
|
|
// For Blaze.renderWithData and Blaze.toHTMLWithData, wrap content
|
|
// in a function, if necessary, so it can be a content arg to
|
|
// a Blaze.With.
|
|
var contentAsFunc = function (content) {
|
|
checkRenderContent(content);
|
|
|
|
if (typeof content !== 'function') {
|
|
return function () {
|
|
return content;
|
|
};
|
|
} else {
|
|
return content;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* @summary Renders a template or View to DOM nodes and inserts it into the DOM, returning a rendered [View](#blaze_view) which can be passed to [`Blaze.remove`](#blaze_remove).
|
|
* @locus Client
|
|
* @param {Template|Blaze.View} templateOrView The template (e.g. `Template.myTemplate`) or View object to render. If a template, a View object is [constructed](#template_constructview). If a View, it must be an unrendered View, which becomes a rendered View and is returned.
|
|
* @param {DOMNode} parentNode The node that will be the parent of the rendered template. It must be an Element node.
|
|
* @param {DOMNode} [nextNode] Optional. If provided, must be a child of <em>parentNode</em>; the template will be inserted before this node. If not provided, the template will be inserted as the last child of parentNode.
|
|
* @param {Blaze.View} [parentView] Optional. If provided, it will be set as the rendered View's [`parentView`](#view_parentview).
|
|
*/
|
|
Blaze.render = function (content, parentElement, nextNode, parentView) {
|
|
if (! parentElement) {
|
|
Blaze._warn("Blaze.render without a parent element is deprecated. " +
|
|
"You must specify where to insert the rendered content.");
|
|
}
|
|
|
|
if (nextNode instanceof Blaze.View) {
|
|
// handle omitted nextNode
|
|
parentView = nextNode;
|
|
nextNode = null;
|
|
}
|
|
|
|
// parentElement must be a DOM node. in particular, can't be the
|
|
// result of a call to `$`. Can't check if `parentElement instanceof
|
|
// Node` since 'Node' is undefined in IE8.
|
|
if (parentElement && typeof parentElement.nodeType !== 'number')
|
|
throw new Error("'parentElement' must be a DOM node");
|
|
if (nextNode && typeof nextNode.nodeType !== 'number') // 'nextNode' is optional
|
|
throw new Error("'nextNode' must be a DOM node");
|
|
|
|
parentView = parentView || currentViewIfRendering();
|
|
|
|
var view = contentAsView(content);
|
|
Blaze._materializeView(view, parentView);
|
|
|
|
if (parentElement) {
|
|
view._domrange.attach(parentElement, nextNode);
|
|
}
|
|
|
|
return view;
|
|
};
|
|
|
|
Blaze.insert = function (view, parentElement, nextNode) {
|
|
Blaze._warn("Blaze.insert has been deprecated. Specify where to insert the " +
|
|
"rendered content in the call to Blaze.render.");
|
|
|
|
if (! (view && (view._domrange instanceof Blaze._DOMRange)))
|
|
throw new Error("Expected template rendered with Blaze.render");
|
|
|
|
view._domrange.attach(parentElement, nextNode);
|
|
};
|
|
|
|
/**
|
|
* @summary Renders a template or View to DOM nodes with a data context. Otherwise identical to `Blaze.render`.
|
|
* @locus Client
|
|
* @param {Template|Blaze.View} templateOrView The template (e.g. `Template.myTemplate`) or View object to render.
|
|
* @param {Object|Function} data The data context to use, or a function returning a data context. If a function is provided, it will be reactively re-run.
|
|
* @param {DOMNode} parentNode The node that will be the parent of the rendered template. It must be an Element node.
|
|
* @param {DOMNode} [nextNode] Optional. If provided, must be a child of <em>parentNode</em>; the template will be inserted before this node. If not provided, the template will be inserted as the last child of parentNode.
|
|
* @param {Blaze.View} [parentView] Optional. If provided, it will be set as the rendered View's [`parentView`](#view_parentview).
|
|
*/
|
|
Blaze.renderWithData = function (content, data, parentElement, nextNode, parentView) {
|
|
// We defer the handling of optional arguments to Blaze.render. At this point,
|
|
// `nextNode` may actually be `parentView`.
|
|
return Blaze.render(Blaze._TemplateWith(data, contentAsFunc(content)),
|
|
parentElement, nextNode, parentView);
|
|
};
|
|
|
|
/**
|
|
* @summary Removes a rendered View from the DOM, stopping all reactive updates and event listeners on it.
|
|
* @locus Client
|
|
* @param {Blaze.View} renderedView The return value from `Blaze.render` or `Blaze.renderWithData`.
|
|
*/
|
|
Blaze.remove = function (view) {
|
|
if (! (view && (view._domrange instanceof Blaze._DOMRange)))
|
|
throw new Error("Expected template rendered with Blaze.render");
|
|
|
|
while (view) {
|
|
if (! view.isDestroyed) {
|
|
var range = view._domrange;
|
|
if (range.attached && ! range.parentRange)
|
|
range.detach();
|
|
range.destroy();
|
|
}
|
|
|
|
view = view._hasGeneratedParent && view.parentView;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* @summary Renders a template or View to a string of HTML.
|
|
* @locus Client
|
|
* @param {Template|Blaze.View} templateOrView The template (e.g. `Template.myTemplate`) or View object from which to generate HTML.
|
|
*/
|
|
Blaze.toHTML = function (content, parentView) {
|
|
parentView = parentView || currentViewIfRendering();
|
|
|
|
return HTML.toHTML(Blaze._expandView(contentAsView(content), parentView));
|
|
};
|
|
|
|
/**
|
|
* @summary Renders a template or View to HTML with a data context. Otherwise identical to `Blaze.toHTML`.
|
|
* @locus Client
|
|
* @param {Template|Blaze.View} templateOrView The template (e.g. `Template.myTemplate`) or View object from which to generate HTML.
|
|
* @param {Object|Function} data The data context to use, or a function returning a data context.
|
|
*/
|
|
Blaze.toHTMLWithData = function (content, data, parentView) {
|
|
parentView = parentView || currentViewIfRendering();
|
|
|
|
return HTML.toHTML(Blaze._expandView(Blaze._TemplateWith(
|
|
data, contentAsFunc(content)), parentView));
|
|
};
|
|
|
|
Blaze._toText = function (htmljs, parentView, textMode) {
|
|
if (typeof htmljs === 'function')
|
|
throw new Error("Blaze._toText doesn't take a function, just HTMLjs");
|
|
|
|
if ((parentView != null) && ! (parentView instanceof Blaze.View)) {
|
|
// omitted parentView argument
|
|
textMode = parentView;
|
|
parentView = null;
|
|
}
|
|
parentView = parentView || currentViewIfRendering();
|
|
|
|
if (! textMode)
|
|
throw new Error("textMode required");
|
|
if (! (textMode === HTML.TEXTMODE.STRING ||
|
|
textMode === HTML.TEXTMODE.RCDATA ||
|
|
textMode === HTML.TEXTMODE.ATTRIBUTE))
|
|
throw new Error("Unknown textMode: " + textMode);
|
|
|
|
return HTML.toText(Blaze._expand(htmljs, parentView), textMode);
|
|
};
|
|
|
|
/**
|
|
* @summary Returns the current data context, or the data context that was used when rendering a particular DOM element or View from a Meteor template.
|
|
* @locus Client
|
|
* @param {DOMElement|Blaze.View} [elementOrView] Optional. An element that was rendered by a Meteor, or a View.
|
|
*/
|
|
Blaze.getData = function (elementOrView) {
|
|
var theWith;
|
|
|
|
if (! elementOrView) {
|
|
theWith = Blaze.getView('with');
|
|
} else if (elementOrView instanceof Blaze.View) {
|
|
var view = elementOrView;
|
|
theWith = (view.name === 'with' ? view :
|
|
Blaze.getView(view, 'with'));
|
|
} else if (typeof elementOrView.nodeType === 'number') {
|
|
if (elementOrView.nodeType !== 1)
|
|
throw new Error("Expected DOM element");
|
|
theWith = Blaze.getView(elementOrView, 'with');
|
|
} else {
|
|
throw new Error("Expected DOM element or View");
|
|
}
|
|
|
|
return theWith ? theWith.dataVar.get() : null;
|
|
};
|
|
|
|
// For back-compat
|
|
Blaze.getElementData = function (element) {
|
|
Blaze._warn("Blaze.getElementData has been deprecated. Use " +
|
|
"Blaze.getData(element) instead.");
|
|
|
|
if (element.nodeType !== 1)
|
|
throw new Error("Expected DOM element");
|
|
|
|
return Blaze.getData(element);
|
|
};
|
|
|
|
// Both arguments are optional.
|
|
|
|
/**
|
|
* @summary Gets either the current View, or the View enclosing the given DOM element.
|
|
* @locus Client
|
|
* @param {DOMElement} [element] Optional. If specified, the View enclosing `element` is returned.
|
|
*/
|
|
Blaze.getView = function (elementOrView, _viewName) {
|
|
var viewName = _viewName;
|
|
|
|
if ((typeof elementOrView) === 'string') {
|
|
// omitted elementOrView; viewName present
|
|
viewName = elementOrView;
|
|
elementOrView = null;
|
|
}
|
|
|
|
// We could eventually shorten the code by folding the logic
|
|
// from the other methods into this method.
|
|
if (! elementOrView) {
|
|
return Blaze._getCurrentView(viewName);
|
|
} else if (elementOrView instanceof Blaze.View) {
|
|
return Blaze._getParentView(elementOrView, viewName);
|
|
} else if (typeof elementOrView.nodeType === 'number') {
|
|
return Blaze._getElementView(elementOrView, viewName);
|
|
} else {
|
|
throw new Error("Expected DOM element or View");
|
|
}
|
|
};
|
|
|
|
// Gets the current view or its nearest ancestor of name
|
|
// `name`.
|
|
Blaze._getCurrentView = function (name) {
|
|
var view = Blaze.currentView;
|
|
// Better to fail in cases where it doesn't make sense
|
|
// to use Blaze._getCurrentView(). There will be a current
|
|
// view anywhere it does. You can check Blaze.currentView
|
|
// if you want to know whether there is one or not.
|
|
if (! view)
|
|
throw new Error("There is no current view");
|
|
|
|
if (name) {
|
|
while (view && view.name !== name)
|
|
view = view.parentView;
|
|
return view || null;
|
|
} else {
|
|
// Blaze._getCurrentView() with no arguments just returns
|
|
// Blaze.currentView.
|
|
return view;
|
|
}
|
|
};
|
|
|
|
Blaze._getParentView = function (view, name) {
|
|
var v = view.parentView;
|
|
|
|
if (name) {
|
|
while (v && v.name !== name)
|
|
v = v.parentView;
|
|
}
|
|
|
|
return v || null;
|
|
};
|
|
|
|
Blaze._getElementView = function (elem, name) {
|
|
var range = Blaze._DOMRange.forElement(elem);
|
|
var view = null;
|
|
while (range && ! view) {
|
|
view = (range.view || null);
|
|
if (! view) {
|
|
if (range.parentRange)
|
|
range = range.parentRange;
|
|
else
|
|
range = Blaze._DOMRange.forElement(range.parentElement);
|
|
}
|
|
}
|
|
|
|
if (name) {
|
|
while (view && view.name !== name)
|
|
view = view.parentView;
|
|
return view || null;
|
|
} else {
|
|
return view;
|
|
}
|
|
};
|
|
|
|
Blaze._addEventMap = function (view, eventMap, thisInHandler) {
|
|
thisInHandler = (thisInHandler || null);
|
|
var handles = [];
|
|
|
|
if (! view._domrange)
|
|
throw new Error("View must have a DOMRange");
|
|
|
|
view._domrange.onAttached(function attached_eventMaps(range, element) {
|
|
_.each(eventMap, function (handler, spec) {
|
|
var clauses = spec.split(/,\s+/);
|
|
// iterate over clauses of spec, e.g. ['click .foo', 'click .bar']
|
|
_.each(clauses, function (clause) {
|
|
var parts = clause.split(/\s+/);
|
|
if (parts.length === 0)
|
|
return;
|
|
|
|
var newEvents = parts.shift();
|
|
var selector = parts.join(' ');
|
|
handles.push(Blaze._EventSupport.listen(
|
|
element, newEvents, selector,
|
|
function (evt) {
|
|
if (! range.containsElement(evt.currentTarget))
|
|
return null;
|
|
var handlerThis = thisInHandler || this;
|
|
var handlerArgs = arguments;
|
|
return Blaze._withCurrentView(view, function () {
|
|
return handler.apply(handlerThis, handlerArgs);
|
|
});
|
|
},
|
|
range, function (r) {
|
|
return r.parentRange;
|
|
}));
|
|
});
|
|
});
|
|
});
|
|
|
|
view.onViewDestroyed(function () {
|
|
_.each(handles, function (h) {
|
|
h.stop();
|
|
});
|
|
handles.length = 0;
|
|
});
|
|
};
|