UNPKG

16.7 kBJavaScriptView Raw
1var expressions = require('derby-templates').expressions;
2
3// The many trees of bindings:
4//
5// - Model tree, containing your actual data. Eg:
6// {users:{fred:{age:40}, wilma:{age:37}}}
7//
8// - Event model tree, whose structure mirrors the model tree. The event model
9// tree lets us annotate the model tree with listeners which fire when events
10// change. I think there are three types of listeners:
11//
12// 1. Reference binding binds to whatever is referred to by the path. Eg,
13// {{each items as item}} binds item by reference as it goes through the
14// list.
15// 2. Fixed path bindings explicitly bind to whatever is at that path
16// regardless of how the model changes underneath the event model
17// 3. Listen on a subtree and fire when anything in the subtree changes. This
18// is used for custom functions.
19//
20// {{foo.id}} would listen on the fixed path ['foo', 'id'].
21//
22//
23// - Context tree represents the changing (embedded) contexts of the templating
24// engine. This maps to the tree of templates and allows templates to reference
25// anything in any of their enclosing template scopes.
26//
27
28module.exports = EventModel;
29
30// The code here uses object-based set pattern where objects are keyed using
31// sequentially generated IDs.
32var nextId = 1;
33
34// A binding object is something with update(), insert()/move()/remove() defined.
35
36
37// Given x[y] with model.get(y) == 5:
38// item = 5
39// segments = ['y']
40// outside = the EventModel for x.
41//
42// Note that item could be a Context or another ModelRef - eg:
43//
44// {{ each foo as bar }} ... {{ x[bar] }} -or- {{ x[y[z]] }}
45function ModelRef(model, item, segments, outside) {
46 this.id = nextId++;
47
48 // We need a reference to the model & our segment list so we can update our
49 // value.
50 this.model = model;
51 this.segments = segments;
52
53 // Our current value.
54 this.item = item;
55
56 // outside is a reference to the EventModel of the thing on the lhs of the
57 // brackets. For example, in x[y].z, outside is the EventModel of x.
58 this.outside = outside;
59
60 // result is the EventModel of the evaluated version of the brackets. In
61 // x[y].z, its the EventModel of x[y].
62 this.result = outside.child(item).refChild(this);
63}
64
65ModelRef.prototype.update = function() {
66 var segments = expressions.pathSegments(this.segments);
67 var newItem = expressions.lookup(segments, this.model.data);
68 if (this.item === newItem) return;
69
70 // First remove myself.
71 delete this.outside.child(this.item).refChildren[this.id];
72
73 this.item = newItem;
74
75 var container = this.outside.child(this.item);
76 // I want to just call refChild but that would create a new EM. Instead I
77 // want to just implant my current EM there.
78 if (!container.refChildren) container.refChildren = new RefChildrenMap();
79 container.refChildren[this.id] = this.result;
80
81 // Finally, update all the bindings in the tree.
82 this.result.update();
83};
84
85
86function RefOutMap() {}
87function RefChildrenMap() {}
88function BindingsMap() {}
89function ItemContextsMap() {}
90function EventModelsMap() {}
91
92function EventModel() {
93 this.id = nextId++;
94
95 // Most of these won't ever be filled in, so I'm just leaving them null.
96 //
97 // These contain our EventModel children.
98 this.object = null;
99 this.array = null;
100
101 // This contains any EventModel children which have floating references.
102 this.arrayByReference = null;
103
104 // If the data stored here is ever used to lookup other values, this is an
105 // object mapping remote child ID -> ref.
106 //
107 // Eg given x[y], y.refOut[x.id] = <Binding>
108 this.refOut = null;
109
110 // This is a map from ref id -> event model for events bound to this
111 // EventModel but via a ref. We could just merge them into the main tree, but
112 // this way they're easy to move.
113 //
114 // Eg, given x[y] (y=1), x.1.refChildren[ref id] is an EventModel.
115 this.refChildren = null;
116
117 this.bindings = null;
118
119 // Item contexts are contexts which need their item number changed as this
120 // EventModel object moves around its surrounding list.
121 this.itemContexts = null;
122}
123
124EventModel.prototype.refChild = function(ref) {
125 if (!this.refChildren) this.refChildren = new RefChildrenMap();
126 var id = ref.id;
127
128 if (!this.refChildren[id]) {
129 this.refChildren[id] = new EventModel();
130 }
131 return this.refChildren[id];
132};
133
134EventModel.prototype.arrayLookup = function(model, segmentsBefore, segmentsInside) {
135 var segments = expressions.pathSegments(segmentsInside);
136 var item = expressions.lookup(segments, model.data);
137
138 var source = this.at(segmentsInside);
139
140 // What the array currently resolves to. Given x[y] with y=1, container is
141 // the EM for x
142 var container = this.at(segmentsBefore);
143
144 if (!source.refOut) source.refOut = new RefOutMap();
145
146 var ref = source.refOut[container.id];
147 if (ref == null) {
148 ref = new ModelRef(model, item, segmentsInside, container);
149 source.refOut[container.id] = ref;
150 }
151
152 return ref;
153};
154
155// Returns the EventModel node of the named child.
156EventModel.prototype.child = function(segment) {
157 var container;
158 if (typeof segment === 'string') {
159 // Object
160 if (!this.object) this.object = {};
161 container = this.object;
162
163 } else if (typeof segment === 'number') {
164 // Array by value
165 if (!this.array) this.array = [];
166 container = this.array;
167
168 } else if (segment instanceof ModelRef) {
169 // Array reference. We'll need to lookup the child with the right
170 // value, then look inside its ref children for the right EventModel
171 // (so we can update it later). This is pretty janky, but should be
172 // *correct* even in the face of recursive array accessors.
173 //
174 // This will calculate it based on the current segment values, but refs
175 // cache the EM anyway.
176 //return this.child(segment.item).refChild(segment);
177 return segment.result;
178
179 } else {
180 // Array by reference
181 if (!this.arrayByReference) this.arrayByReference = [];
182 container = this.arrayByReference;
183 segment = segment.item;
184 }
185
186 return container[segment] || (container[segment] = new EventModel());
187};
188
189// Returns the EventModel node at the given segments list. Note that although
190// EventModel nodes are unique, its possible for multiple EventModel nodes to
191// refer to the same section of the model because of references.
192//
193// If you want to update the bindings that refer to a specific path, use
194// each().
195//
196// EventModel objects are created as needed.
197EventModel.prototype.at = function(segments) {
198 // For unbound dependancies.
199 if (segments == null) return this;
200
201 var eventModel = this;
202
203 for (var i = 0; i < segments.length; i++) {
204 eventModel = eventModel.child(segments[i]);
205 }
206
207 return eventModel;
208};
209
210EventModel.prototype.isEmpty = function() {
211 if (hasKeys(this.dependancies)) return false;
212 if (hasKeys(this.itemContexts)) return false;
213
214 if (this.object) {
215 if (hasKeys(this.object)) return false;
216 this.object = null;
217 }
218
219 if (this.arrayByReference) {
220 for (var i = 0; i < this.arrayByReference.length; i++) {
221 if (this.arrayByReference[i] != null) return false;
222 }
223 this.arrayByReference = null;
224 }
225
226 if (this.array) {
227 for (var i = 0; i < this.array.length; i++) {
228 if (this.array[i] != null) return false;
229 }
230 this.array = null;
231 }
232
233 return true;
234};
235
236function hasKeys(object) {
237 for (var key in object) {
238 return true;
239 }
240 return false;
241}
242
243
244// **** Updating the EventModel
245
246EventModel.prototype._addItemContext = function(context) {
247 if (!context._id) context._id = nextId++;
248 if (!this.itemContexts) this.itemContexts = new ItemContextsMap();
249 this.itemContexts[context._id] = context;
250};
251
252EventModel.prototype._removeItemContext = function(context) {
253 if (this.itemContexts) {
254 delete this.itemContexts[context._id];
255 }
256};
257
258EventModel.prototype._addBinding = function(binding) {
259 var bindings = this.bindings || (this.bindings = new BindingsMap());
260 binding.eventModels || (binding.eventModels = new EventModelsMap());
261 bindings[binding.id] = binding;
262 binding.eventModels[this.id] = this;
263};
264
265// This is the main hook to add bindings to the event model tree. It should
266// only be called on the root EventModel object.
267EventModel.prototype.addBinding = function(segments, binding) {
268 this.at(segments)._addBinding(binding);
269};
270
271// This is used for objects (contexts in derby's case) that have a .item
272// property which refers to an array index.
273EventModel.prototype.addItemContext = function(segments, context) {
274 this.at(segments)._addItemContext(context);
275};
276
277EventModel.prototype.removeBinding = function(binding) {
278 if (!binding.eventModels) return;
279 for (var id in binding.eventModels) {
280 var eventModel = binding.eventModels[id];
281 if (eventModel.bindings) delete eventModel.bindings[binding.id];
282 }
283 binding.eventModels = null;
284};
285
286EventModel.prototype._each = function(segments, pos, fn) {
287 // Our refChildren are effectively merged into this object.
288 if (this.refChildren) {
289 for (var id in this.refChildren) {
290 var refChild = this.refChildren[id];
291 if (refChild) refChild._each(segments, pos, fn);
292 }
293 }
294
295 if (segments.length === pos) {
296 fn(this);
297 return;
298 }
299
300 var segment = segments[pos];
301 var child;
302 if (typeof segment === 'string') {
303 // Object. Just recurse into our objects set. Its possible to rewrite this
304 // function to simply loop in the case of object lookups, but I don't think
305 // it'll buy us much.
306 child = this.object && this.object[segment];
307 if (child) child._each(segments, pos + 1, fn);
308
309 } else {
310 // Number. Recurse both into the fixed list and the reference list.
311 child = this.array && this.array[segment];
312 if (child) child._each(segments, pos + 1, fn);
313
314 child = this.arrayByReference && this.arrayByReference[segment];
315 if (child) child._each(segments, pos + 1, fn);
316 }
317};
318
319// Called when the scalar value at the path changes. This only calls update()
320// on this node. See update() below if you want to update entire
321// subtrees.
322EventModel.prototype.localUpdate = function(previous, pass) {
323 if (this.bindings) {
324 for (var id in this.bindings) {
325 var binding = this.bindings[id];
326 if (binding) binding.update(previous, pass);
327 }
328 }
329
330 // If our value changed, we also need to update anything that depends on it
331 // via refOut.
332 if (this.refOut) {
333 for (var id in this.refOut) {
334 var ref = this.refOut[id];
335 if (ref) ref.update();
336 }
337 }
338};
339
340// This is used when an object subtree is replaced / removed.
341EventModel.prototype.update = function(previous, pass) {
342 this.localUpdate(previous, pass);
343
344 if (this.object) {
345 for (var key in this.object) {
346 var binding = this.object[key];
347 if (binding) binding.update();
348 }
349 }
350
351 if (this.array) {
352 for (var i = 0; i < this.array.length; i++) {
353 var binding = this.array[i];
354 if (binding) binding.update();
355 }
356 }
357
358 if (this.arrayByReference) {
359 for (var i = 0; i < this.arrayByReference.length; i++) {
360 var binding = this.arrayByReference[i];
361 if (binding) binding.update();
362 }
363 }
364};
365
366// Updates the indexes in itemContexts of our children in the range of
367// [from, to). from and to both optional.
368EventModel.prototype._updateChildItemContexts = function(from, to) {
369 if (!this.arrayByReference) return;
370
371 if (from == null) from = 0;
372 if (to == null) to = this.arrayByReference.length;
373
374 for (var i = from; i < to; i++) {
375 var contexts = this.arrayByReference[i] &&
376 this.arrayByReference[i].itemContexts;
377 if (contexts) {
378 for (var key in contexts) {
379 contexts[key].item = i;
380 }
381 }
382 }
383};
384
385// Updates our array-by-value values. They have to recursively update every
386// binding in their children. Sad.
387EventModel.prototype._updateArray = function(from, to) {
388 if (!this.array) return;
389
390 if (from == null) from = 0;
391 if (to == null) to = this.array.length;
392
393 for (var i = from; i < to; i++) {
394 var binding = this.array[i];
395 if (binding) binding.update();
396 }
397};
398
399EventModel.prototype._updateObject = function() {
400 if (this.object) {
401 for (var key in this.object) {
402 var binding = this.object[key];
403 if (binding) binding.update();
404 }
405 }
406};
407
408EventModel.prototype._set = function(previous, pass) {
409 // This just updates anything thats bound to the whole subtree. An alternate
410 // implementation could be passed in the new value at this node (which we
411 // cache), then compare with the old version and only update parts of the
412 // subtree which are relevant. I don't know if thats an important
413 // optimization - it really depends on your use case.
414 this.update(previous, pass);
415};
416
417// Insert into this EventModel node.
418EventModel.prototype._insert = function(index, howMany) {
419 // Update fixed paths
420 this._updateArray(index);
421
422 // Update relative paths
423 if (this.arrayByReference && this.arrayByReference.length > index) {
424 // Shift the actual items in the array references array.
425
426 // This probably isn't the best way to implement insert. Other options are
427 // using concat() on slices or though constructing a temporary array and
428 // using splice.call. Hopefully if this method is slow it'll come up during
429 // profiling.
430 for (var i = 0; i < howMany; i++) {
431 this.arrayByReference.splice(index, 0, null);
432 }
433
434 // Update the path in the contexts
435 this._updateChildItemContexts(index + howMany);
436 }
437
438 // Finally call our bindings.
439 if (this.bindings) {
440 for (var id in this.bindings) {
441 var binding = this.bindings[id];
442 if (binding) binding.insert(index, howMany);
443 }
444 }
445 this._updateObject();
446};
447
448// Remove howMany child elements from this EventModel at index.
449EventModel.prototype._remove = function(index, howMany) {
450 // Update fixed paths. Both the removed items and items after it may have changed.
451 this._updateArray(index);
452
453 if (this.arrayByReference) {
454 // Update relative paths. First throw away all the children which have been removed.
455 this.arrayByReference.splice(index, howMany);
456
457 this._updateChildItemContexts(index);
458 }
459
460 // Call bindings.
461 if (this.bindings) {
462 for (var id in this.bindings) {
463 var binding = this.bindings[id];
464 if (binding) binding.remove(index, howMany);
465 }
466 }
467 this._updateObject();
468};
469
470// Move howMany items from `from` to `to`.
471EventModel.prototype._move = function(from, to, howMany) {
472 // first points to the first element that was moved. end points to the list
473 // element past the end of the changed region.
474 var first, end;
475 if (from < to) {
476 first = from;
477 end = to + howMany;
478 } else {
479 first = to;
480 end = from + howMany;
481 }
482
483 // Update fixed paths.
484 this._updateArray(first, end);
485
486 // Update relative paths
487 var arr = this.arrayByReference;
488 if (arr && arr.length > first) {
489 // Remove from the old location
490 var values = arr.splice(from, howMany);
491
492 // Insert at the new location
493 arr.splice.apply(arr, [to, 0].concat(values));
494
495 // Update the path in the contexts
496 this._updateChildItemContexts(first, end);
497 }
498
499 // Finally call our bindings.
500 if (this.bindings) {
501 for (var id in this.bindings) {
502 var binding = this.bindings[id];
503 if (binding) binding.move(from, to, howMany);
504 }
505 }
506 this._updateObject();
507};
508
509
510// Helpers.
511
512EventModel.prototype.mutate = function(segments, fn) {
513 // This finds & returns a list of all event models which exist and could match
514 // the specified path. The path cannot contain contexts like derby expression
515 // segment lists (just because I don't think thats a useful feature and its not
516 // implemented)
517 this._each(segments, 0, fn);
518
519 // Also emit all mutations as sets on star paths, which are how dependencies
520 // for view helper functions are represented. They should react to a path
521 // or any child path being modified
522 for (var i = 0, len = segments.length; i++ < len;) {
523 var wildcardSegments = segments.slice(0, i);
524 wildcardSegments.push('*');
525 this._each(wildcardSegments, 0, childSetWildcard);
526 }
527};
528
529function childSetWildcard(child) {
530 child._set();
531}
532
533EventModel.prototype.set = function(segments, previous, pass) {
534 this.mutate(segments, function childSet(child) {
535 child._set(previous, pass);
536 });
537};
538
539EventModel.prototype.insert = function(segments, index, howMany) {
540 this.mutate(segments, function childInsert(child) {
541 child._insert(index, howMany);
542 });
543};
544
545EventModel.prototype.remove = function(segments, index, howMany) {
546 this.mutate(segments, function childRemove(child) {
547 child._remove(index, howMany);
548 });
549};
550
551EventModel.prototype.move = function(segments, from, to, howMany) {
552 this.mutate(segments, function childMove(child) {
553 child._move(from, to, howMany);
554 });
555};
556
\No newline at end of file