aberdeen 0.0.10 → 0.0.12
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/LICENSE.txt +16 -0
- package/README.md +2 -155
- package/dist/aberdeen.d.ts +429 -0
- package/dist/aberdeen.js +623 -192
- package/dist/aberdeen.min.js +1 -1
- package/package.json +28 -16
package/dist/aberdeen.js
CHANGED
|
@@ -1,74 +1,79 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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';
|
|
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;
|
|
70
61
|
}
|
|
71
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;
|
|
76
|
+
}
|
|
72
77
|
/*
|
|
73
78
|
* Scope
|
|
74
79
|
*
|
|
@@ -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
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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,6 +148,7 @@ 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
153
|
onChange(index, newData, oldData) {
|
|
144
154
|
queue(this);
|
|
@@ -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);
|
|
@@ -224,8 +258,8 @@ class OnEachScope extends Scope {
|
|
|
224
258
|
_clean() {
|
|
225
259
|
super._clean();
|
|
226
260
|
this.collection.observers.delete(this);
|
|
227
|
-
for (
|
|
228
|
-
|
|
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 =
|
|
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);
|
|
375
|
+
sortKey = this.parent.makeSortKey(itemStore);
|
|
339
376
|
}
|
|
340
377
|
catch (e) {
|
|
341
378
|
handleError(e);
|
|
@@ -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 >
|
|
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,7 +504,9 @@ class ObsArray extends ObsCollection {
|
|
|
467
504
|
}
|
|
468
505
|
iterateIndexes(scope) {
|
|
469
506
|
for (let i = 0; i < this.data.length; i++) {
|
|
470
|
-
|
|
507
|
+
if (this.data[i] !== undefined) {
|
|
508
|
+
scope.addChild(i);
|
|
509
|
+
}
|
|
471
510
|
}
|
|
472
511
|
}
|
|
473
512
|
normalizeIndex(index) {
|
|
@@ -480,7 +519,10 @@ class ObsArray extends ObsCollection {
|
|
|
480
519
|
if (index.length && num == index)
|
|
481
520
|
return index;
|
|
482
521
|
}
|
|
483
|
-
throw new Error(`Invalid index ${JSON.stringify(index)}
|
|
522
|
+
throw new Error(`Invalid array index ${JSON.stringify(index)}`);
|
|
523
|
+
}
|
|
524
|
+
getCount() {
|
|
525
|
+
return this.data.length;
|
|
484
526
|
}
|
|
485
527
|
}
|
|
486
528
|
class ObsMap extends ObsCollection {
|
|
@@ -538,6 +580,9 @@ class ObsMap extends ObsCollection {
|
|
|
538
580
|
normalizeIndex(index) {
|
|
539
581
|
return index;
|
|
540
582
|
}
|
|
583
|
+
getCount() {
|
|
584
|
+
return this.data.size;
|
|
585
|
+
}
|
|
541
586
|
}
|
|
542
587
|
class ObsObject extends ObsMap {
|
|
543
588
|
getType() {
|
|
@@ -577,7 +622,13 @@ class ObsObject extends ObsMap {
|
|
|
577
622
|
return index;
|
|
578
623
|
if (type === 'number')
|
|
579
624
|
return '' + index;
|
|
580
|
-
throw new Error(`Invalid index ${JSON.stringify(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;
|
|
581
632
|
}
|
|
582
633
|
}
|
|
583
634
|
/*
|
|
@@ -607,34 +658,85 @@ export class Store {
|
|
|
607
658
|
this.idx = index;
|
|
608
659
|
}
|
|
609
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
|
+
*/
|
|
610
675
|
index() {
|
|
611
676
|
return this.idx;
|
|
612
677
|
}
|
|
613
|
-
|
|
614
|
-
return this.collection.rawGet(this.idx);
|
|
615
|
-
}
|
|
678
|
+
/** @internal */
|
|
616
679
|
_clean(scope) {
|
|
617
680
|
this.collection.removeObserver(this.idx, scope);
|
|
618
681
|
}
|
|
619
682
|
/**
|
|
620
|
-
* Resolves `path`
|
|
621
|
-
* to all read Store values. If `path` does not exist, `undefined` is returned.
|
|
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
|
+
* ```
|
|
622
691
|
*/
|
|
623
692
|
get(...path) {
|
|
624
693
|
return this.query({ path });
|
|
625
694
|
}
|
|
626
|
-
/**
|
|
627
|
-
*
|
|
695
|
+
/**
|
|
696
|
+
* @returns The same as [[`get`]], but doesn't subscribe to changes.
|
|
697
|
+
*/
|
|
698
|
+
peek(...path) {
|
|
699
|
+
return this.query({ path, peek: true });
|
|
700
|
+
}
|
|
701
|
+
/**
|
|
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.
|
|
628
704
|
*/
|
|
629
705
|
getNumber(...path) { return this.query({ path, type: 'number' }); }
|
|
706
|
+
/**
|
|
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.
|
|
709
|
+
*/
|
|
630
710
|
getString(...path) { return this.query({ path, type: 'string' }); }
|
|
711
|
+
/**
|
|
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.
|
|
714
|
+
*/
|
|
631
715
|
getBoolean(...path) { return this.query({ path, type: 'boolean' }); }
|
|
716
|
+
/**
|
|
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.
|
|
719
|
+
*/
|
|
632
720
|
getFunction(...path) { return this.query({ path, type: 'function' }); }
|
|
721
|
+
/**
|
|
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.
|
|
724
|
+
*/
|
|
633
725
|
getArray(...path) { return this.query({ path, type: 'array' }); }
|
|
726
|
+
/**
|
|
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.
|
|
729
|
+
*/
|
|
634
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
|
+
*/
|
|
635
735
|
getMap(...path) { return this.query({ path, type: 'map' }); }
|
|
636
|
-
/**
|
|
637
|
-
*
|
|
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.
|
|
638
740
|
*/
|
|
639
741
|
getOr(defaultValue, ...path) {
|
|
640
742
|
let type = typeof defaultValue;
|
|
@@ -646,31 +748,77 @@ export class Store {
|
|
|
646
748
|
}
|
|
647
749
|
return this.query({ type, defaultValue, path });
|
|
648
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
|
+
*/
|
|
649
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;
|
|
761
|
+
}
|
|
650
762
|
let store = opts.path && opts.path.length ? this.ref(...opts.path) : this;
|
|
651
|
-
let value = store
|
|
763
|
+
let value = store._observe();
|
|
652
764
|
if (opts.type && (value !== undefined || opts.defaultValue === undefined)) {
|
|
653
|
-
let type = (value instanceof ObsCollection) ? value.getType() : typeof value;
|
|
765
|
+
let type = (value instanceof ObsCollection) ? value.getType() : (value === null ? "null" : typeof value);
|
|
654
766
|
if (type !== opts.type)
|
|
655
|
-
throw new
|
|
767
|
+
throw new TypeError(`Expecting ${opts.type} but got ${type}`);
|
|
656
768
|
}
|
|
657
769
|
if (value instanceof ObsCollection) {
|
|
658
|
-
return value.getRecursive(opts.depth == null ? -1 : opts.depth);
|
|
770
|
+
return value.getRecursive(opts.depth == null ? -1 : opts.depth - 1);
|
|
659
771
|
}
|
|
660
772
|
return value === undefined ? opts.defaultValue : value;
|
|
661
773
|
}
|
|
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;
|
|
807
|
+
}
|
|
808
|
+
else {
|
|
809
|
+
throw new Error(`count() expects a collection or undefined, but got ${JSON.stringify(value)}`);
|
|
810
|
+
}
|
|
811
|
+
}
|
|
662
812
|
/**
|
|
663
813
|
* Returns "undefined", "null", "boolean", "number", "string", "function", "array", "map" or "object"
|
|
664
814
|
*/
|
|
665
815
|
getType(...path) {
|
|
666
816
|
let store = this.ref(...path);
|
|
667
|
-
if (!store)
|
|
668
|
-
return "undefined";
|
|
669
817
|
let value = store._observe();
|
|
670
|
-
return (value instanceof ObsCollection) ? value.getType() : typeof value;
|
|
818
|
+
return (value instanceof ObsCollection) ? value.getType() : (value === null ? "null" : typeof value);
|
|
671
819
|
}
|
|
672
820
|
/**
|
|
673
|
-
* Sets the Store value to the last given argument.
|
|
821
|
+
* Sets the Store value to the last given argument. Any earlier argument are a Store-path that is first
|
|
674
822
|
* resolved/created using `makeRef`.
|
|
675
823
|
*/
|
|
676
824
|
set(...pathAndValue) {
|
|
@@ -679,57 +827,94 @@ export class Store {
|
|
|
679
827
|
store.collection.setIndex(store.idx, newValue, true);
|
|
680
828
|
}
|
|
681
829
|
/**
|
|
682
|
-
*
|
|
683
|
-
*
|
|
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.
|
|
684
833
|
*/
|
|
685
|
-
merge(
|
|
686
|
-
|
|
687
|
-
let store = this.makeRef(...pathAndValue);
|
|
688
|
-
store.collection.setIndex(store.idx, newValue, false);
|
|
834
|
+
merge(mergeValue) {
|
|
835
|
+
this.collection.setIndex(this.idx, mergeValue, false);
|
|
689
836
|
}
|
|
690
837
|
/**
|
|
691
|
-
* Sets the value for the store to `undefined`, which causes it to be
|
|
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)
|
|
692
839
|
*/
|
|
693
840
|
delete(...path) {
|
|
694
841
|
let store = this.makeRef(...path);
|
|
695
842
|
store.collection.setIndex(store.idx, undefined, true);
|
|
696
843
|
}
|
|
697
844
|
/**
|
|
698
|
-
*
|
|
699
|
-
*
|
|
700
|
-
*
|
|
701
|
-
* 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.
|
|
702
848
|
*/
|
|
703
|
-
|
|
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) {
|
|
704
881
|
let store = this;
|
|
705
|
-
for (let i = 0; i <
|
|
882
|
+
for (let i = 0; i < path.length; i++) {
|
|
706
883
|
let value = store._observe();
|
|
707
884
|
if (value instanceof ObsCollection) {
|
|
708
|
-
store = new Store(value, value.normalizeIndex(
|
|
885
|
+
store = new Store(value, value.normalizeIndex(path[i]));
|
|
709
886
|
}
|
|
710
887
|
else {
|
|
711
888
|
if (value !== undefined)
|
|
712
|
-
throw new Error(`Value ${JSON.stringify(value)} is not a collection (nor undefined) in step ${i} of $(${JSON.stringify(
|
|
713
|
-
return;
|
|
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();
|
|
714
891
|
}
|
|
715
892
|
}
|
|
716
893
|
return store;
|
|
717
894
|
}
|
|
718
|
-
|
|
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) {
|
|
719
903
|
let store = this;
|
|
720
|
-
for (let i = 0; i <
|
|
904
|
+
for (let i = 0; i < path.length; i++) {
|
|
721
905
|
let value = store.collection.rawGet(store.idx);
|
|
722
906
|
if (!(value instanceof ObsCollection)) {
|
|
723
907
|
if (value !== undefined)
|
|
724
|
-
throw new Error(`Value ${JSON.stringify(value)} is not a collection (nor undefined) in step ${i} of $(${JSON.stringify(
|
|
908
|
+
throw new Error(`Value ${JSON.stringify(value)} is not a collection (nor undefined) in step ${i} of $(${JSON.stringify(path)})`);
|
|
725
909
|
value = new ObsObject();
|
|
726
910
|
store.collection.rawSet(store.idx, value);
|
|
727
911
|
store.collection.emitChange(store.idx, value, undefined);
|
|
728
912
|
}
|
|
729
|
-
store = new Store(value, value.normalizeIndex(
|
|
913
|
+
store = new Store(value, value.normalizeIndex(path[i]));
|
|
730
914
|
}
|
|
731
915
|
return store;
|
|
732
916
|
}
|
|
917
|
+
/** @Internal */
|
|
733
918
|
_observe() {
|
|
734
919
|
if (currentScope) {
|
|
735
920
|
if (this.collection.addObserver(this.idx, currentScope)) {
|
|
@@ -738,29 +923,6 @@ export class Store {
|
|
|
738
923
|
}
|
|
739
924
|
return this.collection.rawGet(this.idx);
|
|
740
925
|
}
|
|
741
|
-
/**
|
|
742
|
-
* Adds `newValue` as a value to a Map, indexed by the old `size()` of the Map. An
|
|
743
|
-
* error is thrown if that index already exists.
|
|
744
|
-
* In case the Store does not refers to `undefined`, the Array is created first.
|
|
745
|
-
* @param newValue
|
|
746
|
-
*/
|
|
747
|
-
push(...pathAndValue) {
|
|
748
|
-
let newValue = pathAndValue.pop();
|
|
749
|
-
let store = this.makeRef(...pathAndValue);
|
|
750
|
-
let obsArray = store.collection.rawGet(store.idx);
|
|
751
|
-
if (obsArray === undefined) {
|
|
752
|
-
obsArray = new ObsArray();
|
|
753
|
-
store.collection.setIndex(store.idx, obsArray, true);
|
|
754
|
-
}
|
|
755
|
-
else if (!(obsArray instanceof ObsArray)) {
|
|
756
|
-
throw new Error(`push() is only allowed for an array or undefined (which would become an array)`);
|
|
757
|
-
}
|
|
758
|
-
let newData = valueToData(newValue);
|
|
759
|
-
let pos = obsArray.data.length;
|
|
760
|
-
obsArray.data.push(newData);
|
|
761
|
-
obsArray.emitChange(pos, newData, undefined);
|
|
762
|
-
return pos;
|
|
763
|
-
}
|
|
764
926
|
onEach(...pathAndFuncs) {
|
|
765
927
|
let makeSortKey = defaultMakeSortKey;
|
|
766
928
|
let renderer = pathAndFuncs.pop();
|
|
@@ -772,10 +934,8 @@ export class Store {
|
|
|
772
934
|
if (typeof renderer !== 'function')
|
|
773
935
|
throw new Error(`onEach() expects a render function as its last argument but got ${JSON.stringify(renderer)}`);
|
|
774
936
|
if (!currentScope)
|
|
775
|
-
throw new
|
|
937
|
+
throw new ScopeError(false);
|
|
776
938
|
let store = this.ref(...pathAndFuncs);
|
|
777
|
-
if (!store)
|
|
778
|
-
return;
|
|
779
939
|
let val = store._observe();
|
|
780
940
|
if (val instanceof ObsCollection) {
|
|
781
941
|
// Subscribe to changes using the specialized OnEachScope
|
|
@@ -789,33 +949,144 @@ export class Store {
|
|
|
789
949
|
throw new Error(`onEach() attempted on a value that is neither a collection nor undefined`);
|
|
790
950
|
}
|
|
791
951
|
}
|
|
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
|
+
});
|
|
976
|
+
}
|
|
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);
|
|
1006
|
+
}
|
|
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;
|
|
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
|
+
* Dump a live view of the `Store` tree as HTML text, `ul` and `li` nodes at
|
|
1033
|
+
* the current mount position. Meant for debugging purposes.
|
|
1034
|
+
*/
|
|
1035
|
+
dump() {
|
|
1036
|
+
let type = this.getType();
|
|
1037
|
+
if (type === 'array' || type === 'object' || type === 'map') {
|
|
1038
|
+
text('<' + type + '>');
|
|
1039
|
+
node('ul', () => {
|
|
1040
|
+
this.onEach((sub) => {
|
|
1041
|
+
node('li', () => {
|
|
1042
|
+
text(JSON.stringify(sub.index()) + ': ');
|
|
1043
|
+
sub.dump();
|
|
1044
|
+
});
|
|
1045
|
+
});
|
|
1046
|
+
});
|
|
1047
|
+
}
|
|
1048
|
+
else {
|
|
1049
|
+
text(JSON.stringify(this.get()));
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
class DetachedStore extends Store {
|
|
1054
|
+
isDetached() { return true; }
|
|
792
1055
|
}
|
|
793
1056
|
/**
|
|
794
1057
|
* Create a new DOM element.
|
|
795
|
-
* @param
|
|
1058
|
+
* @param tag - The tag of the element to be created and optionally dot-separated class names. For example: `h1` or `p.intro.has_avatar`.
|
|
796
1059
|
* @param rest - The other arguments are flexible and interpreted based on their types:
|
|
797
1060
|
* - `string`: Used as textContent for the element.
|
|
798
1061
|
* - `object`: Used as attributes/properties for the element. See `applyProp` on how the distinction is made.
|
|
799
1062
|
* - `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.
|
|
1063
|
+
* - `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.
|
|
800
1064
|
* @example
|
|
801
1065
|
* node('aside.editorial', 'Yada yada yada....', () => {
|
|
802
|
-
*
|
|
803
|
-
*
|
|
804
|
-
*
|
|
1066
|
+
* node('a', {href: '/bio'}, () => {
|
|
1067
|
+
* node('img.author', {src: '/me.jpg', alt: 'The author'})
|
|
1068
|
+
* })
|
|
805
1069
|
* })
|
|
806
1070
|
*/
|
|
807
|
-
export function node(
|
|
1071
|
+
export function node(tag = "", ...rest) {
|
|
808
1072
|
if (!currentScope)
|
|
809
|
-
throw new
|
|
1073
|
+
throw new ScopeError(true);
|
|
810
1074
|
let el;
|
|
811
|
-
if (
|
|
812
|
-
|
|
813
|
-
let tag = classes.shift();
|
|
814
|
-
el = document.createElement(tag);
|
|
815
|
-
el.className = classes.join(' ');
|
|
1075
|
+
if (tag instanceof Element) {
|
|
1076
|
+
el = tag;
|
|
816
1077
|
}
|
|
817
1078
|
else {
|
|
818
|
-
|
|
1079
|
+
let pos = tag.indexOf('.');
|
|
1080
|
+
let classes;
|
|
1081
|
+
if (pos >= 0) {
|
|
1082
|
+
classes = tag.substr(pos + 1);
|
|
1083
|
+
tag = tag.substr(0, pos);
|
|
1084
|
+
}
|
|
1085
|
+
el = document.createElement(tag || 'div');
|
|
1086
|
+
if (classes) {
|
|
1087
|
+
// @ts-ignore (replaceAll is polyfilled)
|
|
1088
|
+
el.className = classes.replaceAll('.', ' ');
|
|
1089
|
+
}
|
|
819
1090
|
}
|
|
820
1091
|
currentScope.addNode(el);
|
|
821
1092
|
for (let item of rest) {
|
|
@@ -835,24 +1106,62 @@ export function node(tagClass, ...rest) {
|
|
|
835
1106
|
applyProp(el, k, item[k]);
|
|
836
1107
|
}
|
|
837
1108
|
}
|
|
1109
|
+
else if (item instanceof Store) {
|
|
1110
|
+
bindInput(el, item);
|
|
1111
|
+
}
|
|
838
1112
|
else if (item != null) {
|
|
839
1113
|
throw new Error(`Unexpected argument ${JSON.stringify(item)}`);
|
|
840
1114
|
}
|
|
841
1115
|
}
|
|
842
1116
|
}
|
|
1117
|
+
function bindInput(el, store) {
|
|
1118
|
+
let updater;
|
|
1119
|
+
let type = el.getAttribute('type');
|
|
1120
|
+
let value = store.query({ peek: true });
|
|
1121
|
+
if (type === 'checkbox') {
|
|
1122
|
+
if (value === undefined)
|
|
1123
|
+
store.set(el.checked);
|
|
1124
|
+
else
|
|
1125
|
+
el.checked = value;
|
|
1126
|
+
updater = () => store.set(el.checked);
|
|
1127
|
+
}
|
|
1128
|
+
else if (type === 'radio') {
|
|
1129
|
+
if (value === undefined) {
|
|
1130
|
+
if (el.checked)
|
|
1131
|
+
store.set(el.value);
|
|
1132
|
+
}
|
|
1133
|
+
else
|
|
1134
|
+
el.checked = value === el.value;
|
|
1135
|
+
updater = () => {
|
|
1136
|
+
if (el.checked)
|
|
1137
|
+
store.set(el.value);
|
|
1138
|
+
};
|
|
1139
|
+
}
|
|
1140
|
+
else {
|
|
1141
|
+
if (value === undefined)
|
|
1142
|
+
store.set(el.value);
|
|
1143
|
+
else
|
|
1144
|
+
el.value = value;
|
|
1145
|
+
updater = () => store.set(el.value);
|
|
1146
|
+
}
|
|
1147
|
+
el.addEventListener('input', updater);
|
|
1148
|
+
clean(() => {
|
|
1149
|
+
el.removeEventListener('input', updater);
|
|
1150
|
+
});
|
|
1151
|
+
}
|
|
843
1152
|
/**
|
|
844
1153
|
* Add a text node at the current Scope position.
|
|
845
1154
|
*/
|
|
846
1155
|
export function text(text) {
|
|
847
1156
|
if (!currentScope)
|
|
848
|
-
throw new
|
|
849
|
-
if (
|
|
1157
|
+
throw new ScopeError(true);
|
|
1158
|
+
if (text == null)
|
|
850
1159
|
return;
|
|
851
1160
|
currentScope.addNode(document.createTextNode(text));
|
|
852
1161
|
}
|
|
853
1162
|
export function prop(prop, value = undefined) {
|
|
854
|
-
if (!currentScope)
|
|
855
|
-
throw new
|
|
1163
|
+
if (!currentScope || !currentScope.parentElement)
|
|
1164
|
+
throw new ScopeError(true);
|
|
856
1165
|
if (typeof prop === 'object') {
|
|
857
1166
|
for (let k in prop) {
|
|
858
1167
|
applyProp(currentScope.parentElement, k, prop[k]);
|
|
@@ -863,54 +1172,153 @@ export function prop(prop, value = undefined) {
|
|
|
863
1172
|
}
|
|
864
1173
|
}
|
|
865
1174
|
/**
|
|
866
|
-
*
|
|
1175
|
+
* Return the browser Element that `node()`s would be rendered to at this point.
|
|
1176
|
+
* NOTE: Manually changing the DOM is not recommended in most cases. There is
|
|
1177
|
+
* usually a better, declarative way. Although there are no hard guarantees on
|
|
1178
|
+
* how your changes interact with Aberdeen, in most cases results will not be
|
|
1179
|
+
* terribly surprising. Be careful within the parent element of onEach() though.
|
|
1180
|
+
*/
|
|
1181
|
+
export function getParentElement() {
|
|
1182
|
+
if (!currentScope || !currentScope.parentElement)
|
|
1183
|
+
throw new ScopeError(true);
|
|
1184
|
+
return currentScope.parentElement;
|
|
1185
|
+
}
|
|
1186
|
+
/**
|
|
1187
|
+
* Register a function that is to be executed right before the current reactive scope
|
|
1188
|
+
* disappears or redraws.
|
|
1189
|
+
* @param clean - The function to be executed.
|
|
867
1190
|
*/
|
|
868
1191
|
export function clean(clean) {
|
|
869
1192
|
if (!currentScope)
|
|
870
|
-
throw new
|
|
1193
|
+
throw new ScopeError(false);
|
|
871
1194
|
currentScope.cleaners.push({ _clean: clean });
|
|
872
1195
|
}
|
|
873
1196
|
/**
|
|
874
|
-
* Create a new
|
|
875
|
-
* `Store`s that the `
|
|
876
|
-
*
|
|
1197
|
+
* Create a new reactive scope and execute the `func` within that scope. When
|
|
1198
|
+
* `Store`s that the `func` reads are updated, only this scope will need to be refreshed,
|
|
1199
|
+
* leaving the parent scope untouched.
|
|
1200
|
+
*
|
|
1201
|
+
* In case this function is called outside of a an existing scope, it will create a new
|
|
1202
|
+
* top-level scope (a [[`Mount`]]) without a `parentElement`, meaning that aberdeen operations
|
|
1203
|
+
* that create/modify DOM elements are not permitted.
|
|
1204
|
+
* @param func - The function to be (repeatedly) executed within the newly created scope.
|
|
1205
|
+
* @returns The newly created `Mount` object in case this is a top-level reactive scope.
|
|
1206
|
+
* @example
|
|
1207
|
+
* ```
|
|
1208
|
+
* let store = new Store('John Doe')
|
|
1209
|
+
* mount(document.body, () => {
|
|
1210
|
+
* node('div.card', () => {
|
|
1211
|
+
* node('input', {placeholder: 'Name'}, store)
|
|
1212
|
+
* observe(() => {
|
|
1213
|
+
* prop('class', {correct: store.get().length > 5})
|
|
1214
|
+
* })
|
|
1215
|
+
* })
|
|
1216
|
+
* })
|
|
1217
|
+
* ```
|
|
877
1218
|
*/
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
1219
|
+
/**
|
|
1220
|
+
* Reactively run a function, meaning the function will rerun when any `Store` that was read
|
|
1221
|
+
* during its execution is updated.
|
|
1222
|
+
* Calls to `observe` can be nested, such that changes to `Store`s read by the inner function do
|
|
1223
|
+
* no cause the outer function to rerun.
|
|
1224
|
+
*
|
|
1225
|
+
* @param func - The function to be (repeatedly) executed.
|
|
1226
|
+
* @example
|
|
1227
|
+
* ```
|
|
1228
|
+
* let number = new Store(0)
|
|
1229
|
+
* let doubled = new Store()
|
|
1230
|
+
* setInterval(() => number.set(0|Math.random()*100)), 1000)
|
|
1231
|
+
*
|
|
1232
|
+
* observe(() => {
|
|
1233
|
+
* doubled.set(number.get() * 2)
|
|
1234
|
+
* })
|
|
1235
|
+
*
|
|
1236
|
+
* observe(() => {
|
|
1237
|
+
* console.log(doubled.get())
|
|
1238
|
+
* })
|
|
1239
|
+
*/
|
|
1240
|
+
export function observe(func) {
|
|
1241
|
+
mount(undefined, func);
|
|
887
1242
|
}
|
|
888
1243
|
/**
|
|
889
|
-
*
|
|
890
|
-
|
|
891
|
-
* @param
|
|
1244
|
+
* Like [[`observe`]], but allow the function to create DOM elements using [[`node`]].
|
|
1245
|
+
|
|
1246
|
+
* @param func - The function to be (repeatedly) executed, possibly adding DOM elements to `parentElement`.
|
|
1247
|
+
* @param parentElement - A DOM element that will be used as the parent element for calls to `node`.
|
|
1248
|
+
*
|
|
892
1249
|
* @example
|
|
1250
|
+
* ```
|
|
1251
|
+
* let store = new Store(0)
|
|
1252
|
+
* setInterval(() => store.modify(v => v+1), 1000)
|
|
1253
|
+
*
|
|
893
1254
|
* mount(document.body, () => {
|
|
894
|
-
*
|
|
895
|
-
* node('img.logo', {src: '/logo.png'})
|
|
896
|
-
* })
|
|
1255
|
+
* node('h2', `${store.get()} seconds have passed`)
|
|
897
1256
|
* })
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
1257
|
+
* ```
|
|
1258
|
+
*
|
|
1259
|
+
* An example nesting [[`observe`]] within `mount`:
|
|
1260
|
+
* ```
|
|
1261
|
+
* let selected = new Store(0)
|
|
1262
|
+
* let colors = new Store(new Map())
|
|
1263
|
+
*
|
|
1264
|
+
* mount(document.body, () => {
|
|
1265
|
+
* // This function will never rerun (as it does not read any `Store`s)
|
|
1266
|
+
* node('button', '<<', {click: () => selected.modify(n => n-1)})
|
|
1267
|
+
* node('button', '>>', {click: () => selected.modify(n => n+1)})
|
|
1268
|
+
*
|
|
1269
|
+
* observe(() => {
|
|
1270
|
+
* // This will rerun whenever `selected` changes, recreating the <h2> and <input>.
|
|
1271
|
+
* node('h2', '#'+selected.get())
|
|
1272
|
+
* node('input', {type: 'color', value: '#ffffff'}, colors.ref(selected.get()))
|
|
1273
|
+
* })
|
|
1274
|
+
*
|
|
1275
|
+
* observe(() => {
|
|
1276
|
+
* // This function will rerun when `selected` or the selected color changes.
|
|
1277
|
+
* // It will change the <body> background-color.
|
|
1278
|
+
* prop({style: {backgroundColor: colors.get(selected.get()) || 'white'}})
|
|
1279
|
+
* })
|
|
1280
|
+
* })
|
|
1281
|
+
* ```
|
|
1282
|
+
*/
|
|
1283
|
+
export function mount(parentElement, func) {
|
|
1284
|
+
let scope;
|
|
1285
|
+
if (parentElement || !currentScope) {
|
|
1286
|
+
scope = new SimpleScope(parentElement, undefined, 0, func);
|
|
902
1287
|
}
|
|
903
|
-
|
|
904
|
-
|
|
1288
|
+
else {
|
|
1289
|
+
scope = new SimpleScope(currentScope.parentElement, currentScope.lastChild || currentScope.precedingSibling, currentScope.queueOrder + 1, func);
|
|
1290
|
+
currentScope.lastChild = scope;
|
|
905
1291
|
}
|
|
906
|
-
|
|
907
|
-
export function mount(parentElement, renderer) {
|
|
908
|
-
if (currentScope)
|
|
909
|
-
throw new Error('mount() from within a render scope');
|
|
910
|
-
let scope = new SimpleScope(parentElement, undefined, 0, renderer);
|
|
1292
|
+
// Do the initial run
|
|
911
1293
|
scope.update();
|
|
912
|
-
|
|
1294
|
+
// Add it to our list of cleaners. Even if `scope` currently has
|
|
1295
|
+
// no cleaners, it may get them in a future refresh.
|
|
1296
|
+
if (currentScope) {
|
|
1297
|
+
currentScope.cleaners.push(scope);
|
|
1298
|
+
}
|
|
913
1299
|
}
|
|
1300
|
+
/** Runs the given function, while not subscribing the current scope when reading [[`Store`]] values.
|
|
1301
|
+
*
|
|
1302
|
+
* @param func Function to be executed immediately.
|
|
1303
|
+
* @returns Whatever `func()` returns.
|
|
1304
|
+
* @example
|
|
1305
|
+
* ```
|
|
1306
|
+
* import {Store, peek, text} from aberdeen
|
|
1307
|
+
*
|
|
1308
|
+
* let store = new Store(['a', 'b', 'c'])
|
|
1309
|
+
*
|
|
1310
|
+
* mount(document.body, () => {
|
|
1311
|
+
* // Prevent rerender when store changes
|
|
1312
|
+
* peek(() => {
|
|
1313
|
+
* text(`Store has ${store.count()} elements, and the first is ${store.get(0)}`)
|
|
1314
|
+
* })
|
|
1315
|
+
* })
|
|
1316
|
+
* ```
|
|
1317
|
+
*
|
|
1318
|
+
* In the above example `store.get(0)` could be replaced with `store.peek(0)` to achieve the
|
|
1319
|
+
* same result without `peek()` wrapping everything. There is no non-subscribing equivalent
|
|
1320
|
+
* for `count()` however.
|
|
1321
|
+
*/
|
|
914
1322
|
export function peek(func) {
|
|
915
1323
|
let savedScope = currentScope;
|
|
916
1324
|
currentScope = undefined;
|
|
@@ -925,16 +1333,24 @@ export function peek(func) {
|
|
|
925
1333
|
* Helper functions
|
|
926
1334
|
*/
|
|
927
1335
|
function applyProp(el, prop, value) {
|
|
928
|
-
if (prop === '
|
|
1336
|
+
if ((prop === 'class' || prop === 'className') && typeof value === 'object') {
|
|
1337
|
+
// Allow setting classes using an object where the keys are the names and
|
|
1338
|
+
// the values are booleans stating whether to set or remove.
|
|
1339
|
+
for (let name in value) {
|
|
1340
|
+
if (value[name])
|
|
1341
|
+
el.classList.add(name);
|
|
1342
|
+
else
|
|
1343
|
+
el.classList.remove(name);
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
else if (prop === 'value' || prop === 'className' || prop === 'selectedIndex' || value === true || value === false) {
|
|
929
1347
|
// All boolean values and a few specific keys should be set as a property
|
|
930
1348
|
el[prop] = value;
|
|
931
1349
|
}
|
|
932
1350
|
else if (typeof value === 'function') {
|
|
933
1351
|
// Set an event listener; remove it again on clean.
|
|
934
1352
|
el.addEventListener(prop, value);
|
|
935
|
-
|
|
936
|
-
clean(() => el.removeEventListener(prop, value));
|
|
937
|
-
}
|
|
1353
|
+
clean(() => el.removeEventListener(prop, value));
|
|
938
1354
|
}
|
|
939
1355
|
else if (prop === 'style' && typeof value === 'object') {
|
|
940
1356
|
// `style` can receive an object
|
|
@@ -995,11 +1411,26 @@ function valueToData(value) {
|
|
|
995
1411
|
function defaultMakeSortKey(store) {
|
|
996
1412
|
return store.index();
|
|
997
1413
|
}
|
|
1414
|
+
/* istanbul ignore next */
|
|
998
1415
|
function internalError(code) {
|
|
999
|
-
|
|
1416
|
+
let error = new Error("internal error " + code);
|
|
1417
|
+
setTimeout(() => { throw error; }, 0);
|
|
1000
1418
|
}
|
|
1001
1419
|
function handleError(e) {
|
|
1002
|
-
console.error(e);
|
|
1003
1420
|
// Throw the error async, so the rest of the rendering can continue
|
|
1004
1421
|
setTimeout(() => { throw e; }, 0);
|
|
1005
1422
|
}
|
|
1423
|
+
class ScopeError extends Error {
|
|
1424
|
+
constructor(mount) {
|
|
1425
|
+
super(`Operation not permitted outside of ${mount ? "a mount" : "an observe"}() scope`);
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1428
|
+
let arrayFromSet = Array.from || /* istanbul ignore next */ ((set) => {
|
|
1429
|
+
let array = [];
|
|
1430
|
+
set.forEach(item => array.push(item));
|
|
1431
|
+
return array;
|
|
1432
|
+
});
|
|
1433
|
+
// @ts-ignore
|
|
1434
|
+
// istanbul ignore next
|
|
1435
|
+
if (!String.prototype.replaceAll)
|
|
1436
|
+
String.prototype.replaceAll = function (from, to) { return this.split(from).join(to); };
|