aberdeen 1.0.13 → 1.2.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 +51 -8
- package/dist/aberdeen.d.ts +80 -105
- package/dist/aberdeen.js +293 -228
- package/dist/aberdeen.js.map +3 -3
- package/dist/dispatcher.d.ts +54 -0
- package/dist/dispatcher.js +65 -0
- package/dist/dispatcher.js.map +10 -0
- package/dist/prediction.js.map +1 -1
- package/dist/route.d.ts +80 -30
- package/dist/route.js +162 -135
- package/dist/route.js.map +3 -3
- package/dist-min/aberdeen.js +5 -5
- package/dist-min/aberdeen.js.map +3 -3
- package/dist-min/dispatcher.js +4 -0
- package/dist-min/dispatcher.js.map +10 -0
- package/dist-min/prediction.js +2 -2
- package/dist-min/prediction.js.map +2 -2
- package/dist-min/route.js +2 -2
- package/dist-min/route.js.map +3 -3
- package/package.json +10 -5
- package/src/aberdeen.ts +427 -400
- package/src/dispatcher.ts +130 -0
- package/src/route.ts +273 -181
package/src/aberdeen.ts
CHANGED
|
@@ -166,7 +166,7 @@ abstract class Scope implements QueueRunner {
|
|
|
166
166
|
|
|
167
167
|
[ptr: ReverseSortedSetPointer]: this;
|
|
168
168
|
|
|
169
|
-
abstract onChange(index: any
|
|
169
|
+
abstract onChange(index: any): void;
|
|
170
170
|
abstract queueRun(): void;
|
|
171
171
|
|
|
172
172
|
abstract getLastNode(): Node | undefined;
|
|
@@ -245,7 +245,7 @@ abstract class ContentScope extends Scope {
|
|
|
245
245
|
return this.getLastNode() || this.getPrecedingNode();
|
|
246
246
|
}
|
|
247
247
|
|
|
248
|
-
onChange(
|
|
248
|
+
onChange() {
|
|
249
249
|
queue(this);
|
|
250
250
|
}
|
|
251
251
|
|
|
@@ -451,43 +451,6 @@ class SetArgScope extends ChainedScope {
|
|
|
451
451
|
}
|
|
452
452
|
}
|
|
453
453
|
|
|
454
|
-
let immediateQueue: ReverseSortedSet<Scope, "prio"> = new ReverseSortedSet(
|
|
455
|
-
"prio",
|
|
456
|
-
);
|
|
457
|
-
|
|
458
|
-
class ImmediateScope extends RegularScope {
|
|
459
|
-
onChange(index: any, newData: any, oldData: any) {
|
|
460
|
-
immediateQueue.add(this);
|
|
461
|
-
}
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
let immediateQueueRunning = false;
|
|
465
|
-
function runImmediateQueue() {
|
|
466
|
-
for (
|
|
467
|
-
let count = 0;
|
|
468
|
-
!immediateQueue.isEmpty() && !immediateQueueRunning;
|
|
469
|
-
count++
|
|
470
|
-
) {
|
|
471
|
-
if (count > 42) {
|
|
472
|
-
immediateQueue.clear();
|
|
473
|
-
throw new Error("Too many immediate-mode recursive updates");
|
|
474
|
-
}
|
|
475
|
-
immediateQueueRunning = true;
|
|
476
|
-
const copy = immediateQueue;
|
|
477
|
-
immediateQueue = new ReverseSortedSet("prio");
|
|
478
|
-
try {
|
|
479
|
-
for (const scope of copy) {
|
|
480
|
-
// On exception, the exception will be bubbled up to the call site, discarding any
|
|
481
|
-
// remaining immediate scopes from the queue. This behavior is perhaps debatable,
|
|
482
|
-
// but getting a synchronous exception at the call site can be very helpful.
|
|
483
|
-
scope.queueRun();
|
|
484
|
-
}
|
|
485
|
-
} finally {
|
|
486
|
-
immediateQueueRunning = false;
|
|
487
|
-
}
|
|
488
|
-
}
|
|
489
|
-
}
|
|
490
|
-
|
|
491
454
|
/** @internal */
|
|
492
455
|
class OnEachScope extends Scope {
|
|
493
456
|
// biome-ignore lint/correctness/noInvalidUseBeforeDeclaration: circular, as currentScope is initialized with a Scope
|
|
@@ -528,15 +491,11 @@ class OnEachScope extends Scope {
|
|
|
528
491
|
// Do _addChild() calls for initial items
|
|
529
492
|
if (target instanceof Array) {
|
|
530
493
|
for (let i = 0; i < target.length; i++) {
|
|
531
|
-
|
|
532
|
-
new OnEachItemScope(this, i, false);
|
|
533
|
-
}
|
|
494
|
+
new OnEachItemScope(this, i, false);
|
|
534
495
|
}
|
|
535
496
|
} else {
|
|
536
|
-
for (const
|
|
537
|
-
|
|
538
|
-
new OnEachItemScope(this, key, false);
|
|
539
|
-
}
|
|
497
|
+
for (const key of (target instanceof Map ? target.keys() : Object.keys(target))) {
|
|
498
|
+
new OnEachItemScope(this, key, false);
|
|
540
499
|
}
|
|
541
500
|
}
|
|
542
501
|
}
|
|
@@ -545,7 +504,7 @@ class OnEachScope extends Scope {
|
|
|
545
504
|
return findLastNodeInPrevSiblings(this.prevSibling);
|
|
546
505
|
}
|
|
547
506
|
|
|
548
|
-
onChange(index: any
|
|
507
|
+
onChange(index: any) {
|
|
549
508
|
if (!(this.target instanceof Array) || typeof index === "number")
|
|
550
509
|
this.changedIndexes.add(index);
|
|
551
510
|
queue(this);
|
|
@@ -558,17 +517,12 @@ class OnEachScope extends Scope {
|
|
|
558
517
|
const oldScope = this.byIndex.get(index);
|
|
559
518
|
if (oldScope) oldScope.remove();
|
|
560
519
|
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
520
|
+
if (this.target instanceof Map ? this.target.has(index) : index in this.target) {
|
|
521
|
+
// Item still exists
|
|
522
|
+
new OnEachItemScope(this, index, true);
|
|
564
523
|
} else {
|
|
565
|
-
|
|
566
|
-
}
|
|
567
|
-
|
|
568
|
-
if (!hasValue) {
|
|
524
|
+
// Item has disappeared
|
|
569
525
|
this.byIndex.delete(index);
|
|
570
|
-
} else {
|
|
571
|
-
new OnEachItemScope(this, index, true);
|
|
572
526
|
}
|
|
573
527
|
}
|
|
574
528
|
topRedrawScope = undefined;
|
|
@@ -774,6 +728,23 @@ function addNode(node: Node) {
|
|
|
774
728
|
const ROOT_SCOPE = new RootScope();
|
|
775
729
|
let currentScope: ContentScope = ROOT_SCOPE;
|
|
776
730
|
|
|
731
|
+
/**
|
|
732
|
+
* Execute a function in a never-cleaned root scope. Even {@link unmountAll} will not
|
|
733
|
+
* clean up observers/nodes created by the function.
|
|
734
|
+
* @param func The function to execute.
|
|
735
|
+
* @returns The return value of the function.
|
|
736
|
+
* @internal
|
|
737
|
+
*/
|
|
738
|
+
export function leakScope<T>(func: () => T): T {
|
|
739
|
+
const savedScope = currentScope;
|
|
740
|
+
currentScope = new RootScope();
|
|
741
|
+
try {
|
|
742
|
+
return func();
|
|
743
|
+
} finally {
|
|
744
|
+
currentScope = savedScope;
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
|
|
777
748
|
/**
|
|
778
749
|
* A special Node observer index to subscribe to any value in the map changing.
|
|
779
750
|
*/
|
|
@@ -925,10 +896,12 @@ export function onEach(
|
|
|
925
896
|
}
|
|
926
897
|
|
|
927
898
|
function isObjEmpty(obj: object): boolean {
|
|
928
|
-
for (const k
|
|
899
|
+
for (const k of Object.keys(obj)) return false;
|
|
929
900
|
return true;
|
|
930
901
|
}
|
|
931
902
|
|
|
903
|
+
const EMPTY = Symbol("empty");
|
|
904
|
+
|
|
932
905
|
/**
|
|
933
906
|
* Reactively checks if an observable array or object is empty.
|
|
934
907
|
*
|
|
@@ -938,7 +911,7 @@ function isObjEmpty(obj: object): boolean {
|
|
|
938
911
|
* is deleted from an object), the scope that called `isEmpty` will be automatically
|
|
939
912
|
* scheduled for re-evaluation.
|
|
940
913
|
*
|
|
941
|
-
* @param proxied The observable array or object
|
|
914
|
+
* @param proxied The observable array or object to check.
|
|
942
915
|
* @returns `true` if the array has length 0 or the object has no own enumerable properties, `false` otherwise.
|
|
943
916
|
*
|
|
944
917
|
* @example
|
|
@@ -981,7 +954,7 @@ export function isEmpty(proxied: TargetType): boolean {
|
|
|
981
954
|
|
|
982
955
|
const result = isObjEmpty(target);
|
|
983
956
|
subscribe(target, ANY_SYMBOL, (index: any, newData: any, oldData: any) => {
|
|
984
|
-
if (result ? oldData ===
|
|
957
|
+
if (result ? oldData === EMPTY : newData === EMPTY) queue(scope);
|
|
985
958
|
});
|
|
986
959
|
return result;
|
|
987
960
|
}
|
|
@@ -1006,8 +979,8 @@ export interface ValueRef<T> {
|
|
|
1006
979
|
* $('div', {text: cnt});
|
|
1007
980
|
* // <div>2</div>
|
|
1008
981
|
|
|
1009
|
-
* // Or we can use it in an {@link
|
|
1010
|
-
*
|
|
982
|
+
* // Or we can use it in an {@link derive} function:
|
|
983
|
+
* $(() => console.log("The count is now", cnt.value));
|
|
1011
984
|
* // The count is now 2
|
|
1012
985
|
*
|
|
1013
986
|
* // Adding/removing items will update the count
|
|
@@ -1023,7 +996,7 @@ export function count(proxied: TargetType): ValueRef<number> {
|
|
|
1023
996
|
|
|
1024
997
|
const target = (proxied as any)[TARGET_SYMBOL] || proxied;
|
|
1025
998
|
let cnt = 0;
|
|
1026
|
-
for (const k
|
|
999
|
+
for (const k of Object.keys(target)) if (target[k] !== undefined) cnt++;
|
|
1027
1000
|
|
|
1028
1001
|
const result = proxy(cnt);
|
|
1029
1002
|
subscribe(
|
|
@@ -1031,8 +1004,8 @@ export function count(proxied: TargetType): ValueRef<number> {
|
|
|
1031
1004
|
ANY_SYMBOL,
|
|
1032
1005
|
(index: any, newData: any, oldData: any) => {
|
|
1033
1006
|
if (oldData === newData) {
|
|
1034
|
-
} else if (oldData ===
|
|
1035
|
-
else if (newData ===
|
|
1007
|
+
} else if (oldData === EMPTY) result.value = ++cnt;
|
|
1008
|
+
else if (newData === EMPTY) result.value = --cnt;
|
|
1036
1009
|
},
|
|
1037
1010
|
);
|
|
1038
1011
|
|
|
@@ -1058,7 +1031,7 @@ export function defaultEmitHandler(
|
|
|
1058
1031
|
if (byIndex) {
|
|
1059
1032
|
for (const observer of byIndex) {
|
|
1060
1033
|
if (typeof observer === "function") observer(index, newData, oldData);
|
|
1061
|
-
else observer.onChange(index
|
|
1034
|
+
else observer.onChange(index);
|
|
1062
1035
|
}
|
|
1063
1036
|
}
|
|
1064
1037
|
}
|
|
@@ -1075,25 +1048,22 @@ const objectHandler: ProxyHandler<any> = {
|
|
|
1075
1048
|
// Make sure newData is unproxied
|
|
1076
1049
|
if (typeof newData === "object" && newData)
|
|
1077
1050
|
newData = (newData as any)[TARGET_SYMBOL] || newData;
|
|
1078
|
-
const oldData = target[prop];
|
|
1051
|
+
const oldData = target.hasOwnProperty(prop) ? target[prop] : EMPTY;
|
|
1079
1052
|
if (newData !== oldData) {
|
|
1080
1053
|
target[prop] = newData;
|
|
1081
1054
|
emit(target, prop, newData, oldData);
|
|
1082
|
-
runImmediateQueue();
|
|
1083
1055
|
}
|
|
1084
1056
|
return true;
|
|
1085
1057
|
},
|
|
1086
1058
|
deleteProperty(target: any, prop: any) {
|
|
1087
|
-
const old = target[prop];
|
|
1059
|
+
const old = target.hasOwnProperty(prop) ? target[prop] : EMPTY;
|
|
1088
1060
|
delete target[prop];
|
|
1089
|
-
emit(target, prop,
|
|
1090
|
-
runImmediateQueue();
|
|
1061
|
+
emit(target, prop, EMPTY, old);
|
|
1091
1062
|
return true;
|
|
1092
1063
|
},
|
|
1093
1064
|
has(target: any, prop: any) {
|
|
1094
|
-
const result = prop in target;
|
|
1095
1065
|
subscribe(target, prop);
|
|
1096
|
-
return
|
|
1066
|
+
return target.hasOwnProperty(prop);
|
|
1097
1067
|
},
|
|
1098
1068
|
ownKeys(target: any) {
|
|
1099
1069
|
subscribe(target, ANY_SYMBOL);
|
|
@@ -1103,9 +1073,11 @@ const objectHandler: ProxyHandler<any> = {
|
|
|
1103
1073
|
|
|
1104
1074
|
function arraySet(target: any, prop: any, newData: any) {
|
|
1105
1075
|
// Make sure newData is unproxied
|
|
1106
|
-
if (typeof newData === "object" && newData)
|
|
1076
|
+
if (typeof newData === "object" && newData) {
|
|
1107
1077
|
newData = (newData as any)[TARGET_SYMBOL] || newData;
|
|
1108
|
-
|
|
1078
|
+
}
|
|
1079
|
+
let oldData = target[prop];
|
|
1080
|
+
if (oldData === undefined && !target.hasOwnProperty(prop)) oldData = EMPTY;
|
|
1109
1081
|
if (newData !== oldData) {
|
|
1110
1082
|
const oldLength = target.length;
|
|
1111
1083
|
|
|
@@ -1114,11 +1086,13 @@ function arraySet(target: any, prop: any, newData: any) {
|
|
|
1114
1086
|
|
|
1115
1087
|
// We only need to emit for shrinking, as growing just adds undefineds
|
|
1116
1088
|
for (let i = newData; i < oldLength; i++) {
|
|
1117
|
-
emit(target, i,
|
|
1089
|
+
emit(target, i, EMPTY, target[i]);
|
|
1118
1090
|
}
|
|
1119
1091
|
} else {
|
|
1120
|
-
|
|
1121
|
-
|
|
1092
|
+
if (typeof prop === 'string') { // Convert to int when possible
|
|
1093
|
+
const n = 0|prop as any;
|
|
1094
|
+
if (String(n) === prop && n >= 0) prop = n;
|
|
1095
|
+
}
|
|
1122
1096
|
|
|
1123
1097
|
target[prop] = newData;
|
|
1124
1098
|
emit(target, prop, newData, oldData);
|
|
@@ -1126,7 +1100,6 @@ function arraySet(target: any, prop: any, newData: any) {
|
|
|
1126
1100
|
if (target.length !== oldLength) {
|
|
1127
1101
|
emit(target, "length", target.length, oldLength);
|
|
1128
1102
|
}
|
|
1129
|
-
runImmediateQueue();
|
|
1130
1103
|
}
|
|
1131
1104
|
return true;
|
|
1132
1105
|
}
|
|
@@ -1134,17 +1107,24 @@ function arraySet(target: any, prop: any, newData: any) {
|
|
|
1134
1107
|
const arrayHandler: ProxyHandler<any[]> = {
|
|
1135
1108
|
get(target: any, prop: any) {
|
|
1136
1109
|
if (prop === TARGET_SYMBOL) return target;
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
if (intProp.toString() === prop) subProp = intProp;
|
|
1110
|
+
if (typeof prop === 'string') { // Convert to int when possible
|
|
1111
|
+
const n = 0|prop as any;
|
|
1112
|
+
if (String(n) === prop && n >= 0) prop = n;
|
|
1141
1113
|
}
|
|
1142
|
-
subscribe(target,
|
|
1114
|
+
subscribe(target, prop);
|
|
1143
1115
|
return optProxy(target[prop]);
|
|
1144
1116
|
},
|
|
1145
1117
|
set: arraySet,
|
|
1146
|
-
deleteProperty(target: any, prop:
|
|
1147
|
-
|
|
1118
|
+
deleteProperty(target: any, prop: any) {
|
|
1119
|
+
if (typeof prop === 'string') { // Convert to int when possible
|
|
1120
|
+
const n = 0|prop as any;
|
|
1121
|
+
if (String(n) === prop && n >= 0) prop = n;
|
|
1122
|
+
}
|
|
1123
|
+
let oldData = target[prop];
|
|
1124
|
+
if (oldData === undefined && !target.hasOwnProperty(prop)) oldData = EMPTY;
|
|
1125
|
+
delete target[prop];
|
|
1126
|
+
emit(target, prop, EMPTY, oldData);
|
|
1127
|
+
return true;
|
|
1148
1128
|
},
|
|
1149
1129
|
};
|
|
1150
1130
|
|
|
@@ -1190,31 +1170,34 @@ const mapMethodHandlers = {
|
|
|
1190
1170
|
set(this: any, key: any, newData: any): any {
|
|
1191
1171
|
const target: Map<any, any> = this[TARGET_SYMBOL];
|
|
1192
1172
|
// Make sure key and newData are unproxied
|
|
1193
|
-
if (typeof key === "object" && key)
|
|
1173
|
+
if (typeof key === "object" && key) {
|
|
1194
1174
|
key = (key as any)[TARGET_SYMBOL] || key;
|
|
1195
|
-
|
|
1175
|
+
}
|
|
1176
|
+
if (typeof newData === "object" && newData) {
|
|
1196
1177
|
newData = (newData as any)[TARGET_SYMBOL] || newData;
|
|
1197
|
-
|
|
1178
|
+
}
|
|
1179
|
+
let oldData = target.get(key);
|
|
1180
|
+
if (oldData === undefined && !target.has(key)) oldData = EMPTY;
|
|
1198
1181
|
if (newData !== oldData) {
|
|
1199
1182
|
const oldSize = target.size;
|
|
1200
1183
|
target.set(key, newData);
|
|
1201
1184
|
emit(target, key, newData, oldData);
|
|
1202
1185
|
emit(target, MAP_SIZE_SYMBOL, target.size, oldSize);
|
|
1203
|
-
runImmediateQueue();
|
|
1204
1186
|
}
|
|
1205
1187
|
return this;
|
|
1206
1188
|
},
|
|
1207
1189
|
delete(this: any, key: any): boolean {
|
|
1208
1190
|
const target: Map<any, any> = this[TARGET_SYMBOL];
|
|
1209
1191
|
// Make sure key is unproxied
|
|
1210
|
-
if (typeof key === "object" && key)
|
|
1192
|
+
if (typeof key === "object" && key) {
|
|
1211
1193
|
key = (key as any)[TARGET_SYMBOL] || key;
|
|
1212
|
-
|
|
1194
|
+
}
|
|
1195
|
+
let oldData = target.get(key);
|
|
1196
|
+
if (oldData === undefined && !target.has(key)) oldData = EMPTY;
|
|
1213
1197
|
const result: boolean = target.delete(key);
|
|
1214
1198
|
if (result) {
|
|
1215
|
-
emit(target, key,
|
|
1199
|
+
emit(target, key, EMPTY, oldData);
|
|
1216
1200
|
emit(target, MAP_SIZE_SYMBOL, target.size, target.size + 1);
|
|
1217
|
-
runImmediateQueue();
|
|
1218
1201
|
}
|
|
1219
1202
|
return result;
|
|
1220
1203
|
},
|
|
@@ -1222,18 +1205,17 @@ const mapMethodHandlers = {
|
|
|
1222
1205
|
const target: Map<any, any> = this[TARGET_SYMBOL];
|
|
1223
1206
|
const oldSize = target.size;
|
|
1224
1207
|
for (const key of target.keys()) {
|
|
1225
|
-
|
|
1226
|
-
emit(target, key, undefined, oldData);
|
|
1208
|
+
emit(target, key, undefined, target.get(key));
|
|
1227
1209
|
}
|
|
1228
1210
|
target.clear();
|
|
1229
1211
|
emit(target, MAP_SIZE_SYMBOL, 0, oldSize);
|
|
1230
|
-
runImmediateQueue();
|
|
1231
1212
|
},
|
|
1232
1213
|
has(this: any, key: any): boolean {
|
|
1233
1214
|
const target: Map<any, any> = this[TARGET_SYMBOL];
|
|
1234
1215
|
// Make sure key is unproxied
|
|
1235
|
-
if (typeof key === "object" && key)
|
|
1216
|
+
if (typeof key === "object" && key) {
|
|
1236
1217
|
key = (key as any)[TARGET_SYMBOL] || key;
|
|
1218
|
+
}
|
|
1237
1219
|
subscribe(target, key);
|
|
1238
1220
|
return target.has(key);
|
|
1239
1221
|
},
|
|
@@ -1264,7 +1246,7 @@ const mapHandler: ProxyHandler<Map<any, any>> = {
|
|
|
1264
1246
|
if (prop === TARGET_SYMBOL) return target;
|
|
1265
1247
|
|
|
1266
1248
|
// Handle Map methods using lookup object
|
|
1267
|
-
if (prop
|
|
1249
|
+
if (mapMethodHandlers.hasOwnProperty(prop)) {
|
|
1268
1250
|
return (mapMethodHandlers as any)[prop];
|
|
1269
1251
|
}
|
|
1270
1252
|
|
|
@@ -1275,8 +1257,7 @@ const mapHandler: ProxyHandler<Map<any, any>> = {
|
|
|
1275
1257
|
}
|
|
1276
1258
|
|
|
1277
1259
|
// Handle other properties normally
|
|
1278
|
-
|
|
1279
|
-
return optProxy((target as any)[prop]);
|
|
1260
|
+
return (target as any)[prop];
|
|
1280
1261
|
},
|
|
1281
1262
|
};
|
|
1282
1263
|
|
|
@@ -1308,41 +1289,32 @@ function optProxy(value: any): any {
|
|
|
1308
1289
|
return proxied;
|
|
1309
1290
|
}
|
|
1310
1291
|
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
T
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
? boolean
|
|
1320
|
-
: T
|
|
1321
|
-
>;
|
|
1292
|
+
interface PromiseProxy<T> {
|
|
1293
|
+
busy: boolean;
|
|
1294
|
+
error?: any;
|
|
1295
|
+
value?: T;
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
export function proxy<T extends any>(target: Promise<T>): PromiseProxy<T>;
|
|
1299
|
+
export function proxy<T extends any>(target: Array<T>): Array<T extends number ? number : T extends string ? string : T extends boolean ? boolean : T >;
|
|
1322
1300
|
export function proxy<T extends object>(target: T): T;
|
|
1323
|
-
export function proxy<T extends any>(
|
|
1324
|
-
target: T,
|
|
1325
|
-
): ValueRef<
|
|
1326
|
-
T extends number
|
|
1327
|
-
? number
|
|
1328
|
-
: T extends string
|
|
1329
|
-
? string
|
|
1330
|
-
: T extends boolean
|
|
1331
|
-
? boolean
|
|
1332
|
-
: T
|
|
1333
|
-
>;
|
|
1301
|
+
export function proxy<T extends any>(target: T): ValueRef<T extends number ? number : T extends string ? string : T extends boolean ? boolean : T>;
|
|
1334
1302
|
|
|
1335
1303
|
/**
|
|
1336
1304
|
* Creates a reactive proxy around the given data.
|
|
1337
1305
|
*
|
|
1338
1306
|
* Reading properties from the returned proxy within a reactive scope (like one created by
|
|
1339
|
-
* {@link $} or {@link
|
|
1307
|
+
* {@link $} or {@link derive}) establishes a subscription. Modifying properties *through*
|
|
1340
1308
|
* the proxy will notify subscribed scopes, causing them to re-execute.
|
|
1341
1309
|
*
|
|
1342
1310
|
* - Plain objects and arrays are wrapped in a standard JavaScript `Proxy` that intercepts
|
|
1343
1311
|
* property access and mutations, but otherwise works like the underlying data.
|
|
1344
1312
|
* - Primitives (string, number, boolean, null, undefined) are wrapped in an object
|
|
1345
1313
|
* `{ value: T }` which is then proxied. Access the primitive via the `.value` property.
|
|
1314
|
+
* - Promises are represented by proxied objects `{ busy: boolean, value?: T, error?: any }`.
|
|
1315
|
+
* Initially, `busy` is `true`. When the promise resolves, `value` is set and `busy`
|
|
1316
|
+
* is set to `false`. If the promise is rejected, `error` is set and `busy` is also
|
|
1317
|
+
* set to `false`.
|
|
1346
1318
|
*
|
|
1347
1319
|
* Use {@link unproxy} to get the original underlying data back.
|
|
1348
1320
|
*
|
|
@@ -1353,23 +1325,23 @@ export function proxy<T extends any>(
|
|
|
1353
1325
|
* @example Object
|
|
1354
1326
|
* ```javascript
|
|
1355
1327
|
* const state = proxy({ count: 0, message: 'Hello' });
|
|
1356
|
-
*
|
|
1357
|
-
* setTimeout(() => state.message = 'World', 1000); // Triggers the
|
|
1328
|
+
* $(() => console.log(state.message)); // Subscribes to message
|
|
1329
|
+
* setTimeout(() => state.message = 'World', 1000); // Triggers the observing function
|
|
1358
1330
|
* setTimeout(() => state.count++, 2000); // Triggers nothing
|
|
1359
1331
|
* ```
|
|
1360
1332
|
*
|
|
1361
1333
|
* @example Array
|
|
1362
1334
|
* ```javascript
|
|
1363
1335
|
* const items = proxy(['a', 'b']);
|
|
1364
|
-
*
|
|
1365
|
-
* setTimeout(() => items.push('c'), 2000); // Triggers the
|
|
1336
|
+
* $(() => console.log(items.length)); // Subscribes to length
|
|
1337
|
+
* setTimeout(() => items.push('c'), 2000); // Triggers the observing function
|
|
1366
1338
|
* ```
|
|
1367
1339
|
*
|
|
1368
1340
|
* @example Primitive
|
|
1369
1341
|
* ```javascript
|
|
1370
1342
|
* const name = proxy('Aberdeen');
|
|
1371
|
-
*
|
|
1372
|
-
* setTimeout(() => name.value = 'UI', 2000); // Triggers the
|
|
1343
|
+
* $(() => console.log(name.value)); // Subscribes to value
|
|
1344
|
+
* setTimeout(() => name.value = 'UI', 2000); // Triggers the observing function
|
|
1373
1345
|
* ```
|
|
1374
1346
|
*
|
|
1375
1347
|
* @example Class instance
|
|
@@ -1380,12 +1352,27 @@ export function proxy<T extends any>(
|
|
|
1380
1352
|
* toString() { return `${this.name}Widget (${this.width}x${this.height})`; }
|
|
1381
1353
|
* }
|
|
1382
1354
|
* let graph: Widget = proxy(new Widget('Graph', 200, 100));
|
|
1383
|
-
*
|
|
1355
|
+
* $(() => console.log(''+graph));
|
|
1384
1356
|
* setTimeout(() => graph.grow(), 2000);
|
|
1385
1357
|
* setTimeout(() => graph.grow(), 4000);
|
|
1386
1358
|
* ```
|
|
1387
1359
|
*/
|
|
1388
1360
|
export function proxy(target: TargetType): TargetType {
|
|
1361
|
+
if (target instanceof Promise) {
|
|
1362
|
+
const result: PromiseProxy<any> = optProxy({
|
|
1363
|
+
busy: true,
|
|
1364
|
+
});
|
|
1365
|
+
target
|
|
1366
|
+
.then((value) => {
|
|
1367
|
+
result.value = value;
|
|
1368
|
+
result.busy = false;
|
|
1369
|
+
})
|
|
1370
|
+
.catch((err) => {
|
|
1371
|
+
result.error = err;
|
|
1372
|
+
result.busy = false;
|
|
1373
|
+
});
|
|
1374
|
+
return result;
|
|
1375
|
+
}
|
|
1389
1376
|
return optProxy(
|
|
1390
1377
|
typeof target === "object" && target !== null ? target : { value: target },
|
|
1391
1378
|
);
|
|
@@ -1453,12 +1440,9 @@ function destroyWithClass(element: Element, cls: string) {
|
|
|
1453
1440
|
*
|
|
1454
1441
|
* @param dst - The destination object/array/Map (proxied or unproxied).
|
|
1455
1442
|
* @param src - The source object/array/Map (proxied or unproxied). It won't be modified.
|
|
1456
|
-
* @
|
|
1457
|
-
*
|
|
1458
|
-
*
|
|
1459
|
-
* - {@link SHALLOW}: Performs a shallow copy; when an array/object of the right type doesn't exist in `dst` yet, a reference to the array/object in `src` will be made, instead of creating a copy. If the array/object already exists, it won't be replaced (by a reference), but all items will be individually checked and copied like normal, keeping changes (and therefore UI updates) to a minimum.
|
|
1460
|
-
* @template T - The type of the destination object.
|
|
1461
|
-
* @throws Error if attempting to copy an array into a non-array or vice versa (unless {@link MERGE} is set, allowing for sparse array updates).
|
|
1443
|
+
* @template T - The type of the objects being copied.
|
|
1444
|
+
* @returns `true` if any changes were made to `dst`, or `false` if not.
|
|
1445
|
+
* @throws Error if attempting to copy an array into a non-array or vice versa.
|
|
1462
1446
|
*
|
|
1463
1447
|
* @example Basic Copy
|
|
1464
1448
|
* ```typescript
|
|
@@ -1466,204 +1450,231 @@ function destroyWithClass(element: Element, cls: string) {
|
|
|
1466
1450
|
* const dest = proxy({ b: { d: 3 } });
|
|
1467
1451
|
* copy(dest, source);
|
|
1468
1452
|
* console.log(dest); // proxy({ a: 1, b: { c: 2 } })
|
|
1453
|
+
* copy(dest, 'b', { e: 4 });
|
|
1454
|
+
* console.log(dest); // proxy({ a: 1, b: { e: 4 } })
|
|
1469
1455
|
* ```
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
*
|
|
1475
|
-
*
|
|
1476
|
-
*
|
|
1477
|
-
*
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1456
|
+
*/
|
|
1457
|
+
|
|
1458
|
+
export function copy<T extends object>(dst: T, src: T): boolean;
|
|
1459
|
+
/**
|
|
1460
|
+
* Like above, but copies `src` into `dst[dstKey]`. This is useful if you're unsure if dst[dstKey]
|
|
1461
|
+
* already exists (as the right type of object) or if you don't want to subscribe to dst[dstKey].
|
|
1462
|
+
*
|
|
1463
|
+
* @param dstKey - Optional key in `dst` to copy into.
|
|
1464
|
+
*/
|
|
1465
|
+
export function copy<T extends object>(dst: T, dstKey: keyof T, src: T[typeof dstKey]): boolean;
|
|
1466
|
+
export function copy(a: any, b: any, c?: any): boolean {
|
|
1467
|
+
if (arguments.length > 2) return copySet(a, b, c, 0);
|
|
1468
|
+
return copyImpl(a, b, 0);
|
|
1469
|
+
}
|
|
1470
|
+
|
|
1471
|
+
function copySet(dst: any, dstKey: any, src: any, flags: number): boolean {
|
|
1472
|
+
let dstVal = peek(dst, dstKey);
|
|
1473
|
+
if (src === dstVal) return false;
|
|
1474
|
+
if (typeof dstVal === "object" && dstVal && typeof src === "object" && src && dstVal.constructor === src.constructor) {
|
|
1475
|
+
return copyImpl(dstVal, src, flags);
|
|
1476
|
+
}
|
|
1477
|
+
src = clone(src);
|
|
1478
|
+
if (dst instanceof Map) dst.set(dstKey, src);
|
|
1479
|
+
else dst[dstKey] = clone(src);
|
|
1480
|
+
return true;
|
|
1481
|
+
}
|
|
1482
|
+
|
|
1483
|
+
/**
|
|
1484
|
+
* Like {@link copy}, but uses merge semantics. Properties in `dst` not present in `src` are kept.
|
|
1485
|
+
* `null`/`undefined` in `src` delete properties in `dst`.
|
|
1486
|
+
*
|
|
1487
|
+
* When the destination is an object and the source is an array, its keys are used as (sparse) array indices.
|
|
1488
|
+
*
|
|
1489
|
+
* @example Basic merge
|
|
1488
1490
|
* ```typescript
|
|
1489
1491
|
* const source = { b: { c: 99 }, d: undefined }; // d: undefined will delete
|
|
1490
1492
|
* const dest = proxy({ a: 1, b: { x: 5 }, d: 4 });
|
|
1491
|
-
*
|
|
1492
|
-
*
|
|
1493
|
+
* merge(dest, source);
|
|
1494
|
+
* merge(dest, 'b', { y: 6 }); // merge into dest.b
|
|
1495
|
+
* merge(dest, 'c', { z: 7 }); // merge.c doesn't exist yet, so it will just be assigned
|
|
1496
|
+
* console.log(dest); // proxy({ a: 1, b: { c: 99, x: 5, y: 6 }, c: { z: 7 } })
|
|
1493
1497
|
* ```
|
|
1494
1498
|
*
|
|
1495
|
-
* @example Partial Array
|
|
1499
|
+
* @example Partial Array Merge
|
|
1496
1500
|
* ```typescript
|
|
1497
1501
|
* const messages = proxy(['msg1', 'msg2', 'msg3']);
|
|
1498
1502
|
* const update = { 1: 'updated msg2' }; // Update using object key as index
|
|
1499
|
-
*
|
|
1503
|
+
* merge(messages, update);
|
|
1500
1504
|
* console.log(messages); // proxy(['msg1', 'updated msg2', 'msg3'])
|
|
1501
1505
|
* ```
|
|
1502
1506
|
*
|
|
1503
|
-
* @example SHALLOW
|
|
1504
|
-
* ```typescript
|
|
1505
|
-
* const source = { nested: [1, 2] };
|
|
1506
|
-
* const dest = {};
|
|
1507
|
-
* copy(dest, source, SHALLOW);
|
|
1508
|
-
* dest.nested.push(3);
|
|
1509
|
-
* console.log(source.nested); // [1, 2, 3] (source was modified)
|
|
1510
|
-
* ```
|
|
1511
1507
|
*/
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
export function
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
// Overload for object destination with Map source
|
|
1518
|
-
export function copy<T extends Record<string | number | symbol, any>>(dst: T, src: Map<keyof T, T[keyof T]>, flags?: number): void;
|
|
1519
|
-
// Overload for same-type copying
|
|
1520
|
-
export function copy<T extends object>(dst: T, src: Partial<T>, flags?: number): void;
|
|
1521
|
-
// Implementation
|
|
1522
|
-
export function copy(dst: any, src: any, flags = 0) {
|
|
1523
|
-
copyRecurse(dst, src, flags);
|
|
1524
|
-
runImmediateQueue();
|
|
1508
|
+
export function merge<T extends object>(dst: T, value: Partial<T>): boolean;
|
|
1509
|
+
export function merge<T extends object>(dst: T, dstKey: keyof T, value: Partial<T[typeof dstKey]>): boolean;
|
|
1510
|
+
export function merge(a: any, b: any, c?: any) {
|
|
1511
|
+
if (arguments.length > 2) return copySet(a, b, c, MERGE);
|
|
1512
|
+
return copyImpl(a, b, MERGE);
|
|
1525
1513
|
}
|
|
1526
|
-
/** Flag to {@link copy} causing it to use merge semantics. See {@link copy} for details. */
|
|
1527
|
-
export const MERGE = 1;
|
|
1528
|
-
/** Flag to {@link copy} and {@link clone} causing them to create a shallow copy (instead of the deep copy done by default).*/
|
|
1529
|
-
export const SHALLOW = 2;
|
|
1530
|
-
const COPY_SUBSCRIBE = 32;
|
|
1531
|
-
const COPY_EMIT = 64;
|
|
1532
1514
|
|
|
1533
|
-
|
|
1534
|
-
* Clone an (optionally proxied) object or array.
|
|
1535
|
-
*
|
|
1536
|
-
* @param src The object or array to clone. If it is proxied, `clone` will subscribe to any changes to the (nested) data structure.
|
|
1537
|
-
* @param flags
|
|
1538
|
-
* - {@link SHALLOW}: Performs a shallow clone, meaning that only the top-level array or object will be copied, while object/array values will just be references to the original data in `src`.
|
|
1539
|
-
* @template T - The type of the objects being copied.
|
|
1540
|
-
* @returns A new unproxied array or object (of the same type as `src`), containing a deep (by default) copy of `src`.
|
|
1541
|
-
*/
|
|
1542
|
-
export function clone<T extends object>(src: T, flags = 0): T {
|
|
1543
|
-
let dst: T;
|
|
1544
|
-
if (src instanceof Map) {
|
|
1545
|
-
dst = new Map() as T;
|
|
1546
|
-
} else {
|
|
1547
|
-
dst = Object.create(Object.getPrototypeOf(src)) as T;
|
|
1548
|
-
}
|
|
1549
|
-
copyRecurse(dst, src, flags);
|
|
1550
|
-
return dst;
|
|
1551
|
-
}
|
|
1552
|
-
|
|
1553
|
-
function getEntries(subject: any) {
|
|
1554
|
-
return (subject instanceof Map) ? subject.entries() : Object.entries(subject);
|
|
1555
|
-
}
|
|
1556
|
-
|
|
1557
|
-
function getKeys(subject: any) {
|
|
1558
|
-
return (subject instanceof Map) ? subject.keys() : Object.keys(subject);
|
|
1559
|
-
}
|
|
1560
|
-
|
|
1561
|
-
function copyRecurse(dst: any, src: any, flags: number) {
|
|
1515
|
+
function copyImpl(dst: any, src: any, flags: number): boolean {
|
|
1562
1516
|
// We never want to subscribe to reads we do to the target (to find changes). So we'll
|
|
1563
1517
|
// take the unproxied version and `emit` updates ourselve.
|
|
1564
|
-
let unproxied = dst[TARGET_SYMBOL];
|
|
1518
|
+
let unproxied = (dst as any)[TARGET_SYMBOL];
|
|
1565
1519
|
if (unproxied) {
|
|
1566
1520
|
dst = unproxied;
|
|
1567
1521
|
flags |= COPY_EMIT;
|
|
1568
1522
|
}
|
|
1569
1523
|
// For performance, we'll work on the unproxied `src` and manually subscribe to changes.
|
|
1570
|
-
unproxied = src[TARGET_SYMBOL];
|
|
1524
|
+
unproxied = (src as any)[TARGET_SYMBOL];
|
|
1571
1525
|
if (unproxied) {
|
|
1572
1526
|
src = unproxied;
|
|
1573
1527
|
// If we're not in peek mode, we'll manually subscribe to all source reads.
|
|
1574
1528
|
if (currentScope !== ROOT_SCOPE && !peeking) flags |= COPY_SUBSCRIBE;
|
|
1575
1529
|
}
|
|
1576
1530
|
|
|
1531
|
+
return copyRecursive(dst, src, flags);
|
|
1532
|
+
}
|
|
1533
|
+
|
|
1534
|
+
// The dst and src parameters must be objects. Will throw a friendly message if they're not both the same type.
|
|
1535
|
+
function copyRecursive<T extends object>(dst: T, src: T, flags: number): boolean {
|
|
1536
|
+
|
|
1577
1537
|
if (flags & COPY_SUBSCRIBE) subscribe(src, ANY_SYMBOL);
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1538
|
+
let changed = false;
|
|
1539
|
+
|
|
1540
|
+
// The following loops are somewhat repetitive, but it keeps performance high by avoiding
|
|
1541
|
+
// function calls and extra checks within the loops.
|
|
1542
|
+
|
|
1543
|
+
if (src instanceof Array && dst instanceof Array) {
|
|
1581
1544
|
const dstLen = dst.length;
|
|
1582
1545
|
const srcLen = src.length;
|
|
1583
|
-
for (let
|
|
1584
|
-
copyValue(dst, i, src[i], flags);
|
|
1546
|
+
for (let index = 0; index < srcLen; index++) {
|
|
1547
|
+
// changed = copyValue(dst, i, src[i], flags) || changed;
|
|
1548
|
+
|
|
1549
|
+
let dstValue = dst[index];
|
|
1550
|
+
if (dstValue === undefined && !dst.hasOwnProperty(index)) dstValue = EMPTY;
|
|
1551
|
+
let srcValue = src[index];
|
|
1552
|
+
if (srcValue === undefined && !src.hasOwnProperty(index)) {
|
|
1553
|
+
delete dst[index];
|
|
1554
|
+
if (flags & COPY_EMIT) emit(dst, index, EMPTY, dstValue);
|
|
1555
|
+
changed = true;
|
|
1556
|
+
}
|
|
1557
|
+
else if (dstValue !== srcValue) {
|
|
1558
|
+
if (srcValue && typeof srcValue === "object") {
|
|
1559
|
+
if (typeof dstValue === "object" && dstValue && srcValue.constructor === dstValue.constructor) {
|
|
1560
|
+
changed = copyRecursive(dstValue, srcValue, flags) || changed;
|
|
1561
|
+
continue;
|
|
1562
|
+
}
|
|
1563
|
+
srcValue = clone(srcValue);
|
|
1564
|
+
}
|
|
1565
|
+
dst[index] = srcValue;
|
|
1566
|
+
|
|
1567
|
+
if (flags & COPY_EMIT) emit(dst, index, srcValue, dstValue);
|
|
1568
|
+
changed = true;
|
|
1569
|
+
}
|
|
1585
1570
|
}
|
|
1571
|
+
|
|
1586
1572
|
// Leaving additional values in the old array doesn't make sense, so we'll do this even when MERGE is set:
|
|
1587
1573
|
if (srcLen !== dstLen) {
|
|
1588
1574
|
if (flags & COPY_EMIT) {
|
|
1589
1575
|
for (let i = srcLen; i < dstLen; i++) {
|
|
1590
1576
|
const old = dst[i];
|
|
1591
|
-
dst[i]
|
|
1592
|
-
emit(dst, i,
|
|
1577
|
+
delete dst[i];
|
|
1578
|
+
emit(dst, i, EMPTY, old);
|
|
1593
1579
|
}
|
|
1594
1580
|
dst.length = srcLen;
|
|
1595
1581
|
emit(dst, "length", srcLen, dstLen);
|
|
1596
1582
|
} else {
|
|
1597
1583
|
dst.length = srcLen;
|
|
1598
1584
|
}
|
|
1585
|
+
changed = true;
|
|
1599
1586
|
}
|
|
1600
|
-
} else {
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1587
|
+
} else if (src instanceof Map && dst instanceof Map) {
|
|
1588
|
+
for (const key of src.keys()) {
|
|
1589
|
+
// changed = copyValue(dst, k, src.get(k), flags) || changed;
|
|
1590
|
+
let srcValue = src.get(key);
|
|
1591
|
+
let dstValue = dst.get(key);
|
|
1592
|
+
if (dstValue === undefined && !dst.has(key)) dstValue = EMPTY;
|
|
1593
|
+
if (dstValue !== srcValue) {
|
|
1594
|
+
if (srcValue && typeof srcValue === "object") {
|
|
1595
|
+
if (typeof dstValue === "object" && dstValue && srcValue.constructor === dstValue.constructor) {
|
|
1596
|
+
changed = copyRecursive(dstValue, srcValue, flags) || changed;
|
|
1597
|
+
continue;
|
|
1598
|
+
}
|
|
1599
|
+
srcValue = clone(srcValue);
|
|
1600
|
+
}
|
|
1601
|
+
|
|
1602
|
+
dst.set(key, srcValue);
|
|
1603
|
+
|
|
1604
|
+
if (flags & COPY_EMIT) emit(dst, key, srcValue, dstValue);
|
|
1605
|
+
changed = true;
|
|
1606
|
+
}
|
|
1604
1607
|
}
|
|
1605
|
-
|
|
1606
|
-
// Remove entries from dst that don't exist in src (unless MERGE flag is set)
|
|
1608
|
+
|
|
1607
1609
|
if (!(flags & MERGE)) {
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1610
|
+
for (const k of dst.keys()) {
|
|
1611
|
+
if (!src.has(k)) {
|
|
1612
|
+
const old = dst.get(k);
|
|
1613
|
+
dst.delete(k);
|
|
1614
|
+
if (flags & COPY_EMIT) {
|
|
1615
|
+
emit(dst, k, undefined, old);
|
|
1612
1616
|
}
|
|
1617
|
+
changed = true;
|
|
1613
1618
|
}
|
|
1614
|
-
}
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1619
|
+
}
|
|
1620
|
+
}
|
|
1621
|
+
} else if (src.constructor === dst.constructor) {
|
|
1622
|
+
for (const key of Object.keys(src) as (keyof typeof src)[]) {
|
|
1623
|
+
// changed = copyValue(dst, k, src[k as keyof typeof src], flags) || changed;
|
|
1624
|
+
let srcValue = src[key];
|
|
1625
|
+
const dstValue = dst.hasOwnProperty(key) ? dst[key] : EMPTY;
|
|
1626
|
+
if (dstValue !== srcValue) {
|
|
1627
|
+
if (srcValue && typeof srcValue === "object") {
|
|
1628
|
+
if (typeof dstValue === "object" && dstValue && srcValue.constructor === dstValue.constructor) {
|
|
1629
|
+
changed = copyRecursive(dstValue as typeof srcValue, srcValue, flags) || changed;
|
|
1630
|
+
continue;
|
|
1618
1631
|
}
|
|
1632
|
+
srcValue = clone(srcValue);
|
|
1619
1633
|
}
|
|
1634
|
+
|
|
1635
|
+
dst[key] = srcValue;
|
|
1636
|
+
|
|
1637
|
+
if (flags & COPY_EMIT) emit(dst, key, srcValue, dstValue);
|
|
1638
|
+
changed = true;
|
|
1620
1639
|
}
|
|
1621
1640
|
}
|
|
1622
|
-
}
|
|
1623
|
-
}
|
|
1624
1641
|
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1642
|
+
if (!(flags & MERGE)) {
|
|
1643
|
+
for (const k of Object.keys(dst) as (keyof typeof dst)[]) {
|
|
1644
|
+
if (!src.hasOwnProperty(k)) {
|
|
1645
|
+
const old = dst[k];
|
|
1646
|
+
delete dst[k];
|
|
1647
|
+
if (flags & COPY_EMIT && old !== undefined) {
|
|
1648
|
+
emit(dst, k, undefined, old);
|
|
1649
|
+
}
|
|
1650
|
+
changed = true;
|
|
1651
|
+
}
|
|
1652
|
+
}
|
|
1653
|
+
}
|
|
1630
1654
|
} else {
|
|
1631
|
-
|
|
1632
|
-
delete dst[key];
|
|
1633
|
-
}
|
|
1634
|
-
if (flags & COPY_EMIT && old !== undefined) {
|
|
1635
|
-
emit(dst, key, undefined, old);
|
|
1655
|
+
throw new Error(`Incompatible or non-object types: ${src?.constructor?.name || typeof src} vs ${dst?.constructor?.name || typeof dst}`);
|
|
1636
1656
|
}
|
|
1657
|
+
return changed;
|
|
1637
1658
|
}
|
|
1638
1659
|
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
if (
|
|
1643
|
-
srcValue && dstValue && typeof srcValue === "object" && typeof dstValue === "object" &&
|
|
1644
|
-
(srcValue.constructor === dstValue.constructor || (flags & MERGE && dstValue instanceof Array))
|
|
1645
|
-
) {
|
|
1646
|
-
copyRecurse(dstValue, srcValue, flags);
|
|
1647
|
-
return;
|
|
1648
|
-
}
|
|
1660
|
+
const MERGE = 1;
|
|
1661
|
+
const COPY_SUBSCRIBE = 32;
|
|
1662
|
+
const COPY_EMIT = 64;
|
|
1649
1663
|
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
}
|
|
1665
|
-
if (flags & COPY_EMIT) emit(dst, index, srcValue, dstValue);
|
|
1666
|
-
}
|
|
1664
|
+
/**
|
|
1665
|
+
* Clone an (optionally proxied) object or array.
|
|
1666
|
+
*
|
|
1667
|
+
* @param src The object or array to clone. If it is proxied, `clone` will subscribe to any changes to the (nested) data structure.
|
|
1668
|
+
* @template T - The type of the objects being copied.
|
|
1669
|
+
* @returns A new unproxied array or object (of the same type as `src`), containing a deep copy of `src`.
|
|
1670
|
+
*/
|
|
1671
|
+
export function clone<T extends object>(src: T): T {
|
|
1672
|
+
// Create an empty object of the same type
|
|
1673
|
+
const copied = Array.isArray(src) ? [] : src instanceof Map ? new Map() : Object.create(Object.getPrototypeOf(src));
|
|
1674
|
+
// Copy all properties to it. This doesn't need to emit anything, and because
|
|
1675
|
+
// the destination is an empty object, we can just MERGE, which is a bit faster.
|
|
1676
|
+
copyImpl(copied, src, MERGE);
|
|
1677
|
+
return copied;
|
|
1667
1678
|
}
|
|
1668
1679
|
|
|
1669
1680
|
interface RefTarget {
|
|
@@ -1771,7 +1782,7 @@ function applyBind(el: HTMLInputElement, target: any) {
|
|
|
1771
1782
|
throw new Error(`SELECT has no '${target.value}' OPTION (yet)`);
|
|
1772
1783
|
};
|
|
1773
1784
|
}
|
|
1774
|
-
|
|
1785
|
+
derive(onProxyChange);
|
|
1775
1786
|
el.addEventListener("input", onInputChange);
|
|
1776
1787
|
clean(() => {
|
|
1777
1788
|
el.removeEventListener("input", onInputChange);
|
|
@@ -1808,11 +1819,6 @@ const SPECIAL_PROPS: { [key: string]: (value: any) => void } = {
|
|
|
1808
1819
|
text: (value: any) => {
|
|
1809
1820
|
addNode(document.createTextNode(value));
|
|
1810
1821
|
},
|
|
1811
|
-
element: (value: any) => {
|
|
1812
|
-
console.log("Aberdeen: $({element: myElement}) is deprecated, use $(myElement) instead");
|
|
1813
|
-
addNode(value);
|
|
1814
|
-
SPECIAL_PROPS.element = addNode; // Avoid the console.log next time
|
|
1815
|
-
},
|
|
1816
1822
|
};
|
|
1817
1823
|
|
|
1818
1824
|
/**
|
|
@@ -1823,12 +1829,14 @@ const SPECIAL_PROPS: { [key: string]: (value: any) => void } = {
|
|
|
1823
1829
|
* @param {...(string | function | object | false | undefined | null)} args - Any number of arguments can be given. How they're interpreted depends on their types:
|
|
1824
1830
|
*
|
|
1825
1831
|
* - `string`: Strings can be used to create and insert new elements, set classnames for the *current* element, and add text to the current element.
|
|
1826
|
-
* The format of a string is: **tag
|
|
1827
|
-
*
|
|
1828
|
-
* -
|
|
1829
|
-
* - Any number of CSS classes prefixed by `.` characters. These classes will be added to the *current* element.
|
|
1830
|
-
* -
|
|
1831
|
-
*
|
|
1832
|
+
* The format of a string is: (**tag** | `.` **class** | **key**=**val** | **key**="**long val**")* (':' **text** | **key**=)?
|
|
1833
|
+
* So there can be:
|
|
1834
|
+
* - Any number of **tag** element, like `h1` or `div`. These elements are created, added to the *current* element, and become the new *current* element for the rest of this `$` function execution.
|
|
1835
|
+
* - Any number of CSS classes prefixed by `.` characters. These classes will be added to the *current* element. Optionally, CSS classes can be appended to a **tag** without a space. So both `div.myclass` and `div .myclass` are valid and do the same thing.
|
|
1836
|
+
* - Any number of key/value pairs with string values, like `placeholder="Your name"` or `data-id=123`. These will be handled according to the rules specified for `object`, below, but with the caveat that values can only be strings. The quotes around string values are optional, unless the value contains spaces. It's not possible to escape quotes within the value. If you want to do that, or if you have user-provided values, use the `object` syntax (see below) or end your string with `key=` followed by the data as a separate argument (see below).
|
|
1837
|
+
* - The string may end in a ':' followed by text, which will be added as a TextNode to the *current* element. The text ranges til the end of the string, and may contain any characters, including spaces and quotes.
|
|
1838
|
+
* - Alternatively, the string may end in a key followed by an '=' character, in which case the value is expected as a separate argument. The key/value pair is set according to the rules specified for `object` below. This is useful when the value is not a string or contains spaces or user data. Example: `$('button text="Click me" click=', () => alert('Clicked!'))` or `$('input.value=', someUserData, "placeholder=", "Type your stuff")`.
|
|
1839
|
+
* - `function`: When a function (without argument nor a return value) is passed in, it will be reactively executed in its own observer scope, preserving the *current element*. So any `$()` invocations within this function will create DOM elements with our *current* element as parent. If the function reads observable data, and that data is changed later on, the function we re-execute (after side effects, such as DOM modifications through `$`, have been cleaned - see also {@link clean}).
|
|
1832
1840
|
* - `object`: When an object is passed in, its key-value pairs are used to modify the *current* element in the following ways...
|
|
1833
1841
|
* - `{<attrName>: any}`: The common case is setting the value as an HTML attribute named key. So `{placeholder: "Your name"}` would add `placeholder="Your name"` to the current HTML element.
|
|
1834
1842
|
* - `{<propName>: boolean}` or `{value: any}` or `{selectedIndex: number}`: If the value is a boolean, or if the key is `value` or `selectedIndex`, it is set on the `current` element as a DOM property instead of an HTML attribute. For example `{checked: true}` would do `el.checked = true` for the *current* element.
|
|
@@ -1839,7 +1847,7 @@ const SPECIAL_PROPS: { [key: string]: (value: any) => void } = {
|
|
|
1839
1847
|
* - `{destroy: string}`: When the *current* element is a top-level element to be removed (due to reactivity cleanup), actual removal from the DOM is delayed by 2 seconds, and in the mean time the value string is added as a CSS class to the element, allowing for a deletion transition. The string may also contain multiple dot-separated CSS classes, such as `.fade.shrink`.
|
|
1840
1848
|
* - `{create: function}` and `{destroy: function}`: The function is invoked when the *current* element is the top-level element being created/destroyed. It can be used for more involved creation/deletion animations. In case of `destroy`, the function is responsible for actually removing the element from the DOM (eventually). See `transitions.ts` in the Aberdeen source code for some examples.
|
|
1841
1849
|
* - `{bind: <obsValue>}`: Create a two-way binding element between the `value` property of the given observable (proxy) variable, and the *current* input element (`<input>`, `<select>` or `<textarea>`). This is often used together with {@link ref}, in order to use properties other than `.value`.
|
|
1842
|
-
* - `{<any>: <obsvalue>}`: Create a new
|
|
1850
|
+
* - `{<any>: <obsvalue>}`: Create a new observer scope and read the `value` property of the given observable (proxy) variable from within it, and apply the contained value using any of the other rules in this list. Example:
|
|
1843
1851
|
* ```typescript
|
|
1844
1852
|
* const myColor = proxy('red');
|
|
1845
1853
|
* $('p:Test', {$color: myColor, click: () => myColor.value = 'yellow'})
|
|
@@ -1861,6 +1869,14 @@ const SPECIAL_PROPS: { [key: string]: (value: any) => void } = {
|
|
|
1861
1869
|
* $color: 'red'
|
|
1862
1870
|
* });
|
|
1863
1871
|
* ```
|
|
1872
|
+
*
|
|
1873
|
+
* Which can also be written as:
|
|
1874
|
+
* ```typescript
|
|
1875
|
+
* $('button.secondary.outline text=Submit $color=red disabled=', false, 'click=', () => console.log('Clicked!'));
|
|
1876
|
+
* ```
|
|
1877
|
+
*
|
|
1878
|
+
* We want to set `disabled` as a property instead of an attribute, so we must use the `key=` syntax in order to provide
|
|
1879
|
+
* `false` as a boolean instead of a string.
|
|
1864
1880
|
*
|
|
1865
1881
|
* @example Create Nested Elements
|
|
1866
1882
|
* ```typescript
|
|
@@ -1914,24 +1930,85 @@ export function $(
|
|
|
1914
1930
|
let savedCurrentScope: undefined | ContentScope;
|
|
1915
1931
|
let err: undefined | string;
|
|
1916
1932
|
let result: undefined | Element;
|
|
1933
|
+
let nextArgIsProp: undefined | string;
|
|
1917
1934
|
|
|
1918
1935
|
for (let arg of args) {
|
|
1919
|
-
if (
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1936
|
+
if (nextArgIsProp) {
|
|
1937
|
+
applyArg(nextArgIsProp, arg);
|
|
1938
|
+
nextArgIsProp = undefined;
|
|
1939
|
+
} else if (arg == null || arg === false) {
|
|
1940
|
+
// Ignore
|
|
1941
|
+
} else if (typeof arg === "string") {
|
|
1942
|
+
let pos = 0;
|
|
1943
|
+
let argLen = arg.length;
|
|
1944
|
+
while(pos < argLen) {
|
|
1945
|
+
let nextSpace = arg.indexOf(" ", pos);
|
|
1946
|
+
if (nextSpace < 0) nextSpace = arg.length;
|
|
1947
|
+
let part = arg.substring(pos, nextSpace);
|
|
1948
|
+
const oldPos = pos;
|
|
1949
|
+
pos = nextSpace + 1;
|
|
1950
|
+
|
|
1951
|
+
const firstIs = part.indexOf('=');
|
|
1952
|
+
const firstColon = part.indexOf(':');
|
|
1953
|
+
if (firstIs >= 0 && (firstColon < 0 || firstIs < firstColon)) {
|
|
1954
|
+
const prop = part.substring(0, firstIs);
|
|
1955
|
+
if (firstIs < part.length - 1) {
|
|
1956
|
+
let value = part.substring(firstIs + 1);
|
|
1957
|
+
if (value[0] === '"') {
|
|
1958
|
+
const closeIndex = arg.indexOf('"', firstIs+2+oldPos);
|
|
1959
|
+
if (closeIndex < 0) throw new Error(`Unterminated string for '${prop}'`);
|
|
1960
|
+
value = arg.substring(firstIs+2+oldPos, closeIndex);
|
|
1961
|
+
pos = closeIndex + 1;
|
|
1962
|
+
if (arg[pos] === ' ') pos++;
|
|
1963
|
+
}
|
|
1964
|
+
applyArg(prop, value);
|
|
1965
|
+
continue;
|
|
1966
|
+
} else {
|
|
1967
|
+
if (pos < argLen) throw new Error(`No value given for '${part}'`);
|
|
1968
|
+
nextArgIsProp = prop;
|
|
1969
|
+
break
|
|
1970
|
+
}
|
|
1971
|
+
}
|
|
1972
|
+
|
|
1973
|
+
let text;
|
|
1974
|
+
if (firstColon >= 0) {
|
|
1975
|
+
// Read to the end of the arg, ignoring any spaces
|
|
1976
|
+
text = arg.substring(firstColon + 1 + oldPos);
|
|
1977
|
+
part = part.substring(0, firstColon);
|
|
1978
|
+
if (!text) {
|
|
1979
|
+
if (pos < argLen) throw new Error(`No value given for '${part}'`);
|
|
1980
|
+
nextArgIsProp = 'text';
|
|
1981
|
+
break;
|
|
1982
|
+
}
|
|
1983
|
+
pos = argLen;
|
|
1984
|
+
}
|
|
1985
|
+
|
|
1986
|
+
let classes: undefined | string;
|
|
1987
|
+
const classPos = part.indexOf(".");
|
|
1988
|
+
if (classPos >= 0) {
|
|
1989
|
+
classes = part.substring(classPos + 1);
|
|
1990
|
+
part = part.substring(0, classPos);
|
|
1991
|
+
}
|
|
1992
|
+
|
|
1993
|
+
if (part) { // Add an element
|
|
1994
|
+
// Determine which namespace to use for element creation
|
|
1995
|
+
const svg = currentScope.inSvgNamespace || part === 'svg';
|
|
1996
|
+
if (svg) {
|
|
1997
|
+
result = document.createElementNS('http://www.w3.org/2000/svg', part);
|
|
1998
|
+
} else {
|
|
1999
|
+
result = document.createElement(part);
|
|
2000
|
+
}
|
|
2001
|
+
addNode(result);
|
|
2002
|
+
if (!savedCurrentScope) savedCurrentScope = currentScope;
|
|
2003
|
+
const newScope = new ChainedScope(result, true);
|
|
2004
|
+
|
|
2005
|
+
// SVG namespace should be inherited by children
|
|
2006
|
+
if (svg) newScope.inSvgNamespace = true;
|
|
2007
|
+
|
|
2008
|
+
if (topRedrawScope === currentScope) topRedrawScope = newScope;
|
|
2009
|
+
currentScope = newScope;
|
|
2010
|
+
}
|
|
2011
|
+
|
|
1935
2012
|
if (text) addNode(document.createTextNode(text));
|
|
1936
2013
|
if (classes) {
|
|
1937
2014
|
const el = currentScope.parentElement;
|
|
@@ -1940,34 +2017,6 @@ export function $(
|
|
|
1940
2017
|
clean(() => el.classList.remove(...classes.split(".")));
|
|
1941
2018
|
}
|
|
1942
2019
|
}
|
|
1943
|
-
} else if (arg.indexOf(" ") >= 0) {
|
|
1944
|
-
err = `Tag '${arg}' cannot contain space`;
|
|
1945
|
-
break;
|
|
1946
|
-
} else {
|
|
1947
|
-
// Determine which namespace to use for element creation
|
|
1948
|
-
const useNamespace = currentScope.inSvgNamespace || arg === 'svg';
|
|
1949
|
-
if (useNamespace) {
|
|
1950
|
-
result = document.createElementNS('http://www.w3.org/2000/svg', arg);
|
|
1951
|
-
} else {
|
|
1952
|
-
result = document.createElement(arg);
|
|
1953
|
-
}
|
|
1954
|
-
|
|
1955
|
-
if (classes) result.className = classes.replaceAll(".", " ");
|
|
1956
|
-
if (text) result.textContent = text;
|
|
1957
|
-
addNode(result);
|
|
1958
|
-
if (!savedCurrentScope) {
|
|
1959
|
-
savedCurrentScope = currentScope;
|
|
1960
|
-
}
|
|
1961
|
-
const newScope = new ChainedScope(result, true);
|
|
1962
|
-
|
|
1963
|
-
// If we're creating an SVG element, set the SVG namespace flag for child scopes
|
|
1964
|
-
if (arg === 'svg') {
|
|
1965
|
-
newScope.inSvgNamespace = true;
|
|
1966
|
-
}
|
|
1967
|
-
|
|
1968
|
-
newScope.lastChild = result.lastChild || undefined;
|
|
1969
|
-
if (topRedrawScope === currentScope) topRedrawScope = newScope;
|
|
1970
|
-
currentScope = newScope;
|
|
1971
2020
|
}
|
|
1972
2021
|
} else if (typeof arg === "object") {
|
|
1973
2022
|
if (arg.constructor !== Object) {
|
|
@@ -1984,7 +2033,7 @@ export function $(
|
|
|
1984
2033
|
break;
|
|
1985
2034
|
}
|
|
1986
2035
|
} else {
|
|
1987
|
-
for (const key
|
|
2036
|
+
for (const key of Object.keys(arg)) {
|
|
1988
2037
|
const val = arg[key];
|
|
1989
2038
|
applyArg(key, val);
|
|
1990
2039
|
}
|
|
@@ -1996,9 +2045,8 @@ export function $(
|
|
|
1996
2045
|
break;
|
|
1997
2046
|
}
|
|
1998
2047
|
}
|
|
1999
|
-
if (
|
|
2000
|
-
|
|
2001
|
-
}
|
|
2048
|
+
if (nextArgIsProp !== undefined) throw new Error(`No value given for '${nextArgIsProp}='`);
|
|
2049
|
+
if (savedCurrentScope) currentScope = savedCurrentScope;
|
|
2002
2050
|
if (err) throw new Error(err);
|
|
2003
2051
|
return result;
|
|
2004
2052
|
}
|
|
@@ -2072,7 +2120,7 @@ export function insertCss(style: object, global = false): string {
|
|
|
2072
2120
|
function styleToCss(style: object, prefix: string): string {
|
|
2073
2121
|
let props = "";
|
|
2074
2122
|
let rules = "";
|
|
2075
|
-
for (const kOr
|
|
2123
|
+
for (const kOr of Object.keys(style)) {
|
|
2076
2124
|
const v = (style as any)[kOr];
|
|
2077
2125
|
for (const k of kOr.split(/, ?/g)) {
|
|
2078
2126
|
if (v && typeof v === "object") {
|
|
@@ -2147,7 +2195,7 @@ let onError: (error: Error) => boolean | undefined = defaultOnError;
|
|
|
2147
2195
|
/**
|
|
2148
2196
|
* Sets a custom error handler function for errors that occur asynchronously
|
|
2149
2197
|
* within reactive scopes (e.g., during updates triggered by proxy changes in
|
|
2150
|
-
* {@link
|
|
2198
|
+
* {@link derive} or {@link $} render functions).
|
|
2151
2199
|
*
|
|
2152
2200
|
* The default handler logs the error to `console.error` and adds a simple
|
|
2153
2201
|
* 'Error' message div to the DOM at the location where the error occurred (if possible).
|
|
@@ -2235,7 +2283,7 @@ export function getParentElement(): Element {
|
|
|
2235
2283
|
* This is useful for releasing resources, removing manual event listeners, or cleaning up
|
|
2236
2284
|
* side effects associated with the scope. Cleaners are run in reverse order of registration.
|
|
2237
2285
|
*
|
|
2238
|
-
* Scopes are created by functions like {@link
|
|
2286
|
+
* Scopes are created by functions like {@link derive}, {@link mount}, {@link $} (when given a render function),
|
|
2239
2287
|
* and internally by constructs like {@link onEach}.
|
|
2240
2288
|
*
|
|
2241
2289
|
* @param cleaner - The function to execute during cleanup.
|
|
@@ -2253,7 +2301,7 @@ export function getParentElement(): Element {
|
|
|
2253
2301
|
* peek(() => sum.value += item);
|
|
2254
2302
|
* // Clean gets called before each rerun for a certain item index
|
|
2255
2303
|
* // No need for peek here, as the clean code doesn't run in an
|
|
2256
|
-
* //
|
|
2304
|
+
* // observer scope.
|
|
2257
2305
|
* clean(() => sum.value -= item);
|
|
2258
2306
|
* })
|
|
2259
2307
|
*
|
|
@@ -2281,7 +2329,7 @@ export function clean(cleaner: () => void) {
|
|
|
2281
2329
|
*
|
|
2282
2330
|
* @param func - The function to execute reactively. Any DOM manipulations should typically
|
|
2283
2331
|
* be done using {@link $} within this function. Its return value will be made available as an
|
|
2284
|
-
* observable returned by the `
|
|
2332
|
+
* observable returned by the `derive()` function.
|
|
2285
2333
|
* @returns An observable object, with its `value` property containing whatever the last run of `func` returned.
|
|
2286
2334
|
*
|
|
2287
2335
|
* @example Observation creating a UI components
|
|
@@ -2292,7 +2340,7 @@ export function clean(cleaner: () => void) {
|
|
|
2292
2340
|
* console.log('Welcome');
|
|
2293
2341
|
* $('h3:Welcome, ' + data.user); // Reactive text
|
|
2294
2342
|
*
|
|
2295
|
-
*
|
|
2343
|
+
* derive(() => {
|
|
2296
2344
|
* // When data.notifications changes, only this inner scope reruns,
|
|
2297
2345
|
* // leaving the `<p>Welcome, ..</p>` untouched.
|
|
2298
2346
|
* console.log('Notifications');
|
|
@@ -2302,13 +2350,13 @@ export function clean(cleaner: () => void) {
|
|
|
2302
2350
|
* });
|
|
2303
2351
|
* ```
|
|
2304
2352
|
*
|
|
2305
|
-
* ***Note*** that the above could just as easily be done using `$(func)` instead of `
|
|
2353
|
+
* ***Note*** that the above could just as easily be done using `$(func)` instead of `derive(func)`.
|
|
2306
2354
|
*
|
|
2307
2355
|
* @example Observation with return value
|
|
2308
2356
|
* ```typescript
|
|
2309
2357
|
* const counter = proxy(0);
|
|
2310
2358
|
* setInterval(() => counter.value++, 1000);
|
|
2311
|
-
* const double =
|
|
2359
|
+
* const double = derive(() => counter.value * 2);
|
|
2312
2360
|
*
|
|
2313
2361
|
* $('h3', () => {
|
|
2314
2362
|
* $(`:counter=${counter.value} double=${double.value}`);
|
|
@@ -2318,42 +2366,10 @@ export function clean(cleaner: () => void) {
|
|
|
2318
2366
|
* @overload
|
|
2319
2367
|
* @param func Func without a return value.
|
|
2320
2368
|
*/
|
|
2321
|
-
export function
|
|
2369
|
+
export function derive<T>(func: () => T): ValueRef<T> {
|
|
2322
2370
|
return new ResultScope<T>(currentScope.parentElement, func).result;
|
|
2323
2371
|
}
|
|
2324
2372
|
|
|
2325
|
-
/**
|
|
2326
|
-
* Similar to {@link observe}, creates a reactive scope that re-executes the function
|
|
2327
|
-
* when its proxied dependencies change.
|
|
2328
|
-
*
|
|
2329
|
-
* **Difference:** Updates run **synchronously and immediately** after the proxy modification
|
|
2330
|
-
* that triggered the update occurs.
|
|
2331
|
-
*
|
|
2332
|
-
* **Caution:** Use sparingly. Immediate execution bypasses Aberdeen's usual batching and
|
|
2333
|
-
* ordering optimizations, which can lead to performance issues or observing inconsistent
|
|
2334
|
-
* intermediate states if multiple related updates are applied sequentially.
|
|
2335
|
-
* Prefer {@link observe} or {@link $} for most use cases.
|
|
2336
|
-
*
|
|
2337
|
-
* @param func - The function to execute reactively and synchronously.
|
|
2338
|
-
*
|
|
2339
|
-
* @example
|
|
2340
|
-
* ```javascript
|
|
2341
|
-
* const state = proxy({ single: 'A' });
|
|
2342
|
-
*
|
|
2343
|
-
* immediateObserve(() => {
|
|
2344
|
-
* state.double = state.single + state.single
|
|
2345
|
-
* });
|
|
2346
|
-
* console.log(state.double); // 'AA'
|
|
2347
|
-
*
|
|
2348
|
-
* state.single = 'B';
|
|
2349
|
-
* // Synchronously:
|
|
2350
|
-
* console.log(state.double); // 'BB'
|
|
2351
|
-
* ```
|
|
2352
|
-
*/
|
|
2353
|
-
export function immediateObserve(func: () => void) {
|
|
2354
|
-
new ImmediateScope(currentScope.parentElement, func);
|
|
2355
|
-
}
|
|
2356
|
-
|
|
2357
2373
|
/**
|
|
2358
2374
|
* Attaches a reactive Aberdeen UI fragment to an existing DOM element. Without the use of
|
|
2359
2375
|
* this function, {@link $} will assume `document.body` as its root.
|
|
@@ -2363,11 +2379,11 @@ export function immediateObserve(func: () => void) {
|
|
|
2363
2379
|
* will cause it to re-execute when the data changes, updating the DOM elements created within it.
|
|
2364
2380
|
*
|
|
2365
2381
|
* Calls to {@link $} inside `func` will append nodes to `parentElement`.
|
|
2366
|
-
* You can nest {@link
|
|
2382
|
+
* You can nest {@link derive} or other {@link $} scopes within `func`.
|
|
2367
2383
|
* Use {@link unmountAll} to clean up all mounted scopes and their DOM nodes.
|
|
2368
2384
|
*
|
|
2369
2385
|
* Mounting scopes happens reactively, meaning that if this function is called from within another
|
|
2370
|
-
* ({@link
|
|
2386
|
+
* ({@link derive} or {@link $} or {@link mount}) scope that gets cleaned up, so will the mount.
|
|
2371
2387
|
*
|
|
2372
2388
|
* @param parentElement - The native DOM `Element` to which the UI fragment will be appended.
|
|
2373
2389
|
* @param func - The function that defines the UI fragment, typically containing calls to {@link $}.
|
|
@@ -2403,7 +2419,7 @@ export function mount(parentElement: Element, func: () => void) {
|
|
|
2403
2419
|
|
|
2404
2420
|
/**
|
|
2405
2421
|
* Removes all Aberdeen-managed DOM nodes and stops all active reactive scopes
|
|
2406
|
-
* (created by {@link mount}, {@link
|
|
2422
|
+
* (created by {@link mount}, {@link derive}, {@link $} with functions, etc.).
|
|
2407
2423
|
*
|
|
2408
2424
|
* This effectively cleans up the entire Aberdeen application state.
|
|
2409
2425
|
*/
|
|
@@ -2413,20 +2429,21 @@ export function unmountAll() {
|
|
|
2413
2429
|
}
|
|
2414
2430
|
|
|
2415
2431
|
/**
|
|
2416
|
-
* Executes a function *without* creating subscriptions in the current reactive scope, and returns its result.
|
|
2432
|
+
* Executes a function or retrieves a value *without* creating subscriptions in the current reactive scope, and returns its result.
|
|
2417
2433
|
*
|
|
2418
|
-
* This is useful when you need to access reactive data inside a reactive scope (like {@link
|
|
2434
|
+
* This is useful when you need to access reactive data inside a reactive scope (like {@link $})
|
|
2419
2435
|
* but do not want changes to that specific data to trigger a re-execute of the scope.
|
|
2436
|
+
*
|
|
2437
|
+
* Note: You may also use {@link unproxy} to get to the raw underlying data structure, which can be used to similar effect.
|
|
2420
2438
|
*
|
|
2421
|
-
* @
|
|
2422
|
-
*
|
|
2423
|
-
* @
|
|
2424
|
-
* @returns Whatever `func` returns.
|
|
2439
|
+
* @param target - Either a function to execute, or an object (which may also be an Array or a Map) to index.
|
|
2440
|
+
* @param key - Optional key/index to use when `target` is an object.
|
|
2441
|
+
* @returns The result of the function call, or the value at `target[key]` when `target` is an object or `target.get(key)` when it's a Map.
|
|
2425
2442
|
*
|
|
2426
|
-
* @example Peeking within
|
|
2443
|
+
* @example Peeking within observer
|
|
2427
2444
|
* ```typescript
|
|
2428
2445
|
* const data = proxy({ a: 1, b: 2 });
|
|
2429
|
-
*
|
|
2446
|
+
* $(() => {
|
|
2430
2447
|
* // re-executes only when data.a changes, because data.b is peeked.
|
|
2431
2448
|
* const b = peek(() => data.b);
|
|
2432
2449
|
* console.log(`A is ${data.a}, B was ${b} when A changed.`);
|
|
@@ -2436,10 +2453,20 @@ export function unmountAll() {
|
|
|
2436
2453
|
* ```
|
|
2437
2454
|
*
|
|
2438
2455
|
*/
|
|
2439
|
-
|
|
2456
|
+
|
|
2457
|
+
export function peek<T extends object>(target: T, key: keyof T): T[typeof key];
|
|
2458
|
+
export function peek<K,V>(target: Map<K,V>, key: K): V | undefined;
|
|
2459
|
+
export function peek<T>(target: T[], key: number): T | undefined;
|
|
2460
|
+
export function peek<T>(target: () => T): T;
|
|
2461
|
+
|
|
2462
|
+
export function peek(target: any, key?: any) {
|
|
2440
2463
|
peeking++;
|
|
2441
2464
|
try {
|
|
2442
|
-
|
|
2465
|
+
if (arguments.length === 1) {
|
|
2466
|
+
return target();
|
|
2467
|
+
} else {
|
|
2468
|
+
return (target instanceof Map) ? target.get(key) : target[key];
|
|
2469
|
+
}
|
|
2443
2470
|
} finally {
|
|
2444
2471
|
peeking--;
|
|
2445
2472
|
}
|
|
@@ -2450,16 +2477,16 @@ export function map<K, IN, OUT>(
|
|
|
2450
2477
|
source: Map<K, IN>,
|
|
2451
2478
|
func: (value: IN, key: K) => undefined | OUT,
|
|
2452
2479
|
): Map<K, OUT>;
|
|
2453
|
-
/** When using an object as `source`. */
|
|
2454
|
-
export function map<IN, const IN_KEY extends string | number | symbol, OUT>(
|
|
2455
|
-
source: Record<IN_KEY, IN>,
|
|
2456
|
-
func: (value: IN, index: KeyToString<IN_KEY>) => undefined | OUT,
|
|
2457
|
-
): Record<string | symbol, OUT>;
|
|
2458
2480
|
/** When using an array as `source`. */
|
|
2459
2481
|
export function map<IN, OUT>(
|
|
2460
2482
|
source: Array<IN>,
|
|
2461
2483
|
func: (value: IN, index: number) => undefined | OUT,
|
|
2462
2484
|
): Array<OUT>;
|
|
2485
|
+
/** When using an object as `source`. */
|
|
2486
|
+
export function map<IN, const IN_KEY extends string | number | symbol, OUT>(
|
|
2487
|
+
source: Record<IN_KEY, IN>,
|
|
2488
|
+
func: (value: IN, index: KeyToString<IN_KEY>) => undefined | OUT,
|
|
2489
|
+
): Record<string | symbol, OUT>;
|
|
2463
2490
|
/**
|
|
2464
2491
|
* Reactively maps/filters items from a proxied source array or object to a new proxied array or object.
|
|
2465
2492
|
*
|
|
@@ -2483,7 +2510,7 @@ export function map<IN, OUT>(
|
|
|
2483
2510
|
* const doubled = map(numbers, (n) => n * 2);
|
|
2484
2511
|
* // doubled is proxy([2, 4, 6])
|
|
2485
2512
|
*
|
|
2486
|
-
*
|
|
2513
|
+
* $(() => console.log(doubled)); // Logs updates
|
|
2487
2514
|
* numbers.push(4); // doubled becomes proxy([2, 4, 6, 8])
|
|
2488
2515
|
* ```
|
|
2489
2516
|
*
|
|
@@ -2497,7 +2524,7 @@ export function map<IN, OUT>(
|
|
|
2497
2524
|
*
|
|
2498
2525
|
* const activeUserNames = map(users, (user) => user.active ? user.name : undefined);
|
|
2499
2526
|
* // activeUserNames is proxy({ u1: 'Alice', u3: 'Charlie' })
|
|
2500
|
-
*
|
|
2527
|
+
* $(() => console.log(Object.values(activeUserNames)));
|
|
2501
2528
|
*
|
|
2502
2529
|
* users.u2.active = true;
|
|
2503
2530
|
* // activeUserNames becomes proxy({ u1: 'Alice', u2: 'Bob', u3: 'Charlie' })
|
|
@@ -2601,9 +2628,9 @@ export function multiMap(
|
|
|
2601
2628
|
onEach(source, (item: any, key: symbol | string | number) => {
|
|
2602
2629
|
const pairs = func(item, key);
|
|
2603
2630
|
if (pairs) {
|
|
2604
|
-
for (const key
|
|
2631
|
+
for (const key of Object.keys(pairs)) out[key] = pairs[key];
|
|
2605
2632
|
clean(() => {
|
|
2606
|
-
for (const key
|
|
2633
|
+
for (const key of Object.keys(pairs)) delete out[key];
|
|
2607
2634
|
});
|
|
2608
2635
|
}
|
|
2609
2636
|
});
|