@praxisjs/decorators 0.8.0 → 1.0.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/CHANGELOG.md +139 -3
- package/dist/__tests__/lazy.test.js +74 -10
- package/dist/__tests__/lazy.test.js.map +1 -1
- package/dist/__tests__/properties.test.js +61 -3
- package/dist/__tests__/properties.test.js.map +1 -1
- package/dist/component/index.d.ts +0 -1
- package/dist/component/index.d.ts.map +1 -1
- package/dist/component/index.js +0 -1
- package/dist/component/index.js.map +1 -1
- package/dist/component/lazy.d.ts +15 -1
- package/dist/component/lazy.d.ts.map +1 -1
- package/dist/component/lazy.js +15 -9
- package/dist/component/lazy.js.map +1 -1
- package/dist/create-class-decorator.d.ts.map +1 -1
- package/dist/create-class-decorator.js +16 -3
- package/dist/create-class-decorator.js.map +1 -1
- package/dist/create-field-decorator.d.ts +1 -1
- package/dist/create-field-decorator.d.ts.map +1 -1
- package/dist/create-field-decorator.js.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/properties/compose.d.ts +8 -1
- package/dist/properties/compose.d.ts.map +1 -1
- package/dist/properties/compose.js +18 -3
- package/dist/properties/compose.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.map +1 -1
- package/dist/properties/history.d.ts +1 -1
- package/dist/properties/history.d.ts.map +1 -1
- package/dist/properties/index.d.ts +1 -1
- package/dist/properties/index.d.ts.map +1 -1
- package/dist/properties/index.js +1 -1
- package/dist/properties/index.js.map +1 -1
- package/dist/properties/persisted.d.ts +1 -1
- package/dist/properties/persisted.d.ts.map +1 -1
- package/dist/properties/prop.d.ts +1 -1
- package/dist/properties/prop.d.ts.map +1 -1
- package/dist/properties/resource.d.ts +1 -1
- package/dist/properties/resource.d.ts.map +1 -1
- package/dist/properties/slot.d.ts +1 -1
- package/dist/properties/slot.d.ts.map +1 -1
- package/dist/properties/state.d.ts +2 -1
- package/dist/properties/state.d.ts.map +1 -1
- package/dist/properties/state.js.map +1 -1
- package/dist/properties/synced.d.ts +1 -1
- package/dist/properties/synced.d.ts.map +1 -1
- package/dist/reactive-host.d.ts +4 -0
- package/dist/reactive-host.d.ts.map +1 -0
- package/dist/reactive-host.js +2 -0
- package/dist/reactive-host.js.map +1 -0
- package/package.json +1 -1
- package/src/__tests__/lazy.test.ts +88 -10
- package/src/__tests__/properties.test.ts +71 -3
- package/src/component/index.ts +0 -1
- package/src/component/lazy.ts +29 -7
- package/src/create-class-decorator.ts +16 -4
- package/src/create-field-decorator.ts +3 -3
- package/src/index.ts +3 -1
- package/src/properties/compose.ts +21 -8
- package/src/properties/deep-state.ts +3 -1
- package/src/properties/index.ts +1 -1
- package/src/properties/state.ts +3 -1
- package/src/reactive-host.ts +3 -0
- package/dist/__tests__/virtual.test.d.ts +0 -2
- package/dist/__tests__/virtual.test.d.ts.map +0 -1
- package/dist/__tests__/virtual.test.js +0 -352
- package/dist/__tests__/virtual.test.js.map +0 -1
- package/dist/component/virtual.d.ts +0 -3
- package/dist/component/virtual.d.ts.map +0 -1
- package/dist/component/virtual.js +0 -102
- package/dist/component/virtual.js.map +0 -1
- package/src/__tests__/virtual.test.ts +0 -371
- package/src/component/virtual.tsx +0 -163
|
@@ -967,7 +967,7 @@ describe("@Compose decorator — new cases", () => {
|
|
|
967
967
|
}).toThrow("setup failed");
|
|
968
968
|
});
|
|
969
969
|
|
|
970
|
-
it("string argument
|
|
970
|
+
it("string argument that matches no instance property falls back to the literal string", () => {
|
|
971
971
|
class ArgComposable {
|
|
972
972
|
constructor(private val: unknown) {}
|
|
973
973
|
setup() { return { resolved: this.val }; }
|
|
@@ -978,11 +978,79 @@ describe("@Compose decorator — new cases", () => {
|
|
|
978
978
|
Compose(ArgComposable as any, "missingProp")(undefined, ctx);
|
|
979
979
|
|
|
980
980
|
const instance = new TestComponent();
|
|
981
|
-
// missingProp is not set on instance —
|
|
981
|
+
// missingProp is not set on instance — falls back to the literal "missingProp"
|
|
982
982
|
run(instance);
|
|
983
983
|
|
|
984
984
|
const view = (instance as unknown as Record<string, unknown>).strArgUndef as Record<string, unknown>;
|
|
985
|
-
expect(view.resolved).
|
|
985
|
+
expect(view.resolved).toBe("missingProp");
|
|
986
|
+
});
|
|
987
|
+
|
|
988
|
+
it("string argument that matches an instance property resolves to its value", () => {
|
|
989
|
+
class ArgComposable {
|
|
990
|
+
constructor(private val: unknown) {}
|
|
991
|
+
setup() { return { resolved: this.val }; }
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
const { ctx, run } = fieldCtx("strArgFound");
|
|
995
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
996
|
+
Compose(ArgComposable as any, "myProp")(undefined, ctx);
|
|
997
|
+
|
|
998
|
+
const instance = new TestComponent();
|
|
999
|
+
(instance as unknown as Record<string, unknown>).myProp = 42;
|
|
1000
|
+
run(instance);
|
|
1001
|
+
|
|
1002
|
+
const view = (instance as unknown as Record<string, unknown>).strArgFound as Record<string, unknown>;
|
|
1003
|
+
expect(view.resolved).toBe(42);
|
|
1004
|
+
});
|
|
1005
|
+
});
|
|
1006
|
+
|
|
1007
|
+
// ── getter() helper ───────────────────────────────────────────────────────────
|
|
1008
|
+
|
|
1009
|
+
import { getter } from "../properties/compose";
|
|
1010
|
+
|
|
1011
|
+
describe("getter() factory", () => {
|
|
1012
|
+
it("resolves to a function that reads the named property from the instance", () => {
|
|
1013
|
+
class ArgComposable {
|
|
1014
|
+
constructor(private getVal: () => unknown) {}
|
|
1015
|
+
setup() { return { current: this.getVal() }; }
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
const { ctx, run } = fieldCtx("withGetter");
|
|
1019
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1020
|
+
Compose(ArgComposable as any, getter("dynProp"))(undefined, ctx);
|
|
1021
|
+
|
|
1022
|
+
const instance = new TestComponent();
|
|
1023
|
+
(instance as unknown as Record<string, unknown>).dynProp = 99;
|
|
1024
|
+
run(instance);
|
|
1025
|
+
|
|
1026
|
+
const view = (instance as unknown as Record<string, unknown>).withGetter as Record<string, unknown>;
|
|
1027
|
+
expect(view.current).toBe(99);
|
|
1028
|
+
});
|
|
1029
|
+
|
|
1030
|
+
it("getter() produces a live reader — reads the property at call time", () => {
|
|
1031
|
+
class ArgComposable {
|
|
1032
|
+
constructor(private getVal: () => unknown) {}
|
|
1033
|
+
setup() { return { read: this.getVal }; }
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
const { ctx, run } = fieldCtx("liveGetter");
|
|
1037
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1038
|
+
Compose(ArgComposable as any, getter("mutableProp"))(undefined, ctx);
|
|
1039
|
+
|
|
1040
|
+
const instance = new TestComponent();
|
|
1041
|
+
(instance as unknown as Record<string, unknown>).mutableProp = "first";
|
|
1042
|
+
run(instance);
|
|
1043
|
+
|
|
1044
|
+
const view = (instance as unknown as Record<string, unknown>).liveGetter as Record<string, () => unknown>;
|
|
1045
|
+
expect(view.read()).toBe("first");
|
|
1046
|
+
|
|
1047
|
+
(instance as unknown as Record<string, unknown>).mutableProp = "second";
|
|
1048
|
+
expect(view.read()).toBe("second");
|
|
1049
|
+
});
|
|
1050
|
+
|
|
1051
|
+
it("getter() is marked as a ComposeFactory (has __isComposeFactory)", () => {
|
|
1052
|
+
const g = getter("foo");
|
|
1053
|
+
expect((g as unknown as Record<string, unknown>).__isComposeFactory).toBe(true);
|
|
986
1054
|
});
|
|
987
1055
|
});
|
|
988
1056
|
|
package/src/component/index.ts
CHANGED
package/src/component/lazy.ts
CHANGED
|
@@ -2,13 +2,26 @@ import { type RootComponent, signal } from "@praxisjs/core/internal";
|
|
|
2
2
|
|
|
3
3
|
import { ClassBehavior, createClassDecorator, type ClassEnhancement } from "../create-class-decorator";
|
|
4
4
|
|
|
5
|
+
export interface LazyOptions {
|
|
6
|
+
/** Placeholder height in pixels while the component is off-screen. */
|
|
7
|
+
placeholder?: number;
|
|
8
|
+
/**
|
|
9
|
+
* Scroll container to observe against. Defaults to the viewport (`null`).
|
|
10
|
+
* Pass a ref object `{ current: HTMLElement | null }` to scope intersection
|
|
11
|
+
* to a specific scrollable container.
|
|
12
|
+
*/
|
|
13
|
+
root?: { current: HTMLElement | null } | null;
|
|
14
|
+
/** Extra margin around the root before triggering (e.g. `"200px"`). Defaults to `"100px"`. */
|
|
15
|
+
rootMargin?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
5
18
|
class LazyBehavior extends ClassBehavior {
|
|
6
|
-
constructor(private readonly
|
|
19
|
+
constructor(private readonly options: LazyOptions) {
|
|
7
20
|
super();
|
|
8
21
|
}
|
|
9
22
|
|
|
10
23
|
create(instance: RootComponent): ClassEnhancement {
|
|
11
|
-
const placeholder = this.
|
|
24
|
+
const { placeholder = 200, root, rootMargin = "100px" } = this.options;
|
|
12
25
|
const visible = signal(false);
|
|
13
26
|
let observer: IntersectionObserver | undefined;
|
|
14
27
|
|
|
@@ -25,6 +38,9 @@ class LazyBehavior extends ClassBehavior {
|
|
|
25
38
|
|
|
26
39
|
if (!visible()) el.style.minHeight = `${String(placeholder)}px`;
|
|
27
40
|
|
|
41
|
+
const rootEl =
|
|
42
|
+
root === undefined || root === null ? null : root.current ?? null;
|
|
43
|
+
|
|
28
44
|
observer = new IntersectionObserver(
|
|
29
45
|
(entries) => {
|
|
30
46
|
if (entries[0]?.isIntersecting) {
|
|
@@ -33,7 +49,7 @@ class LazyBehavior extends ClassBehavior {
|
|
|
33
49
|
observer?.disconnect();
|
|
34
50
|
}
|
|
35
51
|
},
|
|
36
|
-
{
|
|
52
|
+
{ root: rootEl, rootMargin },
|
|
37
53
|
);
|
|
38
54
|
|
|
39
55
|
observer.observe(el);
|
|
@@ -44,13 +60,19 @@ class LazyBehavior extends ClassBehavior {
|
|
|
44
60
|
},
|
|
45
61
|
|
|
46
62
|
render(originalRender) {
|
|
47
|
-
|
|
48
|
-
|
|
63
|
+
return (() => {
|
|
64
|
+
if (!visible()) return null;
|
|
65
|
+
return originalRender();
|
|
66
|
+
}) as unknown as Node;
|
|
49
67
|
},
|
|
50
68
|
};
|
|
51
69
|
}
|
|
52
70
|
}
|
|
53
71
|
|
|
54
|
-
export function Lazy(
|
|
55
|
-
|
|
72
|
+
export function Lazy(placeholderOrOptions: number | LazyOptions = 200) {
|
|
73
|
+
const options: LazyOptions =
|
|
74
|
+
typeof placeholderOrOptions === "number"
|
|
75
|
+
? { placeholder: placeholderOrOptions }
|
|
76
|
+
: placeholderOrOptions;
|
|
77
|
+
return createClassDecorator(new LazyBehavior(options));
|
|
56
78
|
}
|
|
@@ -19,6 +19,12 @@ export abstract class ClassBehavior {
|
|
|
19
19
|
initialize?(_Enhanced: new (...args: any[]) => unknown, _original: new (...args: any[]) => unknown): void;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
// Tracks instances whose render is currently being called as `originalRender`
|
|
23
|
+
// from within an enhancement. When set, the Enhanced render skips the
|
|
24
|
+
// this._enh.render dispatch so stacked decorators (e.g. @Lazy + @Component)
|
|
25
|
+
// don't re-enter the outermost enhancement recursively.
|
|
26
|
+
const _originalRenderInProgress = new WeakSet();
|
|
27
|
+
|
|
22
28
|
export function createClassDecorator(behavior: ClassBehavior) {
|
|
23
29
|
return function <
|
|
24
30
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
@@ -40,12 +46,18 @@ export function createClassDecorator(behavior: ClassBehavior) {
|
|
|
40
46
|
}
|
|
41
47
|
|
|
42
48
|
render(...args: unknown[]): Node | Node[] | null {
|
|
43
|
-
const originalRender = () =>
|
|
44
|
-
|
|
45
|
-
|
|
49
|
+
const originalRender = () => {
|
|
50
|
+
_originalRenderInProgress.add(this);
|
|
51
|
+
try {
|
|
52
|
+
return (constructor.prototype as { render: (...a: unknown[]) => Node | Node[] | null }).render.apply(this, args);
|
|
53
|
+
} finally {
|
|
54
|
+
_originalRenderInProgress.delete(this);
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
if (this._enh.render && !_originalRenderInProgress.has(this)) {
|
|
46
58
|
return this._enh.render(originalRender);
|
|
47
59
|
}
|
|
48
|
-
return
|
|
60
|
+
return (constructor.prototype as { render: (...a: unknown[]) => Node | Node[] | null }).render.apply(this, args);
|
|
49
61
|
}
|
|
50
62
|
};
|
|
51
63
|
|
|
@@ -18,12 +18,12 @@ export interface FieldBehavior<T = object> {
|
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
export function createFieldDecorator<T = StatefulComponent>(behavior: FieldBehavior<T>) {
|
|
21
|
-
return function (
|
|
21
|
+
return function <This extends T, Value = unknown>(
|
|
22
22
|
_value: undefined,
|
|
23
|
-
context: ClassFieldDecoratorContext<
|
|
23
|
+
context: ClassFieldDecoratorContext<This, Value>,
|
|
24
24
|
): void {
|
|
25
25
|
context.addInitializer(function (this: unknown) {
|
|
26
|
-
const instance = this as
|
|
26
|
+
const instance = this as This & StatefulComponent & Record<string, unknown>;
|
|
27
27
|
const name = context.name as string;
|
|
28
28
|
const initialValue = instance[name];
|
|
29
29
|
|
package/src/index.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { Component, Lazy
|
|
1
|
+
export { Component, Lazy } from "./component";
|
|
2
2
|
export {
|
|
3
3
|
Memo,
|
|
4
4
|
Bind,
|
|
@@ -23,6 +23,7 @@ export {
|
|
|
23
23
|
History,
|
|
24
24
|
type HistoryOf,
|
|
25
25
|
Compose,
|
|
26
|
+
getter,
|
|
26
27
|
Resource,
|
|
27
28
|
type ResourceInstance,
|
|
28
29
|
type ResourceOptions,
|
|
@@ -35,6 +36,7 @@ export {
|
|
|
35
36
|
type FieldBehavior,
|
|
36
37
|
type FieldBinding,
|
|
37
38
|
} from "./create-field-decorator";
|
|
39
|
+
export { type ReactiveHost } from "./reactive-host";
|
|
38
40
|
export {
|
|
39
41
|
createClassDecorator,
|
|
40
42
|
ClassBehavior,
|
|
@@ -2,9 +2,21 @@ import type { StatefulComponent, Composable } from "@praxisjs/core";
|
|
|
2
2
|
|
|
3
3
|
import { createFieldDecorator } from "../create-field-decorator";
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
interface ComposeFactory { __isComposeFactory: true; resolve: (instance: object) => unknown }
|
|
6
|
+
|
|
7
|
+
/** Wraps a property as a live getter — passes `() => instance[propName]` to the composable. */
|
|
8
|
+
export function getter(propName: string): ComposeFactory {
|
|
9
|
+
return {
|
|
10
|
+
__isComposeFactory: true,
|
|
11
|
+
resolve: (instance) => () => (instance as Record<string, unknown>)[propName],
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function isFactory(value: unknown): value is ComposeFactory {
|
|
16
|
+
return typeof value === "object" && value !== null && "__isComposeFactory" in value;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function isReactive(value: unknown): value is () => unknown {
|
|
8
20
|
return (
|
|
9
21
|
typeof value === "function" &&
|
|
10
22
|
("__isSignal" in (value as object) || "__isComputed" in (value as object))
|
|
@@ -34,11 +46,12 @@ export function Compose(
|
|
|
34
46
|
) {
|
|
35
47
|
return createFieldDecorator({
|
|
36
48
|
bind(instance: StatefulComponent, _name: string, _initialValue: unknown) {
|
|
37
|
-
const resolvedArgs = ctorArgs.map((arg) =>
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
49
|
+
const resolvedArgs = ctorArgs.map((arg) => {
|
|
50
|
+
if (isFactory(arg)) return arg.resolve(instance);
|
|
51
|
+
if (typeof arg !== "string") return arg;
|
|
52
|
+
const prop = (instance as unknown as Record<string, unknown>)[arg];
|
|
53
|
+
return prop !== undefined ? prop : arg;
|
|
54
|
+
});
|
|
42
55
|
|
|
43
56
|
const composable = new ComposableClass(...resolvedArgs);
|
|
44
57
|
const view = buildView(composable.setup());
|
|
@@ -2,6 +2,8 @@ import { signal } from "@praxisjs/core/internal";
|
|
|
2
2
|
|
|
3
3
|
import { createFieldDecorator } from "../create-field-decorator";
|
|
4
4
|
|
|
5
|
+
import type { ReactiveHost } from "../reactive-host";
|
|
6
|
+
|
|
5
7
|
function deepProxy<T extends object>(target: T, notify: () => void): T {
|
|
6
8
|
return new Proxy(target, {
|
|
7
9
|
get(obj, key, receiver) {
|
|
@@ -25,7 +27,7 @@ function deepProxy<T extends object>(target: T, notify: () => void): T {
|
|
|
25
27
|
}
|
|
26
28
|
|
|
27
29
|
export function DeepState() {
|
|
28
|
-
return createFieldDecorator({
|
|
30
|
+
return createFieldDecorator<ReactiveHost>({
|
|
29
31
|
bind(instance, _name, initialValue) {
|
|
30
32
|
const version = signal(0);
|
|
31
33
|
let current = initialValue;
|
package/src/properties/index.ts
CHANGED
|
@@ -4,7 +4,7 @@ export { Computed } from "./computed";
|
|
|
4
4
|
export { Persisted } from "./persisted";
|
|
5
5
|
export { Slot, initSlots } from "./slot";
|
|
6
6
|
export { History, type HistoryOf } from "./history";
|
|
7
|
-
export { Compose } from "./compose";
|
|
7
|
+
export { Compose, getter } from "./compose";
|
|
8
8
|
export { Resource, type ResourceInstance, type ResourceOptions } from "./resource";
|
|
9
9
|
export { Synced } from "./synced";
|
|
10
10
|
export { DeepState } from "./deep-state";
|
package/src/properties/state.ts
CHANGED
|
@@ -2,8 +2,10 @@ import { signal } from "@praxisjs/core/internal";
|
|
|
2
2
|
|
|
3
3
|
import { createFieldDecorator } from "../create-field-decorator";
|
|
4
4
|
|
|
5
|
+
import type { ReactiveHost } from "../reactive-host";
|
|
6
|
+
|
|
5
7
|
export function State() {
|
|
6
|
-
return createFieldDecorator({
|
|
8
|
+
return createFieldDecorator<ReactiveHost>({
|
|
7
9
|
bind(instance, _name, initialValue) {
|
|
8
10
|
const sig = signal(initialValue);
|
|
9
11
|
return {
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"virtual.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/virtual.test.ts"],"names":[],"mappings":""}
|