3 ko.setTemplateEngine = function (templateEngine) {
4 if ((templateEngine != undefined) && !(templateEngine instanceof ko.templateEngine))
5 throw new Error("templateEngine must inherit from ko.templateEngine");
6 _templateEngine = templateEngine;
9 function invokeForEachNodeInContinuousRange(firstNode, lastNode, action) {
10 var node, nextInQueue = firstNode, firstOutOfRangeNode = ko.virtualElements.nextSibling(lastNode);
11 while (nextInQueue && ((node = nextInQueue) !== firstOutOfRangeNode)) {
12 nextInQueue = ko.virtualElements.nextSibling(node);
13 action(node, nextInQueue);
17 function activateBindingsOnContinuousNodeArray(continuousNodeArray, bindingContext) {
18 // To be used on any nodes that have been rendered by a template and have been inserted into some parent element
19 // Walks through continuousNodeArray (which *must* be continuous, i.e., an uninterrupted sequence of sibling nodes, because
20 // the algorithm for walking them relies on this), and for each top-level item in the virtual-element sense,
21 // (1) Does a regular "applyBindings" to associate bindingContext with this node and to activate any non-memoized bindings
22 // (2) Unmemoizes any memos in the DOM subtree (e.g., to activate bindings that had been memoized during template rewriting)
24 if (continuousNodeArray.length) {
25 var firstNode = continuousNodeArray[0],
26 lastNode = continuousNodeArray[continuousNodeArray.length - 1],
27 parentNode = firstNode.parentNode,
28 provider = ko.bindingProvider['instance'],
29 preprocessNode = provider['preprocessNode'];
32 invokeForEachNodeInContinuousRange(firstNode, lastNode, function(node, nextNodeInRange) {
33 var nodePreviousSibling = node.previousSibling;
34 var newNodes = preprocessNode.call(provider, node);
36 if (node === firstNode)
37 firstNode = newNodes[0] || nextNodeInRange;
38 if (node === lastNode)
39 lastNode = newNodes[newNodes.length - 1] || nodePreviousSibling;
43 // Because preprocessNode can change the nodes, including the first and last nodes, update continuousNodeArray to match.
44 // We need the full set, including inner nodes, because the unmemoize step might remove the first node (and so the real
45 // first node needs to be in the array).
46 continuousNodeArray.length = 0;
47 if (!firstNode) { // preprocessNode might have removed all the nodes, in which case there's nothing left to do
50 if (firstNode === lastNode) {
51 continuousNodeArray.push(firstNode);
53 continuousNodeArray.push(firstNode, lastNode);
54 ko.utils.fixUpContinuousNodeArray(continuousNodeArray, parentNode);
58 // Need to applyBindings *before* unmemoziation, because unmemoization might introduce extra nodes (that we don't want to re-bind)
59 // whereas a regular applyBindings won't introduce new memoized nodes
60 invokeForEachNodeInContinuousRange(firstNode, lastNode, function(node) {
61 if (node.nodeType === 1 || node.nodeType === 8)
62 ko.applyBindings(bindingContext, node);
64 invokeForEachNodeInContinuousRange(firstNode, lastNode, function(node) {
65 if (node.nodeType === 1 || node.nodeType === 8)
66 ko.memoization.unmemoizeDomNodeAndDescendants(node, [bindingContext]);
69 // Make sure any changes done by applyBindings or unmemoize are reflected in the array
70 ko.utils.fixUpContinuousNodeArray(continuousNodeArray, parentNode);
74 function getFirstNodeFromPossibleArray(nodeOrNodeArray) {
75 return nodeOrNodeArray.nodeType ? nodeOrNodeArray
76 : nodeOrNodeArray.length > 0 ? nodeOrNodeArray[0]
80 function executeTemplate(targetNodeOrNodeArray, renderMode, template, bindingContext, options) {
81 options = options || {};
82 var firstTargetNode = targetNodeOrNodeArray && getFirstNodeFromPossibleArray(targetNodeOrNodeArray);
83 var templateDocument = (firstTargetNode || template || {}).ownerDocument;
84 var templateEngineToUse = (options['templateEngine'] || _templateEngine);
85 ko.templateRewriting.ensureTemplateIsRewritten(template, templateEngineToUse, templateDocument);
86 var renderedNodesArray = templateEngineToUse['renderTemplate'](template, bindingContext, options, templateDocument);
88 // Loosely check result is an array of DOM nodes
89 if ((typeof renderedNodesArray.length != "number") || (renderedNodesArray.length > 0 && typeof renderedNodesArray[0].nodeType != "number"))
90 throw new Error("Template engine must return an array of DOM nodes");
92 var haveAddedNodesToParent = false;
94 case "replaceChildren":
95 ko.virtualElements.setDomNodeChildren(targetNodeOrNodeArray, renderedNodesArray);
96 haveAddedNodesToParent = true;
99 ko.utils.replaceDomNodes(targetNodeOrNodeArray, renderedNodesArray);
100 haveAddedNodesToParent = true;
102 case "ignoreTargetNode": break;
104 throw new Error("Unknown renderMode: " + renderMode);
107 if (haveAddedNodesToParent) {
108 activateBindingsOnContinuousNodeArray(renderedNodesArray, bindingContext);
109 if (options['afterRender'])
110 ko.dependencyDetection.ignore(options['afterRender'], null, [renderedNodesArray, bindingContext['$data']]);
113 return renderedNodesArray;
116 function resolveTemplateName(template, data, context) {
117 // The template can be specified as:
118 if (ko.isObservable(template)) {
119 // 1. An observable, with string value
121 } else if (typeof template === 'function') {
122 // 2. A function of (data, context) returning a string
123 return template(data, context);
130 ko.renderTemplate = function (template, dataOrBindingContext, options, targetNodeOrNodeArray, renderMode) {
131 options = options || {};
132 if ((options['templateEngine'] || _templateEngine) == undefined)
133 throw new Error("Set a template engine before calling renderTemplate");
134 renderMode = renderMode || "replaceChildren";
136 if (targetNodeOrNodeArray) {
137 var firstTargetNode = getFirstNodeFromPossibleArray(targetNodeOrNodeArray);
139 var whenToDispose = function () { return (!firstTargetNode) || !ko.utils.domNodeIsAttachedToDocument(firstTargetNode); }; // Passive disposal (on next evaluation)
140 var activelyDisposeWhenNodeIsRemoved = (firstTargetNode && renderMode == "replaceNode") ? firstTargetNode.parentNode : firstTargetNode;
142 return ko.dependentObservable( // So the DOM is automatically updated when any dependency changes
144 // Ensure we've got a proper binding context to work with
145 var bindingContext = (dataOrBindingContext && (dataOrBindingContext instanceof ko.bindingContext))
146 ? dataOrBindingContext
147 : new ko.bindingContext(ko.utils.unwrapObservable(dataOrBindingContext));
149 var templateName = resolveTemplateName(template, bindingContext['$data'], bindingContext),
150 renderedNodesArray = executeTemplate(targetNodeOrNodeArray, renderMode, templateName, bindingContext, options);
152 if (renderMode == "replaceNode") {
153 targetNodeOrNodeArray = renderedNodesArray;
154 firstTargetNode = getFirstNodeFromPossibleArray(targetNodeOrNodeArray);
158 { disposeWhen: whenToDispose, disposeWhenNodeIsRemoved: activelyDisposeWhenNodeIsRemoved }
161 // We don't yet have a DOM node to evaluate, so use a memo and render the template later when there is a DOM node
162 return ko.memoization.memoize(function (domNode) {
163 ko.renderTemplate(template, dataOrBindingContext, options, domNode, "replaceNode");
168 ko.renderTemplateForEach = function (template, arrayOrObservableArray, options, targetNode, parentBindingContext) {
169 // Since setDomNodeChildrenFromArrayMapping always calls executeTemplateForArrayItem and then
170 // activateBindingsCallback for added items, we can store the binding context in the former to use in the latter.
171 var arrayItemContext;
173 // This will be called by setDomNodeChildrenFromArrayMapping to get the nodes to add to targetNode
174 var executeTemplateForArrayItem = function (arrayValue, index) {
175 // Support selecting template as a function of the data being rendered
176 arrayItemContext = parentBindingContext['createChildContext'](arrayValue, options['as'], function(context) {
177 context['$index'] = index;
180 var templateName = resolveTemplateName(template, arrayValue, arrayItemContext);
181 return executeTemplate(null, "ignoreTargetNode", templateName, arrayItemContext, options);
184 // This will be called whenever setDomNodeChildrenFromArrayMapping has added nodes to targetNode
185 var activateBindingsCallback = function(arrayValue, addedNodesArray, index) {
186 activateBindingsOnContinuousNodeArray(addedNodesArray, arrayItemContext);
187 if (options['afterRender'])
188 options['afterRender'](addedNodesArray, arrayValue);
190 // release the "cache" variable, so that it can be collected by
191 // the GC when its value isn't used from within the bindings anymore.
192 arrayItemContext = null;
195 return ko.dependentObservable(function () {
196 var unwrappedArray = ko.utils.unwrapObservable(arrayOrObservableArray) || [];
197 if (typeof unwrappedArray.length == "undefined") // Coerce single value into array
198 unwrappedArray = [unwrappedArray];
200 // Filter out any entries marked as destroyed
201 var filteredArray = ko.utils.arrayFilter(unwrappedArray, function(item) {
202 return options['includeDestroyed'] || item === undefined || item === null || !ko.utils.unwrapObservable(item['_destroy']);
205 // Call setDomNodeChildrenFromArrayMapping, ignoring any observables unwrapped within (most likely from a callback function).
206 // If the array items are observables, though, they will be unwrapped in executeTemplateForArrayItem and managed within setDomNodeChildrenFromArrayMapping.
207 ko.dependencyDetection.ignore(ko.utils.setDomNodeChildrenFromArrayMapping, null, [targetNode, filteredArray, executeTemplateForArrayItem, options, activateBindingsCallback]);
209 }, null, { disposeWhenNodeIsRemoved: targetNode });
212 var templateComputedDomDataKey = ko.utils.domData.nextKey();
213 function disposeOldComputedAndStoreNewOne(element, newComputed) {
214 var oldComputed = ko.utils.domData.get(element, templateComputedDomDataKey);
215 if (oldComputed && (typeof(oldComputed.dispose) == 'function'))
216 oldComputed.dispose();
217 ko.utils.domData.set(element, templateComputedDomDataKey, (newComputed && newComputed.isActive()) ? newComputed : undefined);
220 ko.bindingHandlers['template'] = {
221 'init': function(element, valueAccessor) {
222 // Support anonymous templates
223 var bindingValue = ko.utils.unwrapObservable(valueAccessor());
224 if (typeof bindingValue == "string" || bindingValue['name']) {
225 // It's a named template - clear the element
226 ko.virtualElements.emptyNode(element);
227 } else if ('nodes' in bindingValue) {
228 // We've been given an array of DOM nodes. Save them as the template source.
229 // There is no known use case for the node array being an observable array (if the output
230 // varies, put that behavior *into* your template - that's what templates are for), and
231 // the implementation would be a mess, so assert that it's not observable.
232 var nodes = bindingValue['nodes'] || [];
233 if (ko.isObservable(nodes)) {
234 throw new Error('The "nodes" option must be a plain, non-observable array.');
236 var container = ko.utils.moveCleanedNodesToContainerElement(nodes); // This also removes the nodes from their current parent
237 new ko.templateSources.anonymousTemplate(element)['nodes'](container);
239 // It's an anonymous template - store the element contents, then clear the element
240 var templateNodes = ko.virtualElements.childNodes(element),
241 container = ko.utils.moveCleanedNodesToContainerElement(templateNodes); // This also removes the nodes from their current parent
242 new ko.templateSources.anonymousTemplate(element)['nodes'](container);
244 return { 'controlsDescendantBindings': true };
246 'update': function (element, valueAccessor, allBindings, viewModel, bindingContext) {
247 var value = valueAccessor(),
249 options = ko.utils.unwrapObservable(value),
250 shouldDisplay = true,
251 templateComputed = null,
254 if (typeof options == "string") {
255 templateName = value;
258 templateName = options['name'];
260 // Support "if"/"ifnot" conditions
262 shouldDisplay = ko.utils.unwrapObservable(options['if']);
263 if (shouldDisplay && 'ifnot' in options)
264 shouldDisplay = !ko.utils.unwrapObservable(options['ifnot']);
266 dataValue = ko.utils.unwrapObservable(options['data']);
269 if ('foreach' in options) {
270 // Render once for each data point (treating data set as empty if shouldDisplay==false)
271 var dataArray = (shouldDisplay && options['foreach']) || [];
272 templateComputed = ko.renderTemplateForEach(templateName || element, dataArray, options, element, bindingContext);
273 } else if (!shouldDisplay) {
274 ko.virtualElements.emptyNode(element);
276 // Render once for this single data point (or use the viewModel if no data was provided)
277 var innerBindingContext = ('data' in options) ?
278 bindingContext['createChildContext'](dataValue, options['as']) : // Given an explitit 'data' value, we create a child binding context for it
279 bindingContext; // Given no explicit 'data' value, we retain the same binding context
280 templateComputed = ko.renderTemplate(templateName || element, innerBindingContext, options, element);
283 // It only makes sense to have a single template computed per element (otherwise which one should have its output displayed?)
284 disposeOldComputedAndStoreNewOne(element, templateComputed);
288 // Anonymous templates can't be rewritten. Give a nice error message if you try to do it.
289 ko.expressionRewriting.bindingRewriteValidators['template'] = function(bindingValue) {
290 var parsedBindingValue = ko.expressionRewriting.parseObjectLiteral(bindingValue);
292 if ((parsedBindingValue.length == 1) && parsedBindingValue[0]['unknown'])
293 return null; // It looks like a string literal, not an object literal, so treat it as a named template (which is allowed for rewriting)
295 if (ko.expressionRewriting.keyValueArrayContainsKey(parsedBindingValue, "name"))
296 return null; // Named templates can be rewritten, so return "no error"
297 return "This template engine does not support anonymous templates nested within its templates";
300 ko.virtualElements.allowedBindings['template'] = true;
303 ko.exportSymbol('setTemplateEngine', ko.setTemplateEngine);
304 ko.exportSymbol('renderTemplate', ko.renderTemplate);