forked from knockout/knockout
/
templating.js
228 lines (192 loc) · 12.8 KB
/
templating.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
(function () {
var _templateEngine;
ko.setTemplateEngine = function (templateEngine) {
if ((templateEngine != undefined) && !(templateEngine instanceof ko.templateEngine))
throw "templateEngine must inherit from ko.templateEngine";
_templateEngine = templateEngine;
}
function invokeForEachNodeOrCommentInParent(nodeArray, parent, action) {
for (var i = 0; node = nodeArray[i]; i++) {
if (node.parentNode !== parent) // Skip anything that has been removed during binding
continue;
if ((node.nodeType === 1) || (node.nodeType === 8))
action(node);
}
}
ko.activateBindingsOnTemplateRenderedNodes = function(nodeArray, bindingContext) {
// To be used on any nodes that have been rendered by a template and have been inserted into some parent element.
// Safely iterates through nodeArray (being tolerant of any changes made to it during binding, e.g.,
// if a binding inserts siblings), and for each:
// (1) Does a regular "applyBindings" to associate bindingContext with this node and to activate any non-memoized bindings
// (2) Unmemoizes any memos in the DOM subtree (e.g., to activate bindings that had been memoized during template rewriting)
var nodeArrayClone = ko.utils.arrayPushAll([], nodeArray); // So we can tolerate insertions/deletions during binding
var commonParentElement = (nodeArray.length > 0) ? nodeArray[0].parentNode : null; // All items must be in the same parent, so this is OK
// Need to applyBindings *before* unmemoziation, because unmemoization might introduce extra nodes (that we don't want to re-bind)
// whereas a regular applyBindings won't introduce new memoized nodes
invokeForEachNodeOrCommentInParent(nodeArrayClone, commonParentElement, function(node) {
ko.applyBindings(bindingContext, node);
});
invokeForEachNodeOrCommentInParent(nodeArrayClone, commonParentElement, function(node) {
ko.memoization.unmemoizeDomNodeAndDescendants(node, [bindingContext]);
});
}
function getFirstNodeFromPossibleArray(nodeOrNodeArray) {
return nodeOrNodeArray.nodeType ? nodeOrNodeArray
: nodeOrNodeArray.length > 0 ? nodeOrNodeArray[0]
: null;
}
function executeTemplate(targetNodeOrNodeArray, renderMode, template, bindingContext, options) {
options = options || {};
var templateEngineToUse = (options['templateEngine'] || _templateEngine);
ko.templateRewriting.ensureTemplateIsRewritten(template, templateEngineToUse);
var renderedNodesArray = templateEngineToUse['renderTemplate'](template, bindingContext, options);
// Loosely check result is an array of DOM nodes
if ((typeof renderedNodesArray.length != "number") || (renderedNodesArray.length > 0 && typeof renderedNodesArray[0].nodeType != "number"))
throw "Template engine must return an array of DOM nodes";
var haveAddedNodesToParent = false;
switch (renderMode) {
case "replaceChildren":
ko.virtualElements.setDomNodeChildren(targetNodeOrNodeArray, renderedNodesArray);
haveAddedNodesToParent = true;
break;
case "replaceNode":
ko.utils.replaceDomNodes(targetNodeOrNodeArray, renderedNodesArray);
haveAddedNodesToParent = true;
break;
case "ignoreTargetNode": break;
default:
throw new Error("Unknown renderMode: " + renderMode);
}
if (haveAddedNodesToParent) {
ko.activateBindingsOnTemplateRenderedNodes(renderedNodesArray, bindingContext);
if (options['afterRender'])
options['afterRender'](renderedNodesArray, bindingContext['$data']);
}
return renderedNodesArray;
}
ko.renderTemplate = function (template, dataOrBindingContext, options, targetNodeOrNodeArray, renderMode) {
options = options || {};
if ((options['templateEngine'] || _templateEngine) == undefined)
throw "Set a template engine before calling renderTemplate";
renderMode = renderMode || "replaceChildren";
if (targetNodeOrNodeArray) {
var firstTargetNode = getFirstNodeFromPossibleArray(targetNodeOrNodeArray);
var whenToDispose = function () { return (!firstTargetNode) || !ko.utils.domNodeIsAttachedToDocument(firstTargetNode); }; // Passive disposal (on next evaluation)
var activelyDisposeWhenNodeIsRemoved = (firstTargetNode && renderMode == "replaceNode") ? firstTargetNode.parentNode : firstTargetNode;
return new ko.dependentObservable( // So the DOM is automatically updated when any dependency changes
function () {
// Ensure we've got a proper binding context to work with
var bindingContext = (dataOrBindingContext && (dataOrBindingContext instanceof ko.bindingContext))
? dataOrBindingContext
: new ko.bindingContext(ko.utils.unwrapObservable(dataOrBindingContext));
// Support selecting template as a function of the data being rendered
var templateName = typeof(template) == 'function' ? template(bindingContext['$data']) : template;
var renderedNodesArray = executeTemplate(targetNodeOrNodeArray, renderMode, templateName, bindingContext, options);
if (renderMode == "replaceNode") {
targetNodeOrNodeArray = renderedNodesArray;
firstTargetNode = getFirstNodeFromPossibleArray(targetNodeOrNodeArray);
}
},
null,
{ 'disposeWhen': whenToDispose, 'disposeWhenNodeIsRemoved': activelyDisposeWhenNodeIsRemoved }
);
} else {
// 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
return ko.memoization.memoize(function (domNode) {
ko.renderTemplate(template, dataOrBindingContext, options, domNode, "replaceNode");
});
}
};
ko.renderTemplateForEach = function (template, arrayOrObservableArray, options, targetNode, parentBindingContext) {
var createInnerBindingContext = function(arrayValue, index) {
var result = parentBindingContext.createChildContext(ko.utils.unwrapObservable(arrayValue));
result['$index'] = ko.observable(index);
return result;
};
// This will be called whenever setDomNodeChildrenFromArrayMapping has added nodes to targetNode
var activateBindingsCallback = function(arrayValue, addedNodesArray, index) {
var bindingContext = createInnerBindingContext(arrayValue, index);
ko.activateBindingsOnTemplateRenderedNodes(addedNodesArray, bindingContext);
if (options['afterRender'])
options['afterRender'](addedNodesArray, bindingContext['$data']);
};
return new ko.dependentObservable(function () {
var unwrappedArray = ko.utils.unwrapObservable(arrayOrObservableArray) || [];
if (typeof unwrappedArray.length == "undefined") // Coerce single value into array
unwrappedArray = [unwrappedArray];
// Filter out any entries marked as destroyed
var filteredArray = ko.utils.arrayFilter(unwrappedArray, function(item) {
return options['includeDestroyed'] || !ko.utils.unwrapObservable(item['_destroy']);
});
ko.utils.setDomNodeChildrenFromArrayMapping(targetNode, filteredArray, function (arrayValue, index) {
// Support selecting template as a function of the data being rendered
var templateName = typeof(template) == 'function' ? template(arrayValue) : template;
return executeTemplate(null, "ignoreTargetNode", templateName, createInnerBindingContext(arrayValue, index), options);
}, options, activateBindingsCallback);
}, null, { 'disposeWhenNodeIsRemoved': targetNode });
};
var templateSubscriptionDomDataKey = '__ko__templateSubscriptionDomDataKey__';
function disposeOldSubscriptionAndStoreNewOne(element, newSubscription) {
var oldSubscription = ko.utils.domData.get(element, templateSubscriptionDomDataKey);
if (oldSubscription && (typeof(oldSubscription.dispose) == 'function'))
oldSubscription.dispose();
ko.utils.domData.set(element, templateSubscriptionDomDataKey, newSubscription);
}
ko.bindingHandlers['template'] = {
'init': function(element, valueAccessor) {
// Support anonymous templates
var bindingValue = ko.utils.unwrapObservable(valueAccessor());
if ((typeof bindingValue != "string") && (!bindingValue.name) && (element.nodeType == 1)) {
// It's an anonymous template - store the element contents, then clear the element
new ko.templateSources.anonymousTemplate(element).text(element.innerHTML);
ko.utils.emptyDomNode(element);
}
return { 'controlsDescendantBindings': true };
},
'update': function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
var bindingValue = ko.utils.unwrapObservable(valueAccessor());
var templateName;
var shouldDisplay = true;
if (typeof bindingValue == "string") {
templateName = bindingValue;
} else {
templateName = bindingValue.name;
// Support "if"/"ifnot" conditions
if ('if' in bindingValue)
shouldDisplay = shouldDisplay && ko.utils.unwrapObservable(bindingValue['if']);
if ('ifnot' in bindingValue)
shouldDisplay = shouldDisplay && !ko.utils.unwrapObservable(bindingValue['ifnot']);
}
var templateSubscription = null;
if (typeof bindingValue['foreach'] != "undefined") {
// Render once for each data point (treating data set as empty if shouldDisplay==false)
var dataArray = (shouldDisplay && bindingValue['foreach']) || [];
templateSubscription = ko.renderTemplateForEach(templateName || element, dataArray, /* options: */ bindingValue, element, bindingContext);
}
else {
if (shouldDisplay) {
// Render once for this single data point (or use the viewModel if no data was provided)
var innerBindingContext = (typeof bindingValue == 'object') && ('data' in bindingValue)
? bindingContext.createChildContext(ko.utils.unwrapObservable(bindingValue['data'])) // Given an explitit 'data' value, we create a child binding context for it
: bindingContext; // Given no explicit 'data' value, we retain the same binding context
templateSubscription = ko.renderTemplate(templateName || element, innerBindingContext, /* options: */ bindingValue, element);
} else
ko.virtualElements.emptyNode(element);
}
// It only makes sense to have a single template subscription per element (otherwise which one should have its output displayed?)
disposeOldSubscriptionAndStoreNewOne(element, templateSubscription);
}
};
// Anonymous templates can't be rewritten. Give a nice error message if you try to do it.
ko.jsonExpressionRewriting.bindingRewriteValidators['template'] = function(bindingValue) {
var parsedBindingValue = ko.jsonExpressionRewriting.parseObjectLiteral(bindingValue);
if ((parsedBindingValue.length == 1) && parsedBindingValue[0]['unknown'])
return null; // It looks like a string literal, not an object literal, so treat it as a named template (which is allowed for rewriting)
if (ko.jsonExpressionRewriting.keyValueArrayContainsKey(parsedBindingValue, "name"))
return null; // Named templates can be rewritten, so return "no error"
return "This template engine does not support anonymous templates nested within its templates";
};
ko.virtualElements.allowedBindings['template'] = true;
})();
ko.exportSymbol('ko.setTemplateEngine', ko.setTemplateEngine);
ko.exportSymbol('ko.renderTemplate', ko.renderTemplate);