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/src/aberdeen.ts
CHANGED
|
@@ -11,11 +11,13 @@ interface QueueRunner {
|
|
|
11
11
|
_queueRun(): void
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
let queueArray: Array<QueueRunner> = []
|
|
15
|
-
let
|
|
16
|
-
let
|
|
17
|
-
let
|
|
18
|
-
let
|
|
14
|
+
let queueArray: Array<QueueRunner> = [] // When not empty, a runQueue is scheduled or currently running.
|
|
15
|
+
let queueIndex = 0 // This first element in queueArray that still needs to be processed.
|
|
16
|
+
let queueSet: Set<QueueRunner> = new Set() // Contains the subset of queueArray at index >= queueIndex.
|
|
17
|
+
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.
|
|
18
|
+
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.
|
|
19
|
+
let showCreateTransitions = false // Set to `true` only when creating top level elements in response to `Store` changes, triggering `create` transitions.
|
|
20
|
+
|
|
19
21
|
|
|
20
22
|
/** @internal */
|
|
21
23
|
export type Patch = Map<ObsCollection, Map<any, [any, any]>>;
|
|
@@ -35,9 +37,18 @@ function queue(runner: QueueRunner) {
|
|
|
35
37
|
queueSet.add(runner)
|
|
36
38
|
}
|
|
37
39
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
40
|
+
/**
|
|
41
|
+
* Normally, changes to `Store`s are reacted to asynchronously, in an (optimized)
|
|
42
|
+
* batch, after a timeout of 0s. Calling `runQueue()` will do so immediately
|
|
43
|
+
* and synchronously. Doing so may be helpful in cases where you need some DOM
|
|
44
|
+
* modification to be done synchronously.
|
|
45
|
+
*
|
|
46
|
+
* This function is re-entrant, meaning it is safe to call `runQueue` from a
|
|
47
|
+
* function that is called due to another (automatic) invocation of `runQueue`.
|
|
48
|
+
*/
|
|
49
|
+
export function runQueue(): void {
|
|
50
|
+
showCreateTransitions = true
|
|
51
|
+
for(; queueIndex < queueArray.length; ) {
|
|
41
52
|
// Sort queue if new unordered items have been added since last time.
|
|
42
53
|
if (!queueOrdered) {
|
|
43
54
|
queueArray.splice(0, queueIndex)
|
|
@@ -49,8 +60,8 @@ function runQueue(): void {
|
|
|
49
60
|
|
|
50
61
|
// Process the rest of what's currently in the queue.
|
|
51
62
|
let batchEndIndex = queueArray.length
|
|
52
|
-
|
|
53
|
-
let runner = queueArray[queueIndex]
|
|
63
|
+
while(queueIndex < batchEndIndex && queueOrdered) {
|
|
64
|
+
let runner = queueArray[queueIndex++]
|
|
54
65
|
queueSet.delete(runner)
|
|
55
66
|
runner._queueRun()
|
|
56
67
|
}
|
|
@@ -60,19 +71,20 @@ function runQueue(): void {
|
|
|
60
71
|
runQueueDepth++
|
|
61
72
|
}
|
|
62
73
|
|
|
74
|
+
queueIndex = 0
|
|
63
75
|
queueArray.length = 0
|
|
64
|
-
queueIndex = undefined
|
|
65
76
|
runQueueDepth = 0
|
|
66
|
-
|
|
77
|
+
showCreateTransitions = false
|
|
67
78
|
}
|
|
68
79
|
|
|
69
80
|
|
|
81
|
+
let domWaiters: (() => void)[] = []
|
|
82
|
+
let domInReadPhase = false
|
|
83
|
+
|
|
70
84
|
/**
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
* layout recalculations and improving browser performance. A DOM read operation should
|
|
75
|
-
* only *read* from the DOM, such as measuring element dimensions or retrieving computed styles.
|
|
85
|
+
* A promise-like object that you can `await`. It will resolve *after* the current batch
|
|
86
|
+
* of DOM-write operations has completed. This is the best time to retrieve DOM properties
|
|
87
|
+
* that dependent on a layout being completed, such as `offsetHeight`.
|
|
76
88
|
*
|
|
77
89
|
* By batching DOM reads separately from DOM writes, this prevents the browser from
|
|
78
90
|
* interleaving layout reads and writes, which can force additional layout recalculations.
|
|
@@ -81,20 +93,25 @@ function runQueue(): void {
|
|
|
81
93
|
*
|
|
82
94
|
* Unlike `setTimeout` or `requestAnimationFrame`, this mechanism ensures that DOM read
|
|
83
95
|
* operations happen before any DOM writes in the same queue cycle, minimizing layout thrashing.
|
|
84
|
-
*
|
|
85
|
-
*
|
|
96
|
+
*
|
|
97
|
+
* See `transitions.js` for some examples.
|
|
86
98
|
*/
|
|
87
|
-
export function scheduleDomReader(func: () => void): void {
|
|
88
|
-
let order = (queueIndex!=null && queueIndex < queueArray.length && queueArray[queueIndex]._queueOrder >= 1000) ? ((queueArray[queueIndex]._queueOrder+1) & (~1)) : 1000
|
|
89
|
-
queue({_queueOrder: order, _queueRun: func})
|
|
90
|
-
}
|
|
91
99
|
|
|
100
|
+
export const DOM_READ_PHASE = {
|
|
101
|
+
then: function(fulfilled: () => void) {
|
|
102
|
+
if (domInReadPhase) fulfilled()
|
|
103
|
+
else {
|
|
104
|
+
if (!domWaiters.length) queue(DOM_PHASE_RUNNER)
|
|
105
|
+
domWaiters.push(fulfilled)
|
|
106
|
+
}
|
|
107
|
+
return this
|
|
108
|
+
}
|
|
109
|
+
}
|
|
92
110
|
/**
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
* This
|
|
96
|
-
*
|
|
97
|
-
* only *write* to the DOM, such as modifying element properties or applying styles.
|
|
111
|
+
* A promise-like object that you can `await`. It will resolve *after* the current
|
|
112
|
+
* DOM_READ_PHASE has completed (if any) and after any DOM triggered by Aberdeen
|
|
113
|
+
* have completed. This is a good time to do little manual DOM tweaks that depend
|
|
114
|
+
* on a *read phase* first, like triggering transitions.
|
|
98
115
|
*
|
|
99
116
|
* By batching DOM writes separately from DOM reads, this prevents the browser from
|
|
100
117
|
* interleaving layout reads and writes, which can force additional layout recalculations.
|
|
@@ -104,11 +121,34 @@ export function scheduleDomReader(func: () => void): void {
|
|
|
104
121
|
* Unlike `setTimeout` or `requestAnimationFrame`, this mechanism ensures that DOM write
|
|
105
122
|
* operations happen after all DOM reads in the same queue cycle, minimizing layout thrashing.
|
|
106
123
|
*
|
|
107
|
-
*
|
|
124
|
+
* See `transitions.js` for some examples.
|
|
108
125
|
*/
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
126
|
+
|
|
127
|
+
export const DOM_WRITE_PHASE = {
|
|
128
|
+
then: function(fulfilled: () => void) {
|
|
129
|
+
if (!domInReadPhase) fulfilled()
|
|
130
|
+
else {
|
|
131
|
+
if (!domWaiters.length) queue(DOM_PHASE_RUNNER)
|
|
132
|
+
domWaiters.push(fulfilled)
|
|
133
|
+
}
|
|
134
|
+
return this
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const DOM_PHASE_RUNNER = {
|
|
139
|
+
_queueOrder: 99999,
|
|
140
|
+
_queueRun: function() {
|
|
141
|
+
let waiters = domWaiters
|
|
142
|
+
domWaiters = []
|
|
143
|
+
domInReadPhase = !domInReadPhase
|
|
144
|
+
for(let waiter of waiters) {
|
|
145
|
+
try {
|
|
146
|
+
waiter()
|
|
147
|
+
} catch(e) {
|
|
148
|
+
console.error(e)
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
112
152
|
}
|
|
113
153
|
|
|
114
154
|
|
|
@@ -172,15 +212,6 @@ interface Observer {
|
|
|
172
212
|
*/
|
|
173
213
|
|
|
174
214
|
abstract class Scope implements QueueRunner, Observer {
|
|
175
|
-
_parentElement: Element | undefined
|
|
176
|
-
|
|
177
|
-
// How deep is this scope nested in other scopes; we use this to make sure events
|
|
178
|
-
// at lower depths are handled before events at higher depths.
|
|
179
|
-
_queueOrder: number
|
|
180
|
-
|
|
181
|
-
// The node or scope right before this scope that has the same `parentElement`
|
|
182
|
-
_precedingSibling: Node | Scope | undefined
|
|
183
|
-
|
|
184
215
|
// The last child node or scope within this scope that has the same `parentElement`
|
|
185
216
|
_lastChild: Node | Scope | undefined
|
|
186
217
|
|
|
@@ -193,13 +224,13 @@ abstract class Scope implements QueueRunner, Observer {
|
|
|
193
224
|
_isDead: boolean = false
|
|
194
225
|
|
|
195
226
|
constructor(
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
227
|
+
public _parentElement: Element | undefined,
|
|
228
|
+
// The node or scope right before this scope that has the same `parentElement`
|
|
229
|
+
public _precedingSibling: Node | Scope | undefined,
|
|
230
|
+
// How deep is this scope nested in other scopes; we use this to make sure events
|
|
231
|
+
// at lower depths are handled before events at higher depths.
|
|
232
|
+
public _queueOrder: number,
|
|
199
233
|
) {
|
|
200
|
-
this._parentElement = parentElement
|
|
201
|
-
this._precedingSibling = precedingSibling
|
|
202
|
-
this._queueOrder = queueOrder
|
|
203
234
|
}
|
|
204
235
|
|
|
205
236
|
// Get a reference to the last Node preceding this Scope, or undefined if there is none
|
|
@@ -223,10 +254,9 @@ abstract class Scope implements QueueRunner, Observer {
|
|
|
223
254
|
}
|
|
224
255
|
|
|
225
256
|
_addNode(node: Node) {
|
|
226
|
-
if (!this._parentElement) throw new ScopeError(true)
|
|
227
257
|
let prevNode = this._findLastNode() || this._findPrecedingNode()
|
|
228
258
|
|
|
229
|
-
this._parentElement
|
|
259
|
+
this._parentElement!.insertBefore(node, prevNode ? prevNode.nextSibling : this._parentElement!.firstChild)
|
|
230
260
|
this._lastChild = node
|
|
231
261
|
}
|
|
232
262
|
|
|
@@ -288,18 +318,23 @@ abstract class Scope implements QueueRunner, Observer {
|
|
|
288
318
|
}
|
|
289
319
|
|
|
290
320
|
class SimpleScope extends Scope {
|
|
291
|
-
_renderer: () => void
|
|
292
|
-
|
|
293
321
|
constructor(
|
|
294
322
|
parentElement: Element | undefined,
|
|
295
323
|
precedingSibling: Node | Scope | undefined,
|
|
296
324
|
queueOrder: number,
|
|
297
|
-
renderer
|
|
325
|
+
renderer?: () => void,
|
|
298
326
|
) {
|
|
299
327
|
super(parentElement, precedingSibling, queueOrder)
|
|
300
|
-
this._renderer = renderer
|
|
328
|
+
if (renderer) this._renderer = renderer
|
|
301
329
|
}
|
|
302
330
|
|
|
331
|
+
/* c8 ignore start */
|
|
332
|
+
_renderer() {
|
|
333
|
+
// Should be overriden by a subclass or the constructor
|
|
334
|
+
internalError(14)
|
|
335
|
+
}
|
|
336
|
+
/* c8 ignore stop */
|
|
337
|
+
|
|
303
338
|
_queueRun() {
|
|
304
339
|
/* c8 ignore next */
|
|
305
340
|
if (currentScope) internalError(2)
|
|
@@ -318,10 +353,44 @@ class SimpleScope extends Scope {
|
|
|
318
353
|
this._renderer()
|
|
319
354
|
} catch(e) {
|
|
320
355
|
// Throw the error async, so the rest of the rendering can continue
|
|
321
|
-
handleError(e)
|
|
356
|
+
handleError(e, true)
|
|
322
357
|
}
|
|
323
358
|
currentScope = savedScope
|
|
324
359
|
}
|
|
360
|
+
|
|
361
|
+
_install() {
|
|
362
|
+
if (showCreateTransitions) {
|
|
363
|
+
showCreateTransitions = false
|
|
364
|
+
this._update()
|
|
365
|
+
showCreateTransitions = true
|
|
366
|
+
} else {
|
|
367
|
+
this._update()
|
|
368
|
+
}
|
|
369
|
+
// Add it to our list of cleaners. Even if `childScope` currently has
|
|
370
|
+
// no cleaners, it may get them in a future refresh.
|
|
371
|
+
currentScope!._cleaners.push(this)
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* This could have been done with a SimpleScope, but then we'd have to draw along an instance of
|
|
377
|
+
* that as well as a renderer function that closes over quite a few variables, which probably
|
|
378
|
+
* wouldn't be great for the performance of this common feature.
|
|
379
|
+
*/
|
|
380
|
+
class SetArgScope extends SimpleScope {
|
|
381
|
+
constructor(
|
|
382
|
+
parentElement: Element | undefined,
|
|
383
|
+
precedingSibling: Node | Scope | undefined,
|
|
384
|
+
queueOrder: number,
|
|
385
|
+
private _key: string,
|
|
386
|
+
private _value: Store,
|
|
387
|
+
) {
|
|
388
|
+
super(parentElement, precedingSibling, queueOrder)
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
_renderer() {
|
|
392
|
+
applyArg(this._parentElement as Element, this._key, this._value.get())
|
|
393
|
+
}
|
|
325
394
|
}
|
|
326
395
|
|
|
327
396
|
let immediateQueue: Set<Scope> = new Set()
|
|
@@ -607,7 +676,7 @@ class OnEachItemScope extends Scope {
|
|
|
607
676
|
try {
|
|
608
677
|
sortKey = this._parent._makeSortKey(itemStore)
|
|
609
678
|
} catch(e) {
|
|
610
|
-
handleError(e)
|
|
679
|
+
handleError(e, false)
|
|
611
680
|
}
|
|
612
681
|
|
|
613
682
|
let oldSortStr: string = this._sortStr
|
|
@@ -625,7 +694,7 @@ class OnEachItemScope extends Scope {
|
|
|
625
694
|
try {
|
|
626
695
|
this._parent._renderer(itemStore)
|
|
627
696
|
} catch(e) {
|
|
628
|
-
handleError(e)
|
|
697
|
+
handleError(e, true)
|
|
629
698
|
}
|
|
630
699
|
}
|
|
631
700
|
|
|
@@ -636,7 +705,7 @@ class OnEachItemScope extends Scope {
|
|
|
636
705
|
|
|
637
706
|
/**
|
|
638
707
|
* This global is set during the execution of a `Scope.render`. It is used by
|
|
639
|
-
* functions like
|
|
708
|
+
* functions like `$` and `clean`.
|
|
640
709
|
*/
|
|
641
710
|
let currentScope: Scope | undefined
|
|
642
711
|
|
|
@@ -914,56 +983,98 @@ class ObsMap extends ObsCollection {
|
|
|
914
983
|
|
|
915
984
|
|
|
916
985
|
|
|
917
|
-
|
|
918
|
-
|
|
986
|
+
const DETACHED_KEY: any = {}
|
|
987
|
+
|
|
988
|
+
/*
|
|
989
|
+
* A data store that automatically subscribes the current Scope to updates
|
|
919
990
|
* whenever data is read from it.
|
|
920
991
|
*
|
|
921
992
|
* Supported data types are: `string`, `number`, `boolean`, `undefined`, `null`,
|
|
922
993
|
* `Array`, `object` and `Map`. The latter three will always have `Store` objects as
|
|
923
994
|
* values, creating a tree of `Store`-objects.
|
|
924
995
|
*/
|
|
996
|
+
|
|
997
|
+
export interface Store {
|
|
998
|
+
/**
|
|
999
|
+
* Return a `Store` deeper within the tree by resolving the given `path`,
|
|
1000
|
+
* subscribing to every level.
|
|
1001
|
+
* In case `undefined` is encountered while resolving the path, a newly
|
|
1002
|
+
* created `Store` containing `undefined` is returned. In that case, the
|
|
1003
|
+
* `Store`'s [[`isDetached`]] method will return `true`.
|
|
1004
|
+
* In case something other than a collection is encountered, an error is thrown.
|
|
1005
|
+
*/
|
|
1006
|
+
(...path: any[]): Store
|
|
1007
|
+
}
|
|
925
1008
|
|
|
926
1009
|
export class Store {
|
|
927
1010
|
/** @internal */
|
|
1011
|
+
// @ts-ignore
|
|
928
1012
|
private _collection: ObsCollection
|
|
929
1013
|
/** @internal */
|
|
930
1014
|
private _idx: any
|
|
1015
|
+
/** @internal */
|
|
1016
|
+
private _virtual: string[] | undefined
|
|
931
1017
|
|
|
932
1018
|
/**
|
|
933
|
-
* Create a new
|
|
934
|
-
|
|
935
|
-
* `Store`s. (Calling {@link Store.get} on the store will recreate the original data strucure, though.)
|
|
936
|
-
*
|
|
937
|
-
* @example
|
|
938
|
-
* ```
|
|
939
|
-
* let emptyStore = new Store()
|
|
940
|
-
* let numStore = new Store(42)
|
|
941
|
-
* let objStore = new Store({x: {alice: 1, bob: 2}, y: [9,7,5,3,1]})
|
|
942
|
-
* ```
|
|
943
|
-
*/
|
|
1019
|
+
* Create a new `Store` with `undefined` as its initial value.
|
|
1020
|
+
*/
|
|
944
1021
|
constructor()
|
|
1022
|
+
/**
|
|
1023
|
+
* Create a new `Store`.
|
|
1024
|
+
* @param value The initial value. Plain objects, arrays and `Map`s, are converted
|
|
1025
|
+
* into a tree of nested `Store`s. When another `Store` is included somewhere in that
|
|
1026
|
+
* input tree, a reference is made.
|
|
1027
|
+
*/
|
|
945
1028
|
constructor(value: any)
|
|
1029
|
+
|
|
946
1030
|
/** @internal */
|
|
947
1031
|
constructor(collection: ObsCollection, index: any)
|
|
948
1032
|
|
|
1033
|
+
/** @internal */
|
|
949
1034
|
constructor(value: any = undefined, index: any = undefined) {
|
|
1035
|
+
/**
|
|
1036
|
+
* Create and return a new `Store` that represents the subtree at `path` of
|
|
1037
|
+
* the current `Store`.
|
|
1038
|
+
*
|
|
1039
|
+
* The `path` is only actually resolved when this new `Store` is first used,
|
|
1040
|
+
* and how this is done depends on whether a read or a write operation is
|
|
1041
|
+
* performed. Read operations will just use an `undefined` value when a
|
|
1042
|
+
* subtree that we're diving into does not exist. Also, they'll subscribe
|
|
1043
|
+
* to changes at each level of the tree indexed by `path`.
|
|
1044
|
+
*
|
|
1045
|
+
* Write operations will create any missing subtrees as objects. They don't
|
|
1046
|
+
* subscribe to changes (as they are the ones causing the changes).
|
|
1047
|
+
*
|
|
1048
|
+
* Both read and write operations will throw an error if, while resolving
|
|
1049
|
+
* `path`, they encounters a non-collection data type (such as a number)
|
|
1050
|
+
*/
|
|
1051
|
+
const ref: Store = function(...path: any): Store {
|
|
1052
|
+
const result = new Store(ref._collection, ref._idx)
|
|
1053
|
+
if (path.length || ref._virtual) {
|
|
1054
|
+
result._virtual = ref._virtual ? ref._virtual.concat(path) : path
|
|
1055
|
+
}
|
|
1056
|
+
return result
|
|
1057
|
+
} as Store
|
|
1058
|
+
|
|
1059
|
+
Object.setPrototypeOf(ref, Store.prototype)
|
|
950
1060
|
if (index===undefined) {
|
|
951
|
-
|
|
952
|
-
|
|
1061
|
+
ref._collection = new ObsArray()
|
|
1062
|
+
ref._idx = 0
|
|
953
1063
|
if (value!==undefined) {
|
|
954
|
-
|
|
1064
|
+
ref._collection.rawSet(0, valueToData(value))
|
|
955
1065
|
}
|
|
956
1066
|
} else {
|
|
957
1067
|
if (!(value instanceof ObsCollection)) {
|
|
958
1068
|
throw new Error("1st parameter should be an ObsCollection if the 2nd is also given")
|
|
959
1069
|
}
|
|
960
|
-
|
|
961
|
-
|
|
1070
|
+
ref._collection = value
|
|
1071
|
+
ref._idx = index
|
|
962
1072
|
}
|
|
1073
|
+
// @ts-ignore
|
|
1074
|
+
return ref
|
|
963
1075
|
}
|
|
964
1076
|
|
|
965
1077
|
/**
|
|
966
|
-
*
|
|
967
1078
|
* @returns The index for this Store within its parent collection. This will be a `number`
|
|
968
1079
|
* when the parent collection is an array, a `string` when it's an object, or any data type
|
|
969
1080
|
* when it's a `Map`.
|
|
@@ -972,8 +1083,8 @@ export class Store {
|
|
|
972
1083
|
* ```
|
|
973
1084
|
* let store = new Store({x: 123})
|
|
974
1085
|
* let subStore = store.ref('x')
|
|
975
|
-
*
|
|
976
|
-
*
|
|
1086
|
+
* subStore.get() // 123
|
|
1087
|
+
* subStore.index() // 'x'
|
|
977
1088
|
* ```
|
|
978
1089
|
*/
|
|
979
1090
|
index() {
|
|
@@ -985,148 +1096,125 @@ export class Store {
|
|
|
985
1096
|
this._collection._removeObserver(this._idx, scope)
|
|
986
1097
|
}
|
|
987
1098
|
|
|
988
|
-
|
|
989
1099
|
/**
|
|
990
|
-
*
|
|
991
|
-
*
|
|
992
|
-
* @param
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
*
|
|
1100
|
+
* Retrieve the value for store, subscribing the observe scope to changes.
|
|
1101
|
+
*
|
|
1102
|
+
* @param depth Limit the depth of the retrieved data structure to this positive integer.
|
|
1103
|
+
* When `depth` is `1`, only a single level of the value at `path` is unpacked. This
|
|
1104
|
+
* makes no difference for primitive values (like strings), but for objects, maps and
|
|
1105
|
+
* arrays, it means that each *value* in the resulting data structure will be a
|
|
1106
|
+
* reference to the `Store` for that value.
|
|
1107
|
+
*
|
|
1108
|
+
* @returns The resulting value (or `undefined` if the `Store` does not exist).
|
|
1109
|
+
*/
|
|
1110
|
+
get(depth: number = 0): any {
|
|
1111
|
+
let value = this._observe()
|
|
1112
|
+
return value instanceof ObsCollection ? value._getRecursive(depth-1) : value
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
/**
|
|
1116
|
+
* Exactly like {@link Store.get}, except that when executed from an observe scope,
|
|
1117
|
+
* we will not subscribe to changes in the data retrieved data.
|
|
999
1118
|
*/
|
|
1000
|
-
|
|
1001
|
-
|
|
1119
|
+
peek(depth: number = 0): any {
|
|
1120
|
+
|
|
1121
|
+
let savedScope = currentScope
|
|
1122
|
+
currentScope = undefined
|
|
1123
|
+
let result = this.get(depth)
|
|
1124
|
+
currentScope = savedScope
|
|
1125
|
+
return result
|
|
1002
1126
|
}
|
|
1127
|
+
|
|
1003
1128
|
|
|
1004
1129
|
/**
|
|
1005
|
-
* Like {@link Store.get}, but
|
|
1130
|
+
* Like {@link Store.get}, but with return type checking.
|
|
1131
|
+
*
|
|
1132
|
+
* @param expectType A string specifying what type the.get is expected to return. Options are:
|
|
1133
|
+
* "undefined", "null", "boolean", "number", "string", "function", "array", "map"
|
|
1134
|
+
* and "object". If the store holds a different type of value, a `TypeError`
|
|
1135
|
+
* exception is thrown.
|
|
1136
|
+
* @returns
|
|
1006
1137
|
*/
|
|
1007
|
-
|
|
1008
|
-
|
|
1138
|
+
getTyped(expectType: String, depth: number = 0): any {
|
|
1139
|
+
let value = this._observe()
|
|
1140
|
+
let type = (value instanceof ObsCollection) ? value._getType() : (value===null ? "null" : typeof value)
|
|
1141
|
+
if (type !== expectType) throw new TypeError(`Expecting ${expectType} but got ${type}`)
|
|
1142
|
+
return value instanceof ObsCollection ? value._getRecursive(depth-1) : value
|
|
1009
1143
|
}
|
|
1010
1144
|
|
|
1011
1145
|
/**
|
|
1012
1146
|
* @returns Like {@link Store.get}, but throws a `TypeError` if the resulting value is not of type `number`.
|
|
1013
1147
|
* Using this instead of just {@link Store.get} is especially useful from within TypeScript.
|
|
1014
1148
|
*/
|
|
1015
|
-
getNumber(
|
|
1149
|
+
getNumber(): number { return <number>this.getTyped('number') }
|
|
1016
1150
|
/**
|
|
1017
1151
|
* @returns Like {@link Store.get}, but throws a `TypeError` if the resulting value is not of type `string`.
|
|
1018
1152
|
* Using this instead of just {@link Store.get} is especially useful from within TypeScript.
|
|
1019
1153
|
*/
|
|
1020
|
-
getString(
|
|
1154
|
+
getString(): string { return <string>this.getTyped('string') }
|
|
1021
1155
|
/**
|
|
1022
1156
|
* @returns Like {@link Store.get}, but throws a `TypeError` if the resulting value is not of type `boolean`.
|
|
1023
1157
|
* Using this instead of just {@link Store.get} is especially useful from within TypeScript.
|
|
1024
1158
|
*/
|
|
1025
|
-
getBoolean(
|
|
1159
|
+
getBoolean(): boolean { return <boolean>this.getTyped('boolean') }
|
|
1026
1160
|
/**
|
|
1027
1161
|
* @returns Like {@link Store.get}, but throws a `TypeError` if the resulting value is not of type `function`.
|
|
1028
1162
|
* Using this instead of just {@link Store.get} is especially useful from within TypeScript.
|
|
1029
1163
|
*/
|
|
1030
|
-
getFunction(
|
|
1164
|
+
getFunction(): (Function) { return <Function>this.getTyped('function') }
|
|
1031
1165
|
/**
|
|
1032
1166
|
* @returns Like {@link Store.get}, but throws a `TypeError` if the resulting value is not of type `array`.
|
|
1033
1167
|
* Using this instead of just {@link Store.get} is especially useful from within TypeScript.
|
|
1034
1168
|
*/
|
|
1035
|
-
getArray(
|
|
1169
|
+
getArray(depth: number = 0): any[] { return <any[]>this.getTyped('array', depth) }
|
|
1036
1170
|
/**
|
|
1037
1171
|
* @returns Like {@link Store.get}, but throws a `TypeError` if the resulting value is not of type `object`.
|
|
1038
1172
|
* Using this instead of just {@link Store.get} is especially useful from within TypeScript.
|
|
1039
1173
|
*/
|
|
1040
|
-
getObject(
|
|
1174
|
+
getObject(depth: number = 0): object { return <object>this.getTyped('object', depth) }
|
|
1041
1175
|
/**
|
|
1042
1176
|
* @returns Like {@link Store.get}, but throws a `TypeError` if the resulting value is not of type `map`.
|
|
1043
1177
|
* Using this instead of just {@link Store.get} is especially useful from within TypeScript.
|
|
1044
1178
|
*/
|
|
1045
|
-
getMap(
|
|
1179
|
+
getMap(depth: number = 0): Map<any,any> { return <Map<any,any>>this.getTyped('map', depth) }
|
|
1180
|
+
|
|
1046
1181
|
|
|
1047
1182
|
/**
|
|
1048
|
-
* Like {@link Store.get}, but
|
|
1183
|
+
* Like {@link Store.get}, but with a default value (returned when the Store
|
|
1049
1184
|
* contains `undefined`). This default value is also used to determine the expected type,
|
|
1050
1185
|
* and to throw otherwise.
|
|
1051
1186
|
*
|
|
1052
1187
|
* @example
|
|
1053
1188
|
* ```
|
|
1054
|
-
* let store = {x: 42}
|
|
1055
|
-
*
|
|
1056
|
-
*
|
|
1057
|
-
* getOr('hello'
|
|
1189
|
+
* let store = new Store({x: 42})
|
|
1190
|
+
* store('x').getOr(99) // 42
|
|
1191
|
+
* store('y').getOr(99) // 99
|
|
1192
|
+
* store('x').getOr('hello') // throws TypeError (because 42 is not a string)
|
|
1058
1193
|
* ```
|
|
1059
1194
|
*/
|
|
1060
|
-
getOr<T>(defaultValue: T
|
|
1061
|
-
let
|
|
1062
|
-
if (
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
/** Retrieve a value, subscribing to all read `Store` values. This is a more flexible
|
|
1070
|
-
* form of the {@link Store.get} and {@link Store.peek} methods.
|
|
1071
|
-
*
|
|
1072
|
-
* @returns The resulting value, or `undefined` if the `path` does not exist.
|
|
1073
|
-
*/
|
|
1074
|
-
query(opts: {
|
|
1075
|
-
/** The value for this path should be retrieved. Defaults to `[]`, meaning the entire `Store`. */
|
|
1076
|
-
path?: any[],
|
|
1077
|
-
/** A string specifying what type the query is expected to return. Options are:
|
|
1078
|
-
* "undefined", "null", "boolean", "number", "string", "function", "array", "map"
|
|
1079
|
-
* and "object". If the store holds a different type of value, a `TypeError`
|
|
1080
|
-
* exception is thrown. By default (when `type` is `undefined`) no type checking
|
|
1081
|
-
* is done.
|
|
1082
|
-
*/
|
|
1083
|
-
type?: string,
|
|
1084
|
-
/** Limit the depth of the retrieved data structure to this positive integer.
|
|
1085
|
-
* When `depth` is `1`, only a single level of the value at `path` is unpacked. This
|
|
1086
|
-
* makes no difference for primitive values (like strings), but for objects, maps and
|
|
1087
|
-
* arrays, it means that each *value* in the resulting data structure will be a
|
|
1088
|
-
* reference to the `Store` for that value.
|
|
1089
|
-
*/
|
|
1090
|
-
depth?: number,
|
|
1091
|
-
/** Return this value when the `path` does not exist. Defaults to `undefined`. */
|
|
1092
|
-
defaultValue?: any,
|
|
1093
|
-
/** When peek is `undefined` or `false`, the current scope will automatically be
|
|
1094
|
-
* subscribed to changes of any parts of the store being read. When `true`, no
|
|
1095
|
-
* subscribers will be performed.
|
|
1096
|
-
*/
|
|
1097
|
-
peek?: boolean
|
|
1098
|
-
}): any {
|
|
1099
|
-
if (opts.peek && currentScope) {
|
|
1100
|
-
let savedScope = currentScope
|
|
1101
|
-
currentScope = undefined
|
|
1102
|
-
let result = this.query(opts)
|
|
1103
|
-
currentScope = savedScope
|
|
1104
|
-
return result
|
|
1105
|
-
}
|
|
1106
|
-
let store = opts.path && opts.path.length ? this.ref(...opts.path) : this
|
|
1107
|
-
let value = store._observe()
|
|
1108
|
-
|
|
1109
|
-
if (opts.type && (value!==undefined || opts.defaultValue===undefined)) {
|
|
1110
|
-
let type = (value instanceof ObsCollection) ? value._getType() : (value===null ? "null" : typeof value)
|
|
1111
|
-
if (type !== opts.type) throw new TypeError(`Expecting ${opts.type} but got ${type}`)
|
|
1195
|
+
getOr<T>(defaultValue: T): T {
|
|
1196
|
+
let value = this._observe()
|
|
1197
|
+
if (value===undefined) return defaultValue
|
|
1198
|
+
|
|
1199
|
+
let expectType: string = typeof defaultValue
|
|
1200
|
+
if (expectType==='object') {
|
|
1201
|
+
if (defaultValue instanceof Map) expectType = 'map'
|
|
1202
|
+
else if (defaultValue instanceof Array) expectType = 'array'
|
|
1203
|
+
else if (defaultValue === null) expectType = 'null'
|
|
1112
1204
|
}
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
return value===undefined ? opts.defaultValue : value
|
|
1205
|
+
let type = (value instanceof ObsCollection) ? value._getType() : (value===null ? "null" : typeof value)
|
|
1206
|
+
if (type !== expectType) throw new TypeError(`Expecting ${expectType} but got ${type}`)
|
|
1207
|
+
return (value instanceof ObsCollection ? value._getRecursive(-1) : value) as T
|
|
1117
1208
|
}
|
|
1118
1209
|
|
|
1119
1210
|
/**
|
|
1120
|
-
* Checks if the
|
|
1211
|
+
* Checks if the collection held in `Store` is empty, and subscribes the current scope to changes of the emptiness of this collection.
|
|
1121
1212
|
*
|
|
1122
|
-
* @
|
|
1123
|
-
* @returns When the specified collection is not empty `true` is returned. If it is empty or if the value is undefined, `false` is returned.
|
|
1213
|
+
* @returns When the collection is not empty `true` is returned. If it is empty or if the value is undefined, `false` is returned.
|
|
1124
1214
|
* @throws When the value is not a collection and not undefined, an Error will be thrown.
|
|
1125
1215
|
*/
|
|
1126
|
-
isEmpty(
|
|
1127
|
-
let
|
|
1128
|
-
|
|
1129
|
-
let value = store._observe()
|
|
1216
|
+
isEmpty(): boolean {
|
|
1217
|
+
let value = this._observe()
|
|
1130
1218
|
if (value instanceof ObsCollection) {
|
|
1131
1219
|
if (currentScope) {
|
|
1132
1220
|
let observer = new IsEmptyObserver(currentScope, value, false)
|
|
@@ -1142,16 +1230,13 @@ export class Store {
|
|
|
1142
1230
|
}
|
|
1143
1231
|
|
|
1144
1232
|
/**
|
|
1145
|
-
* Returns the number of items in the
|
|
1233
|
+
* Returns the number of items in the collection held in Store, and subscribes the current scope to changes in this count.
|
|
1146
1234
|
*
|
|
1147
|
-
* @param path Any path terms to resolve before retrieving the value.
|
|
1148
1235
|
* @returns The number of items contained in the collection, or 0 if the value is undefined.
|
|
1149
1236
|
* @throws When the value is not a collection and not undefined, an Error will be thrown.
|
|
1150
1237
|
*/
|
|
1151
|
-
count(
|
|
1152
|
-
let
|
|
1153
|
-
|
|
1154
|
-
let value = store._observe()
|
|
1238
|
+
count(): number {
|
|
1239
|
+
let value = this._observe()
|
|
1155
1240
|
if (value instanceof ObsCollection) {
|
|
1156
1241
|
if (currentScope) {
|
|
1157
1242
|
let observer = new IsEmptyObserver(currentScope, value, true)
|
|
@@ -1167,23 +1252,40 @@ export class Store {
|
|
|
1167
1252
|
}
|
|
1168
1253
|
|
|
1169
1254
|
/**
|
|
1170
|
-
* Returns a strings describing the type of the
|
|
1255
|
+
* Returns a strings describing the type of the `Store` value, subscribing to changes of this type.
|
|
1171
1256
|
* Note: this currently also subscribes to changes of primitive values, so changing a value from 3 to 4
|
|
1172
1257
|
* would cause the scope to be rerun. This is not great, and may change in the future. This caveat does
|
|
1173
1258
|
* not apply to changes made *inside* an object, `Array` or `Map`.
|
|
1174
1259
|
*
|
|
1175
|
-
* @param path Any path terms to resolve before retrieving the value.
|
|
1176
1260
|
* @returns Possible options: "undefined", "null", "boolean", "number", "string", "function", "array", "map" or "object".
|
|
1177
1261
|
*/
|
|
1178
|
-
getType(
|
|
1179
|
-
let
|
|
1180
|
-
let value = store._observe()
|
|
1262
|
+
getType(): string {
|
|
1263
|
+
let value = this._observe()
|
|
1181
1264
|
return (value instanceof ObsCollection) ? value._getType() : (value===null ? "null" : typeof value)
|
|
1182
1265
|
}
|
|
1183
1266
|
|
|
1184
1267
|
/**
|
|
1185
|
-
*
|
|
1186
|
-
*
|
|
1268
|
+
* Returns a new `Store` that will always hold either the value of `whenTrue` or the value
|
|
1269
|
+
* of `whenFalse` depending on whether the original `Store` is truthy or not.
|
|
1270
|
+
*
|
|
1271
|
+
* @param whenTrue The value set to the return-`Store` while `this` is truthy. This can be
|
|
1272
|
+
* any type of value. If it's a `Store`, the return-`Store` will reference the same
|
|
1273
|
+
* data (so *no* deep copy will be made).
|
|
1274
|
+
* @param whenFalse Like `whenTrue`, but for falsy values (false, undefined, null, 0, "").
|
|
1275
|
+
* @returns A store holding the result value. The value will keep getting updated while
|
|
1276
|
+
* the observe context from which `if()` was called remains active.
|
|
1277
|
+
*/
|
|
1278
|
+
if(whenTrue: any[], whenFalse?: any[]): Store {
|
|
1279
|
+
const result = new Store()
|
|
1280
|
+
observe(() => {
|
|
1281
|
+
const value = this.get() ? whenTrue : whenFalse
|
|
1282
|
+
result.set(value)
|
|
1283
|
+
})
|
|
1284
|
+
return result
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
/**
|
|
1288
|
+
* Sets the `Store` value to the given argument.
|
|
1187
1289
|
*
|
|
1188
1290
|
* When a `Store` is passed in as the value, its value will be copied (subscribing to changes). In
|
|
1189
1291
|
* case the value is an object, an `Array` or a `Map`, a *reference* to that data structure will
|
|
@@ -1193,69 +1295,110 @@ export class Store {
|
|
|
1193
1295
|
*
|
|
1194
1296
|
* If you intent to make a copy instead of a reference, call {@link Store.get} on the origin `Store`.
|
|
1195
1297
|
*
|
|
1196
|
-
*
|
|
1298
|
+
* @returns The `Store` itself, for chaining other methods.
|
|
1299
|
+
*
|
|
1197
1300
|
* @example
|
|
1198
1301
|
* ```
|
|
1199
1302
|
* let store = new Store() // Value is `undefined`
|
|
1200
1303
|
*
|
|
1201
|
-
* store.set(
|
|
1202
|
-
*
|
|
1304
|
+
* store.set(6)
|
|
1305
|
+
* store.get() // 6
|
|
1203
1306
|
*
|
|
1204
|
-
* store.set(
|
|
1205
|
-
*
|
|
1307
|
+
* store.set({}) // Change value to an empty object
|
|
1308
|
+
* store('a', 'b', 'c').set('d') // Create parent path as objects
|
|
1309
|
+
* store.get() // {x: 6, a: {b: {c: 'd'}}}
|
|
1206
1310
|
*
|
|
1207
1311
|
* store.set(42) // Overwrites all of the above
|
|
1208
|
-
*
|
|
1312
|
+
* store.get() // 42
|
|
1209
1313
|
*
|
|
1210
|
-
* store
|
|
1314
|
+
* store('x').set(6) // Throw Error (42 is not a collection)
|
|
1211
1315
|
* ```
|
|
1212
1316
|
*/
|
|
1213
|
-
set(
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
store._collection._setIndex(store._idx, newValue, true)
|
|
1317
|
+
set(newValue: any): Store {
|
|
1318
|
+
this._materialize(true)
|
|
1319
|
+
this._collection._setIndex(this._idx, newValue, true)
|
|
1217
1320
|
runImmediateQueue()
|
|
1321
|
+
return this
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
/** @internal */
|
|
1325
|
+
_materialize(forWriting: boolean): boolean {
|
|
1326
|
+
if (!this._virtual) return true
|
|
1327
|
+
let collection = this._collection
|
|
1328
|
+
let idx = this._idx
|
|
1329
|
+
for(let i=0; i<this._virtual.length; i++) {
|
|
1330
|
+
if (!forWriting && currentScope) {
|
|
1331
|
+
if (collection._addObserver(idx, currentScope)) {
|
|
1332
|
+
currentScope._cleaners.push(this)
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
let value = collection.rawGet(idx)
|
|
1336
|
+
if (!(value instanceof ObsCollection)) {
|
|
1337
|
+
// Throw an error if trying to index a primitive type
|
|
1338
|
+
if (value!==undefined) throw new Error(`While resolving ${JSON.stringify(this._virtual)}, found ${JSON.stringify(value)} at index ${i} instead of a collection.`)
|
|
1339
|
+
// For reads, we'll just give up. We might reactively get another shot at this.
|
|
1340
|
+
if (!forWriting) return false
|
|
1341
|
+
// For writes, create a new collection.
|
|
1342
|
+
value = new ObsObject()
|
|
1343
|
+
collection.rawSet(idx, value)
|
|
1344
|
+
collection.emitChange(idx, value, undefined)
|
|
1345
|
+
}
|
|
1346
|
+
collection = value
|
|
1347
|
+
const prop = this._virtual[i]
|
|
1348
|
+
idx = collection._normalizeIndex(prop)
|
|
1349
|
+
}
|
|
1350
|
+
this._collection = collection
|
|
1351
|
+
this._idx = idx
|
|
1352
|
+
delete this._virtual
|
|
1353
|
+
return true
|
|
1218
1354
|
}
|
|
1219
1355
|
|
|
1220
1356
|
/**
|
|
1221
1357
|
* Sets the `Store` to the given `mergeValue`, but without deleting any pre-existing
|
|
1222
1358
|
* items when a collection overwrites a similarly typed collection. This results in
|
|
1223
1359
|
* a deep merge.
|
|
1360
|
+
*
|
|
1361
|
+
* @returns The `Store` itself, for chaining other methods.
|
|
1224
1362
|
*
|
|
1225
1363
|
* @example
|
|
1226
1364
|
* ```
|
|
1227
1365
|
* let store = new Store({a: {x: 1}})
|
|
1228
1366
|
* store.merge({a: {y: 2}, b: 3})
|
|
1229
|
-
*
|
|
1367
|
+
* store.get() // {a: {x: 1, y: 2}, b: 3}
|
|
1230
1368
|
* ```
|
|
1231
1369
|
*/
|
|
1232
|
-
merge(
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
store._collection._setIndex(store._idx, mergeValue, false)
|
|
1370
|
+
merge(mergeValue: any): Store {
|
|
1371
|
+
this._materialize(true)
|
|
1372
|
+
this._collection._setIndex(this._idx, mergeValue, false)
|
|
1236
1373
|
runImmediateQueue()
|
|
1374
|
+
return this
|
|
1237
1375
|
}
|
|
1238
1376
|
|
|
1239
1377
|
/**
|
|
1240
1378
|
* 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)
|
|
1379
|
+
*
|
|
1380
|
+
* @returns The `Store` itself, for chaining other methods.
|
|
1241
1381
|
*
|
|
1242
1382
|
* @example
|
|
1243
1383
|
* ```
|
|
1244
1384
|
* let store = new Store({a: 1, b: 2})
|
|
1245
|
-
* store
|
|
1246
|
-
*
|
|
1385
|
+
* store('a').delete()
|
|
1386
|
+
* store.get() // {b: 2}
|
|
1247
1387
|
*
|
|
1248
1388
|
* store = new Store(['a','b','c'])
|
|
1249
|
-
* store.delete(
|
|
1250
|
-
*
|
|
1251
|
-
* store.delete(
|
|
1252
|
-
*
|
|
1389
|
+
* store(1).delete()
|
|
1390
|
+
* store.get() // ['a', undefined, 'c']
|
|
1391
|
+
* store(2).delete()
|
|
1392
|
+
* store.get() // ['a']
|
|
1393
|
+
* store.delete()
|
|
1394
|
+
* store.get() // undefined
|
|
1253
1395
|
* ```
|
|
1254
1396
|
*/
|
|
1255
|
-
delete(
|
|
1256
|
-
|
|
1257
|
-
|
|
1397
|
+
delete(): Store {
|
|
1398
|
+
this._materialize(true)
|
|
1399
|
+
this._collection._setIndex(this._idx, undefined, true)
|
|
1258
1400
|
runImmediateQueue()
|
|
1401
|
+
return this
|
|
1259
1402
|
}
|
|
1260
1403
|
|
|
1261
1404
|
/**
|
|
@@ -1263,28 +1406,30 @@ export class Store {
|
|
|
1263
1406
|
* If that store path is `undefined`, an Array is created first.
|
|
1264
1407
|
* The last argument is the value to be added, any earlier arguments indicate the path.
|
|
1265
1408
|
*
|
|
1409
|
+
* @returns The index at which the item was appended.
|
|
1410
|
+
* @throws TypeError when the store contains a primitive data type.
|
|
1411
|
+
*
|
|
1266
1412
|
* @example
|
|
1267
1413
|
* ```
|
|
1268
1414
|
* let store = new Store()
|
|
1269
1415
|
* store.push(3) // Creates the array
|
|
1270
1416
|
* store.push(6)
|
|
1271
|
-
*
|
|
1417
|
+
* store.get() // [3,6]
|
|
1272
1418
|
*
|
|
1273
1419
|
* store = new Store({myArray: [1,2]})
|
|
1274
|
-
* store
|
|
1275
|
-
*
|
|
1420
|
+
* store('myArray').push(3)
|
|
1421
|
+
* store.get() // {myArray: [1,2,3]}
|
|
1276
1422
|
* ```
|
|
1277
1423
|
*/
|
|
1278
|
-
push(
|
|
1279
|
-
|
|
1280
|
-
let store = this.makeRef(...pathAndValue)
|
|
1424
|
+
push(newValue: any): number {
|
|
1425
|
+
this._materialize(true)
|
|
1281
1426
|
|
|
1282
|
-
let obsArray =
|
|
1427
|
+
let obsArray = this._collection.rawGet(this._idx)
|
|
1283
1428
|
if (obsArray===undefined) {
|
|
1284
1429
|
obsArray = new ObsArray()
|
|
1285
|
-
|
|
1430
|
+
this._collection._setIndex(this._idx, obsArray, true)
|
|
1286
1431
|
} else if (!(obsArray instanceof ObsArray)) {
|
|
1287
|
-
throw new
|
|
1432
|
+
throw new TypeError(`push() is only allowed for an array or undefined (which would become an array)`)
|
|
1288
1433
|
}
|
|
1289
1434
|
|
|
1290
1435
|
let newData = valueToData(newValue)
|
|
@@ -1299,74 +1444,16 @@ export class Store {
|
|
|
1299
1444
|
* {@link Store.peek} the current value, pass it through `func`, and {@link Store.set} the resulting
|
|
1300
1445
|
* value.
|
|
1301
1446
|
* @param func The function transforming the value.
|
|
1447
|
+
* @returns The `Store` itself, for chaining other methods.
|
|
1302
1448
|
*/
|
|
1303
|
-
modify(func: (value: any) => any):
|
|
1304
|
-
this.set(func(this.
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
/**
|
|
1308
|
-
* Return a `Store` deeper within the tree by resolving the given `path`,
|
|
1309
|
-
* subscribing to every level.
|
|
1310
|
-
* In case `undefined` is encountered while resolving the path, a newly
|
|
1311
|
-
* created `Store` containing `undefined` is returned. In that case, the
|
|
1312
|
-
* `Store`'s {@link Store.isDetached} method will return `true`.
|
|
1313
|
-
* In case something other than a collection is encountered, an error is thrown.
|
|
1314
|
-
*/
|
|
1315
|
-
ref(...path: any[]): Store {
|
|
1316
|
-
let store: Store = this
|
|
1317
|
-
|
|
1318
|
-
for(let i=0; i<path.length; i++) {
|
|
1319
|
-
let value = store._observe()
|
|
1320
|
-
if (value instanceof ObsCollection) {
|
|
1321
|
-
store = new Store(value, value._normalizeIndex(path[i]))
|
|
1322
|
-
} else {
|
|
1323
|
-
if (value!==undefined) throw new Error(`Value ${JSON.stringify(value)} is not a collection (nor undefined) in step ${i} of $(${JSON.stringify(path)})`)
|
|
1324
|
-
return new DetachedStore()
|
|
1325
|
-
}
|
|
1326
|
-
}
|
|
1327
|
-
|
|
1328
|
-
return store
|
|
1329
|
-
}
|
|
1330
|
-
|
|
1331
|
-
/**
|
|
1332
|
-
* Similar to `ref()`, but instead of returning `undefined`, new objects are created when
|
|
1333
|
-
* a path does not exist yet. An error is still thrown when the path tries to index an invalid
|
|
1334
|
-
* type.
|
|
1335
|
-
* Unlike `ref`, `makeRef` does *not* subscribe to the path levels, as it is intended to be
|
|
1336
|
-
* a write-only operation.
|
|
1337
|
-
*
|
|
1338
|
-
* @example
|
|
1339
|
-
* ```
|
|
1340
|
-
* let store = new Store() // Value is `undefined`
|
|
1341
|
-
*
|
|
1342
|
-
* let ref = store.makeRef('a', 'b', 'c')
|
|
1343
|
-
* assert(store.get() == {a: {b: {}}}
|
|
1344
|
-
*
|
|
1345
|
-
* ref.set(42)
|
|
1346
|
-
* assert(store.get() == {a: {b: {c: 42}}}
|
|
1347
|
-
*
|
|
1348
|
-
* ref.makeRef('d') // Throw Error (42 is not a collection)
|
|
1349
|
-
* ```
|
|
1350
|
-
*/
|
|
1351
|
-
makeRef(...path: any[]): Store {
|
|
1352
|
-
let store: Store = this
|
|
1353
|
-
|
|
1354
|
-
for(let i=0; i<path.length; i++) {
|
|
1355
|
-
let value = store._collection.rawGet(store._idx)
|
|
1356
|
-
if (!(value instanceof ObsCollection)) {
|
|
1357
|
-
if (value!==undefined) throw new Error(`Value ${JSON.stringify(value)} is not a collection (nor undefined) in step ${i} of $(${JSON.stringify(path)})`)
|
|
1358
|
-
value = new ObsObject()
|
|
1359
|
-
store._collection.rawSet(store._idx, value)
|
|
1360
|
-
store._collection.emitChange(store._idx, value, undefined)
|
|
1361
|
-
}
|
|
1362
|
-
store = new Store(value, value._normalizeIndex(path[i]))
|
|
1363
|
-
}
|
|
1364
|
-
runImmediateQueue()
|
|
1365
|
-
return store
|
|
1449
|
+
modify(func: (value: any) => any): Store {
|
|
1450
|
+
this.set(func(this.peek()))
|
|
1451
|
+
return this
|
|
1366
1452
|
}
|
|
1367
1453
|
|
|
1368
1454
|
/** @internal */
|
|
1369
1455
|
_observe() {
|
|
1456
|
+
if (!this._materialize(false)) return undefined
|
|
1370
1457
|
if (currentScope) {
|
|
1371
1458
|
if (this._collection._addObserver(this._idx, currentScope)) {
|
|
1372
1459
|
currentScope._cleaners.push(this)
|
|
@@ -1379,25 +1466,19 @@ export class Store {
|
|
|
1379
1466
|
* Iterate the specified collection (Array, Map or object), running the given code block for each item.
|
|
1380
1467
|
* When items are added to the collection at some later point, the code block will be ran for them as well.
|
|
1381
1468
|
* When an item is removed, the {@link Store.clean} handlers left by its code block are executed.
|
|
1382
|
-
*
|
|
1383
|
-
*
|
|
1384
|
-
*
|
|
1385
|
-
*
|
|
1469
|
+
*
|
|
1470
|
+
* @param renderer The function to be called for each item. It receives the item's `Store` object as its only argument.
|
|
1471
|
+
* @param makeSortKey An optional function that, given an items `Store` object, returns a value to be sorted on.
|
|
1472
|
+
* This value can be a number, a string, or an array containing a combination of both. When undefined is returned,
|
|
1473
|
+
* the item is *not* rendered. If `makeSortKey` is not specified, the output will be sorted by `index()`.
|
|
1386
1474
|
*/
|
|
1387
|
-
onEach(
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
if (renderer!=null) makeSortKey = renderer
|
|
1392
|
-
renderer = pathAndFuncs.pop()
|
|
1475
|
+
onEach(renderer: (store: Store) => void, makeSortKey: (store: Store) => any = defaultMakeSortKey): void {
|
|
1476
|
+
if (!currentScope) { // Do this in a new top-level scope
|
|
1477
|
+
_mount(undefined, () => this.onEach(renderer, makeSortKey), SimpleScope)
|
|
1478
|
+
return
|
|
1393
1479
|
}
|
|
1394
|
-
if (typeof renderer !== 'function') throw new Error(`onEach() expects a render function as its last argument but got ${JSON.stringify(renderer)}`)
|
|
1395
|
-
|
|
1396
|
-
if (!currentScope) throw new ScopeError(false)
|
|
1397
1480
|
|
|
1398
|
-
let
|
|
1399
|
-
|
|
1400
|
-
let val = store._observe()
|
|
1481
|
+
let val = this._observe()
|
|
1401
1482
|
if (val instanceof ObsCollection) {
|
|
1402
1483
|
// Subscribe to changes using the specialized OnEachScope
|
|
1403
1484
|
let onEachScope = new OnEachScope(currentScope._parentElement, currentScope._lastChild || currentScope._precedingSibling, currentScope._queueOrder+1, val, renderer, makeSortKey)
|
|
@@ -1412,6 +1493,31 @@ export class Store {
|
|
|
1412
1493
|
}
|
|
1413
1494
|
}
|
|
1414
1495
|
|
|
1496
|
+
/**
|
|
1497
|
+
* Derive a new `Store` from this `Store`, by reactively passing its value
|
|
1498
|
+
* through the specified function.
|
|
1499
|
+
* @param func Your function. It should accept a the input store's value, and return
|
|
1500
|
+
* a result to be reactively set to the output store.
|
|
1501
|
+
* @returns The output `Store`.
|
|
1502
|
+
* @example
|
|
1503
|
+
* ```javascript
|
|
1504
|
+
* const store = new Store(21)
|
|
1505
|
+
* const double = store.derive(v => v*2)
|
|
1506
|
+
* double.get() // 42
|
|
1507
|
+
*
|
|
1508
|
+
* store.set(100)
|
|
1509
|
+
* runQueue() // Or after a setTimeout 0, due to batching
|
|
1510
|
+
* double.get() // 200
|
|
1511
|
+
* ```
|
|
1512
|
+
*/
|
|
1513
|
+
derive(func: (value: any) => any): Store {
|
|
1514
|
+
let out = new Store()
|
|
1515
|
+
observe(() => {
|
|
1516
|
+
out.set(func(this.get()))
|
|
1517
|
+
})
|
|
1518
|
+
return out
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1415
1521
|
/**
|
|
1416
1522
|
* Applies a filter/map function on each item within the `Store`'s collection,
|
|
1417
1523
|
* and reactively manages the returned `Map` `Store` to hold any results.
|
|
@@ -1419,25 +1525,30 @@ export class Store {
|
|
|
1419
1525
|
* @param func - Function that transform the given store into an output value or
|
|
1420
1526
|
* `undefined` in case this value should be skipped:
|
|
1421
1527
|
*
|
|
1422
|
-
* @returns - A map `Store` with the values returned by `func` and the
|
|
1423
|
-
* keys from the original map, array or object `Store`.
|
|
1528
|
+
* @returns - A array/map/object `Store` with the values returned by `func` and the
|
|
1529
|
+
* corresponding keys from the original map, array or object `Store`.
|
|
1424
1530
|
*
|
|
1425
1531
|
* When items disappear from the `Store` or are changed in a way that `func` depends
|
|
1426
1532
|
* upon, the resulting items are removed from the output `Store` as well. When multiple
|
|
1427
1533
|
* input items produce the same output keys, this may lead to unexpected results.
|
|
1428
1534
|
*/
|
|
1429
1535
|
map(func: (store: Store) => any): Store {
|
|
1430
|
-
let out = new Store(
|
|
1431
|
-
|
|
1432
|
-
let
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1536
|
+
let out = new Store()
|
|
1537
|
+
observe(() => {
|
|
1538
|
+
let t = this.getType()
|
|
1539
|
+
out.set(t==='array' ? [] : (t==='object' ? {} : new Map()))
|
|
1540
|
+
this.onEach((item: Store) => {
|
|
1541
|
+
let value = func(item)
|
|
1542
|
+
if (value !== undefined) {
|
|
1543
|
+
let key = item.index()
|
|
1544
|
+
const ref = out(key)
|
|
1545
|
+
ref.set(value)
|
|
1546
|
+
clean(() => {
|
|
1547
|
+
ref.delete()
|
|
1548
|
+
})
|
|
1549
|
+
}
|
|
1550
|
+
})
|
|
1551
|
+
})
|
|
1441
1552
|
return out
|
|
1442
1553
|
}
|
|
1443
1554
|
|
|
@@ -1461,24 +1572,26 @@ export class Store {
|
|
|
1461
1572
|
let out = new Store(new Map())
|
|
1462
1573
|
this.onEach((item: Store) => {
|
|
1463
1574
|
let result = func(item)
|
|
1464
|
-
let
|
|
1575
|
+
let refs: Array<Store> = []
|
|
1465
1576
|
if (result.constructor === Object) {
|
|
1466
1577
|
for(let key in result) {
|
|
1467
|
-
out
|
|
1578
|
+
const ref = out(key)
|
|
1579
|
+
ref.set(result[key])
|
|
1580
|
+
refs.push(ref)
|
|
1468
1581
|
}
|
|
1469
|
-
keys = Object.keys(result)
|
|
1470
1582
|
} else if (result instanceof Map) {
|
|
1471
1583
|
result.forEach((value: any, key: any) => {
|
|
1472
|
-
out
|
|
1584
|
+
const ref = out(key)
|
|
1585
|
+
ref.set(value)
|
|
1586
|
+
refs.push(ref)
|
|
1473
1587
|
})
|
|
1474
|
-
keys = [...result.keys()]
|
|
1475
1588
|
} else {
|
|
1476
1589
|
return
|
|
1477
1590
|
}
|
|
1478
|
-
if (
|
|
1591
|
+
if (refs.length) {
|
|
1479
1592
|
clean(() => {
|
|
1480
|
-
for(let
|
|
1481
|
-
|
|
1593
|
+
for(let ref of refs) {
|
|
1594
|
+
ref.delete()
|
|
1482
1595
|
}
|
|
1483
1596
|
})
|
|
1484
1597
|
}
|
|
@@ -1486,42 +1599,31 @@ export class Store {
|
|
|
1486
1599
|
return out
|
|
1487
1600
|
}
|
|
1488
1601
|
|
|
1489
|
-
/**
|
|
1490
|
-
* @returns Returns `true` when the `Store` was created by {@link Store.ref}ing a path that
|
|
1491
|
-
* does not exist.
|
|
1492
|
-
*/
|
|
1493
|
-
isDetached() { return false }
|
|
1494
|
-
|
|
1495
1602
|
/**
|
|
1496
1603
|
* Dump a live view of the `Store` tree as HTML text, `ul` and `li` nodes at
|
|
1497
1604
|
* the current mount position. Meant for debugging purposes.
|
|
1605
|
+
* @returns The `Store` itself, for chaining other methods.
|
|
1498
1606
|
*/
|
|
1499
|
-
dump() {
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
}
|
|
1607
|
+
dump(): Store {
|
|
1608
|
+
let type = this.getType()
|
|
1609
|
+
if (type === 'array' || type === 'object' || type === 'map') {
|
|
1610
|
+
$({text: `<${type}>`})
|
|
1611
|
+
$('ul', () => {
|
|
1612
|
+
this.onEach((sub: Store) => {
|
|
1613
|
+
$('li:'+JSON.stringify(sub.index())+": ", () => {
|
|
1614
|
+
sub.dump()
|
|
1615
|
+
})
|
|
1616
|
+
})
|
|
1617
|
+
})
|
|
1618
|
+
} else {
|
|
1619
|
+
$({text: JSON.stringify(this.get())})
|
|
1620
|
+
}
|
|
1621
|
+
return this
|
|
1515
1622
|
}
|
|
1516
1623
|
}
|
|
1517
1624
|
|
|
1518
|
-
class DetachedStore extends Store {
|
|
1519
|
-
isDetached() { return true }
|
|
1520
|
-
}
|
|
1521
|
-
|
|
1522
1625
|
|
|
1523
1626
|
|
|
1524
|
-
let onCreateEnabled = false
|
|
1525
1627
|
let onDestroyMap: WeakMap<Node, string | Function | true> = new WeakMap()
|
|
1526
1628
|
|
|
1527
1629
|
function destroyWithClass(element: Element, cls: string) {
|
|
@@ -1530,92 +1632,23 @@ function destroyWithClass(element: Element, cls: string) {
|
|
|
1530
1632
|
}
|
|
1531
1633
|
|
|
1532
1634
|
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
* @param rest - The other arguments are flexible and interpreted based on their types:
|
|
1537
|
-
* - `string`: Used as textContent for the element.
|
|
1538
|
-
* - `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.
|
|
1539
|
-
* - `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.
|
|
1540
|
-
* - `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.
|
|
1541
|
-
* @example
|
|
1542
|
-
* node('aside.editorial', 'Yada yada yada....', () => {
|
|
1543
|
-
* node('a', {href: '/bio'}, () => {
|
|
1544
|
-
* node('img.author', {src: '/me.jpg', alt: 'The author'})
|
|
1545
|
-
* })
|
|
1546
|
-
* })
|
|
1547
|
-
*/
|
|
1548
|
-
export function node(tag: string|Element = "", ...rest: any[]) {
|
|
1549
|
-
if (!currentScope) throw new ScopeError(true)
|
|
1550
|
-
|
|
1551
|
-
let el
|
|
1552
|
-
if (tag instanceof Element) {
|
|
1553
|
-
el = tag
|
|
1635
|
+
function addLeafNode(deepEl: Element, node: Node) {
|
|
1636
|
+
if (deepEl === (currentScope as Scope)._parentElement) {
|
|
1637
|
+
currentScope!._addNode(node)
|
|
1554
1638
|
} else {
|
|
1555
|
-
|
|
1556
|
-
let classes
|
|
1557
|
-
if (pos>=0) {
|
|
1558
|
-
classes = tag.substr(pos+1)
|
|
1559
|
-
tag = tag.substr(0, pos)
|
|
1560
|
-
}
|
|
1561
|
-
el = document.createElement(tag || 'div')
|
|
1562
|
-
if (classes) {
|
|
1563
|
-
// @ts-ignore (replaceAll is polyfilled)
|
|
1564
|
-
el.className = classes.replaceAll('.', ' ')
|
|
1565
|
-
}
|
|
1566
|
-
}
|
|
1567
|
-
|
|
1568
|
-
currentScope._addNode(el)
|
|
1569
|
-
|
|
1570
|
-
for(let item of rest) {
|
|
1571
|
-
let type = typeof item
|
|
1572
|
-
if (type === 'function') {
|
|
1573
|
-
let scope = new SimpleScope(el, undefined, currentScope._queueOrder+1, item)
|
|
1574
|
-
if (onCreateEnabled) {
|
|
1575
|
-
onCreateEnabled = false
|
|
1576
|
-
scope._update()
|
|
1577
|
-
onCreateEnabled = true
|
|
1578
|
-
} else {
|
|
1579
|
-
scope._update()
|
|
1580
|
-
}
|
|
1581
|
-
|
|
1582
|
-
// Add it to our list of cleaners. Even if `scope` currently has
|
|
1583
|
-
// no cleaners, it may get them in a future refresh.
|
|
1584
|
-
currentScope._cleaners.push(scope)
|
|
1585
|
-
} else if (type === 'string' || type === 'number') {
|
|
1586
|
-
el.textContent = item
|
|
1587
|
-
} else if (type === 'object' && item && item.constructor === Object) {
|
|
1588
|
-
for(let k in item) {
|
|
1589
|
-
applyProp(el, k, item[k])
|
|
1590
|
-
}
|
|
1591
|
-
} else if (item instanceof Store) {
|
|
1592
|
-
bindInput(<HTMLInputElement>el, item)
|
|
1593
|
-
} else if (item != null) {
|
|
1594
|
-
throw new Error(`Unexpected argument ${JSON.stringify(item)}`)
|
|
1595
|
-
}
|
|
1639
|
+
deepEl.appendChild(node)
|
|
1596
1640
|
}
|
|
1597
1641
|
}
|
|
1598
1642
|
|
|
1599
1643
|
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
*/
|
|
1605
|
-
export function html(html: string) {
|
|
1606
|
-
if (!currentScope || !currentScope._parentElement) throw new ScopeError(true)
|
|
1607
|
-
let tmpParent = document.createElement(currentScope._parentElement.tagName)
|
|
1608
|
-
tmpParent.innerHTML = ''+html
|
|
1609
|
-
while(tmpParent.firstChild) {
|
|
1610
|
-
currentScope._addNode(tmpParent.firstChild)
|
|
1611
|
-
}
|
|
1612
|
-
}
|
|
1613
|
-
|
|
1614
|
-
function bindInput(el: HTMLInputElement, store: Store) {
|
|
1644
|
+
function applyBinding(_el: Element, _key: string, store: Store) {
|
|
1645
|
+
if (store==null) return
|
|
1646
|
+
if (!(store instanceof Store)) throw new Error(`Unexpect bind-argument: ${JSON.parse(store)}`)
|
|
1647
|
+
const el = _el as HTMLInputElement
|
|
1615
1648
|
let onStoreChange: (value: any) => void
|
|
1616
1649
|
let onInputChange: () => void
|
|
1617
1650
|
let type = el.getAttribute('type')
|
|
1618
|
-
let value = store.
|
|
1651
|
+
let value = store.peek()
|
|
1619
1652
|
if (type === 'checkbox') {
|
|
1620
1653
|
if (value === undefined) store.set(el.checked)
|
|
1621
1654
|
onStoreChange = value => el.checked = value
|
|
@@ -1640,98 +1673,254 @@ function bindInput(el: HTMLInputElement, store: Store) {
|
|
|
1640
1673
|
clean(() => {
|
|
1641
1674
|
el.removeEventListener('input', onInputChange)
|
|
1642
1675
|
})
|
|
1643
|
-
|
|
1644
1676
|
}
|
|
1645
1677
|
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1678
|
+
|
|
1679
|
+
const SPECIAL_PROPS: {[key: string]: (el: Element, value: any) => void} = {
|
|
1680
|
+
create: function(el: Element, value: any) {
|
|
1681
|
+
if (!showCreateTransitions) return
|
|
1682
|
+
if (typeof value === 'function') {
|
|
1683
|
+
value(el)
|
|
1684
|
+
} else {
|
|
1685
|
+
el.classList.add(value);
|
|
1686
|
+
(async function(){
|
|
1687
|
+
await DOM_READ_PHASE;
|
|
1688
|
+
(el as HTMLElement).offsetHeight;
|
|
1689
|
+
await DOM_WRITE_PHASE;
|
|
1690
|
+
el.classList.remove(value)
|
|
1691
|
+
})()
|
|
1692
|
+
}
|
|
1693
|
+
},
|
|
1694
|
+
destroy: function(deepEl: Element, value: any) {
|
|
1695
|
+
onDestroyMap.set(deepEl, value)
|
|
1696
|
+
},
|
|
1697
|
+
html: function(deepEl: Element, value: any) {
|
|
1698
|
+
if (!value) return
|
|
1699
|
+
let tmpParent = document.createElement(deepEl.tagName)
|
|
1700
|
+
tmpParent.innerHTML = ''+value
|
|
1701
|
+
while(tmpParent.firstChild) addLeafNode(deepEl, tmpParent.firstChild)
|
|
1702
|
+
},
|
|
1703
|
+
text: function(deepEl: Element, value: any) {
|
|
1704
|
+
if (value!=null) addLeafNode(deepEl, document.createTextNode(value))
|
|
1705
|
+
},
|
|
1706
|
+
element: function(deepEl: Element, value: any) {
|
|
1707
|
+
if (value==null) return
|
|
1708
|
+
if (!(value instanceof Node)) throw new Error(`Unexpect element-argument: ${JSON.parse(value)}`)
|
|
1709
|
+
addLeafNode(deepEl, value)
|
|
1710
|
+
},
|
|
1653
1711
|
}
|
|
1654
1712
|
|
|
1655
1713
|
|
|
1714
|
+
|
|
1656
1715
|
/**
|
|
1657
|
-
*
|
|
1658
|
-
*
|
|
1659
|
-
*
|
|
1660
|
-
*
|
|
1661
|
-
*
|
|
1662
|
-
*
|
|
1663
|
-
*
|
|
1664
|
-
*
|
|
1665
|
-
*
|
|
1666
|
-
*
|
|
1667
|
-
*
|
|
1668
|
-
*
|
|
1669
|
-
*
|
|
1670
|
-
*
|
|
1671
|
-
*
|
|
1672
|
-
*
|
|
1673
|
-
*
|
|
1674
|
-
*
|
|
1675
|
-
*
|
|
1676
|
-
*
|
|
1677
|
-
*
|
|
1678
|
-
*
|
|
1679
|
-
*
|
|
1680
|
-
*
|
|
1681
|
-
*
|
|
1682
|
-
*
|
|
1683
|
-
*
|
|
1684
|
-
*
|
|
1685
|
-
*
|
|
1686
|
-
*
|
|
1687
|
-
*
|
|
1688
|
-
*
|
|
1689
|
-
*
|
|
1690
|
-
*
|
|
1691
|
-
*
|
|
1692
|
-
*
|
|
1716
|
+
* Modifies the *parent* DOM element in the current reactive scope, or adds
|
|
1717
|
+
* new DOM elements to it.
|
|
1718
|
+
*
|
|
1719
|
+
* @param args - Arguments that define how to modify/create elements.
|
|
1720
|
+
*
|
|
1721
|
+
* ### String arguments
|
|
1722
|
+
* Create new elements with optional classes and text content:
|
|
1723
|
+
* ```js
|
|
1724
|
+
* $('div.myClass') // <div class="myClass"></div>
|
|
1725
|
+
* $('span.c1.c2:Hello') // <span class="c1 c2">Hello</span>
|
|
1726
|
+
* $('p:Some text') // <p>Some text</p>
|
|
1727
|
+
* $('.my-thing') // <div class="my-thing"></div>
|
|
1728
|
+
* $('div', 'span', 'p.cls') // <div><span<p class="cls"></p></span></div>
|
|
1729
|
+
* $(':Just some text!') // Just some text! (No new element, just a text node)
|
|
1730
|
+
* ```
|
|
1731
|
+
*
|
|
1732
|
+
* ### Object arguments
|
|
1733
|
+
* Set properties, attributes, events and special features:
|
|
1734
|
+
* ```js
|
|
1735
|
+
* // Classes (dot prefix)
|
|
1736
|
+
* $('div', {'.active': true}) // Add class
|
|
1737
|
+
* $('div', {'.hidden': false}) // Remove (or don't add) class
|
|
1738
|
+
* $('div', {'.selected': myStore}) // Reactively add/remove class
|
|
1739
|
+
*
|
|
1740
|
+
* // Styles (dollar prefixed and camel-cased CSS properties)
|
|
1741
|
+
* $('div', {$color: 'red'}) // style.color = 'red'
|
|
1742
|
+
* $('div', {$marginTop: '10px'}) // style.marginTop = '10px'
|
|
1743
|
+
* $('div', {$color: myColorStore}) // Reactively change color
|
|
1744
|
+
*
|
|
1745
|
+
* // Events (function values)
|
|
1746
|
+
* $('button', {click: () => alert()}) // Add click handler
|
|
1747
|
+
*
|
|
1748
|
+
* // Properties (boolean values, `selectedIndex`, `value`)
|
|
1749
|
+
* $('input', {disabled: true}) // el.disabled = true
|
|
1750
|
+
* $('input', {value: 'test'}) // el.value = 'test'
|
|
1751
|
+
* $('select', {selectedIndex: 2}) // el.selectedIndex = 2
|
|
1752
|
+
*
|
|
1753
|
+
* // Transitions
|
|
1754
|
+
* $('div', {create: 'fade-in'}) // Add class on create
|
|
1755
|
+
* $('div', {create: el => {...}}) // Run function on create
|
|
1756
|
+
* $('div', {destroy: 'fade-out'}) // Add class before remove
|
|
1757
|
+
* $('div', {destroy: el => {...}}) // Run function before remove
|
|
1758
|
+
*
|
|
1759
|
+
* // Content
|
|
1760
|
+
* $('div', {html: '<b>Bold</b>'}) // Set innerHTML
|
|
1761
|
+
* $('div', {text: 'Plain text'}) // Add text node
|
|
1762
|
+
* const myElement = document.createElement('video')
|
|
1763
|
+
* $('div', {element: myElement}) // Add existing DOM element
|
|
1693
1764
|
*
|
|
1694
|
-
*
|
|
1765
|
+
* // Regular attributes (everything else)
|
|
1766
|
+
* $('div', {title: 'Info'}) // el.setAttribute('title', 'info')
|
|
1767
|
+
* ```
|
|
1768
|
+
*
|
|
1769
|
+
* When a `Store` is passed as a value, a seperate observe-scope will
|
|
1770
|
+
* be created for it, such that when the `Store` changes, only *that*
|
|
1771
|
+
* UI property will need to be updated.
|
|
1772
|
+
* So in the following example, when `colorStore` changes, only the
|
|
1773
|
+
* `color` CSS property will be updated.
|
|
1774
|
+
* ```js
|
|
1775
|
+
* $('div', {
|
|
1776
|
+
* '.active': activeStore, // Reactive class
|
|
1777
|
+
* $color: colorStore, // Reactive style
|
|
1778
|
+
* text: textStore // Reactive text
|
|
1779
|
+
* })
|
|
1780
|
+
* ```
|
|
1781
|
+
*
|
|
1782
|
+
* ### Two-way input binding
|
|
1783
|
+
* Set the initial value of an <input> <textarea> or <select> to that
|
|
1784
|
+
* of a `Store`, and then start reflecting user changes to the former
|
|
1785
|
+
* in the latter.
|
|
1786
|
+
* ```js
|
|
1787
|
+
* $('input', {bind: myStore}) // Binds input.value
|
|
1695
1788
|
* ```
|
|
1696
|
-
*
|
|
1697
|
-
*
|
|
1698
|
-
*
|
|
1699
|
-
*
|
|
1700
|
-
*
|
|
1701
|
-
*
|
|
1702
|
-
*
|
|
1703
|
-
*
|
|
1704
|
-
* prop({
|
|
1705
|
-
* class: 'my-class',
|
|
1706
|
-
* text: 'Here is something to read...',
|
|
1707
|
-
* style: {
|
|
1708
|
-
* backgroundColor: 'red',
|
|
1709
|
-
* fontWeight: 'bold',
|
|
1710
|
-
* },
|
|
1711
|
-
* create: aberdeen.fadeIn,
|
|
1712
|
-
* destroy: 'my-fade-out-class',
|
|
1713
|
-
* click: myClickHandler,
|
|
1714
|
-
* })
|
|
1789
|
+
* This is a special case, as changes to the `Store` will *not* be
|
|
1790
|
+
* reflected in the UI.
|
|
1791
|
+
*
|
|
1792
|
+
* ### Function arguments
|
|
1793
|
+
* Create child scopes that re-run on observed `Store` changes:
|
|
1794
|
+
* ```js
|
|
1795
|
+
* $('div', () => {
|
|
1796
|
+
* $(myStore.get() ? 'span' : 'p') // Reactive element type
|
|
1715
1797
|
* })
|
|
1716
1798
|
* ```
|
|
1799
|
+
* When *only* a function is given, `$` behaves exactly like {@link Store.observe},
|
|
1800
|
+
* except that it will only work when we're inside a `mount`.
|
|
1801
|
+
*
|
|
1802
|
+
* @throws {ScopeError} If called outside an observable scope.
|
|
1803
|
+
* @throws {Error} If invalid arguments are provided.
|
|
1717
1804
|
*/
|
|
1718
|
-
export function prop(name: string, value: any): void
|
|
1719
|
-
export function prop(props: object): void
|
|
1720
1805
|
|
|
1721
|
-
export function
|
|
1806
|
+
export function $(...args: (string | (() => void) | false | null | undefined | {[key: string]: any})[]) {
|
|
1722
1807
|
if (!currentScope || !currentScope._parentElement) throw new ScopeError(true)
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1808
|
+
|
|
1809
|
+
let deepEl = currentScope._parentElement
|
|
1810
|
+
|
|
1811
|
+
for(let arg of args) {
|
|
1812
|
+
if (arg == null || arg === false) continue
|
|
1813
|
+
if (typeof arg === 'string') {
|
|
1814
|
+
let text, classes
|
|
1815
|
+
const textPos = arg.indexOf(':')
|
|
1816
|
+
if (textPos >= 0) {
|
|
1817
|
+
text = arg.substring(textPos+1)
|
|
1818
|
+
if (textPos === 0) { // Just a string to add as text, no new node
|
|
1819
|
+
addLeafNode(deepEl, document.createTextNode(text))
|
|
1820
|
+
continue
|
|
1821
|
+
}
|
|
1822
|
+
arg = arg.substring(0,textPos)
|
|
1823
|
+
}
|
|
1824
|
+
const classPos = arg.indexOf('.')
|
|
1825
|
+
if (classPos >= 0) {
|
|
1826
|
+
classes = arg.substring(classPos+1).replaceAll('.', ' ')
|
|
1827
|
+
arg = arg.substring(0, classPos)
|
|
1828
|
+
}
|
|
1829
|
+
if (arg.indexOf(' ') >= 0) throw new Error(`Tag '${arg}' cannot contain space`)
|
|
1830
|
+
const el = document.createElement(arg || 'div')
|
|
1831
|
+
if (classes) el.className = classes
|
|
1832
|
+
if (text) el.textContent = text
|
|
1833
|
+
addLeafNode(deepEl, el)
|
|
1834
|
+
deepEl = el
|
|
1726
1835
|
}
|
|
1727
|
-
|
|
1728
|
-
|
|
1836
|
+
else if (typeof arg === 'object') {
|
|
1837
|
+
if (arg.constructor !== Object) throw new Error(`Unexpected argument: ${arg}`)
|
|
1838
|
+
for(const key in arg) {
|
|
1839
|
+
const val = arg[key]
|
|
1840
|
+
if (key === 'bind') { // Special case, as for this prop we *don't* want to resolve the Store to a value first.
|
|
1841
|
+
applyBinding(deepEl, key, val)
|
|
1842
|
+
} else if (val instanceof Store) {
|
|
1843
|
+
let childScope = new SetArgScope(deepEl, deepEl.lastChild as Node, currentScope!._queueOrder+1, key, val)
|
|
1844
|
+
childScope._install()
|
|
1845
|
+
} else {
|
|
1846
|
+
applyArg(deepEl, key, val)
|
|
1847
|
+
}
|
|
1848
|
+
}
|
|
1849
|
+
} else if (typeof arg === 'function') {
|
|
1850
|
+
if (deepEl === currentScope._parentElement) { // do what observe does
|
|
1851
|
+
_mount(undefined, args[0] as any, SimpleScope)
|
|
1852
|
+
} else { // new scope for a new node without any scope attached yet
|
|
1853
|
+
let childScope = new SimpleScope(deepEl, deepEl.lastChild as Node, currentScope._queueOrder+1, arg)
|
|
1854
|
+
childScope._install()
|
|
1855
|
+
}
|
|
1856
|
+
} else {
|
|
1857
|
+
throw new Error(`Unexpected argument: ${JSON.stringify(arg)}`)
|
|
1858
|
+
}
|
|
1859
|
+
}
|
|
1860
|
+
}
|
|
1861
|
+
|
|
1862
|
+
|
|
1863
|
+
function applyArg(deepEl: Element, key: string, value: any) {
|
|
1864
|
+
if (key[0] === '.') { // CSS class(es)
|
|
1865
|
+
const classes = key.substring(1).split('.')
|
|
1866
|
+
if (value) deepEl.classList.add(...classes)
|
|
1867
|
+
else deepEl.classList.remove(...classes)
|
|
1868
|
+
} else if (key[0] === '$') { // Style
|
|
1869
|
+
const name = key.substring(1);
|
|
1870
|
+
if (value==null || value===false) (deepEl as any).style[name] = ''
|
|
1871
|
+
else (deepEl as any).style[name] = ''+value
|
|
1872
|
+
} else if (key in SPECIAL_PROPS) { // Special property
|
|
1873
|
+
SPECIAL_PROPS[key](deepEl, value)
|
|
1874
|
+
} else if (typeof value === 'function') { // Event listener
|
|
1875
|
+
deepEl.addEventListener(key, value)
|
|
1876
|
+
clean(() => deepEl.removeEventListener(key, value))
|
|
1877
|
+
} else if (value===true || value===false || key==='value' || key==='selectedIndex') { // DOM property
|
|
1878
|
+
(deepEl as any)[key] = value
|
|
1879
|
+
} else { // HTML attribute
|
|
1880
|
+
deepEl.setAttribute(key, value)
|
|
1729
1881
|
}
|
|
1730
1882
|
}
|
|
1883
|
+
|
|
1884
|
+
function defaultOnError(error: Error) {
|
|
1885
|
+
console.error('Error while in Aberdeen render:', error)
|
|
1886
|
+
return true
|
|
1887
|
+
}
|
|
1888
|
+
let onError: (error: Error) => boolean | undefined = defaultOnError
|
|
1889
|
+
|
|
1890
|
+
/**
|
|
1891
|
+
* Set a custome error handling function, thast is called when an error occurs during rendering
|
|
1892
|
+
* while in a reactive scope. The default implementation logs the error to the console, and then
|
|
1893
|
+
* just returns `true`, which causes an 'Error' message to be displayed in the UI. When this function
|
|
1894
|
+
* returns `false`, the error is suppressed. This mechanism exists because rendering errors can occur
|
|
1895
|
+
* at any time, not just synchronous when making a call to Aberdeen, thus normal exception handling
|
|
1896
|
+
* is not always possible.
|
|
1897
|
+
*
|
|
1898
|
+
* @param handler The handler function, getting an `Error` as its argument, and returning `false`
|
|
1899
|
+
* if it does *not* want an error message to be added to the DOM.
|
|
1900
|
+
* When `handler is `undefined`, the default error handling will be reinstated.
|
|
1901
|
+
*
|
|
1902
|
+
* @example
|
|
1903
|
+
* ```javascript
|
|
1904
|
+
* //
|
|
1905
|
+
* setErrorHandler(error => {
|
|
1906
|
+
* // Tell our developers about the problem.
|
|
1907
|
+
* fancyErrorLogger(error)
|
|
1908
|
+
* // Add custom error message to the DOM.
|
|
1909
|
+
* try {
|
|
1910
|
+
* $('.error:Sorry, something went wrong!')
|
|
1911
|
+
* } catch() {} // In case there is no parent element.
|
|
1912
|
+
* // Don't add default error message to the DOM.
|
|
1913
|
+
* return false
|
|
1914
|
+
* })
|
|
1915
|
+
* ```
|
|
1916
|
+
*/
|
|
1917
|
+
export function setErrorHandler(handler?: (error: Error) => boolean | undefined) {
|
|
1918
|
+
onError = handler || defaultOnError
|
|
1919
|
+
}
|
|
1731
1920
|
|
|
1732
1921
|
|
|
1733
1922
|
/**
|
|
1734
|
-
* Return the browser Element that
|
|
1923
|
+
* Return the browser Element that nodes would be rendered to at this point.
|
|
1735
1924
|
* NOTE: Manually changing the DOM is not recommended in most cases. There is
|
|
1736
1925
|
* usually a better, declarative way. Although there are no hard guarantees on
|
|
1737
1926
|
* how your changes interact with Aberdeen, in most cases results will not be
|
|
@@ -1795,10 +1984,10 @@ export function immediateObserve(func: () => void): number | undefined {
|
|
|
1795
1984
|
|
|
1796
1985
|
|
|
1797
1986
|
/**
|
|
1798
|
-
*
|
|
1987
|
+
* Reactively run the function, adding any DOM-elements created using {@link $} to the given parent element.
|
|
1799
1988
|
|
|
1800
1989
|
* @param func - The function to be (repeatedly) executed, possibly adding DOM elements to `parentElement`.
|
|
1801
|
-
* @param parentElement - A DOM element that will be used as the parent element for calls to
|
|
1990
|
+
* @param parentElement - A DOM element that will be used as the parent element for calls to `$`.
|
|
1802
1991
|
* @returns The mount id (usable for `unmount`) if this is a top-level mount.
|
|
1803
1992
|
*
|
|
1804
1993
|
* @example
|
|
@@ -1807,7 +1996,7 @@ export function immediateObserve(func: () => void): number | undefined {
|
|
|
1807
1996
|
* setInterval(() => store.modify(v => v+1), 1000)
|
|
1808
1997
|
*
|
|
1809
1998
|
* mount(document.body, () => {
|
|
1810
|
-
*
|
|
1999
|
+
* $(`h2:${store.get()} seconds have passed`)
|
|
1811
2000
|
* })
|
|
1812
2001
|
* ```
|
|
1813
2002
|
*
|
|
@@ -1817,30 +2006,37 @@ export function immediateObserve(func: () => void): number | undefined {
|
|
|
1817
2006
|
* let colors = new Store(new Map())
|
|
1818
2007
|
*
|
|
1819
2008
|
* mount(document.body, () => {
|
|
1820
|
-
*
|
|
1821
|
-
*
|
|
1822
|
-
*
|
|
1823
|
-
*
|
|
1824
|
-
*
|
|
1825
|
-
*
|
|
1826
|
-
*
|
|
1827
|
-
*
|
|
1828
|
-
*
|
|
1829
|
-
*
|
|
1830
|
-
*
|
|
1831
|
-
*
|
|
1832
|
-
*
|
|
1833
|
-
*
|
|
1834
|
-
*
|
|
2009
|
+
* // This function will never rerun (as it does not read any `Store`s)
|
|
2010
|
+
* $('button:<<', {click: () => selected.modify(n => n-1)})
|
|
2011
|
+
* $('button:>>', {click: () => selected.modify(n => n+1)})
|
|
2012
|
+
*
|
|
2013
|
+
* observe(() => {
|
|
2014
|
+
* // This will rerun whenever `selected` changes, recreating the <h2> and <input>.
|
|
2015
|
+
* $('h2', {text: '#' + selected.get()})
|
|
2016
|
+
* $('input', {type: 'color', value: '#ffffff' bind: colors(selected.get())})
|
|
2017
|
+
* })
|
|
2018
|
+
*
|
|
2019
|
+
* observe(() => {
|
|
2020
|
+
* // This function will rerun when `selected` or the selected color changes.
|
|
2021
|
+
* // It will change the <body> background-color.
|
|
2022
|
+
* $({$backgroundColor: colors.get(selected.get()) || 'white'})
|
|
2023
|
+
* })
|
|
1835
2024
|
* })
|
|
1836
2025
|
* ```
|
|
1837
2026
|
*/
|
|
1838
2027
|
export function mount(parentElement: Element, func: () => void) {
|
|
2028
|
+
for(let scope of topScopes.values()) {
|
|
2029
|
+
if (parentElement === scope._parentElement) {
|
|
2030
|
+
throw new Error("Only a single mount per parent element")
|
|
2031
|
+
}
|
|
2032
|
+
}
|
|
2033
|
+
|
|
1839
2034
|
return _mount(parentElement, func, SimpleScope)
|
|
1840
2035
|
}
|
|
1841
2036
|
|
|
1842
2037
|
let maxTopScopeId = 0
|
|
1843
2038
|
const topScopes: Map<number, SimpleScope> = new Map()
|
|
2039
|
+
|
|
1844
2040
|
function _mount(parentElement: Element | undefined, func: () => void, MountScope: typeof SimpleScope): number | undefined {
|
|
1845
2041
|
let scope
|
|
1846
2042
|
if (parentElement || !currentScope) {
|
|
@@ -1875,6 +2071,7 @@ export function unmount(id?: number) {
|
|
|
1875
2071
|
} else {
|
|
1876
2072
|
let scope = topScopes.get(id)
|
|
1877
2073
|
if (!scope) throw new Error("No such mount "+id)
|
|
2074
|
+
topScopes.delete(id)
|
|
1878
2075
|
scope._remove()
|
|
1879
2076
|
}
|
|
1880
2077
|
}
|
|
@@ -1909,59 +2106,20 @@ export function peek<T>(func: () => T): T {
|
|
|
1909
2106
|
} finally {
|
|
1910
2107
|
currentScope = savedScope
|
|
1911
2108
|
}
|
|
1912
|
-
}
|
|
1913
|
-
|
|
2109
|
+
}
|
|
1914
2110
|
|
|
1915
2111
|
/*
|
|
1916
2112
|
* Helper functions
|
|
1917
2113
|
*/
|
|
1918
2114
|
|
|
1919
|
-
function applyProp(el: Element, prop: any, value: any) {
|
|
1920
|
-
if (prop==='create') {
|
|
1921
|
-
if (onCreateEnabled) {
|
|
1922
|
-
if (typeof value === 'function') {
|
|
1923
|
-
value(el)
|
|
1924
|
-
} else {
|
|
1925
|
-
el.classList.add(value)
|
|
1926
|
-
setTimeout(function(){el.classList.remove(value)}, 0)
|
|
1927
|
-
}
|
|
1928
|
-
}
|
|
1929
|
-
} else if (prop==='destroy') {
|
|
1930
|
-
onDestroyMap.set(el, value)
|
|
1931
|
-
} else if (typeof value === 'function') {
|
|
1932
|
-
// Set an event listener; remove it again on clean.
|
|
1933
|
-
el.addEventListener(prop, value)
|
|
1934
|
-
clean(() => el.removeEventListener(prop, value))
|
|
1935
|
-
} else if (prop==='value' || prop==='className' || prop==='selectedIndex' || value===true || value===false) {
|
|
1936
|
-
// All boolean values and a few specific keys should be set as a property
|
|
1937
|
-
(el as any)[prop] = value
|
|
1938
|
-
} else if (prop==='text') {
|
|
1939
|
-
// `text` is set as textContent
|
|
1940
|
-
el.textContent = value
|
|
1941
|
-
} else if ((prop==='class' || prop==='className') && typeof value === 'object') {
|
|
1942
|
-
// Allow setting classes using an object where the keys are the names and
|
|
1943
|
-
// the values are booleans stating whether to set or remove.
|
|
1944
|
-
for(let name in value) {
|
|
1945
|
-
if (value[name]) el.classList.add(name)
|
|
1946
|
-
else el.classList.remove(name)
|
|
1947
|
-
}
|
|
1948
|
-
} else if (prop==='style' && typeof value === 'object') {
|
|
1949
|
-
// `style` can receive an object
|
|
1950
|
-
Object.assign((<HTMLElement>el).style, value)
|
|
1951
|
-
} else {
|
|
1952
|
-
// Everything else is an HTML attribute
|
|
1953
|
-
el.setAttribute(prop, value)
|
|
1954
|
-
}
|
|
1955
|
-
}
|
|
1956
|
-
|
|
1957
2115
|
function valueToData(value: any) {
|
|
1958
|
-
if (
|
|
1959
|
-
// Simple data types
|
|
1960
|
-
return value
|
|
1961
|
-
} else if (value instanceof Store) {
|
|
2116
|
+
if (value instanceof Store) {
|
|
1962
2117
|
// When a Store is passed pointing at a collection, a reference
|
|
1963
2118
|
// is made to that collection.
|
|
1964
2119
|
return value._observe()
|
|
2120
|
+
} else if (typeof value !== "object" || !value) {
|
|
2121
|
+
// Simple data types
|
|
2122
|
+
return value
|
|
1965
2123
|
} else if (value instanceof Map) {
|
|
1966
2124
|
let result = new ObsMap()
|
|
1967
2125
|
value.forEach((v,k) => {
|
|
@@ -1997,14 +2155,15 @@ function defaultMakeSortKey(store: Store) {
|
|
|
1997
2155
|
|
|
1998
2156
|
/* c8 ignore start */
|
|
1999
2157
|
function internalError(code: number) {
|
|
2000
|
-
|
|
2001
|
-
setTimeout(() => { throw error }, 0)
|
|
2158
|
+
throw new Error("Aberdeen internal error "+code)
|
|
2002
2159
|
}
|
|
2003
2160
|
/* c8 ignore end */
|
|
2004
2161
|
|
|
2005
|
-
function handleError(e: any) {
|
|
2006
|
-
|
|
2007
|
-
|
|
2162
|
+
function handleError(e: any, showMessage: boolean) {
|
|
2163
|
+
try {
|
|
2164
|
+
if (onError(e) === false) showMessage = false
|
|
2165
|
+
} catch {}
|
|
2166
|
+
if (showMessage && currentScope?._parentElement) $('.aberdeen-error:Error')
|
|
2008
2167
|
}
|
|
2009
2168
|
|
|
2010
2169
|
class ScopeError extends Error {
|
|
@@ -2024,14 +2183,11 @@ export function withEmitHandler(handler: (this: ObsCollection, index: any, newDa
|
|
|
2024
2183
|
}
|
|
2025
2184
|
}
|
|
2026
2185
|
|
|
2027
|
-
/**
|
|
2028
|
-
* Run a function, while *not* causing reactive effects for any changes it makes to `Store`s.
|
|
2029
|
-
* @param func The function to be executed once immediately.
|
|
2030
|
-
*/
|
|
2031
|
-
export function inhibitEffects(func: () => void) {
|
|
2032
|
-
withEmitHandler(() => {}, func)
|
|
2033
|
-
}
|
|
2034
|
-
|
|
2035
2186
|
// @ts-ignore
|
|
2036
2187
|
// c8 ignore next
|
|
2037
2188
|
if (!String.prototype.replaceAll) String.prototype.replaceAll = function(from, to) { return this.split(from).join(to) }
|
|
2189
|
+
declare global {
|
|
2190
|
+
interface String {
|
|
2191
|
+
replaceAll(from: string, to: string): string;
|
|
2192
|
+
}
|
|
2193
|
+
}
|