Files
meteor/packages/blaze/template.js
2015-02-11 18:10:33 -08:00

520 lines
16 KiB
JavaScript

// [new] Blaze.Template([viewName], renderFunction)
//
// `Blaze.Template` is the class of templates, like `Template.foo` in
// Meteor, which is `instanceof Template`.
//
// `viewKind` is a string that looks like "Template.foo" for templates
// defined by the compiler.
/**
* @class
* @summary Constructor for a Template, which is used to construct Views with particular name and content.
* @locus Client
* @param {String} [viewName] Optional. A name for Views constructed by this Template. See [`view.name`](#view_name).
* @param {Function} renderFunction A function that returns [*renderable content*](#renderable_content). This function is used as the `renderFunction` for Views constructed by this Template.
*/
Blaze.Template = function (viewName, renderFunction) {
if (! (this instanceof Blaze.Template))
// called without `new`
return new Blaze.Template(viewName, renderFunction);
if (typeof viewName === 'function') {
// omitted "viewName" argument
renderFunction = viewName;
viewName = '';
}
if (typeof viewName !== 'string')
throw new Error("viewName must be a String (or omitted)");
if (typeof renderFunction !== 'function')
throw new Error("renderFunction must be a function");
this.viewName = viewName;
this.renderFunction = renderFunction;
this.__helpers = new HelperMap;
this.__eventMaps = [];
this._callbacks = {
created: [],
rendered: [],
destroyed: []
};
};
var Template = Blaze.Template;
var HelperMap = function () {};
HelperMap.prototype.get = function (name) {
return this[' '+name];
};
HelperMap.prototype.set = function (name, helper) {
this[' '+name] = helper;
};
HelperMap.prototype.has = function (name) {
return (' '+name) in this;
};
/**
* @summary Returns true if `value` is a template object like `Template.myTemplate`.
* @locus Client
* @param {Any} value The value to test.
*/
Blaze.isTemplate = function (t) {
return (t instanceof Blaze.Template);
};
/**
* @name onCreated
* @instance
* @memberOf Template
* @summary Register a function to be called when an instance of this template is created.
* @param {Function} callback A function to be added as a callback.
* @locus Client
*/
Template.prototype.onCreated = function (cb) {
this._callbacks.created.push(cb);
};
/**
* @name onRendered
* @instance
* @memberOf Template
* @summary Register a function to be called when an instance of this template is inserted into the DOM.
* @param {Function} callback A function to be added as a callback.
* @locus Client
*/
Template.prototype.onRendered = function (cb) {
this._callbacks.rendered.push(cb);
};
/**
* @name onDestroyed
* @instance
* @memberOf Template
* @summary Register a function to be called when an instance of this template is removed from the DOM and destroyed.
* @param {Function} callback A function to be added as a callback.
* @locus Client
*/
Template.prototype.onDestroyed = function (cb) {
this._callbacks.destroyed.push(cb);
};
Template.prototype._getCallbacks = function (which) {
var self = this;
var callbacks = self[which] ? [self[which]] : [];
// Fire all callbacks added with the new API (Template.onRendered())
// as well as the old-style callback (e.g. Template.rendered) for
// backwards-compatibility.
callbacks = callbacks.concat(self._callbacks[which]);
return callbacks;
};
var fireCallbacks = function (callbacks, template) {
Template._withTemplateInstanceFunc(
function () { return template; },
function () {
for (var i = 0, N = callbacks.length; i < N; i++) {
callbacks[i].call(template);
}
});
};
Template.prototype.constructView = function (contentFunc, elseFunc) {
var self = this;
var view = Blaze.View(self.viewName, self.renderFunction);
view.template = self;
view.templateContentBlock = (
contentFunc ? new Template('(contentBlock)', contentFunc) : null);
view.templateElseBlock = (
elseFunc ? new Template('(elseBlock)', elseFunc) : null);
if (self.__eventMaps || typeof self.events === 'object') {
view._onViewRendered(function () {
if (view.renderCount !== 1)
return;
if (! self.__eventMaps.length && typeof self.events === "object") {
// Provide limited back-compat support for `.events = {...}`
// syntax. Pass `template.events` to the original `.events(...)`
// function. This code must run only once per template, in
// order to not bind the handlers more than once, which is
// ensured by the fact that we only do this when `__eventMaps`
// is falsy, and we cause it to be set now.
Template.prototype.events.call(self, self.events);
}
_.each(self.__eventMaps, function (m) {
Blaze._addEventMap(view, m, view);
});
});
}
view._templateInstance = new Blaze.TemplateInstance(view);
view.templateInstance = function () {
// Update data, firstNode, and lastNode, and return the TemplateInstance
// object.
var inst = view._templateInstance;
/**
* @instance
* @memberOf Blaze.TemplateInstance
* @name data
* @summary The data context of this instance's latest invocation.
* @locus Client
*/
inst.data = Blaze.getData(view);
if (view._domrange && !view.isDestroyed) {
inst.firstNode = view._domrange.firstNode();
inst.lastNode = view._domrange.lastNode();
} else {
// on 'created' or 'destroyed' callbacks we don't have a DomRange
inst.firstNode = null;
inst.lastNode = null;
}
return inst;
};
/**
* @name created
* @instance
* @memberOf Template
* @summary Provide a callback when an instance of a template is created.
* @locus Client
* @deprecated in 1.1
*/
// To avoid situations when new callbacks are added in between view
// instantiation and event being fired, decide on all callbacks to fire
// immediately and then fire them on the event.
var createdCallbacks = self._getCallbacks('created');
view.onViewCreated(function () {
fireCallbacks(createdCallbacks, view.templateInstance());
});
/**
* @name rendered
* @instance
* @memberOf Template
* @summary Provide a callback when an instance of a template is rendered.
* @locus Client
* @deprecated in 1.1
*/
var renderedCallbacks = self._getCallbacks('rendered');
view.onViewReady(function () {
fireCallbacks(renderedCallbacks, view.templateInstance());
});
/**
* @name destroyed
* @instance
* @memberOf Template
* @summary Provide a callback when an instance of a template is destroyed.
* @locus Client
* @deprecated in 1.1
*/
var destroyedCallbacks = self._getCallbacks('destroyed');
view.onViewDestroyed(function () {
fireCallbacks(destroyedCallbacks, view.templateInstance());
});
return view;
};
/**
* @class
* @summary The class for template instances
* @param {Blaze.View} view
* @instanceName template
*/
Blaze.TemplateInstance = function (view) {
if (! (this instanceof Blaze.TemplateInstance))
// called without `new`
return new Blaze.TemplateInstance(view);
if (! (view instanceof Blaze.View))
throw new Error("View required");
view._templateInstance = this;
/**
* @name view
* @memberOf Blaze.TemplateInstance
* @instance
* @summary The [View](#blaze_view) object for this invocation of the template.
* @locus Client
* @type {Blaze.View}
*/
this.view = view;
this.data = null;
/**
* @name firstNode
* @memberOf Blaze.TemplateInstance
* @instance
* @summary The first top-level DOM node in this template instance.
* @locus Client
* @type {DOMNode}
*/
this.firstNode = null;
/**
* @name lastNode
* @memberOf Blaze.TemplateInstance
* @instance
* @summary The last top-level DOM node in this template instance.
* @locus Client
* @type {DOMNode}
*/
this.lastNode = null;
// This dependency is used to identify state transitions in
// _subscriptionHandles which could cause the result of
// TemplateInstance#subscriptionsReady to change. Basically this is triggered
// whenever a new subscription handle is added or when a subscription handle
// is removed and they are not ready.
this._allSubsReadyDep = new Tracker.Dependency();
this._allSubsReady = false;
this._subscriptionHandles = {};
};
/**
* @summary Find all elements matching `selector` in this template instance, and return them as a JQuery object.
* @locus Client
* @param {String} selector The CSS selector to match, scoped to the template contents.
* @returns {DOMNode[]}
*/
Blaze.TemplateInstance.prototype.$ = function (selector) {
var view = this.view;
if (! view._domrange)
throw new Error("Can't use $ on template instance with no DOM");
return view._domrange.$(selector);
};
/**
* @summary Find all elements matching `selector` in this template instance.
* @locus Client
* @param {String} selector The CSS selector to match, scoped to the template contents.
* @returns {DOMElement[]}
*/
Blaze.TemplateInstance.prototype.findAll = function (selector) {
return Array.prototype.slice.call(this.$(selector));
};
/**
* @summary Find one element matching `selector` in this template instance.
* @locus Client
* @param {String} selector The CSS selector to match, scoped to the template contents.
* @returns {DOMElement}
*/
Blaze.TemplateInstance.prototype.find = function (selector) {
var result = this.$(selector);
return result[0] || null;
};
/**
* @summary A version of [Tracker.autorun](#tracker_autorun) that is stopped when the template is destroyed.
* @locus Client
* @param {Function} runFunc The function to run. It receives one argument: a Tracker.Computation object.
*/
Blaze.TemplateInstance.prototype.autorun = function (f) {
return this.view.autorun(f);
};
/**
* @summary A version of [Meteor.subscribe](#meteor_subscribe) that is stopped
* when the template is destroyed.
* @return {SubscriptionHandle} The subscription handle to the newly made
* subscription. Call `handle.stop()` to manually stop the subscription, or
* `handle.ready()` to find out if this particular subscription has loaded all
* of its inital data.
* @locus Client
* @param {String} name Name of the subscription. Matches the name of the
* server's `publish()` call.
* @param {Any} [arg1,arg2...] Optional arguments passed to publisher function
* on server.
* @param {Function|Object} [callbacks] Optional. May include `onError` and
* `onReady` callbacks. If a function is passed instead of an object, it is
* interpreted as an `onReady` callback.
*/
Blaze.TemplateInstance.prototype.subscribe = function (/* arguments */) {
var self = this;
var subHandles = self._subscriptionHandles;
var args = _.toArray(arguments);
// Duplicate logic from Meteor.subscribe
var callbacks = {};
if (args.length) {
var lastParam = _.last(args);
if (_.isFunction(lastParam)) {
callbacks.onReady = args.pop();
} else if (lastParam &&
// XXX COMPAT WITH 1.0.3.1 onError used to exist, but now we use
// onStop with an error callback instead.
_.any([lastParam.onReady, lastParam.onError, lastParam.onStop],
_.isFunction)) {
callbacks = args.pop();
}
}
var subHandle;
var oldStopped = callbacks.onStop;
callbacks.onStop = function (error) {
// When the subscription is stopped, remove it from the set of tracked
// subscriptions to avoid this list growing without bound
delete subHandles[subHandle.subscriptionId];
// Removing a subscription can only change the result of subscriptionsReady
// if we are not ready (that subscription could be the one blocking us being
// ready).
if (! self._allSubsReady) {
self._allSubsReadyDep.changed();
}
if (oldStopped) {
oldStopped(error);
}
};
args.push(callbacks);
subHandle = self.view.subscribe.call(self.view, args);
if (! _.has(subHandles, subHandle.subscriptionId)) {
subHandles[subHandle.subscriptionId] = subHandle;
// Adding a new subscription will always cause us to transition from ready
// to not ready, but if we are already not ready then this can't make us
// ready.
if (self._allSubsReady) {
self._allSubsReadyDep.changed();
}
}
return subHandle;
};
/**
* @summary A reactive function that returns true when all of the subscriptions
* called with [this.subscribe](#TemplateInstance-subscribe) are ready.
* @return {Boolean} True if all subscriptions on this template instance are
* ready.
*/
Blaze.TemplateInstance.prototype.subscriptionsReady = function () {
this._allSubsReadyDep.depend();
this._allSubsReady = _.all(this._subscriptionHandles, function (handle) {
return handle.ready();
});
return this._allSubsReady;
};
/**
* @summary Specify template helpers available to this template.
* @locus Client
* @param {Object} helpers Dictionary of helper functions by name.
*/
Template.prototype.helpers = function (dict) {
for (var k in dict)
this.__helpers.set(k, dict[k]);
};
// Kind of like Blaze.currentView but for the template instance.
// This is a function, not a value -- so that not all helpers
// are implicitly dependent on the current template instance's `data` property,
// which would make them dependenct on the data context of the template
// inclusion.
Template._currentTemplateInstanceFunc = null;
Template._withTemplateInstanceFunc = function (templateInstanceFunc, func) {
if (typeof func !== 'function')
throw new Error("Expected function, got: " + func);
var oldTmplInstanceFunc = Template._currentTemplateInstanceFunc;
try {
Template._currentTemplateInstanceFunc = templateInstanceFunc;
return func();
} finally {
Template._currentTemplateInstanceFunc = oldTmplInstanceFunc;
}
};
/**
* @summary Specify event handlers for this template.
* @locus Client
* @param {EventMap} eventMap Event handlers to associate with this template.
*/
Template.prototype.events = function (eventMap) {
var template = this;
var eventMap2 = {};
for (var k in eventMap) {
eventMap2[k] = (function (k, v) {
return function (event/*, ...*/) {
var view = this; // passed by EventAugmenter
var data = Blaze.getData(event.currentTarget);
if (data == null)
data = {};
var args = Array.prototype.slice.call(arguments);
var tmplInstanceFunc = _.bind(view.templateInstance, view);
args.splice(1, 0, tmplInstanceFunc());
return Template._withTemplateInstanceFunc(tmplInstanceFunc, function () {
return v.apply(data, args);
});
};
})(k, eventMap[k]);
}
template.__eventMaps.push(eventMap2);
};
/**
* @function
* @name instance
* @memberOf Template
* @summary The [template instance](#template_inst) corresponding to the current template helper, event handler, callback, or autorun. If there isn't one, `null`.
* @locus Client
* @returns Blaze.TemplateInstance
*/
Template.instance = function () {
return Template._currentTemplateInstanceFunc
&& Template._currentTemplateInstanceFunc();
};
// Note: Template.currentData() is documented to take zero arguments,
// while Blaze.getData takes up to one.
/**
* @summary
*
* - Inside an `onCreated`, `onRendered`, or `onDestroyed` callback, returns
* the data context of the template.
* - Inside a helper, returns the data context of the DOM node where the helper
* was used.
* - Inside an event handler, returns the data context of the element that fired
* the event.
*
* Establishes a reactive dependency on the result.
* @locus Client
* @function
*/
Template.currentData = Blaze.getData;
/**
* @summary Accesses other data contexts that enclose the current data context.
* @locus Client
* @function
* @param {Integer} [numLevels] The number of levels beyond the current data context to look. Defaults to 1.
*/
Template.parentData = Blaze._parentData;
/**
* @summary Defines a [helper function](#template_helpers) which can be used from all templates.
* @locus Client
* @function
* @param {String} name The name of the helper function you are defining.
* @param {Function} function The helper function itself.
*/
Template.registerHelper = Blaze.registerHelper;