aberdeen 0.2.4 → 0.4.0
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/README.md +47 -44
- package/dist/aberdeen.d.ts +297 -269
- package/dist/aberdeen.js +708 -452
- package/dist/aberdeen.js.map +1 -1
- package/dist/prediction.js +1 -1
- package/dist/prediction.js.map +1 -1
- package/dist/route.d.ts +18 -4
- package/dist/route.js +110 -57
- package/dist/route.js.map +1 -1
- package/dist/transitions.d.ts +2 -2
- package/dist/transitions.js +40 -33
- package/dist/transitions.js.map +1 -1
- package/dist-min/aberdeen.d.ts +297 -269
- package/dist-min/aberdeen.js +1 -1
- package/dist-min/aberdeen.js.map +1 -1
- package/dist-min/prediction.js +1 -1
- package/dist-min/prediction.js.map +1 -1
- package/dist-min/route.d.ts +18 -4
- package/dist-min/route.js +1 -1
- package/dist-min/route.js.map +1 -1
- package/dist-min/transitions.d.ts +2 -2
- package/dist-min/transitions.js +1 -1
- package/dist-min/transitions.js.map +1 -1
- package/package.json +1 -1
- package/src/aberdeen.ts +724 -568
- package/src/prediction.ts +1 -1
- package/src/route.ts +116 -53
- package/src/transitions.ts +38 -42
package/dist/aberdeen.js
CHANGED
|
@@ -1,8 +1,18 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
2
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
3
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
4
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
5
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
6
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
7
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
8
|
+
});
|
|
9
|
+
};
|
|
10
|
+
let queueArray = []; // When not empty, a runQueue is scheduled or currently running.
|
|
11
|
+
let queueIndex = 0; // This first element in queueArray that still needs to be processed.
|
|
12
|
+
let queueSet = new Set(); // Contains the subset of queueArray at index >= queueIndex.
|
|
13
|
+
let queueOrdered = true; // Set to `false` when `queue()` appends a runner to `queueArray` that should come before the previous last item in the array. Will trigger a sort.
|
|
14
|
+
let runQueueDepth = 0; // Incremented when a queue event causes another queue event to be added. Reset when queue is empty. Throw when >= 42 to break (infinite) recursion.
|
|
15
|
+
let showCreateTransitions = false; // Set to `true` only when creating top level elements in response to `Store` changes, triggering `create` transitions.
|
|
6
16
|
function queue(runner) {
|
|
7
17
|
if (queueSet.has(runner))
|
|
8
18
|
return;
|
|
@@ -18,9 +28,18 @@ function queue(runner) {
|
|
|
18
28
|
queueArray.push(runner);
|
|
19
29
|
queueSet.add(runner);
|
|
20
30
|
}
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
31
|
+
/**
|
|
32
|
+
* Normally, changes to `Store`s are reacted to asynchronously, in an (optimized)
|
|
33
|
+
* batch, after a timeout of 0s. Calling `runQueue()` will do so immediately
|
|
34
|
+
* and synchronously. Doing so may be helpful in cases where you need some DOM
|
|
35
|
+
* modification to be done synchronously.
|
|
36
|
+
*
|
|
37
|
+
* This function is re-entrant, meaning it is safe to call `runQueue` from a
|
|
38
|
+
* function that is called due to another (automatic) invocation of `runQueue`.
|
|
39
|
+
*/
|
|
40
|
+
export function runQueue() {
|
|
41
|
+
showCreateTransitions = true;
|
|
42
|
+
for (; queueIndex < queueArray.length;) {
|
|
24
43
|
// Sort queue if new unordered items have been added since last time.
|
|
25
44
|
if (!queueOrdered) {
|
|
26
45
|
queueArray.splice(0, queueIndex);
|
|
@@ -31,8 +50,8 @@ function runQueue() {
|
|
|
31
50
|
}
|
|
32
51
|
// Process the rest of what's currently in the queue.
|
|
33
52
|
let batchEndIndex = queueArray.length;
|
|
34
|
-
|
|
35
|
-
let runner = queueArray[queueIndex];
|
|
53
|
+
while (queueIndex < batchEndIndex && queueOrdered) {
|
|
54
|
+
let runner = queueArray[queueIndex++];
|
|
36
55
|
queueSet.delete(runner);
|
|
37
56
|
runner._queueRun();
|
|
38
57
|
}
|
|
@@ -40,17 +59,17 @@ function runQueue() {
|
|
|
40
59
|
// batch, we'll need to run this loop again.
|
|
41
60
|
runQueueDepth++;
|
|
42
61
|
}
|
|
62
|
+
queueIndex = 0;
|
|
43
63
|
queueArray.length = 0;
|
|
44
|
-
queueIndex = undefined;
|
|
45
64
|
runQueueDepth = 0;
|
|
46
|
-
|
|
65
|
+
showCreateTransitions = false;
|
|
47
66
|
}
|
|
67
|
+
let domWaiters = [];
|
|
68
|
+
let domInReadPhase = false;
|
|
48
69
|
/**
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
* layout recalculations and improving browser performance. A DOM read operation should
|
|
53
|
-
* only *read* from the DOM, such as measuring element dimensions or retrieving computed styles.
|
|
70
|
+
* A promise-like object that you can `await`. It will resolve *after* the current batch
|
|
71
|
+
* of DOM-write operations has completed. This is the best time to retrieve DOM properties
|
|
72
|
+
* that dependent on a layout being completed, such as `offsetHeight`.
|
|
54
73
|
*
|
|
55
74
|
* By batching DOM reads separately from DOM writes, this prevents the browser from
|
|
56
75
|
* interleaving layout reads and writes, which can force additional layout recalculations.
|
|
@@ -60,18 +79,25 @@ function runQueue() {
|
|
|
60
79
|
* Unlike `setTimeout` or `requestAnimationFrame`, this mechanism ensures that DOM read
|
|
61
80
|
* operations happen before any DOM writes in the same queue cycle, minimizing layout thrashing.
|
|
62
81
|
*
|
|
63
|
-
*
|
|
82
|
+
* See `transitions.js` for some examples.
|
|
64
83
|
*/
|
|
65
|
-
export
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
84
|
+
export const DOM_READ_PHASE = {
|
|
85
|
+
then: function (fulfilled) {
|
|
86
|
+
if (domInReadPhase)
|
|
87
|
+
fulfilled();
|
|
88
|
+
else {
|
|
89
|
+
if (!domWaiters.length)
|
|
90
|
+
queue(DOM_PHASE_RUNNER);
|
|
91
|
+
domWaiters.push(fulfilled);
|
|
92
|
+
}
|
|
93
|
+
return this;
|
|
94
|
+
}
|
|
95
|
+
};
|
|
69
96
|
/**
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
* This
|
|
73
|
-
*
|
|
74
|
-
* only *write* to the DOM, such as modifying element properties or applying styles.
|
|
97
|
+
* A promise-like object that you can `await`. It will resolve *after* the current
|
|
98
|
+
* DOM_READ_PHASE has completed (if any) and after any DOM triggered by Aberdeen
|
|
99
|
+
* have completed. This is a good time to do little manual DOM tweaks that depend
|
|
100
|
+
* on a *read phase* first, like triggering transitions.
|
|
75
101
|
*
|
|
76
102
|
* By batching DOM writes separately from DOM reads, this prevents the browser from
|
|
77
103
|
* interleaving layout reads and writes, which can force additional layout recalculations.
|
|
@@ -81,12 +107,36 @@ export function scheduleDomReader(func) {
|
|
|
81
107
|
* Unlike `setTimeout` or `requestAnimationFrame`, this mechanism ensures that DOM write
|
|
82
108
|
* operations happen after all DOM reads in the same queue cycle, minimizing layout thrashing.
|
|
83
109
|
*
|
|
84
|
-
*
|
|
110
|
+
* See `transitions.js` for some examples.
|
|
85
111
|
*/
|
|
86
|
-
export
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
112
|
+
export const DOM_WRITE_PHASE = {
|
|
113
|
+
then: function (fulfilled) {
|
|
114
|
+
if (!domInReadPhase)
|
|
115
|
+
fulfilled();
|
|
116
|
+
else {
|
|
117
|
+
if (!domWaiters.length)
|
|
118
|
+
queue(DOM_PHASE_RUNNER);
|
|
119
|
+
domWaiters.push(fulfilled);
|
|
120
|
+
}
|
|
121
|
+
return this;
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
const DOM_PHASE_RUNNER = {
|
|
125
|
+
_queueOrder: 99999,
|
|
126
|
+
_queueRun: function () {
|
|
127
|
+
let waiters = domWaiters;
|
|
128
|
+
domWaiters = [];
|
|
129
|
+
domInReadPhase = !domInReadPhase;
|
|
130
|
+
for (let waiter of waiters) {
|
|
131
|
+
try {
|
|
132
|
+
waiter();
|
|
133
|
+
}
|
|
134
|
+
catch (e) {
|
|
135
|
+
console.error(e);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
};
|
|
90
140
|
/**
|
|
91
141
|
* Given an integer number, a string or an array of these, this function returns a string that can be used
|
|
92
142
|
* to compare items in a natural sorting order. So `[3, 'ab']` should be smaller than `[3, 'ac']`.
|
|
@@ -136,16 +186,21 @@ function numToString(num, neg) {
|
|
|
136
186
|
* and the `clean` functions for the scope and all sub-scopes are called.
|
|
137
187
|
*/
|
|
138
188
|
class Scope {
|
|
139
|
-
constructor(
|
|
189
|
+
constructor(_parentElement,
|
|
190
|
+
// The node or scope right before this scope that has the same `parentElement`
|
|
191
|
+
_precedingSibling,
|
|
192
|
+
// How deep is this scope nested in other scopes; we use this to make sure events
|
|
193
|
+
// at lower depths are handled before events at higher depths.
|
|
194
|
+
_queueOrder) {
|
|
195
|
+
this._parentElement = _parentElement;
|
|
196
|
+
this._precedingSibling = _precedingSibling;
|
|
197
|
+
this._queueOrder = _queueOrder;
|
|
140
198
|
// The list of clean functions to be called when this scope is cleaned. These can
|
|
141
199
|
// be for child scopes, subscriptions as well as `clean(..)` hooks.
|
|
142
200
|
this._cleaners = [];
|
|
143
201
|
// Set to true after the scope has been cleaned, causing any spurious reruns to
|
|
144
202
|
// be ignored.
|
|
145
203
|
this._isDead = false;
|
|
146
|
-
this._parentElement = parentElement;
|
|
147
|
-
this._precedingSibling = precedingSibling;
|
|
148
|
-
this._queueOrder = queueOrder;
|
|
149
204
|
}
|
|
150
205
|
// Get a reference to the last Node preceding this Scope, or undefined if there is none
|
|
151
206
|
_findPrecedingNode(stopAt = undefined) {
|
|
@@ -170,8 +225,6 @@ class Scope {
|
|
|
170
225
|
}
|
|
171
226
|
}
|
|
172
227
|
_addNode(node) {
|
|
173
|
-
if (!this._parentElement)
|
|
174
|
-
throw new ScopeError(true);
|
|
175
228
|
let prevNode = this._findLastNode() || this._findPrecedingNode();
|
|
176
229
|
this._parentElement.insertBefore(node, prevNode ? prevNode.nextSibling : this._parentElement.firstChild);
|
|
177
230
|
this._lastChild = node;
|
|
@@ -230,8 +283,15 @@ class Scope {
|
|
|
230
283
|
class SimpleScope extends Scope {
|
|
231
284
|
constructor(parentElement, precedingSibling, queueOrder, renderer) {
|
|
232
285
|
super(parentElement, precedingSibling, queueOrder);
|
|
233
|
-
|
|
286
|
+
if (renderer)
|
|
287
|
+
this._renderer = renderer;
|
|
234
288
|
}
|
|
289
|
+
/* c8 ignore start */
|
|
290
|
+
_renderer() {
|
|
291
|
+
// Should be overriden by a subclass or the constructor
|
|
292
|
+
internalError(14);
|
|
293
|
+
}
|
|
294
|
+
/* c8 ignore stop */
|
|
235
295
|
_queueRun() {
|
|
236
296
|
/* c8 ignore next */
|
|
237
297
|
if (currentScope)
|
|
@@ -250,10 +310,38 @@ class SimpleScope extends Scope {
|
|
|
250
310
|
}
|
|
251
311
|
catch (e) {
|
|
252
312
|
// Throw the error async, so the rest of the rendering can continue
|
|
253
|
-
handleError(e);
|
|
313
|
+
handleError(e, true);
|
|
254
314
|
}
|
|
255
315
|
currentScope = savedScope;
|
|
256
316
|
}
|
|
317
|
+
_install() {
|
|
318
|
+
if (showCreateTransitions) {
|
|
319
|
+
showCreateTransitions = false;
|
|
320
|
+
this._update();
|
|
321
|
+
showCreateTransitions = true;
|
|
322
|
+
}
|
|
323
|
+
else {
|
|
324
|
+
this._update();
|
|
325
|
+
}
|
|
326
|
+
// Add it to our list of cleaners. Even if `childScope` currently has
|
|
327
|
+
// no cleaners, it may get them in a future refresh.
|
|
328
|
+
currentScope._cleaners.push(this);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* This could have been done with a SimpleScope, but then we'd have to draw along an instance of
|
|
333
|
+
* that as well as a renderer function that closes over quite a few variables, which probably
|
|
334
|
+
* wouldn't be great for the performance of this common feature.
|
|
335
|
+
*/
|
|
336
|
+
class SetArgScope extends SimpleScope {
|
|
337
|
+
constructor(parentElement, precedingSibling, queueOrder, _key, _value) {
|
|
338
|
+
super(parentElement, precedingSibling, queueOrder);
|
|
339
|
+
this._key = _key;
|
|
340
|
+
this._value = _value;
|
|
341
|
+
}
|
|
342
|
+
_renderer() {
|
|
343
|
+
applyArg(this._parentElement, this._key, this._value.get());
|
|
344
|
+
}
|
|
257
345
|
}
|
|
258
346
|
let immediateQueue = new Set();
|
|
259
347
|
class ImmediateScope extends SimpleScope {
|
|
@@ -494,7 +582,7 @@ class OnEachItemScope extends Scope {
|
|
|
494
582
|
sortKey = this._parent._makeSortKey(itemStore);
|
|
495
583
|
}
|
|
496
584
|
catch (e) {
|
|
497
|
-
handleError(e);
|
|
585
|
+
handleError(e, false);
|
|
498
586
|
}
|
|
499
587
|
let oldSortStr = this._sortStr;
|
|
500
588
|
let newSortStr = sortKey == null ? '' : sortKeyToString(sortKey);
|
|
@@ -510,7 +598,7 @@ class OnEachItemScope extends Scope {
|
|
|
510
598
|
this._parent._renderer(itemStore);
|
|
511
599
|
}
|
|
512
600
|
catch (e) {
|
|
513
|
-
handleError(e);
|
|
601
|
+
handleError(e, true);
|
|
514
602
|
}
|
|
515
603
|
}
|
|
516
604
|
currentScope = savedScope;
|
|
@@ -518,7 +606,7 @@ class OnEachItemScope extends Scope {
|
|
|
518
606
|
}
|
|
519
607
|
/**
|
|
520
608
|
* This global is set during the execution of a `Scope.render`. It is used by
|
|
521
|
-
* functions like
|
|
609
|
+
* functions like `$` and `clean`.
|
|
522
610
|
*/
|
|
523
611
|
let currentScope;
|
|
524
612
|
/**
|
|
@@ -760,33 +848,52 @@ class ObsObject extends ObsMap {
|
|
|
760
848
|
return cnt;
|
|
761
849
|
}
|
|
762
850
|
}
|
|
763
|
-
|
|
764
|
-
* A data store that automatically subscribes the current scope to updates
|
|
765
|
-
* whenever data is read from it.
|
|
766
|
-
*
|
|
767
|
-
* Supported data types are: `string`, `number`, `boolean`, `undefined`, `null`,
|
|
768
|
-
* `Array`, `object` and `Map`. The latter three will always have `Store` objects as
|
|
769
|
-
* values, creating a tree of `Store`-objects.
|
|
770
|
-
*/
|
|
851
|
+
const DETACHED_KEY = {};
|
|
771
852
|
export class Store {
|
|
853
|
+
/** @internal */
|
|
772
854
|
constructor(value = undefined, index = undefined) {
|
|
855
|
+
/**
|
|
856
|
+
* Create and return a new `Store` that represents the subtree at `path` of
|
|
857
|
+
* the current `Store`.
|
|
858
|
+
*
|
|
859
|
+
* The `path` is only actually resolved when this new `Store` is first used,
|
|
860
|
+
* and how this is done depends on whether a read or a write operation is
|
|
861
|
+
* performed. Read operations will just use an `undefined` value when a
|
|
862
|
+
* subtree that we're diving into does not exist. Also, they'll subscribe
|
|
863
|
+
* to changes at each level of the tree indexed by `path`.
|
|
864
|
+
*
|
|
865
|
+
* Write operations will create any missing subtrees as objects. They don't
|
|
866
|
+
* subscribe to changes (as they are the ones causing the changes).
|
|
867
|
+
*
|
|
868
|
+
* Both read and write operations will throw an error if, while resolving
|
|
869
|
+
* `path`, they encounters a non-collection data type (such as a number)
|
|
870
|
+
*/
|
|
871
|
+
const ref = function (...path) {
|
|
872
|
+
const result = new Store(ref._collection, ref._idx);
|
|
873
|
+
if (path.length || ref._virtual) {
|
|
874
|
+
result._virtual = ref._virtual ? ref._virtual.concat(path) : path;
|
|
875
|
+
}
|
|
876
|
+
return result;
|
|
877
|
+
};
|
|
878
|
+
Object.setPrototypeOf(ref, Store.prototype);
|
|
773
879
|
if (index === undefined) {
|
|
774
|
-
|
|
775
|
-
|
|
880
|
+
ref._collection = new ObsArray();
|
|
881
|
+
ref._idx = 0;
|
|
776
882
|
if (value !== undefined) {
|
|
777
|
-
|
|
883
|
+
ref._collection.rawSet(0, valueToData(value));
|
|
778
884
|
}
|
|
779
885
|
}
|
|
780
886
|
else {
|
|
781
887
|
if (!(value instanceof ObsCollection)) {
|
|
782
888
|
throw new Error("1st parameter should be an ObsCollection if the 2nd is also given");
|
|
783
889
|
}
|
|
784
|
-
|
|
785
|
-
|
|
890
|
+
ref._collection = value;
|
|
891
|
+
ref._idx = index;
|
|
786
892
|
}
|
|
893
|
+
// @ts-ignore
|
|
894
|
+
return ref;
|
|
787
895
|
}
|
|
788
896
|
/**
|
|
789
|
-
*
|
|
790
897
|
* @returns The index for this Store within its parent collection. This will be a `number`
|
|
791
898
|
* when the parent collection is an array, a `string` when it's an object, or any data type
|
|
792
899
|
* when it's a `Map`.
|
|
@@ -795,8 +902,8 @@ export class Store {
|
|
|
795
902
|
* ```
|
|
796
903
|
* let store = new Store({x: 123})
|
|
797
904
|
* let subStore = store.ref('x')
|
|
798
|
-
*
|
|
799
|
-
*
|
|
905
|
+
* subStore.get() // 123
|
|
906
|
+
* subStore.index() // 'x'
|
|
800
907
|
* ```
|
|
801
908
|
*/
|
|
802
909
|
index() {
|
|
@@ -807,118 +914,121 @@ export class Store {
|
|
|
807
914
|
this._collection._removeObserver(this._idx, scope);
|
|
808
915
|
}
|
|
809
916
|
/**
|
|
810
|
-
*
|
|
811
|
-
*
|
|
812
|
-
* @param
|
|
813
|
-
*
|
|
814
|
-
*
|
|
815
|
-
*
|
|
816
|
-
*
|
|
817
|
-
*
|
|
818
|
-
*
|
|
917
|
+
* Retrieve the value for store, subscribing the observe scope to changes.
|
|
918
|
+
*
|
|
919
|
+
* @param depth Limit the depth of the retrieved data structure to this positive integer.
|
|
920
|
+
* When `depth` is `1`, only a single level of the value at `path` is unpacked. This
|
|
921
|
+
* makes no difference for primitive values (like strings), but for objects, maps and
|
|
922
|
+
* arrays, it means that each *value* in the resulting data structure will be a
|
|
923
|
+
* reference to the `Store` for that value.
|
|
924
|
+
*
|
|
925
|
+
* @returns The resulting value (or `undefined` if the `Store` does not exist).
|
|
926
|
+
*/
|
|
927
|
+
get(depth = 0) {
|
|
928
|
+
let value = this._observe();
|
|
929
|
+
return value instanceof ObsCollection ? value._getRecursive(depth - 1) : value;
|
|
930
|
+
}
|
|
931
|
+
/**
|
|
932
|
+
* Exactly like {@link Store.get}, except that when executed from an observe scope,
|
|
933
|
+
* we will not subscribe to changes in the data retrieved data.
|
|
819
934
|
*/
|
|
820
|
-
|
|
821
|
-
|
|
935
|
+
peek(depth = 0) {
|
|
936
|
+
let savedScope = currentScope;
|
|
937
|
+
currentScope = undefined;
|
|
938
|
+
let result = this.get(depth);
|
|
939
|
+
currentScope = savedScope;
|
|
940
|
+
return result;
|
|
822
941
|
}
|
|
823
942
|
/**
|
|
824
|
-
* Like {@link Store.get}, but
|
|
943
|
+
* Like {@link Store.get}, but with return type checking.
|
|
944
|
+
*
|
|
945
|
+
* @param expectType A string specifying what type the.get is expected to return. Options are:
|
|
946
|
+
* "undefined", "null", "boolean", "number", "string", "function", "array", "map"
|
|
947
|
+
* and "object". If the store holds a different type of value, a `TypeError`
|
|
948
|
+
* exception is thrown.
|
|
949
|
+
* @returns
|
|
825
950
|
*/
|
|
826
|
-
|
|
827
|
-
|
|
951
|
+
getTyped(expectType, depth = 0) {
|
|
952
|
+
let value = this._observe();
|
|
953
|
+
let type = (value instanceof ObsCollection) ? value._getType() : (value === null ? "null" : typeof value);
|
|
954
|
+
if (type !== expectType)
|
|
955
|
+
throw new TypeError(`Expecting ${expectType} but got ${type}`);
|
|
956
|
+
return value instanceof ObsCollection ? value._getRecursive(depth - 1) : value;
|
|
828
957
|
}
|
|
829
958
|
/**
|
|
830
959
|
* @returns Like {@link Store.get}, but throws a `TypeError` if the resulting value is not of type `number`.
|
|
831
960
|
* Using this instead of just {@link Store.get} is especially useful from within TypeScript.
|
|
832
961
|
*/
|
|
833
|
-
getNumber(
|
|
962
|
+
getNumber() { return this.getTyped('number'); }
|
|
834
963
|
/**
|
|
835
964
|
* @returns Like {@link Store.get}, but throws a `TypeError` if the resulting value is not of type `string`.
|
|
836
965
|
* Using this instead of just {@link Store.get} is especially useful from within TypeScript.
|
|
837
966
|
*/
|
|
838
|
-
getString(
|
|
967
|
+
getString() { return this.getTyped('string'); }
|
|
839
968
|
/**
|
|
840
969
|
* @returns Like {@link Store.get}, but throws a `TypeError` if the resulting value is not of type `boolean`.
|
|
841
970
|
* Using this instead of just {@link Store.get} is especially useful from within TypeScript.
|
|
842
971
|
*/
|
|
843
|
-
getBoolean(
|
|
972
|
+
getBoolean() { return this.getTyped('boolean'); }
|
|
844
973
|
/**
|
|
845
974
|
* @returns Like {@link Store.get}, but throws a `TypeError` if the resulting value is not of type `function`.
|
|
846
975
|
* Using this instead of just {@link Store.get} is especially useful from within TypeScript.
|
|
847
976
|
*/
|
|
848
|
-
getFunction(
|
|
977
|
+
getFunction() { return this.getTyped('function'); }
|
|
849
978
|
/**
|
|
850
979
|
* @returns Like {@link Store.get}, but throws a `TypeError` if the resulting value is not of type `array`.
|
|
851
980
|
* Using this instead of just {@link Store.get} is especially useful from within TypeScript.
|
|
852
981
|
*/
|
|
853
|
-
getArray(
|
|
982
|
+
getArray(depth = 0) { return this.getTyped('array', depth); }
|
|
854
983
|
/**
|
|
855
984
|
* @returns Like {@link Store.get}, but throws a `TypeError` if the resulting value is not of type `object`.
|
|
856
985
|
* Using this instead of just {@link Store.get} is especially useful from within TypeScript.
|
|
857
986
|
*/
|
|
858
|
-
getObject(
|
|
987
|
+
getObject(depth = 0) { return this.getTyped('object', depth); }
|
|
859
988
|
/**
|
|
860
989
|
* @returns Like {@link Store.get}, but throws a `TypeError` if the resulting value is not of type `map`.
|
|
861
990
|
* Using this instead of just {@link Store.get} is especially useful from within TypeScript.
|
|
862
991
|
*/
|
|
863
|
-
getMap(
|
|
992
|
+
getMap(depth = 0) { return this.getTyped('map', depth); }
|
|
864
993
|
/**
|
|
865
|
-
* Like {@link Store.get}, but
|
|
994
|
+
* Like {@link Store.get}, but with a default value (returned when the Store
|
|
866
995
|
* contains `undefined`). This default value is also used to determine the expected type,
|
|
867
996
|
* and to throw otherwise.
|
|
868
997
|
*
|
|
869
998
|
* @example
|
|
870
999
|
* ```
|
|
871
|
-
* let store = {x: 42}
|
|
872
|
-
*
|
|
873
|
-
*
|
|
874
|
-
* getOr('hello'
|
|
1000
|
+
* let store = new Store({x: 42})
|
|
1001
|
+
* store('x').getOr(99) // 42
|
|
1002
|
+
* store('y').getOr(99) // 99
|
|
1003
|
+
* store('x').getOr('hello') // throws TypeError (because 42 is not a string)
|
|
875
1004
|
* ```
|
|
876
1005
|
*/
|
|
877
|
-
getOr(defaultValue
|
|
878
|
-
let
|
|
879
|
-
if (
|
|
1006
|
+
getOr(defaultValue) {
|
|
1007
|
+
let value = this._observe();
|
|
1008
|
+
if (value === undefined)
|
|
1009
|
+
return defaultValue;
|
|
1010
|
+
let expectType = typeof defaultValue;
|
|
1011
|
+
if (expectType === 'object') {
|
|
880
1012
|
if (defaultValue instanceof Map)
|
|
881
|
-
|
|
1013
|
+
expectType = 'map';
|
|
882
1014
|
else if (defaultValue instanceof Array)
|
|
883
|
-
|
|
1015
|
+
expectType = 'array';
|
|
1016
|
+
else if (defaultValue === null)
|
|
1017
|
+
expectType = 'null';
|
|
884
1018
|
}
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
*
|
|
890
|
-
* @returns The resulting value, or `undefined` if the `path` does not exist.
|
|
891
|
-
*/
|
|
892
|
-
query(opts) {
|
|
893
|
-
if (opts.peek && currentScope) {
|
|
894
|
-
let savedScope = currentScope;
|
|
895
|
-
currentScope = undefined;
|
|
896
|
-
let result = this.query(opts);
|
|
897
|
-
currentScope = savedScope;
|
|
898
|
-
return result;
|
|
899
|
-
}
|
|
900
|
-
let store = opts.path && opts.path.length ? this.ref(...opts.path) : this;
|
|
901
|
-
let value = store._observe();
|
|
902
|
-
if (opts.type && (value !== undefined || opts.defaultValue === undefined)) {
|
|
903
|
-
let type = (value instanceof ObsCollection) ? value._getType() : (value === null ? "null" : typeof value);
|
|
904
|
-
if (type !== opts.type)
|
|
905
|
-
throw new TypeError(`Expecting ${opts.type} but got ${type}`);
|
|
906
|
-
}
|
|
907
|
-
if (value instanceof ObsCollection) {
|
|
908
|
-
return value._getRecursive(opts.depth == null ? -1 : opts.depth - 1);
|
|
909
|
-
}
|
|
910
|
-
return value === undefined ? opts.defaultValue : value;
|
|
1019
|
+
let type = (value instanceof ObsCollection) ? value._getType() : (value === null ? "null" : typeof value);
|
|
1020
|
+
if (type !== expectType)
|
|
1021
|
+
throw new TypeError(`Expecting ${expectType} but got ${type}`);
|
|
1022
|
+
return (value instanceof ObsCollection ? value._getRecursive(-1) : value);
|
|
911
1023
|
}
|
|
912
1024
|
/**
|
|
913
|
-
* Checks if the
|
|
1025
|
+
* Checks if the collection held in `Store` is empty, and subscribes the current scope to changes of the emptiness of this collection.
|
|
914
1026
|
*
|
|
915
|
-
* @
|
|
916
|
-
* @returns When the specified collection is not empty `true` is returned. If it is empty or if the value is undefined, `false` is returned.
|
|
1027
|
+
* @returns When the collection is not empty `true` is returned. If it is empty or if the value is undefined, `false` is returned.
|
|
917
1028
|
* @throws When the value is not a collection and not undefined, an Error will be thrown.
|
|
918
1029
|
*/
|
|
919
|
-
isEmpty(
|
|
920
|
-
let
|
|
921
|
-
let value = store._observe();
|
|
1030
|
+
isEmpty() {
|
|
1031
|
+
let value = this._observe();
|
|
922
1032
|
if (value instanceof ObsCollection) {
|
|
923
1033
|
if (currentScope) {
|
|
924
1034
|
let observer = new IsEmptyObserver(currentScope, value, false);
|
|
@@ -936,15 +1046,13 @@ export class Store {
|
|
|
936
1046
|
}
|
|
937
1047
|
}
|
|
938
1048
|
/**
|
|
939
|
-
* Returns the number of items in the
|
|
1049
|
+
* Returns the number of items in the collection held in Store, and subscribes the current scope to changes in this count.
|
|
940
1050
|
*
|
|
941
|
-
* @param path Any path terms to resolve before retrieving the value.
|
|
942
1051
|
* @returns The number of items contained in the collection, or 0 if the value is undefined.
|
|
943
1052
|
* @throws When the value is not a collection and not undefined, an Error will be thrown.
|
|
944
1053
|
*/
|
|
945
|
-
count(
|
|
946
|
-
let
|
|
947
|
-
let value = store._observe();
|
|
1054
|
+
count() {
|
|
1055
|
+
let value = this._observe();
|
|
948
1056
|
if (value instanceof ObsCollection) {
|
|
949
1057
|
if (currentScope) {
|
|
950
1058
|
let observer = new IsEmptyObserver(currentScope, value, true);
|
|
@@ -962,22 +1070,38 @@ export class Store {
|
|
|
962
1070
|
}
|
|
963
1071
|
}
|
|
964
1072
|
/**
|
|
965
|
-
* Returns a strings describing the type of the
|
|
1073
|
+
* Returns a strings describing the type of the `Store` value, subscribing to changes of this type.
|
|
966
1074
|
* Note: this currently also subscribes to changes of primitive values, so changing a value from 3 to 4
|
|
967
1075
|
* would cause the scope to be rerun. This is not great, and may change in the future. This caveat does
|
|
968
1076
|
* not apply to changes made *inside* an object, `Array` or `Map`.
|
|
969
1077
|
*
|
|
970
|
-
* @param path Any path terms to resolve before retrieving the value.
|
|
971
1078
|
* @returns Possible options: "undefined", "null", "boolean", "number", "string", "function", "array", "map" or "object".
|
|
972
1079
|
*/
|
|
973
|
-
getType(
|
|
974
|
-
let
|
|
975
|
-
let value = store._observe();
|
|
1080
|
+
getType() {
|
|
1081
|
+
let value = this._observe();
|
|
976
1082
|
return (value instanceof ObsCollection) ? value._getType() : (value === null ? "null" : typeof value);
|
|
977
1083
|
}
|
|
978
1084
|
/**
|
|
979
|
-
*
|
|
980
|
-
*
|
|
1085
|
+
* Returns a new `Store` that will always hold either the value of `whenTrue` or the value
|
|
1086
|
+
* of `whenFalse` depending on whether the original `Store` is truthy or not.
|
|
1087
|
+
*
|
|
1088
|
+
* @param whenTrue The value set to the return-`Store` while `this` is truthy. This can be
|
|
1089
|
+
* any type of value. If it's a `Store`, the return-`Store` will reference the same
|
|
1090
|
+
* data (so *no* deep copy will be made).
|
|
1091
|
+
* @param whenFalse Like `whenTrue`, but for falsy values (false, undefined, null, 0, "").
|
|
1092
|
+
* @returns A store holding the result value. The value will keep getting updated while
|
|
1093
|
+
* the observe context from which `if()` was called remains active.
|
|
1094
|
+
*/
|
|
1095
|
+
if(whenTrue, whenFalse) {
|
|
1096
|
+
const result = new Store();
|
|
1097
|
+
observe(() => {
|
|
1098
|
+
const value = this.get() ? whenTrue : whenFalse;
|
|
1099
|
+
result.set(value);
|
|
1100
|
+
});
|
|
1101
|
+
return result;
|
|
1102
|
+
}
|
|
1103
|
+
/**
|
|
1104
|
+
* Sets the `Store` value to the given argument.
|
|
981
1105
|
*
|
|
982
1106
|
* When a `Store` is passed in as the value, its value will be copied (subscribing to changes). In
|
|
983
1107
|
* case the value is an object, an `Array` or a `Map`, a *reference* to that data structure will
|
|
@@ -987,95 +1111,140 @@ export class Store {
|
|
|
987
1111
|
*
|
|
988
1112
|
* If you intent to make a copy instead of a reference, call {@link Store.get} on the origin `Store`.
|
|
989
1113
|
*
|
|
1114
|
+
* @returns The `Store` itself, for chaining other methods.
|
|
990
1115
|
*
|
|
991
1116
|
* @example
|
|
992
1117
|
* ```
|
|
993
1118
|
* let store = new Store() // Value is `undefined`
|
|
994
1119
|
*
|
|
995
|
-
* store.set(
|
|
996
|
-
*
|
|
1120
|
+
* store.set(6)
|
|
1121
|
+
* store.get() // 6
|
|
997
1122
|
*
|
|
998
|
-
* store.set(
|
|
999
|
-
*
|
|
1123
|
+
* store.set({}) // Change value to an empty object
|
|
1124
|
+
* store('a', 'b', 'c').set('d') // Create parent path as objects
|
|
1125
|
+
* store.get() // {x: 6, a: {b: {c: 'd'}}}
|
|
1000
1126
|
*
|
|
1001
1127
|
* store.set(42) // Overwrites all of the above
|
|
1002
|
-
*
|
|
1128
|
+
* store.get() // 42
|
|
1003
1129
|
*
|
|
1004
|
-
* store
|
|
1130
|
+
* store('x').set(6) // Throw Error (42 is not a collection)
|
|
1005
1131
|
* ```
|
|
1006
1132
|
*/
|
|
1007
|
-
set(
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
store._collection._setIndex(store._idx, newValue, true);
|
|
1133
|
+
set(newValue) {
|
|
1134
|
+
this._materialize(true);
|
|
1135
|
+
this._collection._setIndex(this._idx, newValue, true);
|
|
1011
1136
|
runImmediateQueue();
|
|
1137
|
+
return this;
|
|
1138
|
+
}
|
|
1139
|
+
/** @internal */
|
|
1140
|
+
_materialize(forWriting) {
|
|
1141
|
+
if (!this._virtual)
|
|
1142
|
+
return true;
|
|
1143
|
+
let collection = this._collection;
|
|
1144
|
+
let idx = this._idx;
|
|
1145
|
+
for (let i = 0; i < this._virtual.length; i++) {
|
|
1146
|
+
if (!forWriting && currentScope) {
|
|
1147
|
+
if (collection._addObserver(idx, currentScope)) {
|
|
1148
|
+
currentScope._cleaners.push(this);
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
let value = collection.rawGet(idx);
|
|
1152
|
+
if (!(value instanceof ObsCollection)) {
|
|
1153
|
+
// Throw an error if trying to index a primitive type
|
|
1154
|
+
if (value !== undefined)
|
|
1155
|
+
throw new Error(`While resolving ${JSON.stringify(this._virtual)}, found ${JSON.stringify(value)} at index ${i} instead of a collection.`);
|
|
1156
|
+
// For reads, we'll just give up. We might reactively get another shot at this.
|
|
1157
|
+
if (!forWriting)
|
|
1158
|
+
return false;
|
|
1159
|
+
// For writes, create a new collection.
|
|
1160
|
+
value = new ObsObject();
|
|
1161
|
+
collection.rawSet(idx, value);
|
|
1162
|
+
collection.emitChange(idx, value, undefined);
|
|
1163
|
+
}
|
|
1164
|
+
collection = value;
|
|
1165
|
+
const prop = this._virtual[i];
|
|
1166
|
+
idx = collection._normalizeIndex(prop);
|
|
1167
|
+
}
|
|
1168
|
+
this._collection = collection;
|
|
1169
|
+
this._idx = idx;
|
|
1170
|
+
delete this._virtual;
|
|
1171
|
+
return true;
|
|
1012
1172
|
}
|
|
1013
1173
|
/**
|
|
1014
1174
|
* Sets the `Store` to the given `mergeValue`, but without deleting any pre-existing
|
|
1015
1175
|
* items when a collection overwrites a similarly typed collection. This results in
|
|
1016
1176
|
* a deep merge.
|
|
1017
1177
|
*
|
|
1178
|
+
* @returns The `Store` itself, for chaining other methods.
|
|
1179
|
+
*
|
|
1018
1180
|
* @example
|
|
1019
1181
|
* ```
|
|
1020
1182
|
* let store = new Store({a: {x: 1}})
|
|
1021
1183
|
* store.merge({a: {y: 2}, b: 3})
|
|
1022
|
-
*
|
|
1184
|
+
* store.get() // {a: {x: 1, y: 2}, b: 3}
|
|
1023
1185
|
* ```
|
|
1024
1186
|
*/
|
|
1025
|
-
merge(
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
store._collection._setIndex(store._idx, mergeValue, false);
|
|
1187
|
+
merge(mergeValue) {
|
|
1188
|
+
this._materialize(true);
|
|
1189
|
+
this._collection._setIndex(this._idx, mergeValue, false);
|
|
1029
1190
|
runImmediateQueue();
|
|
1191
|
+
return this;
|
|
1030
1192
|
}
|
|
1031
1193
|
/**
|
|
1032
1194
|
* 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)
|
|
1033
1195
|
*
|
|
1196
|
+
* @returns The `Store` itself, for chaining other methods.
|
|
1197
|
+
*
|
|
1034
1198
|
* @example
|
|
1035
1199
|
* ```
|
|
1036
1200
|
* let store = new Store({a: 1, b: 2})
|
|
1037
|
-
* store
|
|
1038
|
-
*
|
|
1201
|
+
* store('a').delete()
|
|
1202
|
+
* store.get() // {b: 2}
|
|
1039
1203
|
*
|
|
1040
1204
|
* store = new Store(['a','b','c'])
|
|
1041
|
-
* store.delete(
|
|
1042
|
-
*
|
|
1043
|
-
* store.delete(
|
|
1044
|
-
*
|
|
1205
|
+
* store(1).delete()
|
|
1206
|
+
* store.get() // ['a', undefined, 'c']
|
|
1207
|
+
* store(2).delete()
|
|
1208
|
+
* store.get() // ['a']
|
|
1209
|
+
* store.delete()
|
|
1210
|
+
* store.get() // undefined
|
|
1045
1211
|
* ```
|
|
1046
1212
|
*/
|
|
1047
|
-
delete(
|
|
1048
|
-
|
|
1049
|
-
|
|
1213
|
+
delete() {
|
|
1214
|
+
this._materialize(true);
|
|
1215
|
+
this._collection._setIndex(this._idx, undefined, true);
|
|
1050
1216
|
runImmediateQueue();
|
|
1217
|
+
return this;
|
|
1051
1218
|
}
|
|
1052
1219
|
/**
|
|
1053
1220
|
* Pushes a value to the end of the Array that is at the specified path in the store.
|
|
1054
1221
|
* If that store path is `undefined`, an Array is created first.
|
|
1055
1222
|
* The last argument is the value to be added, any earlier arguments indicate the path.
|
|
1056
1223
|
*
|
|
1224
|
+
* @returns The index at which the item was appended.
|
|
1225
|
+
* @throws TypeError when the store contains a primitive data type.
|
|
1226
|
+
*
|
|
1057
1227
|
* @example
|
|
1058
1228
|
* ```
|
|
1059
1229
|
* let store = new Store()
|
|
1060
1230
|
* store.push(3) // Creates the array
|
|
1061
1231
|
* store.push(6)
|
|
1062
|
-
*
|
|
1232
|
+
* store.get() // [3,6]
|
|
1063
1233
|
*
|
|
1064
1234
|
* store = new Store({myArray: [1,2]})
|
|
1065
|
-
* store
|
|
1066
|
-
*
|
|
1235
|
+
* store('myArray').push(3)
|
|
1236
|
+
* store.get() // {myArray: [1,2,3]}
|
|
1067
1237
|
* ```
|
|
1068
1238
|
*/
|
|
1069
|
-
push(
|
|
1070
|
-
|
|
1071
|
-
let
|
|
1072
|
-
let obsArray = store._collection.rawGet(store._idx);
|
|
1239
|
+
push(newValue) {
|
|
1240
|
+
this._materialize(true);
|
|
1241
|
+
let obsArray = this._collection.rawGet(this._idx);
|
|
1073
1242
|
if (obsArray === undefined) {
|
|
1074
1243
|
obsArray = new ObsArray();
|
|
1075
|
-
|
|
1244
|
+
this._collection._setIndex(this._idx, obsArray, true);
|
|
1076
1245
|
}
|
|
1077
1246
|
else if (!(obsArray instanceof ObsArray)) {
|
|
1078
|
-
throw new
|
|
1247
|
+
throw new TypeError(`push() is only allowed for an array or undefined (which would become an array)`);
|
|
1079
1248
|
}
|
|
1080
1249
|
let newData = valueToData(newValue);
|
|
1081
1250
|
let pos = obsArray._data.length;
|
|
@@ -1088,71 +1257,16 @@ export class Store {
|
|
|
1088
1257
|
* {@link Store.peek} the current value, pass it through `func`, and {@link Store.set} the resulting
|
|
1089
1258
|
* value.
|
|
1090
1259
|
* @param func The function transforming the value.
|
|
1260
|
+
* @returns The `Store` itself, for chaining other methods.
|
|
1091
1261
|
*/
|
|
1092
1262
|
modify(func) {
|
|
1093
|
-
this.set(func(this.
|
|
1094
|
-
|
|
1095
|
-
/**
|
|
1096
|
-
* Return a `Store` deeper within the tree by resolving the given `path`,
|
|
1097
|
-
* subscribing to every level.
|
|
1098
|
-
* In case `undefined` is encountered while resolving the path, a newly
|
|
1099
|
-
* created `Store` containing `undefined` is returned. In that case, the
|
|
1100
|
-
* `Store`'s {@link Store.isDetached} method will return `true`.
|
|
1101
|
-
* In case something other than a collection is encountered, an error is thrown.
|
|
1102
|
-
*/
|
|
1103
|
-
ref(...path) {
|
|
1104
|
-
let store = this;
|
|
1105
|
-
for (let i = 0; i < path.length; i++) {
|
|
1106
|
-
let value = store._observe();
|
|
1107
|
-
if (value instanceof ObsCollection) {
|
|
1108
|
-
store = new Store(value, value._normalizeIndex(path[i]));
|
|
1109
|
-
}
|
|
1110
|
-
else {
|
|
1111
|
-
if (value !== undefined)
|
|
1112
|
-
throw new Error(`Value ${JSON.stringify(value)} is not a collection (nor undefined) in step ${i} of $(${JSON.stringify(path)})`);
|
|
1113
|
-
return new DetachedStore();
|
|
1114
|
-
}
|
|
1115
|
-
}
|
|
1116
|
-
return store;
|
|
1117
|
-
}
|
|
1118
|
-
/**
|
|
1119
|
-
* Similar to `ref()`, but instead of returning `undefined`, new objects are created when
|
|
1120
|
-
* a path does not exist yet. An error is still thrown when the path tries to index an invalid
|
|
1121
|
-
* type.
|
|
1122
|
-
* Unlike `ref`, `makeRef` does *not* subscribe to the path levels, as it is intended to be
|
|
1123
|
-
* a write-only operation.
|
|
1124
|
-
*
|
|
1125
|
-
* @example
|
|
1126
|
-
* ```
|
|
1127
|
-
* let store = new Store() // Value is `undefined`
|
|
1128
|
-
*
|
|
1129
|
-
* let ref = store.makeRef('a', 'b', 'c')
|
|
1130
|
-
* assert(store.get() == {a: {b: {}}}
|
|
1131
|
-
*
|
|
1132
|
-
* ref.set(42)
|
|
1133
|
-
* assert(store.get() == {a: {b: {c: 42}}}
|
|
1134
|
-
*
|
|
1135
|
-
* ref.makeRef('d') // Throw Error (42 is not a collection)
|
|
1136
|
-
* ```
|
|
1137
|
-
*/
|
|
1138
|
-
makeRef(...path) {
|
|
1139
|
-
let store = this;
|
|
1140
|
-
for (let i = 0; i < path.length; i++) {
|
|
1141
|
-
let value = store._collection.rawGet(store._idx);
|
|
1142
|
-
if (!(value instanceof ObsCollection)) {
|
|
1143
|
-
if (value !== undefined)
|
|
1144
|
-
throw new Error(`Value ${JSON.stringify(value)} is not a collection (nor undefined) in step ${i} of $(${JSON.stringify(path)})`);
|
|
1145
|
-
value = new ObsObject();
|
|
1146
|
-
store._collection.rawSet(store._idx, value);
|
|
1147
|
-
store._collection.emitChange(store._idx, value, undefined);
|
|
1148
|
-
}
|
|
1149
|
-
store = new Store(value, value._normalizeIndex(path[i]));
|
|
1150
|
-
}
|
|
1151
|
-
runImmediateQueue();
|
|
1152
|
-
return store;
|
|
1263
|
+
this.set(func(this.peek()));
|
|
1264
|
+
return this;
|
|
1153
1265
|
}
|
|
1154
1266
|
/** @internal */
|
|
1155
1267
|
_observe() {
|
|
1268
|
+
if (!this._materialize(false))
|
|
1269
|
+
return undefined;
|
|
1156
1270
|
if (currentScope) {
|
|
1157
1271
|
if (this._collection._addObserver(this._idx, currentScope)) {
|
|
1158
1272
|
currentScope._cleaners.push(this);
|
|
@@ -1165,24 +1279,17 @@ export class Store {
|
|
|
1165
1279
|
* When items are added to the collection at some later point, the code block will be ran for them as well.
|
|
1166
1280
|
* When an item is removed, the {@link Store.clean} handlers left by its code block are executed.
|
|
1167
1281
|
*
|
|
1168
|
-
*
|
|
1169
|
-
*
|
|
1170
|
-
*
|
|
1282
|
+
* @param renderer The function to be called for each item. It receives the item's `Store` object as its only argument.
|
|
1283
|
+
* @param makeSortKey An optional function that, given an items `Store` object, returns a value to be sorted on.
|
|
1284
|
+
* This value can be a number, a string, or an array containing a combination of both. When undefined is returned,
|
|
1285
|
+
* the item is *not* rendered. If `makeSortKey` is not specified, the output will be sorted by `index()`.
|
|
1171
1286
|
*/
|
|
1172
|
-
onEach(
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
renderer = pathAndFuncs.pop();
|
|
1179
|
-
}
|
|
1180
|
-
if (typeof renderer !== 'function')
|
|
1181
|
-
throw new Error(`onEach() expects a render function as its last argument but got ${JSON.stringify(renderer)}`);
|
|
1182
|
-
if (!currentScope)
|
|
1183
|
-
throw new ScopeError(false);
|
|
1184
|
-
let store = this.ref(...pathAndFuncs);
|
|
1185
|
-
let val = store._observe();
|
|
1287
|
+
onEach(renderer, makeSortKey = defaultMakeSortKey) {
|
|
1288
|
+
if (!currentScope) { // Do this in a new top-level scope
|
|
1289
|
+
_mount(undefined, () => this.onEach(renderer, makeSortKey), SimpleScope);
|
|
1290
|
+
return;
|
|
1291
|
+
}
|
|
1292
|
+
let val = this._observe();
|
|
1186
1293
|
if (val instanceof ObsCollection) {
|
|
1187
1294
|
// Subscribe to changes using the specialized OnEachScope
|
|
1188
1295
|
let onEachScope = new OnEachScope(currentScope._parentElement, currentScope._lastChild || currentScope._precedingSibling, currentScope._queueOrder + 1, val, renderer, makeSortKey);
|
|
@@ -1195,6 +1302,30 @@ export class Store {
|
|
|
1195
1302
|
throw new Error(`onEach() attempted on a value that is neither a collection nor undefined`);
|
|
1196
1303
|
}
|
|
1197
1304
|
}
|
|
1305
|
+
/**
|
|
1306
|
+
* Derive a new `Store` from this `Store`, by reactively passing its value
|
|
1307
|
+
* through the specified function.
|
|
1308
|
+
* @param func Your function. It should accept a the input store's value, and return
|
|
1309
|
+
* a result to be reactively set to the output store.
|
|
1310
|
+
* @returns The output `Store`.
|
|
1311
|
+
* @example
|
|
1312
|
+
* ```javascript
|
|
1313
|
+
* const store = new Store(21)
|
|
1314
|
+
* const double = store.derive(v => v*2)
|
|
1315
|
+
* double.get() // 42
|
|
1316
|
+
*
|
|
1317
|
+
* store.set(100)
|
|
1318
|
+
* runQueue() // Or after a setTimeout 0, due to batching
|
|
1319
|
+
* double.get() // 200
|
|
1320
|
+
* ```
|
|
1321
|
+
*/
|
|
1322
|
+
derive(func) {
|
|
1323
|
+
let out = new Store();
|
|
1324
|
+
observe(() => {
|
|
1325
|
+
out.set(func(this.get()));
|
|
1326
|
+
});
|
|
1327
|
+
return out;
|
|
1328
|
+
}
|
|
1198
1329
|
/**
|
|
1199
1330
|
* Applies a filter/map function on each item within the `Store`'s collection,
|
|
1200
1331
|
* and reactively manages the returned `Map` `Store` to hold any results.
|
|
@@ -1202,24 +1333,29 @@ export class Store {
|
|
|
1202
1333
|
* @param func - Function that transform the given store into an output value or
|
|
1203
1334
|
* `undefined` in case this value should be skipped:
|
|
1204
1335
|
*
|
|
1205
|
-
* @returns - A map `Store` with the values returned by `func` and the
|
|
1206
|
-
* keys from the original map, array or object `Store`.
|
|
1336
|
+
* @returns - A array/map/object `Store` with the values returned by `func` and the
|
|
1337
|
+
* corresponding keys from the original map, array or object `Store`.
|
|
1207
1338
|
*
|
|
1208
1339
|
* When items disappear from the `Store` or are changed in a way that `func` depends
|
|
1209
1340
|
* upon, the resulting items are removed from the output `Store` as well. When multiple
|
|
1210
1341
|
* input items produce the same output keys, this may lead to unexpected results.
|
|
1211
1342
|
*/
|
|
1212
1343
|
map(func) {
|
|
1213
|
-
let out = new Store(
|
|
1214
|
-
|
|
1215
|
-
let
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1344
|
+
let out = new Store();
|
|
1345
|
+
observe(() => {
|
|
1346
|
+
let t = this.getType();
|
|
1347
|
+
out.set(t === 'array' ? [] : (t === 'object' ? {} : new Map()));
|
|
1348
|
+
this.onEach((item) => {
|
|
1349
|
+
let value = func(item);
|
|
1350
|
+
if (value !== undefined) {
|
|
1351
|
+
let key = item.index();
|
|
1352
|
+
const ref = out(key);
|
|
1353
|
+
ref.set(value);
|
|
1354
|
+
clean(() => {
|
|
1355
|
+
ref.delete();
|
|
1356
|
+
});
|
|
1357
|
+
}
|
|
1358
|
+
});
|
|
1223
1359
|
});
|
|
1224
1360
|
return out;
|
|
1225
1361
|
}
|
|
@@ -1243,154 +1379,80 @@ export class Store {
|
|
|
1243
1379
|
let out = new Store(new Map());
|
|
1244
1380
|
this.onEach((item) => {
|
|
1245
1381
|
let result = func(item);
|
|
1246
|
-
let
|
|
1382
|
+
let refs = [];
|
|
1247
1383
|
if (result.constructor === Object) {
|
|
1248
1384
|
for (let key in result) {
|
|
1249
|
-
out
|
|
1385
|
+
const ref = out(key);
|
|
1386
|
+
ref.set(result[key]);
|
|
1387
|
+
refs.push(ref);
|
|
1250
1388
|
}
|
|
1251
|
-
keys = Object.keys(result);
|
|
1252
1389
|
}
|
|
1253
1390
|
else if (result instanceof Map) {
|
|
1254
1391
|
result.forEach((value, key) => {
|
|
1255
|
-
out
|
|
1392
|
+
const ref = out(key);
|
|
1393
|
+
ref.set(value);
|
|
1394
|
+
refs.push(ref);
|
|
1256
1395
|
});
|
|
1257
|
-
keys = [...result.keys()];
|
|
1258
1396
|
}
|
|
1259
1397
|
else {
|
|
1260
1398
|
return;
|
|
1261
1399
|
}
|
|
1262
|
-
if (
|
|
1400
|
+
if (refs.length) {
|
|
1263
1401
|
clean(() => {
|
|
1264
|
-
for (let
|
|
1265
|
-
|
|
1402
|
+
for (let ref of refs) {
|
|
1403
|
+
ref.delete();
|
|
1266
1404
|
}
|
|
1267
1405
|
});
|
|
1268
1406
|
}
|
|
1269
1407
|
});
|
|
1270
1408
|
return out;
|
|
1271
1409
|
}
|
|
1272
|
-
/**
|
|
1273
|
-
* @returns Returns `true` when the `Store` was created by {@link Store.ref}ing a path that
|
|
1274
|
-
* does not exist.
|
|
1275
|
-
*/
|
|
1276
|
-
isDetached() { return false; }
|
|
1277
1410
|
/**
|
|
1278
1411
|
* Dump a live view of the `Store` tree as HTML text, `ul` and `li` nodes at
|
|
1279
1412
|
* the current mount position. Meant for debugging purposes.
|
|
1413
|
+
* @returns The `Store` itself, for chaining other methods.
|
|
1280
1414
|
*/
|
|
1281
1415
|
dump() {
|
|
1282
1416
|
let type = this.getType();
|
|
1283
1417
|
if (type === 'array' || type === 'object' || type === 'map') {
|
|
1284
|
-
|
|
1285
|
-
|
|
1418
|
+
$({ text: `<${type}>` });
|
|
1419
|
+
$('ul', () => {
|
|
1286
1420
|
this.onEach((sub) => {
|
|
1287
|
-
|
|
1288
|
-
text(JSON.stringify(sub.index()) + ': ');
|
|
1421
|
+
$('li:' + JSON.stringify(sub.index()) + ": ", () => {
|
|
1289
1422
|
sub.dump();
|
|
1290
1423
|
});
|
|
1291
1424
|
});
|
|
1292
1425
|
});
|
|
1293
1426
|
}
|
|
1294
1427
|
else {
|
|
1295
|
-
text
|
|
1428
|
+
$({ text: JSON.stringify(this.get()) });
|
|
1296
1429
|
}
|
|
1430
|
+
return this;
|
|
1297
1431
|
}
|
|
1298
1432
|
}
|
|
1299
|
-
class DetachedStore extends Store {
|
|
1300
|
-
isDetached() { return true; }
|
|
1301
|
-
}
|
|
1302
|
-
let onCreateEnabled = false;
|
|
1303
1433
|
let onDestroyMap = new WeakMap();
|
|
1304
1434
|
function destroyWithClass(element, cls) {
|
|
1305
1435
|
element.classList.add(cls);
|
|
1306
1436
|
setTimeout(() => element.remove(), 2000);
|
|
1307
1437
|
}
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
* @param rest - The other arguments are flexible and interpreted based on their types:
|
|
1312
|
-
* - `string`: Used as textContent for the element.
|
|
1313
|
-
* - `object`: Used as attributes, properties or event listeners for the element. See {@link Store.prop} on how the distinction is made and to read about a couple of special keys.
|
|
1314
|
-
* - `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.
|
|
1315
|
-
* - `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`, or the other way around if the `Store` holds `undefined`. After that, the `Store` will be updated when the input changes and vice versa.
|
|
1316
|
-
* @example
|
|
1317
|
-
* node('aside.editorial', 'Yada yada yada....', () => {
|
|
1318
|
-
* node('a', {href: '/bio'}, () => {
|
|
1319
|
-
* node('img.author', {src: '/me.jpg', alt: 'The author'})
|
|
1320
|
-
* })
|
|
1321
|
-
* })
|
|
1322
|
-
*/
|
|
1323
|
-
export function node(tag = "", ...rest) {
|
|
1324
|
-
if (!currentScope)
|
|
1325
|
-
throw new ScopeError(true);
|
|
1326
|
-
let el;
|
|
1327
|
-
if (tag instanceof Element) {
|
|
1328
|
-
el = tag;
|
|
1438
|
+
function addLeafNode(deepEl, node) {
|
|
1439
|
+
if (deepEl === currentScope._parentElement) {
|
|
1440
|
+
currentScope._addNode(node);
|
|
1329
1441
|
}
|
|
1330
1442
|
else {
|
|
1331
|
-
|
|
1332
|
-
let classes;
|
|
1333
|
-
if (pos >= 0) {
|
|
1334
|
-
classes = tag.substr(pos + 1);
|
|
1335
|
-
tag = tag.substr(0, pos);
|
|
1336
|
-
}
|
|
1337
|
-
el = document.createElement(tag || 'div');
|
|
1338
|
-
if (classes) {
|
|
1339
|
-
// @ts-ignore (replaceAll is polyfilled)
|
|
1340
|
-
el.className = classes.replaceAll('.', ' ');
|
|
1341
|
-
}
|
|
1342
|
-
}
|
|
1343
|
-
currentScope._addNode(el);
|
|
1344
|
-
for (let item of rest) {
|
|
1345
|
-
let type = typeof item;
|
|
1346
|
-
if (type === 'function') {
|
|
1347
|
-
let scope = new SimpleScope(el, undefined, currentScope._queueOrder + 1, item);
|
|
1348
|
-
if (onCreateEnabled) {
|
|
1349
|
-
onCreateEnabled = false;
|
|
1350
|
-
scope._update();
|
|
1351
|
-
onCreateEnabled = true;
|
|
1352
|
-
}
|
|
1353
|
-
else {
|
|
1354
|
-
scope._update();
|
|
1355
|
-
}
|
|
1356
|
-
// Add it to our list of cleaners. Even if `scope` currently has
|
|
1357
|
-
// no cleaners, it may get them in a future refresh.
|
|
1358
|
-
currentScope._cleaners.push(scope);
|
|
1359
|
-
}
|
|
1360
|
-
else if (type === 'string' || type === 'number') {
|
|
1361
|
-
el.textContent = item;
|
|
1362
|
-
}
|
|
1363
|
-
else if (type === 'object' && item && item.constructor === Object) {
|
|
1364
|
-
for (let k in item) {
|
|
1365
|
-
applyProp(el, k, item[k]);
|
|
1366
|
-
}
|
|
1367
|
-
}
|
|
1368
|
-
else if (item instanceof Store) {
|
|
1369
|
-
bindInput(el, item);
|
|
1370
|
-
}
|
|
1371
|
-
else if (item != null) {
|
|
1372
|
-
throw new Error(`Unexpected argument ${JSON.stringify(item)}`);
|
|
1373
|
-
}
|
|
1443
|
+
deepEl.appendChild(node);
|
|
1374
1444
|
}
|
|
1375
1445
|
}
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
throw new ScopeError(true);
|
|
1383
|
-
let tmpParent = document.createElement(currentScope._parentElement.tagName);
|
|
1384
|
-
tmpParent.innerHTML = '' + html;
|
|
1385
|
-
while (tmpParent.firstChild) {
|
|
1386
|
-
currentScope._addNode(tmpParent.firstChild);
|
|
1387
|
-
}
|
|
1388
|
-
}
|
|
1389
|
-
function bindInput(el, store) {
|
|
1446
|
+
function applyBinding(_el, _key, store) {
|
|
1447
|
+
if (store == null)
|
|
1448
|
+
return;
|
|
1449
|
+
if (!(store instanceof Store))
|
|
1450
|
+
throw new Error(`Unexpect bind-argument: ${JSON.parse(store)}`);
|
|
1451
|
+
const el = _el;
|
|
1390
1452
|
let onStoreChange;
|
|
1391
1453
|
let onInputChange;
|
|
1392
1454
|
let type = el.getAttribute('type');
|
|
1393
|
-
let value = store.
|
|
1455
|
+
let value = store.peek();
|
|
1394
1456
|
if (type === 'checkbox') {
|
|
1395
1457
|
if (value === undefined)
|
|
1396
1458
|
store.set(el.checked);
|
|
@@ -1423,30 +1485,268 @@ function bindInput(el, store) {
|
|
|
1423
1485
|
el.removeEventListener('input', onInputChange);
|
|
1424
1486
|
});
|
|
1425
1487
|
}
|
|
1488
|
+
const SPECIAL_PROPS = {
|
|
1489
|
+
create: function (el, value) {
|
|
1490
|
+
if (!showCreateTransitions)
|
|
1491
|
+
return;
|
|
1492
|
+
if (typeof value === 'function') {
|
|
1493
|
+
value(el);
|
|
1494
|
+
}
|
|
1495
|
+
else {
|
|
1496
|
+
el.classList.add(value);
|
|
1497
|
+
(function () {
|
|
1498
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
1499
|
+
yield DOM_READ_PHASE;
|
|
1500
|
+
el.offsetHeight;
|
|
1501
|
+
yield DOM_WRITE_PHASE;
|
|
1502
|
+
el.classList.remove(value);
|
|
1503
|
+
});
|
|
1504
|
+
})();
|
|
1505
|
+
}
|
|
1506
|
+
},
|
|
1507
|
+
destroy: function (deepEl, value) {
|
|
1508
|
+
onDestroyMap.set(deepEl, value);
|
|
1509
|
+
},
|
|
1510
|
+
html: function (deepEl, value) {
|
|
1511
|
+
if (!value)
|
|
1512
|
+
return;
|
|
1513
|
+
let tmpParent = document.createElement(deepEl.tagName);
|
|
1514
|
+
tmpParent.innerHTML = '' + value;
|
|
1515
|
+
while (tmpParent.firstChild)
|
|
1516
|
+
addLeafNode(deepEl, tmpParent.firstChild);
|
|
1517
|
+
},
|
|
1518
|
+
text: function (deepEl, value) {
|
|
1519
|
+
if (value != null)
|
|
1520
|
+
addLeafNode(deepEl, document.createTextNode(value));
|
|
1521
|
+
},
|
|
1522
|
+
element: function (deepEl, value) {
|
|
1523
|
+
if (value == null)
|
|
1524
|
+
return;
|
|
1525
|
+
if (!(value instanceof Node))
|
|
1526
|
+
throw new Error(`Unexpect element-argument: ${JSON.parse(value)}`);
|
|
1527
|
+
addLeafNode(deepEl, value);
|
|
1528
|
+
},
|
|
1529
|
+
};
|
|
1426
1530
|
/**
|
|
1427
|
-
*
|
|
1531
|
+
* Modifies the *parent* DOM element in the current reactive scope, or adds
|
|
1532
|
+
* new DOM elements to it.
|
|
1533
|
+
*
|
|
1534
|
+
* @param args - Arguments that define how to modify/create elements.
|
|
1535
|
+
*
|
|
1536
|
+
* ### String arguments
|
|
1537
|
+
* Create new elements with optional classes and text content:
|
|
1538
|
+
* ```js
|
|
1539
|
+
* $('div.myClass') // <div class="myClass"></div>
|
|
1540
|
+
* $('span.c1.c2:Hello') // <span class="c1 c2">Hello</span>
|
|
1541
|
+
* $('p:Some text') // <p>Some text</p>
|
|
1542
|
+
* $('.my-thing') // <div class="my-thing"></div>
|
|
1543
|
+
* $('div', 'span', 'p.cls') // <div><span<p class="cls"></p></span></div>
|
|
1544
|
+
* $(':Just some text!') // Just some text! (No new element, just a text node)
|
|
1545
|
+
* ```
|
|
1546
|
+
*
|
|
1547
|
+
* ### Object arguments
|
|
1548
|
+
* Set properties, attributes, events and special features:
|
|
1549
|
+
* ```js
|
|
1550
|
+
* // Classes (dot prefix)
|
|
1551
|
+
* $('div', {'.active': true}) // Add class
|
|
1552
|
+
* $('div', {'.hidden': false}) // Remove (or don't add) class
|
|
1553
|
+
* $('div', {'.selected': myStore}) // Reactively add/remove class
|
|
1554
|
+
*
|
|
1555
|
+
* // Styles (dollar prefixed and camel-cased CSS properties)
|
|
1556
|
+
* $('div', {$color: 'red'}) // style.color = 'red'
|
|
1557
|
+
* $('div', {$marginTop: '10px'}) // style.marginTop = '10px'
|
|
1558
|
+
* $('div', {$color: myColorStore}) // Reactively change color
|
|
1559
|
+
*
|
|
1560
|
+
* // Events (function values)
|
|
1561
|
+
* $('button', {click: () => alert()}) // Add click handler
|
|
1562
|
+
*
|
|
1563
|
+
* // Properties (boolean values, `selectedIndex`, `value`)
|
|
1564
|
+
* $('input', {disabled: true}) // el.disabled = true
|
|
1565
|
+
* $('input', {value: 'test'}) // el.value = 'test'
|
|
1566
|
+
* $('select', {selectedIndex: 2}) // el.selectedIndex = 2
|
|
1567
|
+
*
|
|
1568
|
+
* // Transitions
|
|
1569
|
+
* $('div', {create: 'fade-in'}) // Add class on create
|
|
1570
|
+
* $('div', {create: el => {...}}) // Run function on create
|
|
1571
|
+
* $('div', {destroy: 'fade-out'}) // Add class before remove
|
|
1572
|
+
* $('div', {destroy: el => {...}}) // Run function before remove
|
|
1573
|
+
*
|
|
1574
|
+
* // Content
|
|
1575
|
+
* $('div', {html: '<b>Bold</b>'}) // Set innerHTML
|
|
1576
|
+
* $('div', {text: 'Plain text'}) // Add text node
|
|
1577
|
+
* const myElement = document.createElement('video')
|
|
1578
|
+
* $('div', {element: myElement}) // Add existing DOM element
|
|
1579
|
+
*
|
|
1580
|
+
* // Regular attributes (everything else)
|
|
1581
|
+
* $('div', {title: 'Info'}) // el.setAttribute('title', 'info')
|
|
1582
|
+
* ```
|
|
1583
|
+
*
|
|
1584
|
+
* When a `Store` is passed as a value, a seperate observe-scope will
|
|
1585
|
+
* be created for it, such that when the `Store` changes, only *that*
|
|
1586
|
+
* UI property will need to be updated.
|
|
1587
|
+
* So in the following example, when `colorStore` changes, only the
|
|
1588
|
+
* `color` CSS property will be updated.
|
|
1589
|
+
* ```js
|
|
1590
|
+
* $('div', {
|
|
1591
|
+
* '.active': activeStore, // Reactive class
|
|
1592
|
+
* $color: colorStore, // Reactive style
|
|
1593
|
+
* text: textStore // Reactive text
|
|
1594
|
+
* })
|
|
1595
|
+
* ```
|
|
1596
|
+
*
|
|
1597
|
+
* ### Two-way input binding
|
|
1598
|
+
* Set the initial value of an <input> <textarea> or <select> to that
|
|
1599
|
+
* of a `Store`, and then start reflecting user changes to the former
|
|
1600
|
+
* in the latter.
|
|
1601
|
+
* ```js
|
|
1602
|
+
* $('input', {bind: myStore}) // Binds input.value
|
|
1603
|
+
* ```
|
|
1604
|
+
* This is a special case, as changes to the `Store` will *not* be
|
|
1605
|
+
* reflected in the UI.
|
|
1606
|
+
*
|
|
1607
|
+
* ### Function arguments
|
|
1608
|
+
* Create child scopes that re-run on observed `Store` changes:
|
|
1609
|
+
* ```js
|
|
1610
|
+
* $('div', () => {
|
|
1611
|
+
* $(myStore.get() ? 'span' : 'p') // Reactive element type
|
|
1612
|
+
* })
|
|
1613
|
+
* ```
|
|
1614
|
+
* When *only* a function is given, `$` behaves exactly like {@link Store.observe},
|
|
1615
|
+
* except that it will only work when we're inside a `mount`.
|
|
1616
|
+
*
|
|
1617
|
+
* @throws {ScopeError} If called outside an observable scope.
|
|
1618
|
+
* @throws {Error} If invalid arguments are provided.
|
|
1428
1619
|
*/
|
|
1429
|
-
export function
|
|
1430
|
-
if (!currentScope)
|
|
1431
|
-
throw new ScopeError(true);
|
|
1432
|
-
if (text == null)
|
|
1433
|
-
return;
|
|
1434
|
-
currentScope._addNode(document.createTextNode(text));
|
|
1435
|
-
}
|
|
1436
|
-
export function prop(name, value = undefined) {
|
|
1620
|
+
export function $(...args) {
|
|
1437
1621
|
if (!currentScope || !currentScope._parentElement)
|
|
1438
1622
|
throw new ScopeError(true);
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1623
|
+
let deepEl = currentScope._parentElement;
|
|
1624
|
+
for (let arg of args) {
|
|
1625
|
+
if (arg == null || arg === false)
|
|
1626
|
+
continue;
|
|
1627
|
+
if (typeof arg === 'string') {
|
|
1628
|
+
let text, classes;
|
|
1629
|
+
const textPos = arg.indexOf(':');
|
|
1630
|
+
if (textPos >= 0) {
|
|
1631
|
+
text = arg.substring(textPos + 1);
|
|
1632
|
+
if (textPos === 0) { // Just a string to add as text, no new node
|
|
1633
|
+
addLeafNode(deepEl, document.createTextNode(text));
|
|
1634
|
+
continue;
|
|
1635
|
+
}
|
|
1636
|
+
arg = arg.substring(0, textPos);
|
|
1637
|
+
}
|
|
1638
|
+
const classPos = arg.indexOf('.');
|
|
1639
|
+
if (classPos >= 0) {
|
|
1640
|
+
classes = arg.substring(classPos + 1).replaceAll('.', ' ');
|
|
1641
|
+
arg = arg.substring(0, classPos);
|
|
1642
|
+
}
|
|
1643
|
+
if (arg.indexOf(' ') >= 0)
|
|
1644
|
+
throw new Error(`Tag '${arg}' cannot contain space`);
|
|
1645
|
+
const el = document.createElement(arg || 'div');
|
|
1646
|
+
if (classes)
|
|
1647
|
+
el.className = classes;
|
|
1648
|
+
if (text)
|
|
1649
|
+
el.textContent = text;
|
|
1650
|
+
addLeafNode(deepEl, el);
|
|
1651
|
+
deepEl = el;
|
|
1652
|
+
}
|
|
1653
|
+
else if (typeof arg === 'object') {
|
|
1654
|
+
if (arg.constructor !== Object)
|
|
1655
|
+
throw new Error(`Unexpected argument: ${arg}`);
|
|
1656
|
+
for (const key in arg) {
|
|
1657
|
+
const val = arg[key];
|
|
1658
|
+
if (key === 'bind') { // Special case, as for this prop we *don't* want to resolve the Store to a value first.
|
|
1659
|
+
applyBinding(deepEl, key, val);
|
|
1660
|
+
}
|
|
1661
|
+
else if (val instanceof Store) {
|
|
1662
|
+
let childScope = new SetArgScope(deepEl, deepEl.lastChild, currentScope._queueOrder + 1, key, val);
|
|
1663
|
+
childScope._install();
|
|
1664
|
+
}
|
|
1665
|
+
else {
|
|
1666
|
+
applyArg(deepEl, key, val);
|
|
1667
|
+
}
|
|
1668
|
+
}
|
|
1669
|
+
}
|
|
1670
|
+
else if (typeof arg === 'function') {
|
|
1671
|
+
if (deepEl === currentScope._parentElement) { // do what observe does
|
|
1672
|
+
_mount(undefined, args[0], SimpleScope);
|
|
1673
|
+
}
|
|
1674
|
+
else { // new scope for a new node without any scope attached yet
|
|
1675
|
+
let childScope = new SimpleScope(deepEl, deepEl.lastChild, currentScope._queueOrder + 1, arg);
|
|
1676
|
+
childScope._install();
|
|
1677
|
+
}
|
|
1678
|
+
}
|
|
1679
|
+
else {
|
|
1680
|
+
throw new Error(`Unexpected argument: ${JSON.stringify(arg)}`);
|
|
1442
1681
|
}
|
|
1443
1682
|
}
|
|
1444
|
-
|
|
1445
|
-
|
|
1683
|
+
}
|
|
1684
|
+
function applyArg(deepEl, key, value) {
|
|
1685
|
+
if (key[0] === '.') { // CSS class(es)
|
|
1686
|
+
const classes = key.substring(1).split('.');
|
|
1687
|
+
if (value)
|
|
1688
|
+
deepEl.classList.add(...classes);
|
|
1689
|
+
else
|
|
1690
|
+
deepEl.classList.remove(...classes);
|
|
1691
|
+
}
|
|
1692
|
+
else if (key[0] === '$') { // Style
|
|
1693
|
+
const name = key.substring(1);
|
|
1694
|
+
if (value == null || value === false)
|
|
1695
|
+
deepEl.style[name] = '';
|
|
1696
|
+
else
|
|
1697
|
+
deepEl.style[name] = '' + value;
|
|
1698
|
+
}
|
|
1699
|
+
else if (key in SPECIAL_PROPS) { // Special property
|
|
1700
|
+
SPECIAL_PROPS[key](deepEl, value);
|
|
1701
|
+
}
|
|
1702
|
+
else if (typeof value === 'function') { // Event listener
|
|
1703
|
+
deepEl.addEventListener(key, value);
|
|
1704
|
+
clean(() => deepEl.removeEventListener(key, value));
|
|
1705
|
+
}
|
|
1706
|
+
else if (value === true || value === false || key === 'value' || key === 'selectedIndex') { // DOM property
|
|
1707
|
+
deepEl[key] = value;
|
|
1708
|
+
}
|
|
1709
|
+
else { // HTML attribute
|
|
1710
|
+
deepEl.setAttribute(key, value);
|
|
1446
1711
|
}
|
|
1447
1712
|
}
|
|
1713
|
+
function defaultOnError(error) {
|
|
1714
|
+
console.error('Error while in Aberdeen render:', error);
|
|
1715
|
+
return true;
|
|
1716
|
+
}
|
|
1717
|
+
let onError = defaultOnError;
|
|
1718
|
+
/**
|
|
1719
|
+
* Set a custome error handling function, thast is called when an error occurs during rendering
|
|
1720
|
+
* while in a reactive scope. The default implementation logs the error to the console, and then
|
|
1721
|
+
* just returns `true`, which causes an 'Error' message to be displayed in the UI. When this function
|
|
1722
|
+
* returns `false`, the error is suppressed. This mechanism exists because rendering errors can occur
|
|
1723
|
+
* at any time, not just synchronous when making a call to Aberdeen, thus normal exception handling
|
|
1724
|
+
* is not always possible.
|
|
1725
|
+
*
|
|
1726
|
+
* @param handler The handler function, getting an `Error` as its argument, and returning `false`
|
|
1727
|
+
* if it does *not* want an error message to be added to the DOM.
|
|
1728
|
+
* When `handler is `undefined`, the default error handling will be reinstated.
|
|
1729
|
+
*
|
|
1730
|
+
* @example
|
|
1731
|
+
* ```javascript
|
|
1732
|
+
* //
|
|
1733
|
+
* setErrorHandler(error => {
|
|
1734
|
+
* // Tell our developers about the problem.
|
|
1735
|
+
* fancyErrorLogger(error)
|
|
1736
|
+
* // Add custom error message to the DOM.
|
|
1737
|
+
* try {
|
|
1738
|
+
* $('.error:Sorry, something went wrong!')
|
|
1739
|
+
* } catch() {} // In case there is no parent element.
|
|
1740
|
+
* // Don't add default error message to the DOM.
|
|
1741
|
+
* return false
|
|
1742
|
+
* })
|
|
1743
|
+
* ```
|
|
1744
|
+
*/
|
|
1745
|
+
export function setErrorHandler(handler) {
|
|
1746
|
+
onError = handler || defaultOnError;
|
|
1747
|
+
}
|
|
1448
1748
|
/**
|
|
1449
|
-
* Return the browser Element that
|
|
1749
|
+
* Return the browser Element that nodes would be rendered to at this point.
|
|
1450
1750
|
* NOTE: Manually changing the DOM is not recommended in most cases. There is
|
|
1451
1751
|
* usually a better, declarative way. Although there are no hard guarantees on
|
|
1452
1752
|
* how your changes interact with Aberdeen, in most cases results will not be
|
|
@@ -1505,10 +1805,10 @@ export function immediateObserve(func) {
|
|
|
1505
1805
|
return _mount(undefined, func, ImmediateScope);
|
|
1506
1806
|
}
|
|
1507
1807
|
/**
|
|
1508
|
-
*
|
|
1808
|
+
* Reactively run the function, adding any DOM-elements created using {@link $} to the given parent element.
|
|
1509
1809
|
|
|
1510
1810
|
* @param func - The function to be (repeatedly) executed, possibly adding DOM elements to `parentElement`.
|
|
1511
|
-
* @param parentElement - A DOM element that will be used as the parent element for calls to
|
|
1811
|
+
* @param parentElement - A DOM element that will be used as the parent element for calls to `$`.
|
|
1512
1812
|
* @returns The mount id (usable for `unmount`) if this is a top-level mount.
|
|
1513
1813
|
*
|
|
1514
1814
|
* @example
|
|
@@ -1517,7 +1817,7 @@ export function immediateObserve(func) {
|
|
|
1517
1817
|
* setInterval(() => store.modify(v => v+1), 1000)
|
|
1518
1818
|
*
|
|
1519
1819
|
* mount(document.body, () => {
|
|
1520
|
-
*
|
|
1820
|
+
* $(`h2:${store.get()} seconds have passed`)
|
|
1521
1821
|
* })
|
|
1522
1822
|
* ```
|
|
1523
1823
|
*
|
|
@@ -1527,25 +1827,30 @@ export function immediateObserve(func) {
|
|
|
1527
1827
|
* let colors = new Store(new Map())
|
|
1528
1828
|
*
|
|
1529
1829
|
* mount(document.body, () => {
|
|
1530
|
-
*
|
|
1531
|
-
*
|
|
1532
|
-
*
|
|
1830
|
+
* // This function will never rerun (as it does not read any `Store`s)
|
|
1831
|
+
* $('button:<<', {click: () => selected.modify(n => n-1)})
|
|
1832
|
+
* $('button:>>', {click: () => selected.modify(n => n+1)})
|
|
1533
1833
|
*
|
|
1534
|
-
*
|
|
1535
|
-
*
|
|
1536
|
-
*
|
|
1537
|
-
*
|
|
1538
|
-
*
|
|
1834
|
+
* observe(() => {
|
|
1835
|
+
* // This will rerun whenever `selected` changes, recreating the <h2> and <input>.
|
|
1836
|
+
* $('h2', {text: '#' + selected.get()})
|
|
1837
|
+
* $('input', {type: 'color', value: '#ffffff' bind: colors(selected.get())})
|
|
1838
|
+
* })
|
|
1539
1839
|
*
|
|
1540
|
-
*
|
|
1541
|
-
*
|
|
1542
|
-
*
|
|
1543
|
-
*
|
|
1544
|
-
*
|
|
1840
|
+
* observe(() => {
|
|
1841
|
+
* // This function will rerun when `selected` or the selected color changes.
|
|
1842
|
+
* // It will change the <body> background-color.
|
|
1843
|
+
* $({$backgroundColor: colors.get(selected.get()) || 'white'})
|
|
1844
|
+
* })
|
|
1545
1845
|
* })
|
|
1546
1846
|
* ```
|
|
1547
1847
|
*/
|
|
1548
1848
|
export function mount(parentElement, func) {
|
|
1849
|
+
for (let scope of topScopes.values()) {
|
|
1850
|
+
if (parentElement === scope._parentElement) {
|
|
1851
|
+
throw new Error("Only a single mount per parent element");
|
|
1852
|
+
}
|
|
1853
|
+
}
|
|
1549
1854
|
return _mount(parentElement, func, SimpleScope);
|
|
1550
1855
|
}
|
|
1551
1856
|
let maxTopScopeId = 0;
|
|
@@ -1586,6 +1891,7 @@ export function unmount(id) {
|
|
|
1586
1891
|
let scope = topScopes.get(id);
|
|
1587
1892
|
if (!scope)
|
|
1588
1893
|
throw new Error("No such mount " + id);
|
|
1894
|
+
topScopes.delete(id);
|
|
1589
1895
|
scope._remove();
|
|
1590
1896
|
}
|
|
1591
1897
|
}
|
|
@@ -1623,63 +1929,16 @@ export function peek(func) {
|
|
|
1623
1929
|
/*
|
|
1624
1930
|
* Helper functions
|
|
1625
1931
|
*/
|
|
1626
|
-
function applyProp(el, prop, value) {
|
|
1627
|
-
if (prop === 'create') {
|
|
1628
|
-
if (onCreateEnabled) {
|
|
1629
|
-
if (typeof value === 'function') {
|
|
1630
|
-
value(el);
|
|
1631
|
-
}
|
|
1632
|
-
else {
|
|
1633
|
-
el.classList.add(value);
|
|
1634
|
-
setTimeout(function () { el.classList.remove(value); }, 0);
|
|
1635
|
-
}
|
|
1636
|
-
}
|
|
1637
|
-
}
|
|
1638
|
-
else if (prop === 'destroy') {
|
|
1639
|
-
onDestroyMap.set(el, value);
|
|
1640
|
-
}
|
|
1641
|
-
else if (typeof value === 'function') {
|
|
1642
|
-
// Set an event listener; remove it again on clean.
|
|
1643
|
-
el.addEventListener(prop, value);
|
|
1644
|
-
clean(() => el.removeEventListener(prop, value));
|
|
1645
|
-
}
|
|
1646
|
-
else if (prop === 'value' || prop === 'className' || prop === 'selectedIndex' || value === true || value === false) {
|
|
1647
|
-
// All boolean values and a few specific keys should be set as a property
|
|
1648
|
-
el[prop] = value;
|
|
1649
|
-
}
|
|
1650
|
-
else if (prop === 'text') {
|
|
1651
|
-
// `text` is set as textContent
|
|
1652
|
-
el.textContent = value;
|
|
1653
|
-
}
|
|
1654
|
-
else if ((prop === 'class' || prop === 'className') && typeof value === 'object') {
|
|
1655
|
-
// Allow setting classes using an object where the keys are the names and
|
|
1656
|
-
// the values are booleans stating whether to set or remove.
|
|
1657
|
-
for (let name in value) {
|
|
1658
|
-
if (value[name])
|
|
1659
|
-
el.classList.add(name);
|
|
1660
|
-
else
|
|
1661
|
-
el.classList.remove(name);
|
|
1662
|
-
}
|
|
1663
|
-
}
|
|
1664
|
-
else if (prop === 'style' && typeof value === 'object') {
|
|
1665
|
-
// `style` can receive an object
|
|
1666
|
-
Object.assign(el.style, value);
|
|
1667
|
-
}
|
|
1668
|
-
else {
|
|
1669
|
-
// Everything else is an HTML attribute
|
|
1670
|
-
el.setAttribute(prop, value);
|
|
1671
|
-
}
|
|
1672
|
-
}
|
|
1673
1932
|
function valueToData(value) {
|
|
1674
|
-
if (
|
|
1675
|
-
// Simple data types
|
|
1676
|
-
return value;
|
|
1677
|
-
}
|
|
1678
|
-
else if (value instanceof Store) {
|
|
1933
|
+
if (value instanceof Store) {
|
|
1679
1934
|
// When a Store is passed pointing at a collection, a reference
|
|
1680
1935
|
// is made to that collection.
|
|
1681
1936
|
return value._observe();
|
|
1682
1937
|
}
|
|
1938
|
+
else if (typeof value !== "object" || !value) {
|
|
1939
|
+
// Simple data types
|
|
1940
|
+
return value;
|
|
1941
|
+
}
|
|
1683
1942
|
else if (value instanceof Map) {
|
|
1684
1943
|
let result = new ObsMap();
|
|
1685
1944
|
value.forEach((v, k) => {
|
|
@@ -1718,13 +1977,17 @@ function defaultMakeSortKey(store) {
|
|
|
1718
1977
|
}
|
|
1719
1978
|
/* c8 ignore start */
|
|
1720
1979
|
function internalError(code) {
|
|
1721
|
-
|
|
1722
|
-
setTimeout(() => { throw error; }, 0);
|
|
1980
|
+
throw new Error("Aberdeen internal error " + code);
|
|
1723
1981
|
}
|
|
1724
1982
|
/* c8 ignore end */
|
|
1725
|
-
function handleError(e) {
|
|
1726
|
-
|
|
1727
|
-
|
|
1983
|
+
function handleError(e, showMessage) {
|
|
1984
|
+
try {
|
|
1985
|
+
if (onError(e) === false)
|
|
1986
|
+
showMessage = false;
|
|
1987
|
+
}
|
|
1988
|
+
catch (_a) { }
|
|
1989
|
+
if (showMessage && (currentScope === null || currentScope === void 0 ? void 0 : currentScope._parentElement))
|
|
1990
|
+
$('.aberdeen-error:Error');
|
|
1728
1991
|
}
|
|
1729
1992
|
class ScopeError extends Error {
|
|
1730
1993
|
constructor(mount) {
|
|
@@ -1742,13 +2005,6 @@ export function withEmitHandler(handler, func) {
|
|
|
1742
2005
|
ObsCollection.prototype.emitChange = oldEmitHandler;
|
|
1743
2006
|
}
|
|
1744
2007
|
}
|
|
1745
|
-
/**
|
|
1746
|
-
* Run a function, while *not* causing reactive effects for any changes it makes to `Store`s.
|
|
1747
|
-
* @param func The function to be executed once immediately.
|
|
1748
|
-
*/
|
|
1749
|
-
export function inhibitEffects(func) {
|
|
1750
|
-
withEmitHandler(() => { }, func);
|
|
1751
|
-
}
|
|
1752
2008
|
// @ts-ignore
|
|
1753
2009
|
// c8 ignore next
|
|
1754
2010
|
if (!String.prototype.replaceAll)
|