aberdeen 0.0.10 → 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/LICENSE.txt +16 -0
- package/README.md +2 -155
- package/dist/aberdeen.d.ts +428 -0
- package/dist/aberdeen.js +602 -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
|
-
|
|
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.
|
|
848
|
+
*/
|
|
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.
|
|
702
868
|
*/
|
|
703
|
-
|
|
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,123 @@ 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
|
+
class DetachedStore extends Store {
|
|
1033
|
+
isDetached() { return true; }
|
|
792
1034
|
}
|
|
793
1035
|
/**
|
|
794
1036
|
* Create a new DOM element.
|
|
795
|
-
* @param
|
|
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`.
|
|
796
1038
|
* @param rest - The other arguments are flexible and interpreted based on their types:
|
|
797
1039
|
* - `string`: Used as textContent for the element.
|
|
798
1040
|
* - `object`: Used as attributes/properties for the element. See `applyProp` on how the distinction is made.
|
|
799
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.
|
|
800
1043
|
* @example
|
|
801
1044
|
* node('aside.editorial', 'Yada yada yada....', () => {
|
|
802
|
-
*
|
|
803
|
-
*
|
|
804
|
-
*
|
|
1045
|
+
* node('a', {href: '/bio'}, () => {
|
|
1046
|
+
* node('img.author', {src: '/me.jpg', alt: 'The author'})
|
|
1047
|
+
* })
|
|
805
1048
|
* })
|
|
806
1049
|
*/
|
|
807
|
-
export function node(
|
|
1050
|
+
export function node(tag = "", ...rest) {
|
|
808
1051
|
if (!currentScope)
|
|
809
|
-
throw new
|
|
1052
|
+
throw new ScopeError(true);
|
|
810
1053
|
let el;
|
|
811
|
-
if (
|
|
812
|
-
|
|
813
|
-
let tag = classes.shift();
|
|
814
|
-
el = document.createElement(tag);
|
|
815
|
-
el.className = classes.join(' ');
|
|
1054
|
+
if (tag instanceof Element) {
|
|
1055
|
+
el = tag;
|
|
816
1056
|
}
|
|
817
1057
|
else {
|
|
818
|
-
|
|
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
|
+
}
|
|
819
1069
|
}
|
|
820
1070
|
currentScope.addNode(el);
|
|
821
1071
|
for (let item of rest) {
|
|
@@ -835,24 +1085,62 @@ export function node(tagClass, ...rest) {
|
|
|
835
1085
|
applyProp(el, k, item[k]);
|
|
836
1086
|
}
|
|
837
1087
|
}
|
|
1088
|
+
else if (item instanceof Store) {
|
|
1089
|
+
bindInput(el, item);
|
|
1090
|
+
}
|
|
838
1091
|
else if (item != null) {
|
|
839
1092
|
throw new Error(`Unexpected argument ${JSON.stringify(item)}`);
|
|
840
1093
|
}
|
|
841
1094
|
}
|
|
842
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
|
+
}
|
|
843
1131
|
/**
|
|
844
1132
|
* Add a text node at the current Scope position.
|
|
845
1133
|
*/
|
|
846
1134
|
export function text(text) {
|
|
847
1135
|
if (!currentScope)
|
|
848
|
-
throw new
|
|
849
|
-
if (
|
|
1136
|
+
throw new ScopeError(true);
|
|
1137
|
+
if (text == null)
|
|
850
1138
|
return;
|
|
851
1139
|
currentScope.addNode(document.createTextNode(text));
|
|
852
1140
|
}
|
|
853
1141
|
export function prop(prop, value = undefined) {
|
|
854
|
-
if (!currentScope)
|
|
855
|
-
throw new
|
|
1142
|
+
if (!currentScope || !currentScope.parentElement)
|
|
1143
|
+
throw new ScopeError(true);
|
|
856
1144
|
if (typeof prop === 'object') {
|
|
857
1145
|
for (let k in prop) {
|
|
858
1146
|
applyProp(currentScope.parentElement, k, prop[k]);
|
|
@@ -863,54 +1151,153 @@ export function prop(prop, value = undefined) {
|
|
|
863
1151
|
}
|
|
864
1152
|
}
|
|
865
1153
|
/**
|
|
866
|
-
*
|
|
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.
|
|
867
1169
|
*/
|
|
868
1170
|
export function clean(clean) {
|
|
869
1171
|
if (!currentScope)
|
|
870
|
-
throw new
|
|
1172
|
+
throw new ScopeError(false);
|
|
871
1173
|
currentScope.cleaners.push({ _clean: clean });
|
|
872
1174
|
}
|
|
873
1175
|
/**
|
|
874
|
-
* Create a new
|
|
875
|
-
* `Store`s that the `
|
|
876
|
-
*
|
|
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
|
+
* ```
|
|
877
1197
|
*/
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
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);
|
|
887
1221
|
}
|
|
888
1222
|
/**
|
|
889
|
-
*
|
|
890
|
-
|
|
891
|
-
* @param
|
|
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
|
+
*
|
|
892
1228
|
* @example
|
|
1229
|
+
* ```
|
|
1230
|
+
* let store = new Store(0)
|
|
1231
|
+
* setInterval(() => store.modify(v => v+1), 1000)
|
|
1232
|
+
*
|
|
893
1233
|
* mount(document.body, () => {
|
|
894
|
-
*
|
|
895
|
-
* node('img.logo', {src: '/logo.png'})
|
|
896
|
-
* })
|
|
1234
|
+
* node('h2', `${store.get()} seconds have passed`)
|
|
897
1235
|
* })
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
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);
|
|
902
1266
|
}
|
|
903
|
-
|
|
904
|
-
|
|
1267
|
+
else {
|
|
1268
|
+
scope = new SimpleScope(currentScope.parentElement, currentScope.lastChild || currentScope.precedingSibling, currentScope.queueOrder + 1, func);
|
|
1269
|
+
currentScope.lastChild = scope;
|
|
905
1270
|
}
|
|
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);
|
|
1271
|
+
// Do the initial run
|
|
911
1272
|
scope.update();
|
|
912
|
-
|
|
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
|
+
}
|
|
913
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
|
+
*/
|
|
914
1301
|
export function peek(func) {
|
|
915
1302
|
let savedScope = currentScope;
|
|
916
1303
|
currentScope = undefined;
|
|
@@ -925,16 +1312,24 @@ export function peek(func) {
|
|
|
925
1312
|
* Helper functions
|
|
926
1313
|
*/
|
|
927
1314
|
function applyProp(el, prop, value) {
|
|
928
|
-
if (prop === '
|
|
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) {
|
|
929
1326
|
// All boolean values and a few specific keys should be set as a property
|
|
930
1327
|
el[prop] = value;
|
|
931
1328
|
}
|
|
932
1329
|
else if (typeof value === 'function') {
|
|
933
1330
|
// Set an event listener; remove it again on clean.
|
|
934
1331
|
el.addEventListener(prop, value);
|
|
935
|
-
|
|
936
|
-
clean(() => el.removeEventListener(prop, value));
|
|
937
|
-
}
|
|
1332
|
+
clean(() => el.removeEventListener(prop, value));
|
|
938
1333
|
}
|
|
939
1334
|
else if (prop === 'style' && typeof value === 'object') {
|
|
940
1335
|
// `style` can receive an object
|
|
@@ -995,11 +1390,26 @@ function valueToData(value) {
|
|
|
995
1390
|
function defaultMakeSortKey(store) {
|
|
996
1391
|
return store.index();
|
|
997
1392
|
}
|
|
1393
|
+
/* istanbul ignore next */
|
|
998
1394
|
function internalError(code) {
|
|
999
|
-
|
|
1395
|
+
let error = new Error("internal error " + code);
|
|
1396
|
+
setTimeout(() => { throw error; }, 0);
|
|
1000
1397
|
}
|
|
1001
1398
|
function handleError(e) {
|
|
1002
|
-
console.error(e);
|
|
1003
1399
|
// Throw the error async, so the rest of the rendering can continue
|
|
1004
1400
|
setTimeout(() => { throw e; }, 0);
|
|
1005
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); };
|