@praxisjs/decorators 1.7.0 → 2.0.1
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/CHANGELOG.md +41 -0
- package/dist/component/component.d.ts +1 -1
- package/dist/component/component.d.ts.map +1 -1
- package/dist/component/component.js +8 -15
- package/dist/component/component.js.map +1 -1
- package/dist/component/lazy.d.ts +1 -1
- package/dist/component/lazy.d.ts.map +1 -1
- package/dist/component/lazy.js +44 -36
- package/dist/component/lazy.js.map +1 -1
- package/dist/create-class-decorator.d.ts +27 -5
- package/dist/create-class-decorator.d.ts.map +1 -1
- package/dist/create-class-decorator.js +53 -18
- package/dist/create-class-decorator.js.map +1 -1
- package/dist/create-field-decorator.d.ts +42 -3
- package/dist/create-field-decorator.d.ts.map +1 -1
- package/dist/create-field-decorator.js +63 -27
- package/dist/create-field-decorator.js.map +1 -1
- package/dist/create-getter-decorator.d.ts +81 -16
- package/dist/create-getter-decorator.d.ts.map +1 -1
- package/dist/create-getter-decorator.js +127 -15
- package/dist/create-getter-decorator.js.map +1 -1
- package/dist/create-lifecycle-method-decorator.d.ts +21 -3
- package/dist/create-lifecycle-method-decorator.d.ts.map +1 -1
- package/dist/create-lifecycle-method-decorator.js +25 -18
- package/dist/create-lifecycle-method-decorator.js.map +1 -1
- package/dist/create-method-decorator.d.ts +43 -3
- package/dist/create-method-decorator.d.ts.map +1 -1
- package/dist/create-method-decorator.js +49 -8
- package/dist/create-method-decorator.js.map +1 -1
- package/dist/events/command.d.ts +2 -1
- package/dist/events/command.d.ts.map +1 -1
- package/dist/events/command.js +10 -8
- package/dist/events/command.js.map +1 -1
- package/dist/events/emit.d.ts +2 -2
- package/dist/events/emit.d.ts.map +1 -1
- package/dist/events/emit.js +18 -24
- package/dist/events/emit.js.map +1 -1
- package/dist/events/on-command.d.ts +2 -1
- package/dist/events/on-command.d.ts.map +1 -1
- package/dist/events/on-command.js +12 -14
- package/dist/events/on-command.js.map +1 -1
- package/dist/field.d.ts +51 -0
- package/dist/field.d.ts.map +1 -0
- package/dist/field.js +82 -0
- package/dist/field.js.map +1 -0
- package/dist/functions/bind.d.ts +2 -1
- package/dist/functions/bind.d.ts.map +1 -1
- package/dist/functions/bind.js +4 -4
- package/dist/functions/bind.js.map +1 -1
- package/dist/functions/debounce.d.ts +2 -2
- package/dist/functions/debounce.d.ts.map +1 -1
- package/dist/functions/debounce.js +25 -23
- package/dist/functions/debounce.js.map +1 -1
- package/dist/functions/log.d.ts +2 -1
- package/dist/functions/log.d.ts.map +1 -1
- package/dist/functions/log.js +38 -40
- package/dist/functions/log.js.map +1 -1
- package/dist/functions/memo.d.ts +2 -1
- package/dist/functions/memo.d.ts.map +1 -1
- package/dist/functions/memo.js +41 -40
- package/dist/functions/memo.js.map +1 -1
- package/dist/functions/once.d.ts +2 -1
- package/dist/functions/once.d.ts.map +1 -1
- package/dist/functions/once.js +10 -14
- package/dist/functions/once.js.map +1 -1
- package/dist/functions/retry.d.ts +2 -1
- package/dist/functions/retry.d.ts.map +1 -1
- package/dist/functions/retry.js +21 -23
- package/dist/functions/retry.js.map +1 -1
- package/dist/functions/throttle.d.ts +2 -1
- package/dist/functions/throttle.d.ts.map +1 -1
- package/dist/functions/throttle.js +12 -14
- package/dist/functions/throttle.js.map +1 -1
- package/dist/functions/until.d.ts +2 -1
- package/dist/functions/until.d.ts.map +1 -1
- package/dist/functions/until.js +13 -14
- package/dist/functions/until.js.map +1 -1
- package/dist/functions/watch.d.ts +1 -1
- package/dist/functions/watch.d.ts.map +1 -1
- package/dist/functions/watch.js +36 -34
- package/dist/functions/watch.js.map +1 -1
- package/dist/functions/when.d.ts +1 -1
- package/dist/functions/when.d.ts.map +1 -1
- package/dist/functions/when.js +19 -21
- package/dist/functions/when.js.map +1 -1
- package/dist/index.d.ts +5 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/lifecycle.d.ts +24 -0
- package/dist/lifecycle.d.ts.map +1 -0
- package/dist/lifecycle.js +55 -0
- package/dist/lifecycle.js.map +1 -0
- package/dist/properties/compose.d.ts +3 -2
- package/dist/properties/compose.d.ts.map +1 -1
- package/dist/properties/compose.js +33 -19
- package/dist/properties/compose.js.map +1 -1
- package/dist/properties/computed.d.ts +12 -10
- package/dist/properties/computed.d.ts.map +1 -1
- package/dist/properties/computed.js +26 -21
- package/dist/properties/computed.js.map +1 -1
- package/dist/properties/deep-state.d.ts +2 -1
- package/dist/properties/deep-state.d.ts.map +1 -1
- package/dist/properties/deep-state.js +66 -60
- package/dist/properties/deep-state.js.map +1 -1
- package/dist/properties/function-prop.d.ts +2 -1
- package/dist/properties/function-prop.d.ts.map +1 -1
- package/dist/properties/function-prop.js +20 -22
- package/dist/properties/function-prop.js.map +1 -1
- package/dist/properties/history.d.ts +2 -2
- package/dist/properties/history.d.ts.map +1 -1
- package/dist/properties/history.js +34 -33
- package/dist/properties/history.js.map +1 -1
- package/dist/properties/index.d.ts +1 -0
- package/dist/properties/index.d.ts.map +1 -1
- package/dist/properties/index.js +1 -0
- package/dist/properties/index.js.map +1 -1
- package/dist/properties/persisted.d.ts +2 -1
- package/dist/properties/persisted.d.ts.map +1 -1
- package/dist/properties/persisted.js +24 -18
- package/dist/properties/persisted.js.map +1 -1
- package/dist/properties/prop.d.ts +2 -1
- package/dist/properties/prop.d.ts.map +1 -1
- package/dist/properties/prop.js +26 -22
- package/dist/properties/prop.js.map +1 -1
- package/dist/properties/ref.d.ts +2 -1
- package/dist/properties/ref.d.ts.map +1 -1
- package/dist/properties/ref.js +11 -8
- package/dist/properties/ref.js.map +1 -1
- package/dist/properties/resource.d.ts.map +1 -1
- package/dist/properties/resource.js +21 -16
- package/dist/properties/resource.js.map +1 -1
- package/dist/properties/selector.d.ts +12 -0
- package/dist/properties/selector.d.ts.map +1 -0
- package/dist/properties/selector.js +25 -0
- package/dist/properties/selector.js.map +1 -0
- package/dist/properties/slot.d.ts +2 -1
- package/dist/properties/slot.d.ts.map +1 -1
- package/dist/properties/slot.js +22 -20
- package/dist/properties/slot.js.map +1 -1
- package/dist/properties/state.d.ts.map +1 -1
- package/dist/properties/state.js +27 -16
- package/dist/properties/state.js.map +1 -1
- package/dist/properties/synced.d.ts +2 -1
- package/dist/properties/synced.d.ts.map +1 -1
- package/dist/properties/synced.js +27 -19
- package/dist/properties/synced.js.map +1 -1
- package/dist/shared-method.d.ts +14 -0
- package/dist/shared-method.d.ts.map +1 -0
- package/dist/shared-method.js +87 -0
- package/dist/shared-method.js.map +1 -0
- package/package.json +14 -5
- package/src/component/component.ts +8 -27
- package/src/component/lazy.ts +51 -41
- package/src/create-class-decorator.ts +90 -31
- package/src/create-field-decorator.ts +120 -43
- package/src/create-getter-decorator.ts +263 -66
- package/src/create-lifecycle-method-decorator.ts +50 -28
- package/src/create-method-decorator.ts +119 -13
- package/src/events/command.ts +11 -9
- package/src/events/emit.ts +16 -22
- package/src/events/on-command.ts +17 -18
- package/src/field.ts +116 -0
- package/src/functions/bind.ts +4 -4
- package/src/functions/debounce.ts +25 -23
- package/src/functions/log.ts +44 -46
- package/src/functions/memo.ts +39 -37
- package/src/functions/once.ts +10 -13
- package/src/functions/retry.ts +19 -21
- package/src/functions/throttle.ts +12 -13
- package/src/functions/until.ts +14 -14
- package/src/functions/watch.ts +37 -36
- package/src/functions/when.ts +18 -20
- package/src/index.ts +12 -5
- package/src/lifecycle.ts +74 -0
- package/src/properties/compose.ts +33 -20
- package/src/properties/computed.ts +35 -45
- package/src/properties/deep-state.ts +66 -64
- package/src/properties/function-prop.ts +25 -27
- package/src/properties/history.ts +34 -35
- package/src/properties/index.ts +1 -0
- package/src/properties/persisted.ts +30 -20
- package/src/properties/prop.ts +30 -22
- package/src/properties/ref.ts +12 -10
- package/src/properties/resource.ts +22 -17
- package/src/properties/selector.ts +45 -0
- package/src/properties/slot.ts +28 -22
- package/src/properties/state.ts +32 -16
- package/src/properties/synced.ts +31 -19
- package/src/shared-method.ts +101 -0
- package/dist/__tests__/component.test.d.ts +0 -2
- package/dist/__tests__/component.test.d.ts.map +0 -1
- package/dist/__tests__/component.test.js +0 -147
- package/dist/__tests__/component.test.js.map +0 -1
- package/dist/__tests__/compose.test.d.ts +0 -2
- package/dist/__tests__/compose.test.d.ts.map +0 -1
- package/dist/__tests__/compose.test.js +0 -164
- package/dist/__tests__/compose.test.js.map +0 -1
- package/dist/__tests__/deep-state.test.d.ts +0 -2
- package/dist/__tests__/deep-state.test.d.ts.map +0 -1
- package/dist/__tests__/deep-state.test.js +0 -221
- package/dist/__tests__/deep-state.test.js.map +0 -1
- package/dist/__tests__/events.test.d.ts +0 -2
- package/dist/__tests__/events.test.d.ts.map +0 -1
- package/dist/__tests__/events.test.js +0 -295
- package/dist/__tests__/events.test.js.map +0 -1
- package/dist/__tests__/functions.test.d.ts +0 -2
- package/dist/__tests__/functions.test.d.ts.map +0 -1
- package/dist/__tests__/functions.test.js +0 -618
- package/dist/__tests__/functions.test.js.map +0 -1
- package/dist/__tests__/lazy.test.d.ts +0 -2
- package/dist/__tests__/lazy.test.d.ts.map +0 -1
- package/dist/__tests__/lazy.test.js +0 -227
- package/dist/__tests__/lazy.test.js.map +0 -1
- package/dist/__tests__/memo.test.d.ts +0 -2
- package/dist/__tests__/memo.test.d.ts.map +0 -1
- package/dist/__tests__/memo.test.js +0 -249
- package/dist/__tests__/memo.test.js.map +0 -1
- package/dist/__tests__/properties-browser.test.d.ts +0 -2
- package/dist/__tests__/properties-browser.test.d.ts.map +0 -1
- package/dist/__tests__/properties-browser.test.js +0 -227
- package/dist/__tests__/properties-browser.test.js.map +0 -1
- package/dist/__tests__/properties.test.d.ts +0 -2
- package/dist/__tests__/properties.test.d.ts.map +0 -1
- package/dist/__tests__/properties.test.js +0 -1253
- package/dist/__tests__/properties.test.js.map +0 -1
- package/dist/__tests__/ref.test.d.ts +0 -2
- package/dist/__tests__/ref.test.d.ts.map +0 -1
- package/dist/__tests__/ref.test.js +0 -106
- package/dist/__tests__/ref.test.js.map +0 -1
- package/dist/__tests__/resource.test.d.ts +0 -2
- package/dist/__tests__/resource.test.d.ts.map +0 -1
- package/dist/__tests__/resource.test.js +0 -217
- package/dist/__tests__/resource.test.js.map +0 -1
- package/dist/__tests__/slot.test.d.ts +0 -2
- package/dist/__tests__/slot.test.d.ts.map +0 -1
- package/dist/__tests__/slot.test.js +0 -147
- package/dist/__tests__/slot.test.js.map +0 -1
- package/dist/__tests__/synced.test.d.ts +0 -2
- package/dist/__tests__/synced.test.d.ts.map +0 -1
- package/dist/__tests__/synced.test.js +0 -161
- package/dist/__tests__/synced.test.js.map +0 -1
- package/dist/__tests__/until.test.d.ts +0 -2
- package/dist/__tests__/until.test.d.ts.map +0 -1
- package/dist/__tests__/until.test.js +0 -88
- package/dist/__tests__/until.test.js.map +0 -1
- package/dist/create-accessor-decorator.d.ts +0 -14
- package/dist/create-accessor-decorator.d.ts.map +0 -1
- package/dist/create-accessor-decorator.js +0 -27
- package/dist/create-accessor-decorator.js.map +0 -1
- package/src/__tests__/component.test.ts +0 -167
- package/src/__tests__/compose.test.ts +0 -202
- package/src/__tests__/deep-state.test.ts +0 -265
- package/src/__tests__/events.test.ts +0 -366
- package/src/__tests__/functions.test.ts +0 -803
- package/src/__tests__/lazy.test.ts +0 -296
- package/src/__tests__/memo.test.ts +0 -293
- package/src/__tests__/properties-browser.test.ts +0 -288
- package/src/__tests__/properties.test.ts +0 -1556
- package/src/__tests__/ref.test.ts +0 -131
- package/src/__tests__/resource.test.ts +0 -246
- package/src/__tests__/slot.test.ts +0 -179
- package/src/__tests__/synced.test.ts +0 -193
- package/src/__tests__/until.test.ts +0 -113
- package/src/create-accessor-decorator.ts +0 -45
- package/tsconfig.json +0 -9
|
@@ -1,45 +1,44 @@
|
|
|
1
|
-
import type { StatefulComponent } from "@praxisjs/core";
|
|
2
1
|
import { type HistoryElement, history } from "@praxisjs/core/internal";
|
|
3
2
|
import type { Signal } from "@praxisjs/shared";
|
|
4
3
|
|
|
5
|
-
import {
|
|
4
|
+
import { type FieldDecorator, type FieldHost, sharedField } from "../field";
|
|
5
|
+
import { addLifecycleHooks } from "../lifecycle";
|
|
6
6
|
|
|
7
7
|
export type HistoryOf<C, K extends keyof C> = HistoryElement<C[K]>;
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
9
|
+
function createHistory(host: FieldHost, fieldName: string, limit: number): HistoryElement<unknown> {
|
|
10
|
+
const h = history((() => host[fieldName]) as Signal<unknown>, limit);
|
|
11
|
+
const undo = h.undo.bind(h);
|
|
12
|
+
const redo = h.redo.bind(h);
|
|
13
|
+
h.undo = () => {
|
|
14
|
+
const prev = h.values()[h.values().length - 2];
|
|
15
|
+
if (prev === undefined) return;
|
|
16
|
+
undo();
|
|
17
|
+
host[fieldName] = prev;
|
|
18
|
+
};
|
|
19
|
+
h.redo = () => {
|
|
20
|
+
redo();
|
|
21
|
+
host[fieldName] = h.current();
|
|
22
|
+
};
|
|
23
|
+
return h;
|
|
24
|
+
}
|
|
16
25
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
inst[fieldName] = prev;
|
|
29
|
-
};
|
|
30
|
-
hInst.redo = () => {
|
|
31
|
-
_redo();
|
|
32
|
-
inst[fieldName] = hInst.current();
|
|
33
|
-
};
|
|
34
|
-
h = hInst;
|
|
35
|
-
}
|
|
36
|
-
return h;
|
|
37
|
-
},
|
|
38
|
-
enumerable: false,
|
|
26
|
+
export function History(fieldName: string, limit = 50): FieldDecorator {
|
|
27
|
+
return sharedField((name) => {
|
|
28
|
+
const slot = Symbol(`praxis.history.${name}`);
|
|
29
|
+
// The tracking effect would otherwise keep recording a source that outlives the component.
|
|
30
|
+
const destroy = (instance: object) => { ((instance as FieldHost)[slot] as HistoryElement<unknown> | undefined)?.destroy(); };
|
|
31
|
+
return {
|
|
32
|
+
descriptor: {
|
|
33
|
+
enumerable: false,
|
|
34
|
+
// Created on first read, so an instance whose history is never read records nothing.
|
|
35
|
+
get(this: FieldHost) {
|
|
36
|
+
return (this[slot] ??= createHistory(this, fieldName, limit));
|
|
39
37
|
},
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
38
|
+
},
|
|
39
|
+
init(instance) {
|
|
40
|
+
addLifecycleHooks(instance, undefined, destroy);
|
|
41
|
+
},
|
|
42
|
+
};
|
|
44
43
|
});
|
|
45
44
|
}
|
package/src/properties/index.ts
CHANGED
|
@@ -4,21 +4,24 @@ import {
|
|
|
4
4
|
type PersistedSignalOptions,
|
|
5
5
|
} from "@praxisjs/core/internal";
|
|
6
6
|
|
|
7
|
-
import {
|
|
7
|
+
import { type FieldDecorator, type FieldHost, sharedField } from "../field";
|
|
8
|
+
import { addLifecycleHooks } from "../lifecycle";
|
|
8
9
|
|
|
9
|
-
|
|
10
|
+
// Fields of one instance that persist under the same key share one signal. Only read while the
|
|
11
|
+
// instance is built; each field then reads its signal from its own slot.
|
|
12
|
+
const SIGNALS: unique symbol = Symbol("praxis.persistedSignals");
|
|
13
|
+
|
|
14
|
+
interface PersistedHost {
|
|
15
|
+
[SIGNALS]?: Map<string, PersistedSignal<unknown>>;
|
|
16
|
+
}
|
|
10
17
|
|
|
11
18
|
function getOrCreateSignal<T>(
|
|
12
|
-
instance:
|
|
19
|
+
instance: PersistedHost,
|
|
13
20
|
storageKey: string,
|
|
14
21
|
initialValue: T,
|
|
15
22
|
options: PersistedSignalOptions<T>,
|
|
16
23
|
): PersistedSignal<T> {
|
|
17
|
-
|
|
18
|
-
if (!map) {
|
|
19
|
-
map = new Map();
|
|
20
|
-
signalMap.set(instance, map);
|
|
21
|
-
}
|
|
24
|
+
const map = (instance[SIGNALS] ??= new Map<string, PersistedSignal<unknown>>());
|
|
22
25
|
let sig = map.get(storageKey);
|
|
23
26
|
if (!sig) {
|
|
24
27
|
sig = persistedSignal(storageKey, initialValue, options) as PersistedSignal<unknown>;
|
|
@@ -30,18 +33,25 @@ function getOrCreateSignal<T>(
|
|
|
30
33
|
export function Persisted<T>(
|
|
31
34
|
key?: string,
|
|
32
35
|
options: PersistedSignalOptions<T> = {},
|
|
33
|
-
) {
|
|
34
|
-
return
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
36
|
+
): FieldDecorator {
|
|
37
|
+
return sharedField((name) => {
|
|
38
|
+
const slot = Symbol(`praxis.persisted.${name}`);
|
|
39
|
+
const storageKey = key ?? name;
|
|
40
|
+
// Without this every mounted instance would leave a window "storage" listener behind.
|
|
41
|
+
const close = (instance: object) => { ((instance as FieldHost)[slot] as PersistedSignal<T>).close(); };
|
|
42
|
+
return {
|
|
43
|
+
descriptor: {
|
|
44
|
+
get(this: FieldHost) {
|
|
45
|
+
return (this[slot] as PersistedSignal<T>)();
|
|
46
|
+
},
|
|
47
|
+
set(this: FieldHost, value: T) {
|
|
48
|
+
(this[slot] as PersistedSignal<T>).set(value);
|
|
41
49
|
},
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
50
|
+
},
|
|
51
|
+
init(instance, initialValue) {
|
|
52
|
+
instance[slot] = getOrCreateSignal(instance, storageKey, initialValue as T, options);
|
|
53
|
+
addLifecycleHooks(instance, undefined, close);
|
|
54
|
+
},
|
|
55
|
+
};
|
|
46
56
|
});
|
|
47
57
|
}
|
package/src/properties/prop.ts
CHANGED
|
@@ -1,31 +1,39 @@
|
|
|
1
|
+
import type { StatefulComponent } from "@praxisjs/core";
|
|
1
2
|
import {
|
|
2
3
|
getComponentDefault,
|
|
3
4
|
getComponentRawProp,
|
|
4
5
|
setComponentDefault,
|
|
5
6
|
} from "@praxisjs/core/internal";
|
|
6
7
|
|
|
7
|
-
import {
|
|
8
|
+
import { addClaimableInitializer, assertFieldContext } from "../field";
|
|
8
9
|
|
|
9
10
|
export function Prop() {
|
|
10
|
-
return
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
|
|
11
|
+
return function <This extends StatefulComponent, Value = unknown>(
|
|
12
|
+
_value: undefined,
|
|
13
|
+
context: ClassFieldDecoratorContext<This, Value>,
|
|
14
|
+
): void {
|
|
15
|
+
assertFieldContext(context, "@Prop()");
|
|
16
|
+
const name = context.name as string;
|
|
17
|
+
// Shared by every instance, like @State's accessors.
|
|
18
|
+
const descriptor: PropertyDescriptor = {
|
|
19
|
+
enumerable: true,
|
|
20
|
+
configurable: true,
|
|
21
|
+
get(this: object) {
|
|
22
|
+
const fromParent = getComponentRawProp(this, name);
|
|
23
|
+
if (fromParent !== undefined) {
|
|
24
|
+
return typeof fromParent === "function"
|
|
25
|
+
? (fromParent as () => unknown)()
|
|
26
|
+
: fromParent;
|
|
27
|
+
}
|
|
28
|
+
return getComponentDefault(this, name);
|
|
29
|
+
},
|
|
30
|
+
set(this: object, value: unknown) {
|
|
31
|
+
setComponentDefault(this, name, value);
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
addClaimableInitializer(context, function (value) {
|
|
35
|
+
setComponentDefault(this, name, value);
|
|
36
|
+
Object.defineProperty(this, name, descriptor);
|
|
37
|
+
});
|
|
38
|
+
};
|
|
31
39
|
}
|
package/src/properties/ref.ts
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { type FieldDecorator, type FieldHost, sharedField } from "../field";
|
|
2
2
|
|
|
3
3
|
export type Ref<T extends Element = Element> = ((el: T | null) => void) & {
|
|
4
4
|
current: T | null;
|
|
5
5
|
};
|
|
6
6
|
|
|
7
7
|
/** Creates a standalone ref object for use outside of class fields (e.g. module-level or passed to @Lazy). */
|
|
8
|
-
|
|
9
8
|
export function createRef<T extends Element = Element>(): Ref<T> {
|
|
10
9
|
function refFn(el: T | null): void {
|
|
11
10
|
(refFn as Ref<T>).current = el;
|
|
@@ -15,13 +14,16 @@ export function createRef<T extends Element = Element>(): Ref<T> {
|
|
|
15
14
|
}
|
|
16
15
|
|
|
17
16
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters
|
|
18
|
-
export function Ref<T extends Element = Element>() {
|
|
19
|
-
return
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
17
|
+
export function Ref<T extends Element = Element>(): FieldDecorator {
|
|
18
|
+
return sharedField((name) => {
|
|
19
|
+
const ref = Symbol(`praxis.ref.${name}`);
|
|
20
|
+
return {
|
|
21
|
+
descriptor: {
|
|
22
|
+
// Created on first read: a ref that is never handed to an element costs nothing.
|
|
23
|
+
get(this: FieldHost) {
|
|
24
|
+
return (this[ref] ??= createRef<T>());
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
};
|
|
26
28
|
});
|
|
27
29
|
}
|
|
@@ -4,7 +4,8 @@ import {
|
|
|
4
4
|
type ResourceOptions,
|
|
5
5
|
} from "@praxisjs/core/internal";
|
|
6
6
|
|
|
7
|
-
import {
|
|
7
|
+
import { type FieldHost, sharedField } from "../field";
|
|
8
|
+
import { addLifecycleHooks } from "../lifecycle";
|
|
8
9
|
|
|
9
10
|
export { invalidateResource } from "@praxisjs/core/internal";
|
|
10
11
|
export type { ResourceInstance, ResourceOptions };
|
|
@@ -30,22 +31,26 @@ export function Resource<T>(
|
|
|
30
31
|
fetcher: (() => Promise<T>) | ((self: any) => Promise<T>),
|
|
31
32
|
options: ResourceOptions<T> = {},
|
|
32
33
|
): FieldDecoratorReturn {
|
|
33
|
-
return
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
const r = resource(bound, options);
|
|
41
|
-
return {
|
|
42
|
-
descriptor: {
|
|
43
|
-
get(): ResourceInstance<T> { return r; },
|
|
44
|
-
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|
45
|
-
set(): void {},
|
|
34
|
+
return sharedField((name) => {
|
|
35
|
+
const slot = Symbol(`praxis.resource.${name}`);
|
|
36
|
+
const destroy = (instance: object) => { ((instance as FieldHost)[slot] as ResourceInstance<T>).destroy(); };
|
|
37
|
+
return {
|
|
38
|
+
descriptor: {
|
|
39
|
+
get(this: FieldHost): ResourceInstance<T> {
|
|
40
|
+
return this[slot] as ResourceInstance<T>;
|
|
46
41
|
},
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
42
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|
43
|
+
set(): void {},
|
|
44
|
+
},
|
|
45
|
+
init(instance) {
|
|
46
|
+
const bound =
|
|
47
|
+
fetcher.length > 0
|
|
48
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
49
|
+
? () => (fetcher as (self: any) => Promise<T>)(instance)
|
|
50
|
+
: (fetcher as () => Promise<T>);
|
|
51
|
+
instance[slot] = resource(bound, options);
|
|
52
|
+
addLifecycleHooks(instance, undefined, destroy);
|
|
53
|
+
},
|
|
54
|
+
};
|
|
50
55
|
}) as unknown as FieldDecoratorReturn;
|
|
51
56
|
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { selector, type Selector as SelectorFn, type SelectorEquals } from "@praxisjs/core/internal";
|
|
2
|
+
import type { Computed } from "@praxisjs/shared";
|
|
3
|
+
import { isComputed } from "@praxisjs/shared/internal";
|
|
4
|
+
|
|
5
|
+
import { type FieldHost, sharedField } from "../field";
|
|
6
|
+
|
|
7
|
+
export type Selector<K> = SelectorFn<K>;
|
|
8
|
+
|
|
9
|
+
export interface SelectorOptions<K, T> {
|
|
10
|
+
/**
|
|
11
|
+
* Decides whether `key` is selected for the source value `value`. Defaults to a strict
|
|
12
|
+
* comparison (SameValueZero), which notifies only the previous and next keys; a custom function
|
|
13
|
+
* is evaluated against every key in use whenever the source changes.
|
|
14
|
+
*/
|
|
15
|
+
equals?: SelectorEquals<K, T>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function createSelector<K, T>(host: FieldHost, source: string, equals: SelectorEquals<K, T> | undefined): SelectorFn<K> {
|
|
19
|
+
const read = () => {
|
|
20
|
+
const raw = host[source];
|
|
21
|
+
return (isComputed(raw) ? (raw as Computed<unknown>)() : raw) as T;
|
|
22
|
+
};
|
|
23
|
+
return equals ? selector(read, equals) : (selector(read) as unknown as SelectorFn<K>);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function Selector<const Name extends string, K = unknown, T = unknown>(
|
|
27
|
+
source: Name,
|
|
28
|
+
options: SelectorOptions<K, T> = {},
|
|
29
|
+
) {
|
|
30
|
+
const decorator = sharedField<object>((name) => {
|
|
31
|
+
const slot = Symbol(`praxis.selector.${name}`);
|
|
32
|
+
return {
|
|
33
|
+
descriptor: {
|
|
34
|
+
// Created on first read, so the source field may be declared after this one.
|
|
35
|
+
get(this: FieldHost) {
|
|
36
|
+
return (this[slot] ??= createSelector(this, source, options.equals));
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
});
|
|
41
|
+
return decorator as <This extends Record<Name, unknown>, Value extends Selector<never>>(
|
|
42
|
+
value: undefined,
|
|
43
|
+
context: ClassFieldDecoratorContext<This, Value>,
|
|
44
|
+
) => void;
|
|
45
|
+
}
|
package/src/properties/slot.ts
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { type FieldDecorator, type FieldHost, sharedField } from "../field";
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
// On the instance rather than in a WeakMap: every slot field read goes through here.
|
|
4
|
+
const SLOTS: unique symbol = Symbol("praxis.slots");
|
|
5
|
+
|
|
6
|
+
interface SlotHost {
|
|
7
|
+
[SLOTS]?: Map<string, unknown[]>;
|
|
8
|
+
}
|
|
4
9
|
|
|
5
10
|
function resolveSlots(children: unknown): Map<string, unknown[]> {
|
|
6
11
|
const slots = new Map<string, unknown[]>();
|
|
@@ -18,8 +23,9 @@ function resolveSlots(children: unknown): Map<string, unknown[]> {
|
|
|
18
23
|
const slotName = child.getAttribute("slot");
|
|
19
24
|
if (slotName) {
|
|
20
25
|
child.removeAttribute("slot");
|
|
21
|
-
|
|
22
|
-
(slots.
|
|
26
|
+
let named = slots.get(slotName);
|
|
27
|
+
if (!named) slots.set(slotName, (named = []));
|
|
28
|
+
named.push(child);
|
|
23
29
|
continue;
|
|
24
30
|
}
|
|
25
31
|
}
|
|
@@ -30,29 +36,29 @@ function resolveSlots(children: unknown): Map<string, unknown[]> {
|
|
|
30
36
|
}
|
|
31
37
|
|
|
32
38
|
export function initSlots(instance: object, children: unknown): void {
|
|
33
|
-
|
|
39
|
+
(instance as SlotHost)[SLOTS] = resolveSlots(children);
|
|
34
40
|
}
|
|
35
41
|
|
|
36
42
|
export function getSlot(instance: object, name: string): unknown[] {
|
|
37
|
-
return
|
|
43
|
+
return (instance as SlotHost)[SLOTS]?.get(name) ?? [];
|
|
38
44
|
}
|
|
39
45
|
|
|
40
|
-
export function Slot(name?: string) {
|
|
41
|
-
return
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
}
|
|
46
|
+
export function Slot(name?: string): FieldDecorator {
|
|
47
|
+
return sharedField((propName) => {
|
|
48
|
+
const slotName = name ?? propName;
|
|
49
|
+
return {
|
|
50
|
+
descriptor: {
|
|
51
|
+
get(this: FieldHost) {
|
|
52
|
+
return getSlot(this, slotName);
|
|
53
|
+
},
|
|
54
|
+
set(_value: unknown) {
|
|
55
|
+
if (process.env.NODE_ENV !== "production") {
|
|
56
|
+
console.warn(
|
|
57
|
+
`[Slot] "${propName}" is a slot and cannot be assigned directly. Slots are filled by the parent component.`,
|
|
58
|
+
);
|
|
59
|
+
}
|
|
54
60
|
},
|
|
55
|
-
}
|
|
56
|
-
}
|
|
61
|
+
},
|
|
62
|
+
};
|
|
57
63
|
});
|
|
58
64
|
}
|
package/src/properties/state.ts
CHANGED
|
@@ -1,22 +1,38 @@
|
|
|
1
|
-
import { markStateDirty,
|
|
1
|
+
import { markStateDirty, readSignal, SignalNode, writeSignal } from "@praxisjs/core/internal";
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { addClaimableInitializer, assertFieldContext } from "../field";
|
|
4
4
|
|
|
5
5
|
import type { ReactiveHost } from "../reactive-host";
|
|
6
6
|
|
|
7
|
+
type StateHost = Record<PropertyKey, unknown>;
|
|
8
|
+
|
|
7
9
|
export function State() {
|
|
8
|
-
return
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
10
|
+
return function <This extends ReactiveHost, Value = unknown>(
|
|
11
|
+
_value: undefined,
|
|
12
|
+
context: ClassFieldDecoratorContext<This, Value>,
|
|
13
|
+
): void {
|
|
14
|
+
assertFieldContext(context, "@State()");
|
|
15
|
+
const name = context.name;
|
|
16
|
+
// One accessor pair per decorated field, shared by every instance: per-instance closures would
|
|
17
|
+
// give each instance its own hidden class, turning every field read into a megamorphic access.
|
|
18
|
+
// It stays an own property of each instance, so decorators stacked above @State (and custom ones
|
|
19
|
+
// built with createFieldDecorator) find and wrap it, and a Proxy around the instance works.
|
|
20
|
+
const node = Symbol(`praxis.state.${String(name)}`);
|
|
21
|
+
const descriptor: PropertyDescriptor = {
|
|
22
|
+
enumerable: true,
|
|
23
|
+
configurable: true,
|
|
24
|
+
get(this: StateHost) {
|
|
25
|
+
return readSignal(this[node] as SignalNode);
|
|
26
|
+
},
|
|
27
|
+
set(this: StateHost, value: unknown) {
|
|
28
|
+
markStateDirty(this);
|
|
29
|
+
writeSignal(this[node] as SignalNode, value);
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
addClaimableInitializer(context, function (value) {
|
|
33
|
+
const instance = this as unknown as StateHost;
|
|
34
|
+
instance[node] = new SignalNode(value);
|
|
35
|
+
Object.defineProperty(instance, name, descriptor);
|
|
36
|
+
});
|
|
37
|
+
};
|
|
22
38
|
}
|
package/src/properties/synced.ts
CHANGED
|
@@ -1,28 +1,40 @@
|
|
|
1
1
|
import { syncedSignal, type SyncedSignal } from "@praxisjs/core/internal";
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { type FieldDecorator, type FieldHost, sharedField } from "../field";
|
|
4
|
+
import { addLifecycleHooks } from "../lifecycle";
|
|
4
5
|
|
|
5
|
-
|
|
6
|
+
// Fields of one instance synced over the same channel share one signal. Only read while the
|
|
7
|
+
// instance is built; each field then reads its signal from its own slot.
|
|
8
|
+
const SIGNALS: unique symbol = Symbol("praxis.syncedSignals");
|
|
6
9
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
const key = channelName ?? name;
|
|
11
|
-
|
|
12
|
-
if (!signalMap.has(instance)) signalMap.set(instance, new Map());
|
|
13
|
-
const map = signalMap.get(instance) as Map<string, SyncedSignal<unknown>>;
|
|
14
|
-
if (!map.has(key)) map.set(key, syncedSignal(key, initialValue));
|
|
15
|
-
const sig = map.get(key) as SyncedSignal<unknown>;
|
|
10
|
+
interface SyncedHost {
|
|
11
|
+
[SIGNALS]?: Map<string, SyncedSignal<unknown>>;
|
|
12
|
+
}
|
|
16
13
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
14
|
+
export function Synced(channelName?: string): FieldDecorator {
|
|
15
|
+
return sharedField((name) => {
|
|
16
|
+
const slot = Symbol(`praxis.synced.${name}`);
|
|
17
|
+
const key = channelName ?? name;
|
|
18
|
+
const close = (instance: object) => { ((instance as FieldHost)[slot] as SyncedSignal<unknown>).close(); };
|
|
19
|
+
return {
|
|
20
|
+
descriptor: {
|
|
21
|
+
get(this: FieldHost) {
|
|
22
|
+
return (this[slot] as SyncedSignal<unknown>)();
|
|
21
23
|
},
|
|
22
|
-
|
|
23
|
-
|
|
24
|
+
set(this: FieldHost, value: unknown) {
|
|
25
|
+
(this[slot] as SyncedSignal<unknown>).set(value);
|
|
24
26
|
},
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
+
},
|
|
28
|
+
init(instance, initialValue) {
|
|
29
|
+
const map = ((instance as SyncedHost)[SIGNALS] ??= new Map<string, SyncedSignal<unknown>>());
|
|
30
|
+
let sig = map.get(key);
|
|
31
|
+
if (!sig) {
|
|
32
|
+
sig = syncedSignal(key, initialValue);
|
|
33
|
+
map.set(key, sig);
|
|
34
|
+
}
|
|
35
|
+
instance[slot] = sig;
|
|
36
|
+
addLifecycleHooks(instance, undefined, close);
|
|
37
|
+
},
|
|
38
|
+
};
|
|
27
39
|
});
|
|
28
40
|
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
2
|
+
export type AnyMethod = (this: any, ...args: any[]) => any;
|
|
3
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
4
|
+
export type MethodContext = ClassMethodDecoratorContext<any, any>;
|
|
5
|
+
|
|
6
|
+
// Replacements installed through sharedMethod, and the ones an outer decorator wrapped again. Only
|
|
7
|
+
// read while decorating and while building the first instance of a class.
|
|
8
|
+
const shared = new WeakSet<AnyMethod>();
|
|
9
|
+
const superseded = new WeakSet<AnyMethod>();
|
|
10
|
+
|
|
11
|
+
function isPrototype(object: object): boolean {
|
|
12
|
+
// Class prototypes own their `constructor`; instances inherit it.
|
|
13
|
+
return Object.hasOwn(object, "constructor");
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function ownerOf(object: object, name: PropertyKey): object | null {
|
|
17
|
+
let owner: object | null = object;
|
|
18
|
+
while (owner !== null && !Object.hasOwn(owner, name)) owner = Object.getPrototypeOf(owner) as object | null;
|
|
19
|
+
return owner;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function defineOwnMethod(object: object, name: PropertyKey, value: unknown): void {
|
|
23
|
+
Object.defineProperty(object, name, { value, configurable: true, writable: true });
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Returns `method`, the decorator's replacement for `value`, set up so that reading it from an
|
|
28
|
+
* instance returns it bound to that instance, the same function on every read: detached calls
|
|
29
|
+
* (`onClick={this.save}`) keep working. `method` is shared by every instance and keeps any
|
|
30
|
+
* per-instance state in symbol slots.
|
|
31
|
+
*
|
|
32
|
+
* Rather than defining a bound wrapper on each instance as it is built, the first instance turns
|
|
33
|
+
* the method's prototype entry into an accessor: an instance pays for its bound copy only when the
|
|
34
|
+
* method is first read from it, and nothing if it never is.
|
|
35
|
+
*/
|
|
36
|
+
export function sharedMethod<F extends AnyMethod>(
|
|
37
|
+
context: MethodContext,
|
|
38
|
+
value: AnyMethod,
|
|
39
|
+
method: F = value as F,
|
|
40
|
+
): F {
|
|
41
|
+
if (value === method) {
|
|
42
|
+
// @Bind over a decorator that already binds: nothing left to do.
|
|
43
|
+
if (shared.has(method)) return method;
|
|
44
|
+
} else {
|
|
45
|
+
// The inner decorator's replacement now only runs through this one, which does the binding.
|
|
46
|
+
superseded.add(value);
|
|
47
|
+
}
|
|
48
|
+
shared.add(method);
|
|
49
|
+
// `this.#method` can't be read detached through a property, so there is nothing to bind.
|
|
50
|
+
if (context.private) return method;
|
|
51
|
+
|
|
52
|
+
const name = context.name;
|
|
53
|
+
let installed = false;
|
|
54
|
+
let home: object | undefined;
|
|
55
|
+
|
|
56
|
+
const accessor: PropertyDescriptor = {
|
|
57
|
+
configurable: true,
|
|
58
|
+
enumerable: false,
|
|
59
|
+
get(this: object | undefined) {
|
|
60
|
+
// Read from a prototype, or through `super.method()` from a subclass override: caching a
|
|
61
|
+
// bound copy on the instance would then shadow the override. Read with no receiver at all,
|
|
62
|
+
// as `vi.spyOn(instance, name)` does with a getter it finds on the prototype: the spy then
|
|
63
|
+
// wraps the method itself, which takes `this` from each call.
|
|
64
|
+
if (this == null || isPrototype(this) || ownerOf(this, name) !== home) return method;
|
|
65
|
+
const bound = method.bind(this);
|
|
66
|
+
defineOwnMethod(this, name, bound);
|
|
67
|
+
return bound;
|
|
68
|
+
},
|
|
69
|
+
set(this: object, next: unknown) {
|
|
70
|
+
defineOwnMethod(this, name, next);
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
context.addInitializer(function (this: unknown) {
|
|
75
|
+
if (installed) return;
|
|
76
|
+
const target = this as object;
|
|
77
|
+
if (superseded.has(method)) {
|
|
78
|
+
installed = true;
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
if (!context.static) {
|
|
82
|
+
for (
|
|
83
|
+
let proto = Object.getPrototypeOf(target) as object | null;
|
|
84
|
+
proto !== null;
|
|
85
|
+
proto = Object.getPrototypeOf(proto) as object | null
|
|
86
|
+
) {
|
|
87
|
+
if (Object.getOwnPropertyDescriptor(proto, name)?.value === method) {
|
|
88
|
+
installed = true;
|
|
89
|
+
home = proto;
|
|
90
|
+
Object.defineProperty(proto, name, accessor);
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
// A static method (bound to its class, as before), or a decorator applied by hand whose
|
|
96
|
+
// replacement never reached a prototype: bind it on the object itself.
|
|
97
|
+
defineOwnMethod(target, name, method.bind(target));
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
return method;
|
|
101
|
+
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"component.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/component.test.ts"],"names":[],"mappings":""}
|