aberdeen 1.0.12 → 1.1.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 +39 -5
- package/dist/aberdeen.d.ts +58 -100
- package/dist/aberdeen.js +244 -186
- 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/route.d.ts +79 -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/route.js +2 -2
- package/dist-min/route.js.map +3 -3
- package/package.json +6 -1
- package/src/aberdeen.ts +348 -352
- package/src/dispatcher.ts +130 -0
- package/src/route.ts +272 -181
- package/html-to-aberdeen +0 -354
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] : undefined;
|
|
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,48 +1107,97 @@ 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
|
|
|
1131
|
+
/**
|
|
1132
|
+
* Helper functions that wrap iterators to proxy values
|
|
1133
|
+
*/
|
|
1134
|
+
function wrapIteratorSingle(iterator: IterableIterator<any>): IterableIterator<any> {
|
|
1135
|
+
return {
|
|
1136
|
+
[Symbol.iterator]() { return this; },
|
|
1137
|
+
next() {
|
|
1138
|
+
const result = iterator.next();
|
|
1139
|
+
if (result.done) return result;
|
|
1140
|
+
return {
|
|
1141
|
+
done: false,
|
|
1142
|
+
value: optProxy(result.value)
|
|
1143
|
+
};
|
|
1144
|
+
}
|
|
1145
|
+
};
|
|
1146
|
+
}
|
|
1147
|
+
function wrapIteratorPair(iterator: IterableIterator<[any, any]>): IterableIterator<[any, any]> {
|
|
1148
|
+
return {
|
|
1149
|
+
[Symbol.iterator]() { return this; },
|
|
1150
|
+
next() {
|
|
1151
|
+
const result = iterator.next();
|
|
1152
|
+
if (result.done) return result;
|
|
1153
|
+
return {
|
|
1154
|
+
done: false,
|
|
1155
|
+
value: [optProxy(result.value[0]), optProxy(result.value[1])]
|
|
1156
|
+
};
|
|
1157
|
+
}
|
|
1158
|
+
};
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1151
1161
|
const mapMethodHandlers = {
|
|
1152
1162
|
get(this: any, key: any): any {
|
|
1153
1163
|
const target: Map<any, any> = this[TARGET_SYMBOL];
|
|
1164
|
+
// Make sure key is unproxied
|
|
1165
|
+
if (typeof key === "object" && key)
|
|
1166
|
+
key = (key as any)[TARGET_SYMBOL] || key;
|
|
1154
1167
|
subscribe(target, key);
|
|
1155
1168
|
return optProxy(target.get(key));
|
|
1156
1169
|
},
|
|
1157
1170
|
set(this: any, key: any, newData: any): any {
|
|
1158
1171
|
const target: Map<any, any> = this[TARGET_SYMBOL];
|
|
1159
|
-
// Make sure newData
|
|
1160
|
-
if (typeof
|
|
1172
|
+
// Make sure key and newData are unproxied
|
|
1173
|
+
if (typeof key === "object" && key) {
|
|
1174
|
+
key = (key as any)[TARGET_SYMBOL] || key;
|
|
1175
|
+
}
|
|
1176
|
+
if (typeof newData === "object" && newData) {
|
|
1161
1177
|
newData = (newData as any)[TARGET_SYMBOL] || newData;
|
|
1162
|
-
|
|
1178
|
+
}
|
|
1179
|
+
let oldData = target.get(key);
|
|
1180
|
+
if (oldData === undefined && !target.has(key)) oldData = EMPTY;
|
|
1163
1181
|
if (newData !== oldData) {
|
|
1182
|
+
const oldSize = target.size;
|
|
1164
1183
|
target.set(key, newData);
|
|
1165
1184
|
emit(target, key, newData, oldData);
|
|
1166
|
-
emit(target, MAP_SIZE_SYMBOL, target.size,
|
|
1167
|
-
runImmediateQueue();
|
|
1185
|
+
emit(target, MAP_SIZE_SYMBOL, target.size, oldSize);
|
|
1168
1186
|
}
|
|
1169
1187
|
return this;
|
|
1170
1188
|
},
|
|
1171
1189
|
delete(this: any, key: any): boolean {
|
|
1172
1190
|
const target: Map<any, any> = this[TARGET_SYMBOL];
|
|
1173
|
-
|
|
1191
|
+
// Make sure key is unproxied
|
|
1192
|
+
if (typeof key === "object" && key) {
|
|
1193
|
+
key = (key as any)[TARGET_SYMBOL] || key;
|
|
1194
|
+
}
|
|
1195
|
+
let oldData = target.get(key);
|
|
1196
|
+
if (oldData === undefined && !target.has(key)) oldData = EMPTY;
|
|
1174
1197
|
const result: boolean = target.delete(key);
|
|
1175
1198
|
if (result) {
|
|
1176
|
-
emit(target, key,
|
|
1199
|
+
emit(target, key, EMPTY, oldData);
|
|
1177
1200
|
emit(target, MAP_SIZE_SYMBOL, target.size, target.size + 1);
|
|
1178
|
-
runImmediateQueue();
|
|
1179
1201
|
}
|
|
1180
1202
|
return result;
|
|
1181
1203
|
},
|
|
@@ -1183,37 +1205,39 @@ const mapMethodHandlers = {
|
|
|
1183
1205
|
const target: Map<any, any> = this[TARGET_SYMBOL];
|
|
1184
1206
|
const oldSize = target.size;
|
|
1185
1207
|
for (const key of target.keys()) {
|
|
1186
|
-
|
|
1187
|
-
emit(target, key, undefined, oldData);
|
|
1208
|
+
emit(target, key, undefined, target.get(key));
|
|
1188
1209
|
}
|
|
1189
1210
|
target.clear();
|
|
1190
1211
|
emit(target, MAP_SIZE_SYMBOL, 0, oldSize);
|
|
1191
|
-
runImmediateQueue();
|
|
1192
1212
|
},
|
|
1193
1213
|
has(this: any, key: any): boolean {
|
|
1194
1214
|
const target: Map<any, any> = this[TARGET_SYMBOL];
|
|
1215
|
+
// Make sure key is unproxied
|
|
1216
|
+
if (typeof key === "object" && key) {
|
|
1217
|
+
key = (key as any)[TARGET_SYMBOL] || key;
|
|
1218
|
+
}
|
|
1195
1219
|
subscribe(target, key);
|
|
1196
1220
|
return target.has(key);
|
|
1197
1221
|
},
|
|
1198
1222
|
keys(this: any): IterableIterator<any> {
|
|
1199
1223
|
const target: Map<any, any> = this[TARGET_SYMBOL];
|
|
1200
1224
|
subscribe(target, ANY_SYMBOL);
|
|
1201
|
-
return target.keys();
|
|
1225
|
+
return wrapIteratorSingle(target.keys());
|
|
1202
1226
|
},
|
|
1203
1227
|
values(this: any): IterableIterator<any> {
|
|
1204
1228
|
const target: Map<any, any> = this[TARGET_SYMBOL];
|
|
1205
1229
|
subscribe(target, ANY_SYMBOL);
|
|
1206
|
-
return target.values();
|
|
1230
|
+
return wrapIteratorSingle(target.values());
|
|
1207
1231
|
},
|
|
1208
1232
|
entries(this: any): IterableIterator<[any, any]> {
|
|
1209
1233
|
const target: Map<any, any> = this[TARGET_SYMBOL];
|
|
1210
1234
|
subscribe(target, ANY_SYMBOL);
|
|
1211
|
-
return target.entries();
|
|
1235
|
+
return wrapIteratorPair(target.entries());
|
|
1212
1236
|
},
|
|
1213
1237
|
[Symbol.iterator](this: any): IterableIterator<[any, any]> {
|
|
1214
1238
|
const target: Map<any, any> = this[TARGET_SYMBOL];
|
|
1215
1239
|
subscribe(target, ANY_SYMBOL);
|
|
1216
|
-
return target[Symbol.iterator]();
|
|
1240
|
+
return wrapIteratorPair(target[Symbol.iterator]());
|
|
1217
1241
|
}
|
|
1218
1242
|
};
|
|
1219
1243
|
|
|
@@ -1222,7 +1246,7 @@ const mapHandler: ProxyHandler<Map<any, any>> = {
|
|
|
1222
1246
|
if (prop === TARGET_SYMBOL) return target;
|
|
1223
1247
|
|
|
1224
1248
|
// Handle Map methods using lookup object
|
|
1225
|
-
if (prop
|
|
1249
|
+
if (mapMethodHandlers.hasOwnProperty(prop)) {
|
|
1226
1250
|
return (mapMethodHandlers as any)[prop];
|
|
1227
1251
|
}
|
|
1228
1252
|
|
|
@@ -1233,8 +1257,7 @@ const mapHandler: ProxyHandler<Map<any, any>> = {
|
|
|
1233
1257
|
}
|
|
1234
1258
|
|
|
1235
1259
|
// Handle other properties normally
|
|
1236
|
-
|
|
1237
|
-
return optProxy((target as any)[prop]);
|
|
1260
|
+
return (target as any)[prop];
|
|
1238
1261
|
},
|
|
1239
1262
|
};
|
|
1240
1263
|
|
|
@@ -1266,35 +1289,15 @@ function optProxy(value: any): any {
|
|
|
1266
1289
|
return proxied;
|
|
1267
1290
|
}
|
|
1268
1291
|
|
|
1269
|
-
export function proxy<T extends any>(
|
|
1270
|
-
target: Array<T>,
|
|
1271
|
-
): Array<
|
|
1272
|
-
T extends number
|
|
1273
|
-
? number
|
|
1274
|
-
: T extends string
|
|
1275
|
-
? string
|
|
1276
|
-
: T extends boolean
|
|
1277
|
-
? boolean
|
|
1278
|
-
: T
|
|
1279
|
-
>;
|
|
1292
|
+
export function proxy<T extends any>(target: Array<T>): Array<T extends number ? number : T extends string ? string : T extends boolean ? boolean : T >;
|
|
1280
1293
|
export function proxy<T extends object>(target: T): T;
|
|
1281
|
-
export function proxy<T extends any>(
|
|
1282
|
-
target: T,
|
|
1283
|
-
): ValueRef<
|
|
1284
|
-
T extends number
|
|
1285
|
-
? number
|
|
1286
|
-
: T extends string
|
|
1287
|
-
? string
|
|
1288
|
-
: T extends boolean
|
|
1289
|
-
? boolean
|
|
1290
|
-
: T
|
|
1291
|
-
>;
|
|
1294
|
+
export function proxy<T extends any>(target: T): ValueRef<T extends number ? number : T extends string ? string : T extends boolean ? boolean : T>;
|
|
1292
1295
|
|
|
1293
1296
|
/**
|
|
1294
1297
|
* Creates a reactive proxy around the given data.
|
|
1295
1298
|
*
|
|
1296
1299
|
* Reading properties from the returned proxy within a reactive scope (like one created by
|
|
1297
|
-
* {@link $} or {@link
|
|
1300
|
+
* {@link $} or {@link derive}) establishes a subscription. Modifying properties *through*
|
|
1298
1301
|
* the proxy will notify subscribed scopes, causing them to re-execute.
|
|
1299
1302
|
*
|
|
1300
1303
|
* - Plain objects and arrays are wrapped in a standard JavaScript `Proxy` that intercepts
|
|
@@ -1311,23 +1314,23 @@ export function proxy<T extends any>(
|
|
|
1311
1314
|
* @example Object
|
|
1312
1315
|
* ```javascript
|
|
1313
1316
|
* const state = proxy({ count: 0, message: 'Hello' });
|
|
1314
|
-
*
|
|
1315
|
-
* setTimeout(() => state.message = 'World', 1000); // Triggers the
|
|
1317
|
+
* $(() => console.log(state.message)); // Subscribes to message
|
|
1318
|
+
* setTimeout(() => state.message = 'World', 1000); // Triggers the observing function
|
|
1316
1319
|
* setTimeout(() => state.count++, 2000); // Triggers nothing
|
|
1317
1320
|
* ```
|
|
1318
1321
|
*
|
|
1319
1322
|
* @example Array
|
|
1320
1323
|
* ```javascript
|
|
1321
1324
|
* const items = proxy(['a', 'b']);
|
|
1322
|
-
*
|
|
1323
|
-
* setTimeout(() => items.push('c'), 2000); // Triggers the
|
|
1325
|
+
* $(() => console.log(items.length)); // Subscribes to length
|
|
1326
|
+
* setTimeout(() => items.push('c'), 2000); // Triggers the observing function
|
|
1324
1327
|
* ```
|
|
1325
1328
|
*
|
|
1326
1329
|
* @example Primitive
|
|
1327
1330
|
* ```javascript
|
|
1328
1331
|
* const name = proxy('Aberdeen');
|
|
1329
|
-
*
|
|
1330
|
-
* setTimeout(() => name.value = 'UI', 2000); // Triggers the
|
|
1332
|
+
* $(() => console.log(name.value)); // Subscribes to value
|
|
1333
|
+
* setTimeout(() => name.value = 'UI', 2000); // Triggers the observing function
|
|
1331
1334
|
* ```
|
|
1332
1335
|
*
|
|
1333
1336
|
* @example Class instance
|
|
@@ -1338,7 +1341,7 @@ export function proxy<T extends any>(
|
|
|
1338
1341
|
* toString() { return `${this.name}Widget (${this.width}x${this.height})`; }
|
|
1339
1342
|
* }
|
|
1340
1343
|
* let graph: Widget = proxy(new Widget('Graph', 200, 100));
|
|
1341
|
-
*
|
|
1344
|
+
* $(() => console.log(''+graph));
|
|
1342
1345
|
* setTimeout(() => graph.grow(), 2000);
|
|
1343
1346
|
* setTimeout(() => graph.grow(), 4000);
|
|
1344
1347
|
* ```
|
|
@@ -1411,12 +1414,9 @@ function destroyWithClass(element: Element, cls: string) {
|
|
|
1411
1414
|
*
|
|
1412
1415
|
* @param dst - The destination object/array/Map (proxied or unproxied).
|
|
1413
1416
|
* @param src - The source object/array/Map (proxied or unproxied). It won't be modified.
|
|
1414
|
-
* @
|
|
1415
|
-
*
|
|
1416
|
-
*
|
|
1417
|
-
* - {@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.
|
|
1418
|
-
* @template T - The type of the destination object.
|
|
1419
|
-
* @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).
|
|
1417
|
+
* @template T - The type of the objects being copied.
|
|
1418
|
+
* @returns `true` if any changes were made to `dst`, or `false` if not.
|
|
1419
|
+
* @throws Error if attempting to copy an array into a non-array or vice versa.
|
|
1420
1420
|
*
|
|
1421
1421
|
* @example Basic Copy
|
|
1422
1422
|
* ```typescript
|
|
@@ -1424,108 +1424,79 @@ function destroyWithClass(element: Element, cls: string) {
|
|
|
1424
1424
|
* const dest = proxy({ b: { d: 3 } });
|
|
1425
1425
|
* copy(dest, source);
|
|
1426
1426
|
* console.log(dest); // proxy({ a: 1, b: { c: 2 } })
|
|
1427
|
+
* copy(dest, 'b', { e: 4 });
|
|
1428
|
+
* console.log(dest); // proxy({ a: 1, b: { e: 4 } })
|
|
1427
1429
|
* ```
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
*
|
|
1433
|
-
*
|
|
1434
|
-
*
|
|
1435
|
-
*
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1430
|
+
*/
|
|
1431
|
+
|
|
1432
|
+
export function copy<T extends object>(dst: T, src: T): boolean;
|
|
1433
|
+
/**
|
|
1434
|
+
* Like above, but copies `src` into `dst[dstKey]`. This is useful if you're unsure if dst[dstKey]
|
|
1435
|
+
* already exists (as the right type of object) or if you don't want to subscribe to dst[dstKey].
|
|
1436
|
+
*
|
|
1437
|
+
* @param dstKey - Optional key in `dst` to copy into.
|
|
1438
|
+
*/
|
|
1439
|
+
export function copy<T extends object>(dst: T, dstKey: keyof T, src: T[typeof dstKey]): boolean;
|
|
1440
|
+
export function copy(a: any, b: any, c?: any): boolean {
|
|
1441
|
+
if (arguments.length > 2) return copySet(a, b, c, 0);
|
|
1442
|
+
return copyRecursive(a, b, 0);
|
|
1443
|
+
}
|
|
1444
|
+
|
|
1445
|
+
function copySet(dst: any, dstKey: any, src: any, flags: number): boolean {
|
|
1446
|
+
let dstVal = peek(dst, dstKey);
|
|
1447
|
+
if (src === dstVal) return false;
|
|
1448
|
+
if (typeof dstVal === "object" && dstVal && typeof src === "object" && src && dstVal.constructor === src.constructor) {
|
|
1449
|
+
return copyRecursive(dstVal, src, flags);
|
|
1450
|
+
}
|
|
1451
|
+
src = clone(src);
|
|
1452
|
+
if (dst instanceof Map) dst.set(dstKey, src);
|
|
1453
|
+
else dst[dstKey] = clone(src);
|
|
1454
|
+
return true;
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1457
|
+
/**
|
|
1458
|
+
* Like {@link copy}, but uses merge semantics. Properties in `dst` not present in `src` are kept.
|
|
1459
|
+
* `null`/`undefined` in `src` delete properties in `dst`.
|
|
1460
|
+
*
|
|
1461
|
+
* When the destination is an object and the source is an array, its keys are used as (sparse) array indices.
|
|
1462
|
+
*
|
|
1463
|
+
* @example Basic merge
|
|
1446
1464
|
* ```typescript
|
|
1447
1465
|
* const source = { b: { c: 99 }, d: undefined }; // d: undefined will delete
|
|
1448
1466
|
* const dest = proxy({ a: 1, b: { x: 5 }, d: 4 });
|
|
1449
|
-
*
|
|
1450
|
-
*
|
|
1467
|
+
* merge(dest, source);
|
|
1468
|
+
* merge(dest, 'b', { y: 6 }); // merge into dest.b
|
|
1469
|
+
* merge(dest, 'c', { z: 7 }); // merge.c doesn't exist yet, so it will just be assigned
|
|
1470
|
+
* console.log(dest); // proxy({ a: 1, b: { c: 99, x: 5, y: 6 }, c: { z: 7 } })
|
|
1451
1471
|
* ```
|
|
1452
1472
|
*
|
|
1453
|
-
* @example Partial Array
|
|
1473
|
+
* @example Partial Array Merge
|
|
1454
1474
|
* ```typescript
|
|
1455
1475
|
* const messages = proxy(['msg1', 'msg2', 'msg3']);
|
|
1456
1476
|
* const update = { 1: 'updated msg2' }; // Update using object key as index
|
|
1457
|
-
*
|
|
1477
|
+
* merge(messages, update);
|
|
1458
1478
|
* console.log(messages); // proxy(['msg1', 'updated msg2', 'msg3'])
|
|
1459
1479
|
* ```
|
|
1460
1480
|
*
|
|
1461
|
-
* @example SHALLOW
|
|
1462
|
-
* ```typescript
|
|
1463
|
-
* const source = { nested: [1, 2] };
|
|
1464
|
-
* const dest = {};
|
|
1465
|
-
* copy(dest, source, SHALLOW);
|
|
1466
|
-
* dest.nested.push(3);
|
|
1467
|
-
* console.log(source.nested); // [1, 2, 3] (source was modified)
|
|
1468
|
-
* ```
|
|
1469
|
-
*/
|
|
1470
|
-
|
|
1471
|
-
// Overload for Map destination with object source
|
|
1472
|
-
export function copy<K, V>(dst: Map<K, V>, src: Record<K extends string | number | symbol ? K : never, V> | Partial<Record<K extends string | number | symbol ? K : never, V>>, flags?: number): void;
|
|
1473
|
-
// Overload for Map destination with Map source
|
|
1474
|
-
export function copy<K, V>(dst: Map<K, V>, src: Map<K, V> | Partial<Map<K, V>>, flags?: number): void;
|
|
1475
|
-
// Overload for object destination with Map source
|
|
1476
|
-
export function copy<T extends Record<string | number | symbol, any>>(dst: T, src: Map<keyof T, T[keyof T]>, flags?: number): void;
|
|
1477
|
-
// Overload for same-type copying
|
|
1478
|
-
export function copy<T extends object>(dst: T, src: Partial<T>, flags?: number): void;
|
|
1479
|
-
// Implementation
|
|
1480
|
-
export function copy(dst: any, src: any, flags = 0) {
|
|
1481
|
-
copyRecurse(dst, src, flags);
|
|
1482
|
-
runImmediateQueue();
|
|
1483
|
-
}
|
|
1484
|
-
/** Flag to {@link copy} causing it to use merge semantics. See {@link copy} for details. */
|
|
1485
|
-
export const MERGE = 1;
|
|
1486
|
-
/** Flag to {@link copy} and {@link clone} causing them to create a shallow copy (instead of the deep copy done by default).*/
|
|
1487
|
-
export const SHALLOW = 2;
|
|
1488
|
-
const COPY_SUBSCRIBE = 32;
|
|
1489
|
-
const COPY_EMIT = 64;
|
|
1490
|
-
|
|
1491
|
-
/**
|
|
1492
|
-
* Clone an (optionally proxied) object or array.
|
|
1493
|
-
*
|
|
1494
|
-
* @param src The object or array to clone. If it is proxied, `clone` will subscribe to any changes to the (nested) data structure.
|
|
1495
|
-
* @param flags
|
|
1496
|
-
* - {@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`.
|
|
1497
|
-
* @template T - The type of the objects being copied.
|
|
1498
|
-
* @returns A new unproxied array or object (of the same type as `src`), containing a deep (by default) copy of `src`.
|
|
1499
1481
|
*/
|
|
1500
|
-
export function
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
dst = Object.create(Object.getPrototypeOf(src)) as T;
|
|
1506
|
-
}
|
|
1507
|
-
copyRecurse(dst, src, flags);
|
|
1508
|
-
return dst;
|
|
1482
|
+
export function merge<T extends object>(dst: T, value: Partial<T>): boolean;
|
|
1483
|
+
export function merge<T extends object>(dst: T, dstKey: keyof T, value: Partial<T[typeof dstKey]>): boolean;
|
|
1484
|
+
export function merge(a: any, b: any, c?: any) {
|
|
1485
|
+
if (arguments.length > 2) return copySet(a, b, c, MERGE);
|
|
1486
|
+
return copyRecursive(a, b, MERGE);
|
|
1509
1487
|
}
|
|
1510
1488
|
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
}
|
|
1514
|
-
|
|
1515
|
-
function getKeys(subject: any) {
|
|
1516
|
-
return (subject instanceof Map) ? subject.keys() : Object.keys(subject);
|
|
1517
|
-
}
|
|
1518
|
-
|
|
1519
|
-
function copyRecurse(dst: any, src: any, flags: number) {
|
|
1489
|
+
// The dst and src parameters must be objects. Will throw a friendly message if they're not both the same type.
|
|
1490
|
+
function copyRecursive<T extends object>(dst: T, src: T, flags: number): boolean {
|
|
1520
1491
|
// We never want to subscribe to reads we do to the target (to find changes). So we'll
|
|
1521
1492
|
// take the unproxied version and `emit` updates ourselve.
|
|
1522
|
-
let unproxied = dst[TARGET_SYMBOL];
|
|
1493
|
+
let unproxied = (dst as any)[TARGET_SYMBOL] as T;
|
|
1523
1494
|
if (unproxied) {
|
|
1524
1495
|
dst = unproxied;
|
|
1525
1496
|
flags |= COPY_EMIT;
|
|
1526
1497
|
}
|
|
1527
1498
|
// For performance, we'll work on the unproxied `src` and manually subscribe to changes.
|
|
1528
|
-
unproxied = src[TARGET_SYMBOL];
|
|
1499
|
+
unproxied = (src as any)[TARGET_SYMBOL] as T;
|
|
1529
1500
|
if (unproxied) {
|
|
1530
1501
|
src = unproxied;
|
|
1531
1502
|
// If we're not in peek mode, we'll manually subscribe to all source reads.
|
|
@@ -1533,95 +1504,146 @@ function copyRecurse(dst: any, src: any, flags: number) {
|
|
|
1533
1504
|
}
|
|
1534
1505
|
|
|
1535
1506
|
if (flags & COPY_SUBSCRIBE) subscribe(src, ANY_SYMBOL);
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1507
|
+
let changed = false;
|
|
1508
|
+
|
|
1509
|
+
// The following loops are somewhat repetitive, but it keeps performance high by avoiding
|
|
1510
|
+
// function calls and extra checks within the loops.
|
|
1511
|
+
|
|
1512
|
+
if (src instanceof Array && dst instanceof Array) {
|
|
1539
1513
|
const dstLen = dst.length;
|
|
1540
1514
|
const srcLen = src.length;
|
|
1541
|
-
for (let
|
|
1542
|
-
copyValue(dst, i, src[i], flags);
|
|
1515
|
+
for (let index = 0; index < srcLen; index++) {
|
|
1516
|
+
// changed = copyValue(dst, i, src[i], flags) || changed;
|
|
1517
|
+
|
|
1518
|
+
let dstValue = dst[index];
|
|
1519
|
+
if (dstValue === undefined && !dst.hasOwnProperty(index)) dstValue = EMPTY;
|
|
1520
|
+
let srcValue = src[index];
|
|
1521
|
+
if (srcValue === undefined && !src.hasOwnProperty(index)) {
|
|
1522
|
+
delete dst[index];
|
|
1523
|
+
if (flags & COPY_EMIT) emit(dst, index, EMPTY, dstValue);
|
|
1524
|
+
changed = true;
|
|
1525
|
+
}
|
|
1526
|
+
else if (dstValue !== srcValue) {
|
|
1527
|
+
if (srcValue && typeof srcValue === "object") {
|
|
1528
|
+
if (typeof dstValue === "object" && dstValue && srcValue.constructor === dstValue.constructor) {
|
|
1529
|
+
changed = copyRecursive(dstValue, srcValue, flags) || changed;
|
|
1530
|
+
continue;
|
|
1531
|
+
}
|
|
1532
|
+
srcValue = clone(srcValue);
|
|
1533
|
+
}
|
|
1534
|
+
dst[index] = srcValue;
|
|
1535
|
+
|
|
1536
|
+
if (flags & COPY_EMIT) emit(dst, index, srcValue, dstValue);
|
|
1537
|
+
changed = true;
|
|
1538
|
+
}
|
|
1543
1539
|
}
|
|
1540
|
+
|
|
1544
1541
|
// Leaving additional values in the old array doesn't make sense, so we'll do this even when MERGE is set:
|
|
1545
1542
|
if (srcLen !== dstLen) {
|
|
1546
1543
|
if (flags & COPY_EMIT) {
|
|
1547
1544
|
for (let i = srcLen; i < dstLen; i++) {
|
|
1548
1545
|
const old = dst[i];
|
|
1549
|
-
dst[i]
|
|
1550
|
-
emit(dst, i,
|
|
1546
|
+
delete dst[i];
|
|
1547
|
+
emit(dst, i, EMPTY, old);
|
|
1551
1548
|
}
|
|
1552
1549
|
dst.length = srcLen;
|
|
1553
1550
|
emit(dst, "length", srcLen, dstLen);
|
|
1554
1551
|
} else {
|
|
1555
1552
|
dst.length = srcLen;
|
|
1556
1553
|
}
|
|
1554
|
+
changed = true;
|
|
1557
1555
|
}
|
|
1558
|
-
} else {
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1556
|
+
} else if (src instanceof Map && dst instanceof Map) {
|
|
1557
|
+
for (const key of src.keys()) {
|
|
1558
|
+
// changed = copyValue(dst, k, src.get(k), flags) || changed;
|
|
1559
|
+
let srcValue = src.get(key);
|
|
1560
|
+
let dstValue = dst.get(key);
|
|
1561
|
+
if (dstValue === undefined && !dst.has(key)) dstValue = EMPTY;
|
|
1562
|
+
if (dstValue !== srcValue) {
|
|
1563
|
+
if (srcValue && typeof srcValue === "object") {
|
|
1564
|
+
if (typeof dstValue === "object" && dstValue && srcValue.constructor === dstValue.constructor) {
|
|
1565
|
+
changed = copyRecursive(dstValue, srcValue, flags) || changed;
|
|
1566
|
+
continue;
|
|
1567
|
+
}
|
|
1568
|
+
srcValue = clone(srcValue);
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1571
|
+
dst.set(key, srcValue);
|
|
1572
|
+
|
|
1573
|
+
if (flags & COPY_EMIT) emit(dst, key, srcValue, dstValue);
|
|
1574
|
+
changed = true;
|
|
1575
|
+
}
|
|
1562
1576
|
}
|
|
1563
|
-
|
|
1564
|
-
// Remove entries from dst that don't exist in src (unless MERGE flag is set)
|
|
1577
|
+
|
|
1565
1578
|
if (!(flags & MERGE)) {
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1579
|
+
for (const k of dst.keys()) {
|
|
1580
|
+
if (!src.has(k)) {
|
|
1581
|
+
const old = dst.get(k);
|
|
1582
|
+
dst.delete(k);
|
|
1583
|
+
if (flags & COPY_EMIT) {
|
|
1584
|
+
emit(dst, k, undefined, old);
|
|
1570
1585
|
}
|
|
1586
|
+
changed = true;
|
|
1571
1587
|
}
|
|
1572
|
-
}
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1588
|
+
}
|
|
1589
|
+
}
|
|
1590
|
+
} else if (src.constructor === dst.constructor) {
|
|
1591
|
+
for (const key of Object.keys(src) as (keyof typeof src)[]) {
|
|
1592
|
+
// changed = copyValue(dst, k, src[k as keyof typeof src], flags) || changed;
|
|
1593
|
+
let srcValue = src[key];
|
|
1594
|
+
const dstValue = dst.hasOwnProperty(key) ? dst[key] : EMPTY;
|
|
1595
|
+
if (dstValue !== srcValue) {
|
|
1596
|
+
if (srcValue && typeof srcValue === "object") {
|
|
1597
|
+
if (typeof dstValue === "object" && dstValue && srcValue.constructor === dstValue.constructor) {
|
|
1598
|
+
changed = copyRecursive(dstValue as typeof srcValue, srcValue, flags) || changed;
|
|
1599
|
+
continue;
|
|
1576
1600
|
}
|
|
1601
|
+
srcValue = clone(srcValue);
|
|
1577
1602
|
}
|
|
1603
|
+
|
|
1604
|
+
dst[key] = srcValue;
|
|
1605
|
+
|
|
1606
|
+
if (flags & COPY_EMIT) emit(dst, key, srcValue, dstValue);
|
|
1607
|
+
changed = true;
|
|
1578
1608
|
}
|
|
1579
1609
|
}
|
|
1580
|
-
}
|
|
1581
|
-
}
|
|
1582
1610
|
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1611
|
+
if (!(flags & MERGE)) {
|
|
1612
|
+
for (const k of Object.keys(dst) as (keyof typeof dst)[]) {
|
|
1613
|
+
if (!src.hasOwnProperty(k)) {
|
|
1614
|
+
const old = dst[k];
|
|
1615
|
+
delete dst[k];
|
|
1616
|
+
if (flags & COPY_EMIT && old !== undefined) {
|
|
1617
|
+
emit(dst, k, undefined, old);
|
|
1618
|
+
}
|
|
1619
|
+
changed = true;
|
|
1620
|
+
}
|
|
1621
|
+
}
|
|
1622
|
+
}
|
|
1588
1623
|
} else {
|
|
1589
|
-
|
|
1590
|
-
delete dst[key];
|
|
1591
|
-
}
|
|
1592
|
-
if (flags & COPY_EMIT && old !== undefined) {
|
|
1593
|
-
emit(dst, key, undefined, old);
|
|
1624
|
+
throw new Error(`Incompatible or non-object types: ${src?.constructor?.name || typeof src} vs ${dst?.constructor?.name || typeof dst}`);
|
|
1594
1625
|
}
|
|
1626
|
+
return changed;
|
|
1595
1627
|
}
|
|
1596
1628
|
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
if (
|
|
1601
|
-
srcValue && dstValue && typeof srcValue === "object" && typeof dstValue === "object" &&
|
|
1602
|
-
(srcValue.constructor === dstValue.constructor || (flags & MERGE && dstValue instanceof Array))
|
|
1603
|
-
) {
|
|
1604
|
-
copyRecurse(dstValue, srcValue, flags);
|
|
1605
|
-
return;
|
|
1606
|
-
}
|
|
1629
|
+
const MERGE = 1;
|
|
1630
|
+
const COPY_SUBSCRIBE = 32;
|
|
1631
|
+
const COPY_EMIT = 64;
|
|
1607
1632
|
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
}
|
|
1623
|
-
if (flags & COPY_EMIT) emit(dst, index, srcValue, dstValue);
|
|
1624
|
-
}
|
|
1633
|
+
/**
|
|
1634
|
+
* Clone an (optionally proxied) object or array.
|
|
1635
|
+
*
|
|
1636
|
+
* @param src The object or array to clone. If it is proxied, `clone` will subscribe to any changes to the (nested) data structure.
|
|
1637
|
+
* @template T - The type of the objects being copied.
|
|
1638
|
+
* @returns A new unproxied array or object (of the same type as `src`), containing a deep copy of `src`.
|
|
1639
|
+
*/
|
|
1640
|
+
export function clone<T extends object>(src: T): T {
|
|
1641
|
+
// Create an empty object of the same type
|
|
1642
|
+
const copied = Array.isArray(src) ? [] : src instanceof Map ? new Map() : Object.create(Object.getPrototypeOf(src));
|
|
1643
|
+
// Copy all properties to it. This doesn't need to emit anything, and because
|
|
1644
|
+
// the destination is an empty object, we can just MERGE, which is a bit faster.
|
|
1645
|
+
copyRecursive(copied, src, MERGE);
|
|
1646
|
+
return copied;
|
|
1625
1647
|
}
|
|
1626
1648
|
|
|
1627
1649
|
interface RefTarget {
|
|
@@ -1729,7 +1751,7 @@ function applyBind(el: HTMLInputElement, target: any) {
|
|
|
1729
1751
|
throw new Error(`SELECT has no '${target.value}' OPTION (yet)`);
|
|
1730
1752
|
};
|
|
1731
1753
|
}
|
|
1732
|
-
|
|
1754
|
+
derive(onProxyChange);
|
|
1733
1755
|
el.addEventListener("input", onInputChange);
|
|
1734
1756
|
clean(() => {
|
|
1735
1757
|
el.removeEventListener("input", onInputChange);
|
|
@@ -1765,12 +1787,7 @@ const SPECIAL_PROPS: { [key: string]: (value: any) => void } = {
|
|
|
1765
1787
|
},
|
|
1766
1788
|
text: (value: any) => {
|
|
1767
1789
|
addNode(document.createTextNode(value));
|
|
1768
|
-
}
|
|
1769
|
-
element: (value: any) => {
|
|
1770
|
-
console.log("Aberdeen: $({element: myElement}) is deprecated, use $(myElement) instead");
|
|
1771
|
-
addNode(value);
|
|
1772
|
-
SPECIAL_PROPS.element = addNode; // Avoid the console.log next time
|
|
1773
|
-
},
|
|
1790
|
+
}
|
|
1774
1791
|
};
|
|
1775
1792
|
|
|
1776
1793
|
/**
|
|
@@ -1786,7 +1803,7 @@ const SPECIAL_PROPS: { [key: string]: (value: any) => void } = {
|
|
|
1786
1803
|
* - An optional HTML **tag**, something like `h1`. If present, a DOM element of that tag is created, and that element will be the *current* element for the rest of this `$` function execution.
|
|
1787
1804
|
* - Any number of CSS classes prefixed by `.` characters. These classes will be added to the *current* element.
|
|
1788
1805
|
* - Optional content **text** prefixed by a `:` character, ranging til the end of the string. This will be added as a TextNode to the *current* element.
|
|
1789
|
-
* - `function`: When a function (without argument nor a return value) is passed in, it will be reactively executed in its own
|
|
1806
|
+
* - `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}).
|
|
1790
1807
|
* - `object`: When an object is passed in, its key-value pairs are used to modify the *current* element in the following ways...
|
|
1791
1808
|
* - `{<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.
|
|
1792
1809
|
* - `{<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.
|
|
@@ -1797,7 +1814,7 @@ const SPECIAL_PROPS: { [key: string]: (value: any) => void } = {
|
|
|
1797
1814
|
* - `{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`.
|
|
1798
1815
|
* - `{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.
|
|
1799
1816
|
* - `{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`.
|
|
1800
|
-
* - `{<any>: <obsvalue>}`: Create a new
|
|
1817
|
+
* - `{<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:
|
|
1801
1818
|
* ```typescript
|
|
1802
1819
|
* const myColor = proxy('red');
|
|
1803
1820
|
* $('p:Test', {$color: myColor, click: () => myColor.value = 'yellow'})
|
|
@@ -1942,7 +1959,7 @@ export function $(
|
|
|
1942
1959
|
break;
|
|
1943
1960
|
}
|
|
1944
1961
|
} else {
|
|
1945
|
-
for (const key
|
|
1962
|
+
for (const key of Object.keys(arg)) {
|
|
1946
1963
|
const val = arg[key];
|
|
1947
1964
|
applyArg(key, val);
|
|
1948
1965
|
}
|
|
@@ -2030,7 +2047,7 @@ export function insertCss(style: object, global = false): string {
|
|
|
2030
2047
|
function styleToCss(style: object, prefix: string): string {
|
|
2031
2048
|
let props = "";
|
|
2032
2049
|
let rules = "";
|
|
2033
|
-
for (const kOr
|
|
2050
|
+
for (const kOr of Object.keys(style)) {
|
|
2034
2051
|
const v = (style as any)[kOr];
|
|
2035
2052
|
for (const k of kOr.split(/, ?/g)) {
|
|
2036
2053
|
if (v && typeof v === "object") {
|
|
@@ -2105,7 +2122,7 @@ let onError: (error: Error) => boolean | undefined = defaultOnError;
|
|
|
2105
2122
|
/**
|
|
2106
2123
|
* Sets a custom error handler function for errors that occur asynchronously
|
|
2107
2124
|
* within reactive scopes (e.g., during updates triggered by proxy changes in
|
|
2108
|
-
* {@link
|
|
2125
|
+
* {@link derive} or {@link $} render functions).
|
|
2109
2126
|
*
|
|
2110
2127
|
* The default handler logs the error to `console.error` and adds a simple
|
|
2111
2128
|
* 'Error' message div to the DOM at the location where the error occurred (if possible).
|
|
@@ -2193,7 +2210,7 @@ export function getParentElement(): Element {
|
|
|
2193
2210
|
* This is useful for releasing resources, removing manual event listeners, or cleaning up
|
|
2194
2211
|
* side effects associated with the scope. Cleaners are run in reverse order of registration.
|
|
2195
2212
|
*
|
|
2196
|
-
* Scopes are created by functions like {@link
|
|
2213
|
+
* Scopes are created by functions like {@link derive}, {@link mount}, {@link $} (when given a render function),
|
|
2197
2214
|
* and internally by constructs like {@link onEach}.
|
|
2198
2215
|
*
|
|
2199
2216
|
* @param cleaner - The function to execute during cleanup.
|
|
@@ -2211,7 +2228,7 @@ export function getParentElement(): Element {
|
|
|
2211
2228
|
* peek(() => sum.value += item);
|
|
2212
2229
|
* // Clean gets called before each rerun for a certain item index
|
|
2213
2230
|
* // No need for peek here, as the clean code doesn't run in an
|
|
2214
|
-
* //
|
|
2231
|
+
* // observer scope.
|
|
2215
2232
|
* clean(() => sum.value -= item);
|
|
2216
2233
|
* })
|
|
2217
2234
|
*
|
|
@@ -2239,7 +2256,7 @@ export function clean(cleaner: () => void) {
|
|
|
2239
2256
|
*
|
|
2240
2257
|
* @param func - The function to execute reactively. Any DOM manipulations should typically
|
|
2241
2258
|
* be done using {@link $} within this function. Its return value will be made available as an
|
|
2242
|
-
* observable returned by the `
|
|
2259
|
+
* observable returned by the `derive()` function.
|
|
2243
2260
|
* @returns An observable object, with its `value` property containing whatever the last run of `func` returned.
|
|
2244
2261
|
*
|
|
2245
2262
|
* @example Observation creating a UI components
|
|
@@ -2250,7 +2267,7 @@ export function clean(cleaner: () => void) {
|
|
|
2250
2267
|
* console.log('Welcome');
|
|
2251
2268
|
* $('h3:Welcome, ' + data.user); // Reactive text
|
|
2252
2269
|
*
|
|
2253
|
-
*
|
|
2270
|
+
* derive(() => {
|
|
2254
2271
|
* // When data.notifications changes, only this inner scope reruns,
|
|
2255
2272
|
* // leaving the `<p>Welcome, ..</p>` untouched.
|
|
2256
2273
|
* console.log('Notifications');
|
|
@@ -2260,13 +2277,13 @@ export function clean(cleaner: () => void) {
|
|
|
2260
2277
|
* });
|
|
2261
2278
|
* ```
|
|
2262
2279
|
*
|
|
2263
|
-
* ***Note*** that the above could just as easily be done using `$(func)` instead of `
|
|
2280
|
+
* ***Note*** that the above could just as easily be done using `$(func)` instead of `derive(func)`.
|
|
2264
2281
|
*
|
|
2265
2282
|
* @example Observation with return value
|
|
2266
2283
|
* ```typescript
|
|
2267
2284
|
* const counter = proxy(0);
|
|
2268
2285
|
* setInterval(() => counter.value++, 1000);
|
|
2269
|
-
* const double =
|
|
2286
|
+
* const double = derive(() => counter.value * 2);
|
|
2270
2287
|
*
|
|
2271
2288
|
* $('h3', () => {
|
|
2272
2289
|
* $(`:counter=${counter.value} double=${double.value}`);
|
|
@@ -2276,42 +2293,10 @@ export function clean(cleaner: () => void) {
|
|
|
2276
2293
|
* @overload
|
|
2277
2294
|
* @param func Func without a return value.
|
|
2278
2295
|
*/
|
|
2279
|
-
export function
|
|
2296
|
+
export function derive<T>(func: () => T): ValueRef<T> {
|
|
2280
2297
|
return new ResultScope<T>(currentScope.parentElement, func).result;
|
|
2281
2298
|
}
|
|
2282
2299
|
|
|
2283
|
-
/**
|
|
2284
|
-
* Similar to {@link observe}, creates a reactive scope that re-executes the function
|
|
2285
|
-
* when its proxied dependencies change.
|
|
2286
|
-
*
|
|
2287
|
-
* **Difference:** Updates run **synchronously and immediately** after the proxy modification
|
|
2288
|
-
* that triggered the update occurs.
|
|
2289
|
-
*
|
|
2290
|
-
* **Caution:** Use sparingly. Immediate execution bypasses Aberdeen's usual batching and
|
|
2291
|
-
* ordering optimizations, which can lead to performance issues or observing inconsistent
|
|
2292
|
-
* intermediate states if multiple related updates are applied sequentially.
|
|
2293
|
-
* Prefer {@link observe} or {@link $} for most use cases.
|
|
2294
|
-
*
|
|
2295
|
-
* @param func - The function to execute reactively and synchronously.
|
|
2296
|
-
*
|
|
2297
|
-
* @example
|
|
2298
|
-
* ```javascript
|
|
2299
|
-
* const state = proxy({ single: 'A' });
|
|
2300
|
-
*
|
|
2301
|
-
* immediateObserve(() => {
|
|
2302
|
-
* state.double = state.single + state.single
|
|
2303
|
-
* });
|
|
2304
|
-
* console.log(state.double); // 'AA'
|
|
2305
|
-
*
|
|
2306
|
-
* state.single = 'B';
|
|
2307
|
-
* // Synchronously:
|
|
2308
|
-
* console.log(state.double); // 'BB'
|
|
2309
|
-
* ```
|
|
2310
|
-
*/
|
|
2311
|
-
export function immediateObserve(func: () => void) {
|
|
2312
|
-
new ImmediateScope(currentScope.parentElement, func);
|
|
2313
|
-
}
|
|
2314
|
-
|
|
2315
2300
|
/**
|
|
2316
2301
|
* Attaches a reactive Aberdeen UI fragment to an existing DOM element. Without the use of
|
|
2317
2302
|
* this function, {@link $} will assume `document.body` as its root.
|
|
@@ -2321,11 +2306,11 @@ export function immediateObserve(func: () => void) {
|
|
|
2321
2306
|
* will cause it to re-execute when the data changes, updating the DOM elements created within it.
|
|
2322
2307
|
*
|
|
2323
2308
|
* Calls to {@link $} inside `func` will append nodes to `parentElement`.
|
|
2324
|
-
* You can nest {@link
|
|
2309
|
+
* You can nest {@link derive} or other {@link $} scopes within `func`.
|
|
2325
2310
|
* Use {@link unmountAll} to clean up all mounted scopes and their DOM nodes.
|
|
2326
2311
|
*
|
|
2327
2312
|
* Mounting scopes happens reactively, meaning that if this function is called from within another
|
|
2328
|
-
* ({@link
|
|
2313
|
+
* ({@link derive} or {@link $} or {@link mount}) scope that gets cleaned up, so will the mount.
|
|
2329
2314
|
*
|
|
2330
2315
|
* @param parentElement - The native DOM `Element` to which the UI fragment will be appended.
|
|
2331
2316
|
* @param func - The function that defines the UI fragment, typically containing calls to {@link $}.
|
|
@@ -2361,7 +2346,7 @@ export function mount(parentElement: Element, func: () => void) {
|
|
|
2361
2346
|
|
|
2362
2347
|
/**
|
|
2363
2348
|
* Removes all Aberdeen-managed DOM nodes and stops all active reactive scopes
|
|
2364
|
-
* (created by {@link mount}, {@link
|
|
2349
|
+
* (created by {@link mount}, {@link derive}, {@link $} with functions, etc.).
|
|
2365
2350
|
*
|
|
2366
2351
|
* This effectively cleans up the entire Aberdeen application state.
|
|
2367
2352
|
*/
|
|
@@ -2371,20 +2356,21 @@ export function unmountAll() {
|
|
|
2371
2356
|
}
|
|
2372
2357
|
|
|
2373
2358
|
/**
|
|
2374
|
-
* Executes a function *without* creating subscriptions in the current reactive scope, and returns its result.
|
|
2359
|
+
* Executes a function or retrieves a value *without* creating subscriptions in the current reactive scope, and returns its result.
|
|
2375
2360
|
*
|
|
2376
|
-
* This is useful when you need to access reactive data inside a reactive scope (like {@link
|
|
2361
|
+
* This is useful when you need to access reactive data inside a reactive scope (like {@link $})
|
|
2377
2362
|
* but do not want changes to that specific data to trigger a re-execute of the scope.
|
|
2363
|
+
*
|
|
2364
|
+
* Note: You may also use {@link unproxy} to get to the raw underlying data structure, which can be used to similar effect.
|
|
2378
2365
|
*
|
|
2379
|
-
* @
|
|
2380
|
-
*
|
|
2381
|
-
* @
|
|
2382
|
-
* @returns Whatever `func` returns.
|
|
2366
|
+
* @param target - Either a function to execute, or an object (which may also be an Array or a Map) to index.
|
|
2367
|
+
* @param key - Optional key/index to use when `target` is an object.
|
|
2368
|
+
* @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.
|
|
2383
2369
|
*
|
|
2384
|
-
* @example Peeking within
|
|
2370
|
+
* @example Peeking within observer
|
|
2385
2371
|
* ```typescript
|
|
2386
2372
|
* const data = proxy({ a: 1, b: 2 });
|
|
2387
|
-
*
|
|
2373
|
+
* $(() => {
|
|
2388
2374
|
* // re-executes only when data.a changes, because data.b is peeked.
|
|
2389
2375
|
* const b = peek(() => data.b);
|
|
2390
2376
|
* console.log(`A is ${data.a}, B was ${b} when A changed.`);
|
|
@@ -2394,10 +2380,20 @@ export function unmountAll() {
|
|
|
2394
2380
|
* ```
|
|
2395
2381
|
*
|
|
2396
2382
|
*/
|
|
2397
|
-
|
|
2383
|
+
|
|
2384
|
+
export function peek<T extends object>(target: T, key: keyof T): T[typeof key];
|
|
2385
|
+
export function peek<K,V>(target: Map<K,V>, key: K): V | undefined;
|
|
2386
|
+
export function peek<T>(target: T[], key: number): T | undefined;
|
|
2387
|
+
export function peek<T>(target: () => T): T;
|
|
2388
|
+
|
|
2389
|
+
export function peek(target: any, key?: any) {
|
|
2398
2390
|
peeking++;
|
|
2399
2391
|
try {
|
|
2400
|
-
|
|
2392
|
+
if (arguments.length === 1) {
|
|
2393
|
+
return target();
|
|
2394
|
+
} else {
|
|
2395
|
+
return (target instanceof Map) ? target.get(key) : target[key];
|
|
2396
|
+
}
|
|
2401
2397
|
} finally {
|
|
2402
2398
|
peeking--;
|
|
2403
2399
|
}
|
|
@@ -2408,16 +2404,16 @@ export function map<K, IN, OUT>(
|
|
|
2408
2404
|
source: Map<K, IN>,
|
|
2409
2405
|
func: (value: IN, key: K) => undefined | OUT,
|
|
2410
2406
|
): Map<K, OUT>;
|
|
2411
|
-
/** When using an object as `source`. */
|
|
2412
|
-
export function map<IN, const IN_KEY extends string | number | symbol, OUT>(
|
|
2413
|
-
source: Record<IN_KEY, IN>,
|
|
2414
|
-
func: (value: IN, index: KeyToString<IN_KEY>) => undefined | OUT,
|
|
2415
|
-
): Record<string | symbol, OUT>;
|
|
2416
2407
|
/** When using an array as `source`. */
|
|
2417
2408
|
export function map<IN, OUT>(
|
|
2418
2409
|
source: Array<IN>,
|
|
2419
2410
|
func: (value: IN, index: number) => undefined | OUT,
|
|
2420
2411
|
): Array<OUT>;
|
|
2412
|
+
/** When using an object as `source`. */
|
|
2413
|
+
export function map<IN, const IN_KEY extends string | number | symbol, OUT>(
|
|
2414
|
+
source: Record<IN_KEY, IN>,
|
|
2415
|
+
func: (value: IN, index: KeyToString<IN_KEY>) => undefined | OUT,
|
|
2416
|
+
): Record<string | symbol, OUT>;
|
|
2421
2417
|
/**
|
|
2422
2418
|
* Reactively maps/filters items from a proxied source array or object to a new proxied array or object.
|
|
2423
2419
|
*
|
|
@@ -2441,7 +2437,7 @@ export function map<IN, OUT>(
|
|
|
2441
2437
|
* const doubled = map(numbers, (n) => n * 2);
|
|
2442
2438
|
* // doubled is proxy([2, 4, 6])
|
|
2443
2439
|
*
|
|
2444
|
-
*
|
|
2440
|
+
* $(() => console.log(doubled)); // Logs updates
|
|
2445
2441
|
* numbers.push(4); // doubled becomes proxy([2, 4, 6, 8])
|
|
2446
2442
|
* ```
|
|
2447
2443
|
*
|
|
@@ -2455,7 +2451,7 @@ export function map<IN, OUT>(
|
|
|
2455
2451
|
*
|
|
2456
2452
|
* const activeUserNames = map(users, (user) => user.active ? user.name : undefined);
|
|
2457
2453
|
* // activeUserNames is proxy({ u1: 'Alice', u3: 'Charlie' })
|
|
2458
|
-
*
|
|
2454
|
+
* $(() => console.log(Object.values(activeUserNames)));
|
|
2459
2455
|
*
|
|
2460
2456
|
* users.u2.active = true;
|
|
2461
2457
|
* // activeUserNames becomes proxy({ u1: 'Alice', u2: 'Bob', u3: 'Charlie' })
|
|
@@ -2559,9 +2555,9 @@ export function multiMap(
|
|
|
2559
2555
|
onEach(source, (item: any, key: symbol | string | number) => {
|
|
2560
2556
|
const pairs = func(item, key);
|
|
2561
2557
|
if (pairs) {
|
|
2562
|
-
for (const key
|
|
2558
|
+
for (const key of Object.keys(pairs)) out[key] = pairs[key];
|
|
2563
2559
|
clean(() => {
|
|
2564
|
-
for (const key
|
|
2560
|
+
for (const key of Object.keys(pairs)) delete out[key];
|
|
2565
2561
|
});
|
|
2566
2562
|
}
|
|
2567
2563
|
});
|