UNPKG

10.4 kBJavaScriptView Raw
1var Trackr = require("trackr");
2var _ = require("underscore");
3var NODE_TYPE = require("./types");
4var parse = require("./m+xml").parse;
5var utils = require("./utils");
6var View = require("./view");
7var Model = require("./model");
8var Section = require("./section");
9var $track = require("trackr-objects");
10var DOMRange = require("./domrange");
11
12var Mustache =
13module.exports = View.extend({
14 constructor: function(data, options) {
15 options = options || {};
16
17 // add template
18 var template = options.template || _.result(this, "template");
19 if (template != null) this.setTemplate(template);
20
21 // add decorators
22 this.decorate(_.extend({}, options.decorators, _.result(this, "decorators")));
23
24 // initiate like a normal view
25 View.call(this, data, options);
26 },
27
28 // parses and sets the root template
29 setTemplate: function(template) {
30 if (_.isString(template)) template = parse(template);
31
32 if (!_.isObject(template) || template.type !== NODE_TYPE.ROOT)
33 throw new Error("Expecting string or parsed template.");
34
35 this._template = template;
36 return this;
37 },
38
39 // creates a decorator
40 decorate: function(name, fn, options) {
41 if (typeof name === "object" && fn == null) {
42 _.each(name, function(fn, n) {
43 if (_.isArray(fn)) this.decorate(n, fn[0], fn[1]);
44 else this.decorate(n, fn, options);
45 }, this);
46 return this;
47 }
48
49 if (typeof name !== "string" || name === "") throw new Error("Expecting non-empty string for decorator name.");
50 if (typeof fn !== "function") throw new Error("Expecting function for decorator.");
51
52 if (this._decorators == null) this._decorators = {};
53 if (this._decorators[name] == null) this._decorators[name] = [];
54 var decorators = this._decorators[name];
55
56 if (!_.findWhere(decorators, { callback: fn })) {
57 decorators.push({
58 callback: fn,
59 options: options || {}
60 });
61 }
62
63 return this;
64 },
65
66 // finds all decorators, locally and in parent
67 findDecorators: function(name) {
68 var decorators = [],
69 c = this;
70
71
72 while (c != null) {
73 if (c._decorators != null && _.isArray(c._decorators[name])) {
74 c._decorators[name].forEach(function(d) {
75 if (!_.findWhere(decorators, { callback: d.callback })) {
76 decorators.push(_.extend({ context: c }, d));
77 }
78 });
79 }
80
81 c = c.parentRange;
82 }
83
84 return decorators;
85 },
86
87 // removes a decorator
88 stopDecorating: function(name, fn) {
89 if (typeof name === "function" && fn == null) {
90 fn = name;
91 name = null;
92 }
93
94 if (this._decorators == null || (name == null && fn == null)) {
95 this._decorators = {};
96 }
97
98 else if (fn == null) {
99 delete this._decorators[name];
100 }
101
102 else if (name == null) {
103 _.each(this._decorators, function(d, n) {
104 this._decorators[n] = _.filter(d, function(_d) {
105 return _d.callback !== fn;
106 });
107 }, this);
108 }
109
110 else {
111 var d = this._decorators[name];
112 this._decorators[name] = _.filter(d, function(_d) {
113 return _d.callback !== fn;
114 });
115 }
116
117 return this;
118 },
119
120 // special partial setter that converts strings into mustache Views
121 setPartial: function(name, partial) {
122 if (_.isObject(name)) return View.prototype.setPartial.call(this, name);
123
124 if (_.isString(partial)) partial = parse(partial);
125 if (_.isObject(partial) && partial.type === NODE_TYPE.ROOT) partial = Mustache.extend({ template: partial });
126 if (partial != null && !utils.isSubClass(View, partial))
127 throw new Error("Expecting string template, parsed template, View subclass or function for partial.");
128
129 return View.prototype.setPartial.call(this, name, partial);
130 },
131
132 // the main render function called by mount
133 render: function() {
134 if (this._template == null)
135 throw new Error("Expected a template to be set before rendering.");
136
137 var toMount;
138 this.setMembers(this.renderTemplate(this._template, null, toMount = []));
139 _.invoke(toMount, "mount");
140 },
141
142 // converts a template into an array of elements and DOMRanges
143 renderTemplate: function(template, view, toMount) {
144 if (view == null) view = this;
145 if (toMount == null) toMount = [];
146 var self = this;
147
148 if (_.isArray(template)) return template.reduce(function(r, t) {
149 var b = self.renderTemplate(t, view, toMount);
150 if (_.isArray(b)) r.push.apply(r, b);
151 else if (b != null) r.push(b);
152 return r;
153 }, []);
154
155 switch(template.type) {
156 case NODE_TYPE.ROOT:
157 return this.renderTemplate(template.children, view, toMount);
158
159 case NODE_TYPE.ELEMENT:
160 var part = this.renderPartial(template.name, view);
161 var obj;
162
163 if (part != null) {
164 part.addData(obj = $track({}));
165
166 template.attributes.forEach(function(attr) {
167 self.autorun(function(c) {
168 var val = this.renderArguments(attr.arguments, view);
169 if (val.length === 1) val = val[0];
170 else if (!val.length) val = void 0;
171
172 if (c.firstRun) obj.defineProperty(attr.name, val);
173 else obj[attr.name] = val;
174 });
175 });
176
177 toMount.push(part);
178 return part;
179 }
180
181 else {
182 var el = document.createElement(template.name);
183
184 template.attributes.forEach(function(attr) {
185 if (this.renderDecorations(el, attr, view)) return;
186
187 this.autorun(function() {
188 el.setAttribute(attr.name, this.renderTemplateAsString(attr.children, view));
189 });
190 }, this);
191
192 var children = this.renderTemplate(template.children, view, toMount),
193 child, i;
194
195 for (i in children) {
196 child = children[i];
197 if (child instanceof DOMRange) {
198 child.parentRange = view; // fake the parent
199 child.attach(el);
200 } else {
201 el.appendChild(child);
202 }
203 }
204
205 return el;
206 }
207
208 case NODE_TYPE.TEXT:
209 return document.createTextNode(utils.decodeEntities(template.value));
210
211 case NODE_TYPE.HTML:
212 return new DOMRange(utils.parseHTML(template.value));
213
214 case NODE_TYPE.XCOMMENT:
215 return document.createComment(template.value);
216
217 case NODE_TYPE.INTERPOLATOR:
218 var node = document.createTextNode("");
219
220 this.autorun(function() {
221 var val = view.get(template.value);
222 node.nodeValue = typeof val === "string" ? val : val != null ? val.toString() : "";
223 });
224
225 return node;
226
227 case NODE_TYPE.TRIPLE:
228 var range = new DOMRange();
229
230 this.autorun(function() {
231 range.setMembers(utils.parseHTML(view.get(template.value)));
232 });
233
234 return range;
235
236 case NODE_TYPE.INVERTED:
237 case NODE_TYPE.SECTION:
238 var section = new Section(view.model)
239 .invert(template.type === NODE_TYPE.INVERTED)
240 .setPath(template.value)
241 .onRow(function() {
242 var _toMount;
243 this.setMembers(self.renderTemplate(template.children, this, _toMount = []));
244 _.invoke(_toMount, "mount");
245 });
246
247 toMount.push(section);
248 return section;
249
250 case NODE_TYPE.PARTIAL:
251 var partial = this.renderPartial(template.value, view);
252 if (partial) toMount.push(partial);
253 return partial;
254 }
255 },
256
257 // converts a template into a string
258 renderTemplateAsString: function(template, ctx) {
259 if (ctx == null) ctx = this;
260 if (ctx instanceof View) ctx = ctx.model;
261 var self = this;
262
263 if (_.isArray(template)) return template.map(function(t) {
264 return self.renderTemplateAsString(t, ctx);
265 }).filter(function(b) { return b != null; }).join("");
266
267 switch(template.type) {
268 case NODE_TYPE.ROOT:
269 return this.renderTemplateAsString(template.children, ctx);
270
271 case NODE_TYPE.TEXT:
272 return template.value;
273
274 case NODE_TYPE.INTERPOLATOR:
275 case NODE_TYPE.TRIPLE:
276 var val = ctx.get(template.value);
277 return val != null ? val.toString() : "";
278
279 case NODE_TYPE.SECTION:
280 case NODE_TYPE.INVERTED:
281 var inverted, model, val, isEmpty, makeRow, proxy, isList;
282
283 inverted = template.type === NODE_TYPE.INVERTED;
284 val = ctx.get(template.value);
285 model = new Model(val, ctx);
286 proxy = model.getProxyByValue(val);
287 isList = model.callProxyMethod(proxy, val, "isList");
288 isEmpty = Section.isEmpty(model, proxy);
289
290 makeRow = function(i) {
291 var row, data;
292
293 if (i == null) {
294 data = model;
295 } else {
296 data = model.callProxyMethod(proxy, val, "get", i);
297 data = new Model(data, new Model({ $key: i }, ctx));
298 }
299
300 return self.renderTemplateAsString(template.children, data);
301 }
302
303 if (!(isEmpty ^ inverted)) {
304 return isList && !inverted ?
305 model.callProxyMethod(proxy, val, "keys").map(makeRow).join("") :
306 makeRow();
307 }
308 }
309 },
310
311 // converts an argument template into an array of values
312 renderArguments: function(arg, ctx) {
313 if (ctx == null) ctx = this;
314 if (ctx instanceof View) ctx = ctx.model;
315 var self = this;
316
317 if (_.isArray(arg)) return arg.map(function(a) {
318 return self.renderArguments(a, ctx);
319 }).filter(function(b) { return b != null; });
320
321 switch(arg.type) {
322 case NODE_TYPE.INTERPOLATOR:
323 return ctx.get(arg.value);
324
325 case NODE_TYPE.LITERAL:
326 return arg.value;
327 }
328 },
329
330 // renders decorations on an element by template
331 renderDecorations: function(el, attr, ctx) {
332 var self = this;
333
334 // look up decorator by name
335 var decorators = this.findDecorators(attr.name);
336 if (!decorators.length) return;
337
338 // normalize the context
339 if (ctx == null) ctx = this;
340 if (ctx instanceof View) ctx = ctx.model;
341
342 // a wrapper computation to ez-clean the rest
343 return this.autorun(function(_comp) {
344 decorators.forEach(function(d) {
345 if (d.options && d.options.defer) _.defer(execDecorator);
346 else execDecorator();
347
348 function execDecorator() {
349 var dcomp = self.autorun(function(comp) {
350 // assemble the arguments!
351 var args = [ {
352 target: el,
353 model: ctx,
354 view: self,
355 template: attr,
356 comp: comp,
357 options: d.options
358 } ];
359
360 // render arguments based on options
361 if (d.options && d.options.parse === "string") {
362 args.push(self.renderTemplateAsString(attr.children, ctx));
363 } else if (d.options == null || d.options.parse !== false) {
364 args = args.concat(self.renderArguments(attr.arguments, ctx));
365 }
366
367 // execute the callback
368 d.callback.apply(d.context || self, args);
369 });
370
371 // clean up
372 _comp.onInvalidate(function() {
373 dcomp.stop();
374 });
375 }
376 });
377 });
378 }
379
380}, {
381
382 render: function(template, data, options) {
383 options = _.extend({}, options || {}, {
384 template: template
385 });
386
387 return new Mustache(data || null, options);
388 }
389
390});