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