aberdeen 0.0.8 → 0.0.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/aberdeen.js CHANGED
@@ -1,73 +1,78 @@
1
- /*
2
- * QueueRunner
3
- *
4
- * `queue()`d runners are executed on the next timer tick, by order of their
5
- * `queueOrder` values.
6
- */
7
- let queued = new Set();
1
+ let queueArray = [];
2
+ let queueSet = new Set();
3
+ let queueOrdered = true;
4
+ let runQueueDepth = 0;
8
5
  function queue(runner) {
9
- if (!queued.size) {
6
+ if (queueSet.has(runner))
7
+ return;
8
+ if (runQueueDepth > 42) {
9
+ throw new Error("Too many recursive updates from observes");
10
+ }
11
+ if (!queueArray.length) {
10
12
  setTimeout(runQueue, 0);
11
13
  }
12
- queued.add(runner);
14
+ else if (runner.queueOrder < queueArray[queueArray.length - 1].queueOrder) {
15
+ queueOrdered = false;
16
+ }
17
+ queueArray.push(runner);
18
+ queueSet.add(runner);
13
19
  }
14
20
  function runQueue() {
15
- // Order queued observers by depth, lowest first
16
- let ordered;
17
- if (Array.from) {
18
- ordered = Array.from(queued);
19
- }
20
- else { // IE 11
21
- ordered = [];
22
- queued.forEach(item => ordered.push(item));
23
- }
24
- ordered.sort((a, b) => a.queueOrder - b.queueOrder);
25
- for (let runner of ordered) {
26
- queued.delete(runner);
27
- let size = queued.size;
28
- runner.queueRun();
29
- if (queued.size !== size) {
30
- // The queue was modified. We'll need to sort it again.
31
- return runQueue();
32
- }
33
- }
21
+ for (let index = 0; index < queueArray.length;) {
22
+ if (!queueOrdered) {
23
+ queueArray.splice(0, index);
24
+ index = 0;
25
+ // Order queued observers by depth, lowest first
26
+ queueArray.sort((a, b) => a.queueOrder - b.queueOrder);
27
+ queueOrdered = true;
28
+ }
29
+ let batchEndIndex = queueArray.length;
30
+ for (; index < batchEndIndex && queueOrdered; index++) {
31
+ let runner = queueArray[index];
32
+ queueSet.delete(runner);
33
+ runner.queueRun();
34
+ }
35
+ runQueueDepth++;
36
+ }
37
+ queueArray.length = 0;
38
+ runQueueDepth = 0;
34
39
  }
35
40
  /**
36
41
  * Given an integer number, a string or an array of these, this function returns a string that can be used
37
42
  * to compare items in a natural sorting order. So `[3, 'ab']` should be smaller than `[3, 'ac']`.
38
43
  * The resulting string is guaranteed to never be empty.
39
44
  */
40
- export function sortKeyToString(key) {
45
+ function sortKeyToString(key) {
41
46
  if (key instanceof Array) {
42
47
  return key.map(partToStr).join('');
43
48
  }
44
49
  else {
45
50
  return partToStr(key);
46
51
  }
47
- function partToStr(part) {
48
- if (typeof part === 'string') {
49
- return part + '\x01';
50
- }
51
- else {
52
- let result = numToString(Math.abs(Math.round(part)), part < 0);
53
- // Prefix the number of digits, counting down from 128 for negative and up for positive
54
- return String.fromCharCode(128 + (part > 0 ? result.length : -result.length)) + result;
55
- }
56
- }
57
- function numToString(num, neg) {
58
- let result = '';
59
- while (num > 0) {
60
- /*
61
- * We're reserving a few character codes:
62
- * 0 - for compatibility
63
- * 1 - separator between array items
64
- * 65535 - for compatibility
65
- */
66
- result += String.fromCharCode(neg ? 65535 - (num % 65533) : 2 + (num % 65533));
67
- num = Math.floor(num / 65533);
68
- }
69
- return result;
52
+ }
53
+ function partToStr(part) {
54
+ if (typeof part === 'string') {
55
+ return part + '\x01';
70
56
  }
57
+ else {
58
+ let result = numToString(Math.abs(Math.round(part)), part < 0);
59
+ // Prefix the number of digits, counting down from 128 for negative and up for positive
60
+ return String.fromCharCode(128 + (part > 0 ? result.length : -result.length)) + result;
61
+ }
62
+ }
63
+ function numToString(num, neg) {
64
+ let result = '';
65
+ while (num > 0) {
66
+ /*
67
+ * We're reserving a few character codes:
68
+ * 0 - for compatibility
69
+ * 1 - separator between array items
70
+ * 65535 - for compatibility
71
+ */
72
+ result += String.fromCharCode(neg ? 65535 - (num % 65533) : 2 + (num % 65533));
73
+ num = Math.floor(num / 65533);
74
+ }
75
+ return result;
71
76
  }
72
77
  /*
73
78
  * Scope
@@ -109,28 +114,32 @@ class Scope {
109
114
  return this.lastChild.findLastNode() || this.lastChild.findPrecedingNode();
110
115
  }
111
116
  addNode(node) {
117
+ if (!this.parentElement)
118
+ throw new ScopeError(true);
112
119
  let prevNode = this.findLastNode() || this.findPrecedingNode();
113
120
  this.parentElement.insertBefore(node, prevNode ? prevNode.nextSibling : this.parentElement.firstChild);
114
121
  this.lastChild = node;
115
122
  }
116
123
  remove() {
117
- let lastNode = this.findLastNode();
118
- if (lastNode) {
119
- // at least one DOM node to be removed
120
- let precedingNode = this.findPrecedingNode();
121
- // Keep removing DOM nodes starting at our last node, until we encounter the preceding node
122
- // (which can be undefined)
123
- while (lastNode !== precedingNode) {
124
- /* istanbul ignore next */
125
- if (!lastNode) {
126
- return internalError(1);
124
+ if (this.parentElement) {
125
+ let lastNode = this.findLastNode();
126
+ if (lastNode) {
127
+ // at least one DOM node to be removed
128
+ let precedingNode = this.findPrecedingNode();
129
+ // Keep removing DOM nodes starting at our last node, until we encounter the preceding node
130
+ // (which can be undefined)
131
+ while (lastNode !== precedingNode) {
132
+ /* istanbul ignore next */
133
+ if (!lastNode) {
134
+ return internalError(1);
135
+ }
136
+ let nextLastNode = lastNode.previousSibling || undefined;
137
+ this.parentElement.removeChild(lastNode);
138
+ lastNode = nextLastNode;
127
139
  }
128
- let nextLastNode = lastNode.previousSibling || undefined;
129
- this.parentElement.removeChild(lastNode);
130
- lastNode = nextLastNode;
131
140
  }
141
+ this.lastChild = undefined;
132
142
  }
133
- this.lastChild = undefined;
134
143
  // run cleaners
135
144
  this._clean();
136
145
  }
@@ -139,8 +148,9 @@ class Scope {
139
148
  for (let cleaner of this.cleaners) {
140
149
  cleaner._clean(this);
141
150
  }
151
+ this.cleaners.length = 0;
142
152
  }
143
- onChange(collection, index, newData, oldData) {
153
+ onChange(index, newData, oldData) {
144
154
  queue(this);
145
155
  }
146
156
  }
@@ -173,6 +183,30 @@ class SimpleScope extends Scope {
173
183
  currentScope = savedScope;
174
184
  }
175
185
  }
186
+ class IsEmptyObserver {
187
+ constructor(scope, collection, triggerCount) {
188
+ this.scope = scope;
189
+ this.collection = collection;
190
+ this.triggerCount = triggerCount;
191
+ this.count = collection.getCount();
192
+ collection.addObserver(ANY_INDEX, this);
193
+ scope.cleaners.push(this);
194
+ }
195
+ onChange(index, newData, oldData) {
196
+ if (newData === undefined) {
197
+ // oldData is guaranteed not to be undefined
198
+ if (this.triggerCount || !--this.count)
199
+ queue(this.scope);
200
+ }
201
+ else if (oldData === undefined) {
202
+ if (this.triggerCount || !this.count++)
203
+ queue(this.scope);
204
+ }
205
+ }
206
+ _clean() {
207
+ this.collection.removeObserver(ANY_INDEX, this);
208
+ }
209
+ }
176
210
  class OnEachScope extends Scope {
177
211
  constructor(parentElement, precedingSibling, queueOrder, collection, renderer, makeSortKey) {
178
212
  super(parentElement, precedingSibling, queueOrder);
@@ -187,7 +221,7 @@ class OnEachScope extends Scope {
187
221
  this.renderer = renderer;
188
222
  this.makeSortKey = makeSortKey;
189
223
  }
190
- onChange(collection, index, newData, oldData) {
224
+ onChange(index, newData, oldData) {
191
225
  if (oldData === undefined) {
192
226
  if (this.removedIndexes.has(index)) {
193
227
  this.removedIndexes.delete(index);
@@ -224,8 +258,8 @@ class OnEachScope extends Scope {
224
258
  _clean() {
225
259
  super._clean();
226
260
  this.collection.observers.delete(this);
227
- for (let item of this.byPosition) {
228
- item._clean();
261
+ for (const [index, scope] of this.byIndex) {
262
+ scope._clean();
229
263
  }
230
264
  // Help garbage collection:
231
265
  this.byPosition.length = 0;
@@ -257,12 +291,13 @@ class OnEachScope extends Scope {
257
291
  scope.remove();
258
292
  }
259
293
  findPosition(sortStr) {
294
+ // In case of duplicate `sortStr`s, this will return the first match.
260
295
  let items = this.byPosition;
261
- let min = 0, max = this.byPosition.length;
296
+ let min = 0, max = items.length;
262
297
  // Fast-path for elements that are already ordered (as is the case when working with arrays ordered by index)
263
298
  if (!max || sortStr > items[max - 1].sortStr)
264
299
  return max;
265
- // Binary search for the insert position
300
+ // Binary search for the insert position
266
301
  while (min < max) {
267
302
  let mid = (min + max) >> 1;
268
303
  if (items[mid].sortStr < sortStr) {
@@ -288,6 +323,8 @@ class OnEachScope extends Scope {
288
323
  }
289
324
  }
290
325
  removeFromPosition(child) {
326
+ if (child.sortStr === '')
327
+ return;
291
328
  let pos = this.findPosition(child.sortStr);
292
329
  while (true) {
293
330
  if (this.byPosition[pos] === child) {
@@ -335,7 +372,7 @@ class OnEachItemScope extends Scope {
335
372
  let itemStore = new Store(this.parent.collection, this.itemIndex);
336
373
  let sortKey;
337
374
  try {
338
- sortKey = this.parent.makeSortKey(itemStore); // TODO: catch
375
+ sortKey = this.parent.makeSortKey(itemStore);
339
376
  }
340
377
  catch (e) {
341
378
  handleError(e);
@@ -393,10 +430,10 @@ class ObsCollection {
393
430
  emitChange(index, newData, oldData) {
394
431
  let obsSet = this.observers.get(index);
395
432
  if (obsSet)
396
- obsSet.forEach(observer => observer.onChange(this, index, newData, oldData));
433
+ obsSet.forEach(observer => observer.onChange(index, newData, oldData));
397
434
  obsSet = this.observers.get(ANY_INDEX);
398
435
  if (obsSet)
399
- obsSet.forEach(observer => observer.onChange(this, index, newData, oldData));
436
+ obsSet.forEach(observer => observer.onChange(index, newData, oldData));
400
437
  }
401
438
  _clean(observer) {
402
439
  this.removeObserver(ANY_INDEX, observer);
@@ -404,7 +441,7 @@ class ObsCollection {
404
441
  setIndex(index, newValue, deleteMissing) {
405
442
  const curData = this.rawGet(index);
406
443
  if (!(curData instanceof ObsCollection) || newValue instanceof Store || !curData.merge(newValue, deleteMissing)) {
407
- let newData = Store._valueToData(newValue);
444
+ let newData = valueToData(newValue);
408
445
  if (newData !== curData) {
409
446
  this.rawSet(index, newData);
410
447
  this.emitChange(index, newData, curData);
@@ -437,7 +474,7 @@ class ObsArray extends ObsCollection {
437
474
  return this.data[index];
438
475
  }
439
476
  rawSet(index, newData) {
440
- if (index !== (0 | index) || index < 0 || index > 99999) {
477
+ if (index !== (0 | index) || index < 0 || index > 999999) {
441
478
  throw new Error(`Invalid array index ${JSON.stringify(index)}`);
442
479
  }
443
480
  this.data[index] = newData;
@@ -467,11 +504,25 @@ class ObsArray extends ObsCollection {
467
504
  }
468
505
  iterateIndexes(scope) {
469
506
  for (let i = 0; i < this.data.length; i++) {
470
- scope.addChild(i);
507
+ if (this.data[i] !== undefined) {
508
+ scope.addChild(i);
509
+ }
471
510
  }
472
511
  }
473
512
  normalizeIndex(index) {
474
- return 0 | index;
513
+ if (typeof index === 'number')
514
+ return index;
515
+ if (typeof index === 'string') {
516
+ // Convert to int
517
+ let num = 0 | index;
518
+ // Check if the number is still the same after conversion
519
+ if (index.length && num == index)
520
+ return index;
521
+ }
522
+ throw new Error(`Invalid array index ${JSON.stringify(index)}`);
523
+ }
524
+ getCount() {
525
+ return this.data.length;
475
526
  }
476
527
  }
477
528
  class ObsMap extends ObsCollection {
@@ -529,6 +580,9 @@ class ObsMap extends ObsCollection {
529
580
  normalizeIndex(index) {
530
581
  return index;
531
582
  }
583
+ getCount() {
584
+ return this.data.size;
585
+ }
532
586
  }
533
587
  class ObsObject extends ObsMap {
534
588
  getType() {
@@ -563,40 +617,18 @@ class ObsObject extends ObsMap {
563
617
  return true;
564
618
  }
565
619
  normalizeIndex(index) {
566
- return '' + index;
567
- }
568
- }
569
- class ObsDetached extends ObsCollection {
570
- getType() {
571
- return "obect";
572
- }
573
- getRecursive(depth) {
574
- return {};
575
- }
576
- rawGet(index) {
577
- return undefined;
578
- }
579
- rawSet(index) {
580
- throw new Error("Updating a detached Store does not make sense");
581
- }
582
- merge(newValue, deleteMissing) {
583
- throw new Error("Updating a detached Store does not make sense");
584
- }
585
- iterateIndexes(scope) {
586
- }
587
- normalizeIndex(index) {
588
- return index;
589
- }
590
- }
591
- const obsDetached = new ObsDetached();
592
- class StoreAttacher {
593
- constructor(source, target) {
594
- this.sourceStore = source;
595
- this.targetData = target;
596
- }
597
- onChange(collection, index, newData, oldData) {
598
- this.sourceStore.set(this.targetData);
599
- collection.removeObserver(index, this);
620
+ let type = typeof index;
621
+ if (type === 'string')
622
+ return index;
623
+ if (type === 'number')
624
+ return '' + index;
625
+ throw new Error(`Invalid object index ${JSON.stringify(index)}`);
626
+ }
627
+ getCount() {
628
+ let cnt = 0;
629
+ for (let key of this.data)
630
+ cnt++;
631
+ return cnt;
600
632
  }
601
633
  }
602
634
  /*
@@ -615,7 +647,7 @@ export class Store {
615
647
  this.collection = new ObsArray();
616
648
  this.idx = 0;
617
649
  if (value !== undefined) {
618
- this.collection.rawSet(0, Store._valueToData(value));
650
+ this.collection.rawSet(0, valueToData(value));
619
651
  }
620
652
  }
621
653
  else {
@@ -626,172 +658,263 @@ export class Store {
626
658
  this.idx = index;
627
659
  }
628
660
  }
661
+ /**
662
+ *
663
+ * @returns The index for this Store within its parent collection. This will be a `number`
664
+ * when the parent collection is an array, a `string` when it's an object, or any data type
665
+ * when it's a `Map`.
666
+ *
667
+ * @example
668
+ * ```
669
+ * let store = new Store({x: 123})
670
+ * let subStore = store.ref('x')
671
+ * assert(subStore.get() === 123)
672
+ * assert(subStore.index() === 'x') // <----
673
+ * ```
674
+ */
629
675
  index() {
630
676
  return this.idx;
631
677
  }
632
- _read() {
633
- return this.collection.rawGet(this.idx);
634
- }
678
+ /** @internal */
635
679
  _clean(scope) {
636
680
  this.collection.removeObserver(this.idx, scope);
637
681
  }
638
682
  /**
639
- * Return the value for this store, subscribing to the store and any nested sub-stores.
640
- *
641
- * @param defaultValue -
642
- * @param useMaps - When this argument is `true`, objects are represented as Maps. By default, they are plain old JavaScript objects.
643
- *
644
- * options:
645
- * - path
646
- * - peek
647
- * - throw if not type
648
- * - default value (if undefined)
649
- * - depth
683
+ * @returns Resolves `path` and then retrieves the value that is there, subscribing
684
+ * to all read `Store` values. If `path` does not exist, `undefined` is returned.
685
+ * @param path - Any path terms to resolve before retrieving the value.
686
+ * @example
687
+ * ```
688
+ * let store = new Store({a: {b: {c: {d: 42}}}})
689
+ * assert(store.get('a', 'b') === {c: {d: 42}})
690
+ * ```
650
691
  */
651
- get(defaultValue, depth) {
652
- let value = this._observe();
653
- if (value instanceof ObsCollection) {
654
- return value.getRecursive(depth == null ? -1 : depth);
655
- }
656
- return value === undefined ? defaultValue : value;
692
+ get(...path) {
693
+ return this.query({ path });
657
694
  }
658
695
  /**
659
- * Returns "undefined", "null", "boolean", "number", "string", "function", "array", "map" or "object"
696
+ * @returns The same as [[`get`]], but doesn't subscribe to changes.
660
697
  */
661
- getType() {
662
- let value = this._observe();
663
- return (value instanceof ObsCollection) ? value.getType() : typeof value;
698
+ peek(...path) {
699
+ return this.query({ path, peek: true });
664
700
  }
665
701
  /**
666
- * Return the value of this Store as a number. If is has a different type, an error is thrown.
667
- * If the Store contains `undefined` and a defaultValue is given, it is returned instead.
702
+ * @returns Like [[`get`]], but throws a `TypeError` if the resulting value is not of type `number`.
703
+ * Using this instead of just [[`get`]] is especially useful from within TypeScript.
668
704
  */
669
- getNumber(defaultValue) {
670
- let value = this._observe();
671
- if (typeof value === 'number')
672
- return value;
673
- if (value === undefined && defaultValue !== undefined)
674
- return defaultValue;
675
- throw this.getTypeError('number', value);
676
- }
705
+ getNumber(...path) { return this.query({ path, type: 'number' }); }
677
706
  /**
678
- * Return the value of this Store as a string. If is has a different type, an error is thrown.
679
- * If the Store contains `undefined` and a defaultValue is given, it is returned instead.
707
+ * @returns Like [[`get`]], but throws a `TypeError` if the resulting value is not of type `string`.
708
+ * Using this instead of just [[`get`]] is especially useful from within TypeScript.
680
709
  */
681
- getString(defaultValue) {
682
- let value = this._observe();
683
- if (typeof value === 'string')
684
- return value;
685
- if (value === undefined && defaultValue !== undefined)
686
- return defaultValue;
687
- throw this.getTypeError('string', value);
688
- }
710
+ getString(...path) { return this.query({ path, type: 'string' }); }
689
711
  /**
690
- * Return the value of this Store as a boolean. If is has a different type, an error is thrown.
691
- * If the Store contains `undefined` and a defaultValue is given, it is returned instead.
712
+ * @returns Like [[`get`]], but throws a `TypeError` if the resulting value is not of type `boolean`.
713
+ * Using this instead of just [[`get`]] is especially useful from within TypeScript.
692
714
  */
693
- getBoolean(defaultValue) {
694
- let value = this._observe();
695
- if (typeof value === 'boolean')
696
- return value;
697
- if (value === undefined && defaultValue !== undefined)
698
- return defaultValue;
699
- throw this.getTypeError('boolean', value);
700
- }
715
+ getBoolean(...path) { return this.query({ path, type: 'boolean' }); }
701
716
  /**
702
- * Return the values of this Store as an Array. If is has a different type, an error is thrown.
703
- * If the Store contains `undefined` and a defaultValue is given, it is returned instead.
717
+ * @returns Like [[`get`]], but throws a `TypeError` if the resulting value is not of type `function`.
718
+ * Using this instead of just [[`get`]] is especially useful from within TypeScript.
704
719
  */
705
- getArray(defaultValue, depth) {
706
- let value = this._observe();
707
- if (value instanceof ObsArray) {
708
- return value.getRecursive(depth == null ? -1 : depth);
709
- }
710
- if (value === undefined && defaultValue !== undefined)
711
- return defaultValue;
712
- throw this.getTypeError('array', value);
713
- }
720
+ getFunction(...path) { return this.query({ path, type: 'function' }); }
714
721
  /**
715
- * Return the values of this Store as an Object. If is has a different type, an error is thrown.
716
- * If the Store contains `undefined` and a defaultValue is given, it is returned instead.
722
+ * @returns Like [[`get`]], but throws a `TypeError` if the resulting value is not of type `array`.
723
+ * Using this instead of just [[`get`]] is especially useful from within TypeScript.
717
724
  */
718
- getObject(defaultValue, depth) {
719
- let value = this._observe();
720
- if (value instanceof ObsObject) {
721
- return value.getRecursive(depth == null ? -1 : depth);
722
- }
723
- if (value === undefined && defaultValue !== undefined)
724
- return defaultValue;
725
- throw this.getTypeError('object', value);
726
- }
725
+ getArray(...path) { return this.query({ path, type: 'array' }); }
727
726
  /**
728
- * Return the values of this Store as an Object. If is has a different type, an error is thrown.
729
- * If the Store contains `undefined` and a defaultValue is given, it is returned instead.
727
+ * @returns Like [[`get`]], but throws a `TypeError` if the resulting value is not of type `object`.
728
+ * Using this instead of just [[`get`]] is especially useful from within TypeScript.
730
729
  */
731
- getMap(defaultValue, depth) {
732
- let value = this._observe();
733
- if (value instanceof ObsMap) {
734
- return value.getRecursive(depth == null ? -1 : depth);
730
+ getObject(...path) { return this.query({ path, type: 'object' }); }
731
+ /**
732
+ * @returns Like [[`get`]], but throws a `TypeError` if the resulting value is not of type `map`.
733
+ * Using this instead of just [[`get`]] is especially useful from within TypeScript.
734
+ */
735
+ getMap(...path) { return this.query({ path, type: 'map' }); }
736
+ /**
737
+ * @returns Like [[`get`]], but the first parameter is the default value (returned when the Store
738
+ * contains `undefined`). This default value is also used to determine the expected type,
739
+ * and to throw otherwise.
740
+ */
741
+ getOr(defaultValue, ...path) {
742
+ let type = typeof defaultValue;
743
+ if (type === 'object') {
744
+ if (defaultValue instanceof Map)
745
+ type = 'map';
746
+ else if (defaultValue instanceof Array)
747
+ type = 'array';
748
+ }
749
+ return this.query({ type, defaultValue, path });
750
+ }
751
+ /** Retrieve a value. This is a more flexible form of the [[`get`]] and [[`peek`]] methods.
752
+ * @returns The resulting value, or `undefined` if the `path` does not exist.
753
+ */
754
+ query(opts) {
755
+ if (opts.peek && currentScope) {
756
+ let savedScope = currentScope;
757
+ currentScope = undefined;
758
+ let result = this.query(opts);
759
+ currentScope = savedScope;
760
+ return result;
735
761
  }
736
- if (value === undefined && defaultValue !== undefined)
737
- return defaultValue;
738
- throw this.getTypeError('map', value);
762
+ let store = opts.path && opts.path.length ? this.ref(...opts.path) : this;
763
+ let value = store._observe();
764
+ if (opts.type && (value !== undefined || opts.defaultValue === undefined)) {
765
+ let type = (value instanceof ObsCollection) ? value.getType() : (value === null ? "null" : typeof value);
766
+ if (type !== opts.type)
767
+ throw new TypeError(`Expecting ${opts.type} but got ${type}`);
768
+ }
769
+ if (value instanceof ObsCollection) {
770
+ return value.getRecursive(opts.depth == null ? -1 : opts.depth - 1);
771
+ }
772
+ return value === undefined ? opts.defaultValue : value;
739
773
  }
740
- getTypeError(type, value) {
741
- if (value === undefined) {
742
- return new Error(`Expecting ${type} but got undefined, and no default value was given`);
774
+ isEmpty(...path) {
775
+ let store = this.ref(...path);
776
+ let value = store._observe();
777
+ if (value instanceof ObsCollection) {
778
+ if (currentScope) {
779
+ let observer = new IsEmptyObserver(currentScope, value, false);
780
+ return !observer.count;
781
+ }
782
+ else {
783
+ return !value.getCount();
784
+ }
785
+ }
786
+ else if (value === undefined) {
787
+ return true;
788
+ }
789
+ else {
790
+ throw new Error(`isEmpty() expects a collection or undefined, but got ${JSON.stringify(value)}`);
791
+ }
792
+ }
793
+ count(...path) {
794
+ let store = this.ref(...path);
795
+ let value = store._observe();
796
+ if (value instanceof ObsCollection) {
797
+ if (currentScope) {
798
+ let observer = new IsEmptyObserver(currentScope, value, true);
799
+ return observer.count;
800
+ }
801
+ else {
802
+ return value.getCount();
803
+ }
804
+ }
805
+ else if (value === undefined) {
806
+ return 0;
743
807
  }
744
808
  else {
745
- return new Error(`Expecting ${type} but got ${value instanceof ObsCollection ? value.getRecursive(-1) : JSON.stringify(value)}`);
809
+ throw new Error(`count() expects a collection or undefined, but got ${JSON.stringify(value)}`);
746
810
  }
747
811
  }
748
- set(newValue) {
749
- this.collection.setIndex(this.idx, newValue, true);
812
+ /**
813
+ * Returns "undefined", "null", "boolean", "number", "string", "function", "array", "map" or "object"
814
+ */
815
+ getType(...path) {
816
+ let store = this.ref(...path);
817
+ let value = store._observe();
818
+ return (value instanceof ObsCollection) ? value.getType() : (value === null ? "null" : typeof value);
819
+ }
820
+ /**
821
+ * Sets the Store value to the last given argument. Any earlier argument are a Store-path that is first
822
+ * resolved/created using `makeRef`.
823
+ */
824
+ set(...pathAndValue) {
825
+ let newValue = pathAndValue.pop();
826
+ let store = this.makeRef(...pathAndValue);
827
+ store.collection.setIndex(store.idx, newValue, true);
750
828
  }
751
829
  /**
752
- * Does the same as merge, but in case of a top-level map, it doesn't
753
- * delete keys that don't exist in `value`.
830
+ * Sets the `Store` to the given `mergeValue`, but without deleting any pre-existing
831
+ * items when a collection overwrites a similarly typed collection. This results in
832
+ * a deep merge.
754
833
  */
755
- merge(newValue) {
756
- this.collection.setIndex(this.idx, newValue, false);
834
+ merge(mergeValue) {
835
+ this.collection.setIndex(this.idx, mergeValue, false);
757
836
  }
758
837
  /**
759
- * Sets the value for the store to `undefined`, which causes it to be ommitted from the map (or array, if it's at the end)
838
+ * Sets the value for the store to `undefined`, which causes it to be omitted from the map (or array, if it's at the end)
760
839
  */
761
- delete() {
762
- this.collection.setIndex(this.idx, undefined, true);
840
+ delete(...path) {
841
+ let store = this.makeRef(...path);
842
+ store.collection.setIndex(store.idx, undefined, true);
763
843
  }
764
844
  /**
765
- * Return an store deeper within the tree by resolving each of the
766
- * arguments as Map indexes, while subscribing to each level.
767
- * If any level does not exist, a detached Store object is returned,
768
- * that will be automatically attached if it is written to.
845
+ * Pushes a value to the end of the Array that is at the specified path in the store.
846
+ * If that Store path is `undefined`, and Array is created first.
847
+ * The last argument is the value to be added, any earlier arguments indicate the path.
769
848
  */
770
- ref(...indexes) {
849
+ push(newValue) {
850
+ let obsArray = this.collection.rawGet(this.idx);
851
+ if (obsArray === undefined) {
852
+ obsArray = new ObsArray();
853
+ this.collection.setIndex(this.idx, obsArray, true);
854
+ }
855
+ else if (!(obsArray instanceof ObsArray)) {
856
+ throw new Error(`push() is only allowed for an array or undefined (which would become an array)`);
857
+ }
858
+ let newData = valueToData(newValue);
859
+ let pos = obsArray.data.length;
860
+ obsArray.data.push(newData);
861
+ obsArray.emitChange(pos, newData, undefined);
862
+ return pos;
863
+ }
864
+ /**
865
+ * [[`peek`]] the current value, pass it through `func`, and [[`set`]] the resulting
866
+ * value.
867
+ * @param func The function transforming the value.
868
+ */
869
+ modify(func) {
870
+ this.set(func(this.query({ peek: true })));
871
+ }
872
+ /**
873
+ * Return a `Store` deeper within the tree by resolving the given `path`,
874
+ * subscribing to every level.
875
+ * In case `undefined` is encountered while resolving the path, a newly
876
+ * created `Store` containing `undefined` is returned. In that case, the
877
+ * `Store`'s [[`isDetached`]] method will return `true`.
878
+ * In case something other than a collection is encountered, an error is thrown.
879
+ */
880
+ ref(...path) {
771
881
  let store = this;
772
- for (let i = 0; i < indexes.length; i++) {
882
+ for (let i = 0; i < path.length; i++) {
773
883
  let value = store._observe();
774
884
  if (value instanceof ObsCollection) {
775
- store = new Store(value, value.normalizeIndex(indexes[i]));
885
+ store = new Store(value, value.normalizeIndex(path[i]));
776
886
  }
777
887
  else {
778
888
  if (value !== undefined)
779
- throw new Error(`Value ${JSON.stringify(value)} is not a collection (nor undefined) in step ${i} of $(${JSON.stringify(indexes)})`);
780
- // The rest of the path will be created in a detached state
781
- let detachedObject = new ObsObject();
782
- let object = detachedObject;
783
- for (; i < indexes.length - 1; i++) {
784
- let newObject = new ObsObject();
785
- object.data.set("" + indexes[i], newObject);
786
- object = newObject;
787
- }
788
- let index = "" + indexes[indexes.length - 1];
789
- object.addObserver(index, new StoreAttacher(store, detachedObject));
790
- return new Store(object, index);
889
+ throw new Error(`Value ${JSON.stringify(value)} is not a collection (nor undefined) in step ${i} of $(${JSON.stringify(path)})`);
890
+ return new DetachedStore();
791
891
  }
792
892
  }
793
893
  return store;
794
894
  }
895
+ /**
896
+ * Similar to `ref()`, but instead of returning `undefined`, new objects are created when
897
+ * a path does not exist yet. An error is still thrown when the path tries to index an invalid
898
+ * type.
899
+ * Unlike `ref`, `makeRef` does *not* subscribe to the path levels, as it is intended to be
900
+ * a write-only operation.
901
+ */
902
+ makeRef(...path) {
903
+ let store = this;
904
+ for (let i = 0; i < path.length; i++) {
905
+ let value = store.collection.rawGet(store.idx);
906
+ if (!(value instanceof ObsCollection)) {
907
+ if (value !== undefined)
908
+ throw new Error(`Value ${JSON.stringify(value)} is not a collection (nor undefined) in step ${i} of $(${JSON.stringify(path)})`);
909
+ value = new ObsObject();
910
+ store.collection.rawSet(store.idx, value);
911
+ store.collection.emitChange(store.idx, value, undefined);
912
+ }
913
+ store = new Store(value, value.normalizeIndex(path[i]));
914
+ }
915
+ return store;
916
+ }
917
+ /** @Internal */
795
918
  _observe() {
796
919
  if (currentScope) {
797
920
  if (this.collection.addObserver(this.idx, currentScope)) {
@@ -800,31 +923,20 @@ export class Store {
800
923
  }
801
924
  return this.collection.rawGet(this.idx);
802
925
  }
803
- /**
804
- * Adds `newValue` as a value to a Map, indexed by the old `size()` of the Map. An
805
- * error is thrown if that index already exists.
806
- * In case the Store does not refers to `undefined`, the Array is created first.
807
- * @param newValue
808
- */
809
- push(newValue) {
810
- let obsArray = this.collection.rawGet(this.idx);
811
- if (obsArray === undefined) {
812
- obsArray = new ObsArray();
813
- this.collection.setIndex(this.idx, obsArray, true);
926
+ onEach(...pathAndFuncs) {
927
+ let makeSortKey = defaultMakeSortKey;
928
+ let renderer = pathAndFuncs.pop();
929
+ if (typeof pathAndFuncs[pathAndFuncs.length - 1] === 'function' && (typeof renderer === 'function' || renderer == null)) {
930
+ if (renderer != null)
931
+ makeSortKey = renderer;
932
+ renderer = pathAndFuncs.pop();
814
933
  }
815
- else if (!(obsArray instanceof ObsArray)) {
816
- throw new Error(`push() is only allowed for an array or undefined (which would become an array)`);
817
- }
818
- let newData = Store._valueToData(newValue);
819
- let pos = obsArray.data.length;
820
- obsArray.data.push(newData);
821
- obsArray.emitChange(pos, newData, undefined);
822
- return pos;
823
- }
824
- onEach(renderer, makeSortKey = Store._makeDefaultSortKey) {
934
+ if (typeof renderer !== 'function')
935
+ throw new Error(`onEach() expects a render function as its last argument but got ${JSON.stringify(renderer)}`);
825
936
  if (!currentScope)
826
- throw new Error("onEach() is only allowed from a render scope");
827
- let val = this._observe();
937
+ throw new ScopeError(false);
938
+ let store = this.ref(...pathAndFuncs);
939
+ let val = store._observe();
828
940
  if (val instanceof ObsCollection) {
829
941
  // Subscribe to changes using the specialized OnEachScope
830
942
  let onEachScope = new OnEachScope(currentScope.parentElement, currentScope.lastChild || currentScope.precedingSibling, currentScope.queueOrder + 1, val, renderer, makeSortKey);
@@ -837,79 +949,123 @@ export class Store {
837
949
  throw new Error(`onEach() attempted on a value that is neither a collection nor undefined`);
838
950
  }
839
951
  }
840
- static _valueToData(value) {
841
- if (typeof value !== "object" || !value) {
842
- // Simple data types
843
- return value;
844
- }
845
- else if (value instanceof Store) {
846
- // When a Store is passed pointing at a collection, a reference
847
- // is made to that collection.
848
- return value._observe();
849
- }
850
- else if (value instanceof Map) {
851
- let result = new ObsMap();
852
- value.forEach((v, k) => {
853
- let d = Store._valueToData(v);
854
- if (d !== undefined)
855
- result.rawSet(k, d);
856
- });
857
- return result;
858
- }
859
- else if (value instanceof Array) {
860
- let result = new ObsArray();
861
- for (let i = 0; i < value.length; i++) {
862
- let d = Store._valueToData(value[i]);
863
- if (d !== undefined)
864
- result.rawSet(i, d);
952
+ /**
953
+ * Applies a filter/map function on each item within the `Store`'s collection,
954
+ * and reactively manages the returned `Map` `Store` to hold any results.
955
+ *
956
+ * @param func - Function that transform the given store into an output value or
957
+ * `undefined` in case this value should be skipped:
958
+ *
959
+ * @returns - A map `Store` with the values returned by `func` and the corresponding
960
+ * keys from the original map, array or object `Store`.
961
+ *
962
+ * When items disappear from the `Store` or are changed in a way that `func` depends
963
+ * upon, the resulting items are removed from the output `Store` as well. When multiple
964
+ * input items produce the same output keys, this may lead to unexpected results.
965
+ */
966
+ map(func) {
967
+ let out = new Store(new Map());
968
+ this.onEach((item) => {
969
+ let value = func(item);
970
+ if (value !== undefined) {
971
+ let key = item.index();
972
+ out.set(key, value);
973
+ clean(() => {
974
+ out.delete(key);
975
+ });
865
976
  }
866
- return result;
867
- }
868
- else if (value.constructor === Object) {
869
- // A plain (literal) object
870
- let result = new ObsObject();
871
- for (let k in value) {
872
- let d = Store._valueToData(value[k]);
873
- if (d !== undefined)
874
- result.rawSet(k, d);
977
+ });
978
+ return out;
979
+ }
980
+ /*
981
+ * Applies a filter/map function on each item within the `Store`'s collection,
982
+ * each of which can deliver any number of key/value pairs, and reactively manages the
983
+ * returned map `Store` to hold any results.
984
+ *
985
+ * @param func - Function that transform the given store into output values
986
+ * that can take one of the following forms:
987
+ * - an `Object` or a `Map`: Each key/value pair will be added to the output `Store`.
988
+ * - anything else: No key/value pairs are added to the output `Store`.
989
+ *
990
+ * @returns - A map `Store` with the key/value pairs returned by all `func` invocations.
991
+ *
992
+ * When items disappear from the `Store` or are changed in a way that `func` depends
993
+ * upon, the resulting items are removed from the output `Store` as well. When multiple
994
+ * input items produce the same output keys, this may lead to unexpected results.
995
+ */
996
+ multiMap(func) {
997
+ let out = new Store(new Map());
998
+ this.onEach((item) => {
999
+ let result = func(item);
1000
+ let keys;
1001
+ if (result.constructor === Object) {
1002
+ for (let key in result) {
1003
+ out.set(key, result[key]);
1004
+ }
1005
+ keys = Object.keys(result);
875
1006
  }
876
- return result;
877
- }
878
- else {
879
- // Any other type of object (including ObsCollection)
880
- return value;
881
- }
882
- }
883
- static _makeDefaultSortKey(store) {
884
- return store.index();
1007
+ else if (result instanceof Map) {
1008
+ result.forEach((value, key) => {
1009
+ out.set(key, value);
1010
+ });
1011
+ keys = Array.from(result.keys());
1012
+ }
1013
+ else {
1014
+ return;
1015
+ }
1016
+ if (keys.length) {
1017
+ clean(() => {
1018
+ for (let key of keys) {
1019
+ out.delete(key);
1020
+ }
1021
+ });
1022
+ }
1023
+ });
1024
+ return out;
885
1025
  }
1026
+ /**
1027
+ * @returns Returns `true` when the `Store` was created by [[`ref`]]ing a path that
1028
+ * does not exist.
1029
+ */
1030
+ isDetached() { return false; }
1031
+ }
1032
+ class DetachedStore extends Store {
1033
+ isDetached() { return true; }
886
1034
  }
887
1035
  /**
888
1036
  * Create a new DOM element.
889
- * @param tagClass - The tag of the element to be created and optionally dot-seperated class names. For example: `h1` or `p.intro.has_avatar`.
1037
+ * @param tag - The tag of the element to be created and optionally dot-separated class names. For example: `h1` or `p.intro.has_avatar`.
890
1038
  * @param rest - The other arguments are flexible and interpreted based on their types:
891
1039
  * - `string`: Used as textContent for the element.
892
1040
  * - `object`: Used as attributes/properties for the element. See `applyProp` on how the distinction is made.
893
1041
  * - `function`: The render function used to draw the scope of the element. This function gets its own `Scope`, so that if any `Store` it reads changes, it will redraw by itself.
1042
+ * - `Store`: Presuming `tag` is `"input"`, `"textarea"` or `"select"`, create a two-way binding between this `Store` value and the input element. The initial value of the input will be set to the initial value of the `Store`. After that, the `Store` will be updated when the input changes.
894
1043
  * @example
895
1044
  * node('aside.editorial', 'Yada yada yada....', () => {
896
- * node('a', {href: '/bio'}, () => {
897
- * node('img.author', {src: '/me.jpg', alt: 'The author'})
898
- * })
1045
+ * node('a', {href: '/bio'}, () => {
1046
+ * node('img.author', {src: '/me.jpg', alt: 'The author'})
1047
+ * })
899
1048
  * })
900
1049
  */
901
- export function node(tagClass, ...rest) {
1050
+ export function node(tag = "", ...rest) {
902
1051
  if (!currentScope)
903
- throw new Error(`node() outside of a render scope`);
1052
+ throw new ScopeError(true);
904
1053
  let el;
905
- if (tagClass.indexOf('.') >= 0) {
906
- let classes = tagClass.split('.');
907
- let tag = classes.shift();
908
- el = document.createElement(tag);
909
- el.className = classes.join(' ');
1054
+ if (tag instanceof Element) {
1055
+ el = tag;
910
1056
  }
911
1057
  else {
912
- el = document.createElement(tagClass);
1058
+ let pos = tag.indexOf('.');
1059
+ let classes;
1060
+ if (pos >= 0) {
1061
+ classes = tag.substr(pos + 1);
1062
+ tag = tag.substr(0, pos);
1063
+ }
1064
+ el = document.createElement(tag || 'div');
1065
+ if (classes) {
1066
+ // @ts-ignore (replaceAll is polyfilled)
1067
+ el.className = classes.replaceAll('.', ' ');
1068
+ }
913
1069
  }
914
1070
  currentScope.addNode(el);
915
1071
  for (let item of rest) {
@@ -929,24 +1085,62 @@ export function node(tagClass, ...rest) {
929
1085
  applyProp(el, k, item[k]);
930
1086
  }
931
1087
  }
1088
+ else if (item instanceof Store) {
1089
+ bindInput(el, item);
1090
+ }
932
1091
  else if (item != null) {
933
1092
  throw new Error(`Unexpected argument ${JSON.stringify(item)}`);
934
1093
  }
935
1094
  }
936
1095
  }
1096
+ function bindInput(el, store) {
1097
+ let updater;
1098
+ let type = el.getAttribute('type');
1099
+ let value = store.query({ peek: true });
1100
+ if (type === 'checkbox') {
1101
+ if (value === undefined)
1102
+ store.set(el.checked);
1103
+ else
1104
+ el.checked = value;
1105
+ updater = () => store.set(el.checked);
1106
+ }
1107
+ else if (type === 'radio') {
1108
+ if (value === undefined) {
1109
+ if (el.checked)
1110
+ store.set(el.value);
1111
+ }
1112
+ else
1113
+ el.checked = value === el.value;
1114
+ updater = () => {
1115
+ if (el.checked)
1116
+ store.set(el.value);
1117
+ };
1118
+ }
1119
+ else {
1120
+ if (value === undefined)
1121
+ store.set(el.value);
1122
+ else
1123
+ el.value = value;
1124
+ updater = () => store.set(el.value);
1125
+ }
1126
+ el.addEventListener('input', updater);
1127
+ clean(() => {
1128
+ el.removeEventListener('input', updater);
1129
+ });
1130
+ }
937
1131
  /**
938
1132
  * Add a text node at the current Scope position.
939
1133
  */
940
1134
  export function text(text) {
941
1135
  if (!currentScope)
942
- throw new Error(`text() outside of a render scope`);
943
- if (!text)
1136
+ throw new ScopeError(true);
1137
+ if (text == null)
944
1138
  return;
945
1139
  currentScope.addNode(document.createTextNode(text));
946
1140
  }
947
1141
  export function prop(prop, value = undefined) {
948
- if (!currentScope)
949
- throw new Error(`prop() outside of a render scope`);
1142
+ if (!currentScope || !currentScope.parentElement)
1143
+ throw new ScopeError(true);
950
1144
  if (typeof prop === 'object') {
951
1145
  for (let k in prop) {
952
1146
  applyProp(currentScope.parentElement, k, prop[k]);
@@ -957,54 +1151,153 @@ export function prop(prop, value = undefined) {
957
1151
  }
958
1152
  }
959
1153
  /**
960
- * Register a `clean` function that is executed when the current `Scope` disappears or redraws.
1154
+ * Return the browser Element that `node()`s would be rendered to at this point.
1155
+ * NOTE: Manually changing the DOM is not recommended in most cases. There is
1156
+ * usually a better, declarative way. Although there are no hard guarantees on
1157
+ * how your changes interact with Aberdeen, in most cases results will not be
1158
+ * terribly surprising. Be careful within the parent element of onEach() though.
1159
+ */
1160
+ export function getParentElement() {
1161
+ if (!currentScope || !currentScope.parentElement)
1162
+ throw new ScopeError(true);
1163
+ return currentScope.parentElement;
1164
+ }
1165
+ /**
1166
+ * Register a function that is to be executed right before the current reactive scope
1167
+ * disappears or redraws.
1168
+ * @param clean - The function to be executed.
961
1169
  */
962
1170
  export function clean(clean) {
963
1171
  if (!currentScope)
964
- throw new Error(`clean() outside of a render scope`);
1172
+ throw new ScopeError(false);
965
1173
  currentScope.cleaners.push({ _clean: clean });
966
1174
  }
967
1175
  /**
968
- * Create a new Scope and execute the `renderer` within that Scope. When
969
- * `Store`s that the `renderer` reads are updated, only this Scope will
970
- * need to be refreshed, leaving the parent Scope untouched.
1176
+ * Create a new reactive scope and execute the `func` within that scope. When
1177
+ * `Store`s that the `func` reads are updated, only this scope will need to be refreshed,
1178
+ * leaving the parent scope untouched.
1179
+ *
1180
+ * In case this function is called outside of a an existing scope, it will create a new
1181
+ * top-level scope (a [[`Mount`]]) without a `parentElement`, meaning that aberdeen operations
1182
+ * that create/modify DOM elements are not permitted.
1183
+ * @param func - The function to be (repeatedly) executed within the newly created scope.
1184
+ * @returns The newly created `Mount` object in case this is a top-level reactive scope.
1185
+ * @example
1186
+ * ```
1187
+ * let store = new Store('John Doe')
1188
+ * mount(document.body, () => {
1189
+ * node('div.card', () => {
1190
+ * node('input', {placeholder: 'Name'}, store)
1191
+ * observe(() => {
1192
+ * prop('class', {correct: store.get().length > 5})
1193
+ * })
1194
+ * })
1195
+ * })
1196
+ * ```
971
1197
  */
972
- export function scope(renderer) {
973
- if (!currentScope)
974
- throw new Error(`scope() outside of a render scope`);
975
- let scope = new SimpleScope(currentScope.parentElement, currentScope.lastChild || currentScope.precedingSibling, currentScope.queueOrder + 1, renderer);
976
- currentScope.lastChild = scope;
977
- scope.update();
978
- // Add it to our list of cleaners. Even if `scope` currently has
979
- // no cleaners, it may get them in a future refresh.
980
- currentScope.cleaners.push(scope);
1198
+ /**
1199
+ * Reactively run a function, meaning the function will rerun when any `Store` that was read
1200
+ * during its execution is updated.
1201
+ * Calls to `observe` can be nested, such that changes to `Store`s read by the inner function do
1202
+ * no cause the outer function to rerun.
1203
+ *
1204
+ * @param func - The function to be (repeatedly) executed.
1205
+ * @example
1206
+ * ```
1207
+ * let number = new Store(0)
1208
+ * let doubled = new Store()
1209
+ * setInterval(() => number.set(0|Math.random()*100)), 1000)
1210
+ *
1211
+ * observe(() => {
1212
+ * doubled.set(number.get() * 2)
1213
+ * })
1214
+ *
1215
+ * observe(() => {
1216
+ * console.log(doubled.get())
1217
+ * })
1218
+ */
1219
+ export function observe(func) {
1220
+ mount(undefined, func);
981
1221
  }
982
1222
  /**
983
- * Main entry point for using aberdeen. The elements created by the given `render` function are appended to `parentElement` (and updated when read `Store`s change).
984
- * @param parentElement - The DOM element to append to.
985
- * @param renderer - The function that does the rendering.
1223
+ * Like [[`observe`]], but allow the function to create DOM elements using [[`node`]].
1224
+
1225
+ * @param func - The function to be (repeatedly) executed, possibly adding DOM elements to `parentElement`.
1226
+ * @param parentElement - A DOM element that will be used as the parent element for calls to `node`.
1227
+ *
986
1228
  * @example
1229
+ * ```
1230
+ * let store = new Store(0)
1231
+ * setInterval(() => store.modify(v => v+1), 1000)
1232
+ *
987
1233
  * mount(document.body, () => {
988
- * node('h1', 'Hello world!', () => {
989
- * node('img.logo', {src: '/logo.png'})
990
- * })
1234
+ * node('h2', `${store.get()} seconds have passed`)
991
1235
  * })
992
- */
993
- class Mount {
994
- constructor(scope) {
995
- this.scope = scope;
1236
+ * ```
1237
+ *
1238
+ * An example nesting [[`observe`]] within `mount`:
1239
+ * ```
1240
+ * let selected = new Store(0)
1241
+ * let colors = new Store(new Map())
1242
+ *
1243
+ * mount(document.body, () => {
1244
+ * // This function will never rerun (as it does not read any `Store`s)
1245
+ * node('button', '<<', {click: () => selected.modify(n => n-1)})
1246
+ * node('button', '>>', {click: () => selected.modify(n => n+1)})
1247
+ *
1248
+ * observe(() => {
1249
+ * // This will rerun whenever `selected` changes, recreating the <h2> and <input>.
1250
+ * node('h2', '#'+selected.get())
1251
+ * node('input', {type: 'color', value: '#ffffff'}, colors.ref(selected.get()))
1252
+ * })
1253
+ *
1254
+ * observe(() => {
1255
+ * // This function will rerun when `selected` or the selected color changes.
1256
+ * // It will change the <body> background-color.
1257
+ * prop({style: {backgroundColor: colors.get(selected.get()) || 'white'}})
1258
+ * })
1259
+ * })
1260
+ * ```
1261
+ */
1262
+ export function mount(parentElement, func) {
1263
+ let scope;
1264
+ if (parentElement || !currentScope) {
1265
+ scope = new SimpleScope(parentElement, undefined, 0, func);
996
1266
  }
997
- unmount() {
998
- this.scope.remove();
1267
+ else {
1268
+ scope = new SimpleScope(currentScope.parentElement, currentScope.lastChild || currentScope.precedingSibling, currentScope.queueOrder + 1, func);
1269
+ currentScope.lastChild = scope;
999
1270
  }
1000
- }
1001
- export function mount(parentElement, renderer) {
1002
- if (currentScope)
1003
- throw new Error('mount() from within a render scope');
1004
- let scope = new SimpleScope(parentElement, undefined, 0, renderer);
1271
+ // Do the initial run
1005
1272
  scope.update();
1006
- return new Mount(scope);
1273
+ // Add it to our list of cleaners. Even if `scope` currently has
1274
+ // no cleaners, it may get them in a future refresh.
1275
+ if (currentScope) {
1276
+ currentScope.cleaners.push(scope);
1277
+ }
1007
1278
  }
1279
+ /** Runs the given function, while not subscribing the current scope when reading [[`Store`]] values.
1280
+ *
1281
+ * @param func Function to be executed immediately.
1282
+ * @returns Whatever `func()` returns.
1283
+ * @example
1284
+ * ```
1285
+ * import {Store, peek, text} from aberdeen
1286
+ *
1287
+ * let store = new Store(['a', 'b', 'c'])
1288
+ *
1289
+ * mount(document.body, () => {
1290
+ * // Prevent rerender when store changes
1291
+ * peek(() => {
1292
+ * text(`Store has ${store.count()} elements, and the first is ${store.get(0)}`)
1293
+ * })
1294
+ * })
1295
+ * ```
1296
+ *
1297
+ * In the above example `store.get(0)` could be replaced with `store.peek(0)` to achieve the
1298
+ * same result without `peek()` wrapping everything. There is no non-subscribing equivalent
1299
+ * for `count()` however.
1300
+ */
1008
1301
  export function peek(func) {
1009
1302
  let savedScope = currentScope;
1010
1303
  currentScope = undefined;
@@ -1019,16 +1312,24 @@ export function peek(func) {
1019
1312
  * Helper functions
1020
1313
  */
1021
1314
  function applyProp(el, prop, value) {
1022
- if (prop === 'value' || prop === 'className' || prop === 'selectedIndex' || value === true || value === false) {
1315
+ if ((prop === 'class' || prop === 'className') && typeof value === 'object') {
1316
+ // Allow setting classes using an object where the keys are the names and
1317
+ // the values are booleans stating whether to set or remove.
1318
+ for (let name in value) {
1319
+ if (value[name])
1320
+ el.classList.add(name);
1321
+ else
1322
+ el.classList.remove(name);
1323
+ }
1324
+ }
1325
+ else if (prop === 'value' || prop === 'className' || prop === 'selectedIndex' || value === true || value === false) {
1023
1326
  // All boolean values and a few specific keys should be set as a property
1024
1327
  el[prop] = value;
1025
1328
  }
1026
1329
  else if (typeof value === 'function') {
1027
1330
  // Set an event listener; remove it again on clean.
1028
1331
  el.addEventListener(prop, value);
1029
- if (currentScope) {
1030
- clean(() => el.removeEventListener(prop, value));
1031
- }
1332
+ clean(() => el.removeEventListener(prop, value));
1032
1333
  }
1033
1334
  else if (prop === 'style' && typeof value === 'object') {
1034
1335
  // `style` can receive an object
@@ -1043,11 +1344,72 @@ function applyProp(el, prop, value) {
1043
1344
  el.setAttribute(prop, value);
1044
1345
  }
1045
1346
  }
1347
+ function valueToData(value) {
1348
+ if (typeof value !== "object" || !value) {
1349
+ // Simple data types
1350
+ return value;
1351
+ }
1352
+ else if (value instanceof Store) {
1353
+ // When a Store is passed pointing at a collection, a reference
1354
+ // is made to that collection.
1355
+ return value._observe();
1356
+ }
1357
+ else if (value instanceof Map) {
1358
+ let result = new ObsMap();
1359
+ value.forEach((v, k) => {
1360
+ let d = valueToData(v);
1361
+ if (d !== undefined)
1362
+ result.rawSet(k, d);
1363
+ });
1364
+ return result;
1365
+ }
1366
+ else if (value instanceof Array) {
1367
+ let result = new ObsArray();
1368
+ for (let i = 0; i < value.length; i++) {
1369
+ let d = valueToData(value[i]);
1370
+ if (d !== undefined)
1371
+ result.rawSet(i, d);
1372
+ }
1373
+ return result;
1374
+ }
1375
+ else if (value.constructor === Object) {
1376
+ // A plain (literal) object
1377
+ let result = new ObsObject();
1378
+ for (let k in value) {
1379
+ let d = valueToData(value[k]);
1380
+ if (d !== undefined)
1381
+ result.rawSet(k, d);
1382
+ }
1383
+ return result;
1384
+ }
1385
+ else {
1386
+ // Any other type of object (including ObsCollection)
1387
+ return value;
1388
+ }
1389
+ }
1390
+ function defaultMakeSortKey(store) {
1391
+ return store.index();
1392
+ }
1393
+ /* istanbul ignore next */
1046
1394
  function internalError(code) {
1047
- console.error(new Error("internal error " + code));
1395
+ let error = new Error("internal error " + code);
1396
+ setTimeout(() => { throw error; }, 0);
1048
1397
  }
1049
1398
  function handleError(e) {
1050
- console.error(e);
1051
1399
  // Throw the error async, so the rest of the rendering can continue
1052
1400
  setTimeout(() => { throw e; }, 0);
1053
1401
  }
1402
+ class ScopeError extends Error {
1403
+ constructor(mount) {
1404
+ super(`Operation not permitted outside of ${mount ? "a mount" : "an observe"}() scope`);
1405
+ }
1406
+ }
1407
+ let arrayFromSet = Array.from || /* istanbul ignore next */ ((set) => {
1408
+ let array = [];
1409
+ set.forEach(item => array.push(item));
1410
+ return array;
1411
+ });
1412
+ // @ts-ignore
1413
+ // istanbul ignore next
1414
+ if (!String.prototype.replaceAll)
1415
+ String.prototype.replaceAll = function (from, to) { return this.split(from).join(to); };