@praxisjs/decorators 0.4.3 → 0.5.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 +25 -0
- package/dist/__tests__/component.test.js +10 -1
- package/dist/__tests__/component.test.js.map +1 -1
- package/dist/__tests__/compose.test.d.ts +2 -0
- package/dist/__tests__/compose.test.d.ts.map +1 -0
- package/dist/__tests__/compose.test.js +171 -0
- package/dist/__tests__/compose.test.js.map +1 -0
- package/dist/__tests__/events.test.js +26 -0
- package/dist/__tests__/events.test.js.map +1 -1
- package/dist/__tests__/functions.test.js +142 -47
- package/dist/__tests__/functions.test.js.map +1 -1
- package/dist/__tests__/lazy.test.js +5 -7
- package/dist/__tests__/lazy.test.js.map +1 -1
- package/dist/__tests__/memo.test.js +61 -25
- package/dist/__tests__/memo.test.js.map +1 -1
- package/dist/__tests__/resource.test.d.ts +2 -0
- package/dist/__tests__/resource.test.d.ts.map +1 -0
- package/dist/__tests__/resource.test.js +103 -0
- package/dist/__tests__/resource.test.js.map +1 -0
- package/dist/__tests__/slot.test.js +32 -0
- package/dist/__tests__/slot.test.js.map +1 -1
- package/dist/__tests__/virtual.test.js +56 -15
- package/dist/__tests__/virtual.test.js.map +1 -1
- package/dist/component/component.d.ts +1 -1
- package/dist/component/component.d.ts.map +1 -1
- package/dist/component/component.js +15 -10
- package/dist/component/component.js.map +1 -1
- package/dist/component/lazy.d.ts.map +1 -1
- package/dist/component/lazy.js +29 -30
- package/dist/component/lazy.js.map +1 -1
- package/dist/component/virtual.d.ts.map +1 -1
- package/dist/component/virtual.js +46 -50
- package/dist/component/virtual.js.map +1 -1
- package/dist/create-class-decorator.d.ts +16 -0
- package/dist/create-class-decorator.d.ts.map +1 -0
- package/dist/create-class-decorator.js +35 -0
- package/dist/create-class-decorator.js.map +1 -0
- package/dist/create-field-decorator.d.ts +14 -0
- package/dist/create-field-decorator.d.ts.map +1 -0
- package/dist/create-field-decorator.js +37 -0
- package/dist/create-field-decorator.js.map +1 -0
- package/dist/create-getter-decorator.d.ts +18 -0
- package/dist/create-getter-decorator.d.ts.map +1 -0
- package/dist/create-getter-decorator.js +15 -0
- package/dist/create-getter-decorator.js.map +1 -0
- package/dist/create-lifecycle-method-decorator.d.ts +6 -0
- package/dist/create-lifecycle-method-decorator.d.ts.map +1 -0
- package/dist/create-lifecycle-method-decorator.js +22 -0
- package/dist/create-lifecycle-method-decorator.js.map +1 -0
- package/dist/create-method-decorator.d.ts +6 -0
- package/dist/create-method-decorator.d.ts.map +1 -0
- package/dist/create-method-decorator.js +14 -0
- package/dist/create-method-decorator.js.map +1 -0
- package/dist/events/emit.d.ts +1 -1
- package/dist/events/emit.d.ts.map +1 -1
- package/dist/events/emit.js +24 -29
- package/dist/events/emit.js.map +1 -1
- package/dist/events/on-command.d.ts +1 -2
- package/dist/events/on-command.d.ts.map +1 -1
- package/dist/events/on-command.js +15 -32
- package/dist/events/on-command.js.map +1 -1
- package/dist/functions/bind.d.ts +1 -2
- package/dist/functions/bind.d.ts.map +1 -1
- package/dist/functions/bind.js +4 -11
- package/dist/functions/bind.js.map +1 -1
- package/dist/functions/debounce.d.ts +1 -2
- package/dist/functions/debounce.d.ts.map +1 -1
- package/dist/functions/debounce.js +12 -20
- package/dist/functions/debounce.js.map +1 -1
- package/dist/functions/log.d.ts +1 -2
- package/dist/functions/log.d.ts.map +1 -1
- package/dist/functions/log.js +40 -38
- package/dist/functions/log.js.map +1 -1
- package/dist/functions/memo.d.ts +1 -2
- package/dist/functions/memo.d.ts.map +1 -1
- package/dist/functions/memo.js +15 -27
- package/dist/functions/memo.js.map +1 -1
- package/dist/functions/once.d.ts +1 -2
- package/dist/functions/once.d.ts.map +1 -1
- package/dist/functions/once.js +14 -12
- package/dist/functions/once.js.map +1 -1
- package/dist/functions/retry.d.ts +1 -2
- package/dist/functions/retry.d.ts.map +1 -1
- package/dist/functions/retry.js +23 -20
- package/dist/functions/retry.js.map +1 -1
- package/dist/functions/throttle.d.ts +1 -2
- package/dist/functions/throttle.d.ts.map +1 -1
- package/dist/functions/throttle.js +9 -16
- package/dist/functions/throttle.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 +25 -39
- package/dist/functions/watch.js.map +1 -1
- package/dist/functions/when.d.ts +1 -2
- package/dist/functions/when.d.ts.map +1 -1
- package/dist/functions/when.js +10 -26
- package/dist/functions/when.js.map +1 -1
- package/dist/index.d.ts +6 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -1
- package/dist/index.js.map +1 -1
- package/dist/properties/compose.d.ts +3 -0
- package/dist/properties/compose.d.ts.map +1 -0
- package/dist/properties/compose.js +40 -0
- package/dist/properties/compose.js.map +1 -0
- package/dist/properties/computed.d.ts +1 -2
- package/dist/properties/computed.d.ts.map +1 -1
- package/dist/properties/computed.js +10 -9
- package/dist/properties/computed.js.map +1 -1
- package/dist/properties/history.d.ts.map +1 -1
- package/dist/properties/history.js +37 -32
- package/dist/properties/history.js.map +1 -1
- package/dist/properties/index.d.ts +2 -0
- package/dist/properties/index.d.ts.map +1 -1
- package/dist/properties/index.js +2 -0
- package/dist/properties/index.js.map +1 -1
- package/dist/properties/persisted.d.ts +1 -2
- package/dist/properties/persisted.d.ts.map +1 -1
- package/dist/properties/persisted.js +13 -29
- package/dist/properties/persisted.js.map +1 -1
- package/dist/properties/prop.d.ts +1 -2
- package/dist/properties/prop.d.ts.map +1 -1
- package/dist/properties/prop.js +21 -25
- package/dist/properties/prop.js.map +1 -1
- package/dist/properties/resource.d.ts +14 -0
- package/dist/properties/resource.d.ts.map +1 -0
- package/dist/properties/resource.js +27 -0
- package/dist/properties/resource.js.map +1 -0
- package/dist/properties/slot.d.ts +1 -2
- package/dist/properties/slot.d.ts.map +1 -1
- package/dist/properties/slot.js +15 -20
- package/dist/properties/slot.js.map +1 -1
- package/dist/properties/state.d.ts +1 -2
- package/dist/properties/state.d.ts.map +1 -1
- package/dist/properties/state.js +13 -18
- package/dist/properties/state.js.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/component.test.ts +10 -0
- package/src/__tests__/compose.test.ts +202 -0
- package/src/__tests__/events.test.ts +32 -0
- package/src/__tests__/functions.test.ts +177 -46
- package/src/__tests__/lazy.test.ts +3 -5
- package/src/__tests__/memo.test.ts +76 -25
- package/src/__tests__/resource.test.ts +113 -0
- package/src/__tests__/slot.test.ts +38 -0
- package/src/__tests__/virtual.test.ts +26 -7
- package/src/component/component.ts +27 -12
- package/src/component/lazy.ts +33 -37
- package/src/component/virtual.tsx +62 -75
- package/src/create-class-decorator.ts +61 -0
- package/src/create-field-decorator.ts +66 -0
- package/src/create-getter-decorator.ts +52 -0
- package/src/create-lifecycle-method-decorator.ts +38 -0
- package/src/create-method-decorator.ts +26 -0
- package/src/events/emit.ts +22 -33
- package/src/events/on-command.ts +22 -47
- package/src/functions/bind.ts +4 -15
- package/src/functions/debounce.ts +11 -26
- package/src/functions/log.ts +40 -43
- package/src/functions/memo.ts +15 -45
- package/src/functions/once.ts +13 -16
- package/src/functions/retry.ts +21 -22
- package/src/functions/throttle.ts +9 -23
- package/src/functions/watch.ts +27 -43
- package/src/functions/when.ts +11 -36
- package/src/index.ts +28 -0
- package/src/properties/compose.ts +53 -0
- package/src/properties/computed.ts +11 -13
- package/src/properties/history.ts +42 -41
- package/src/properties/index.ts +2 -0
- package/src/properties/persisted.ts +17 -58
- package/src/properties/prop.ts +21 -31
- package/src/properties/resource.ts +34 -0
- package/src/properties/slot.ts +17 -30
- package/src/properties/state.ts +14 -24
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { Resource } from "../properties/resource";
|
|
4
|
+
|
|
5
|
+
function makeFieldCtx(name: string) {
|
|
6
|
+
const initializers: Array<(this: unknown) => void> = [];
|
|
7
|
+
return {
|
|
8
|
+
ctx: {
|
|
9
|
+
name,
|
|
10
|
+
kind: "field" as const,
|
|
11
|
+
addInitializer(fn: (this: unknown) => void) { initializers.push(fn); },
|
|
12
|
+
} as ClassFieldDecoratorContext,
|
|
13
|
+
run(instance: unknown) { initializers.forEach((fn) => { fn.call(instance); }); },
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
describe("@Resource", () => {
|
|
18
|
+
it("binds a Resource object to the field", () => {
|
|
19
|
+
const { ctx, run } = makeFieldCtx("users");
|
|
20
|
+
Resource(() => Promise.resolve([]))(undefined, ctx);
|
|
21
|
+
const instance: Record<string, unknown> = {};
|
|
22
|
+
run(instance);
|
|
23
|
+
const r = instance.users as Record<string, unknown>;
|
|
24
|
+
expect(typeof r.data).toBe("function");
|
|
25
|
+
expect(typeof r.pending).toBe("function");
|
|
26
|
+
expect(typeof r.error).toBe("function");
|
|
27
|
+
expect(typeof r.status).toBe("function");
|
|
28
|
+
expect(typeof r.refetch).toBe("function");
|
|
29
|
+
expect(typeof r.cancel).toBe("function");
|
|
30
|
+
expect(typeof r.mutate).toBe("function");
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("starts in pending state when immediate=true (default)", () => {
|
|
34
|
+
const { ctx, run } = makeFieldCtx("data");
|
|
35
|
+
Resource(() => new Promise(() => {}))(undefined, ctx);
|
|
36
|
+
const instance: Record<string, unknown> = {};
|
|
37
|
+
run(instance);
|
|
38
|
+
const r = instance.data as { pending: () => boolean; status: () => string };
|
|
39
|
+
expect(r.pending()).toBe(true);
|
|
40
|
+
expect(r.status()).toBe("pending");
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("starts idle when immediate=false", () => {
|
|
44
|
+
const { ctx, run } = makeFieldCtx("data");
|
|
45
|
+
Resource(() => Promise.resolve(42), { immediate: false })(undefined, ctx);
|
|
46
|
+
const instance: Record<string, unknown> = {};
|
|
47
|
+
run(instance);
|
|
48
|
+
const r = instance.data as { status: () => string; pending: () => boolean };
|
|
49
|
+
expect(r.status()).toBe("idle");
|
|
50
|
+
expect(r.pending()).toBe(false);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("resolves data asynchronously", async () => {
|
|
54
|
+
const { ctx, run } = makeFieldCtx("items");
|
|
55
|
+
Resource(() => Promise.resolve([1, 2, 3]))(undefined, ctx);
|
|
56
|
+
const instance: Record<string, unknown> = {};
|
|
57
|
+
run(instance);
|
|
58
|
+
const r = instance.items as { data: () => unknown; status: () => string };
|
|
59
|
+
await vi.waitFor(() => r.status() === "success");
|
|
60
|
+
expect(r.data()).toEqual([1, 2, 3]);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("sets error state on rejection", async () => {
|
|
64
|
+
const { ctx, run } = makeFieldCtx("items");
|
|
65
|
+
Resource(() => Promise.reject(new Error("fetch failed")))(undefined, ctx);
|
|
66
|
+
const instance: Record<string, unknown> = {};
|
|
67
|
+
run(instance);
|
|
68
|
+
const r = instance.items as { error: () => Error | null; status: () => string };
|
|
69
|
+
await new Promise((res) => setTimeout(res, 0));
|
|
70
|
+
expect(r.status()).toBe("error");
|
|
71
|
+
expect((r.error() as Error).message).toBe("fetch failed");
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("each instance gets its own resource", () => {
|
|
75
|
+
const { ctx, run } = makeFieldCtx("r");
|
|
76
|
+
Resource(() => Promise.resolve(0))(undefined, ctx);
|
|
77
|
+
const a: Record<string, unknown> = {};
|
|
78
|
+
const b: Record<string, unknown> = {};
|
|
79
|
+
run(a);
|
|
80
|
+
run(b);
|
|
81
|
+
expect(a.r).not.toBe(b.r);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("mutate() updates data immediately", async () => {
|
|
85
|
+
const { ctx, run } = makeFieldCtx("r");
|
|
86
|
+
Resource(() => Promise.resolve(1))(undefined, ctx);
|
|
87
|
+
const instance: Record<string, unknown> = {};
|
|
88
|
+
run(instance);
|
|
89
|
+
const r = instance.r as { mutate: (v: number) => void; data: () => number; status: () => string };
|
|
90
|
+
r.mutate(99);
|
|
91
|
+
expect(r.data()).toBe(99);
|
|
92
|
+
expect(r.status()).toBe("success");
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("cancel() stops a pending fetch", async () => {
|
|
96
|
+
const { ctx, run } = makeFieldCtx("r");
|
|
97
|
+
Resource(() => new Promise(() => {}))(undefined, ctx);
|
|
98
|
+
const instance: Record<string, unknown> = {};
|
|
99
|
+
run(instance);
|
|
100
|
+
const r = instance.r as { cancel: () => void; status: () => string };
|
|
101
|
+
r.cancel();
|
|
102
|
+
expect(r.status()).toBe("idle");
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("uses initialData option", () => {
|
|
106
|
+
const { ctx, run } = makeFieldCtx("r");
|
|
107
|
+
Resource(() => Promise.resolve("new"), { initialData: "default", immediate: false })(undefined, ctx);
|
|
108
|
+
const instance: Record<string, unknown> = {};
|
|
109
|
+
run(instance);
|
|
110
|
+
const r = instance.r as { data: () => string };
|
|
111
|
+
expect(r.data()).toBe("default");
|
|
112
|
+
});
|
|
113
|
+
});
|
|
@@ -61,6 +61,19 @@ describe("initSlots / getSlot", () => {
|
|
|
61
61
|
initSlots(instance, [null, el, undefined]);
|
|
62
62
|
expect(getSlot(instance, "default")).toEqual([el]);
|
|
63
63
|
});
|
|
64
|
+
|
|
65
|
+
it("appends to existing named slot when same slot name appears twice", () => {
|
|
66
|
+
const instance = {};
|
|
67
|
+
const a = document.createElement("div");
|
|
68
|
+
a.setAttribute("slot", "header");
|
|
69
|
+
const b = document.createElement("div");
|
|
70
|
+
b.setAttribute("slot", "header");
|
|
71
|
+
initSlots(instance, [a, b]);
|
|
72
|
+
const headerSlot = getSlot(instance, "header");
|
|
73
|
+
expect(headerSlot).toHaveLength(2);
|
|
74
|
+
expect(headerSlot[0]).toBe(a);
|
|
75
|
+
expect(headerSlot[1]).toBe(b);
|
|
76
|
+
});
|
|
64
77
|
});
|
|
65
78
|
|
|
66
79
|
// ── Slot decorator ───────────────────────────────────────────────────────────
|
|
@@ -116,6 +129,31 @@ describe("Slot decorator", () => {
|
|
|
116
129
|
expect(slot[0]).toBe(el);
|
|
117
130
|
});
|
|
118
131
|
|
|
132
|
+
it("skips warn on direct assignment in production mode", () => {
|
|
133
|
+
class MyComp extends StatefulComponent {
|
|
134
|
+
render() { return null; }
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const initializers: Array<(this: unknown) => void> = [];
|
|
138
|
+
const ctx = {
|
|
139
|
+
name: "default",
|
|
140
|
+
kind: "field" as const,
|
|
141
|
+
addInitializer(fn: (this: unknown) => void) { initializers.push(fn); },
|
|
142
|
+
} as ClassFieldDecoratorContext<StatefulComponent>;
|
|
143
|
+
|
|
144
|
+
Slot()(undefined, ctx);
|
|
145
|
+
|
|
146
|
+
const instance = new MyComp() as unknown as Record<string, unknown>;
|
|
147
|
+
initializers.forEach((fn) => { fn.call(instance); });
|
|
148
|
+
|
|
149
|
+
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
150
|
+
vi.stubEnv("NODE_ENV", "production");
|
|
151
|
+
(instance as Record<string, unknown>).default = "bad";
|
|
152
|
+
expect(warn).not.toHaveBeenCalled();
|
|
153
|
+
vi.unstubAllEnvs();
|
|
154
|
+
warn.mockRestore();
|
|
155
|
+
});
|
|
156
|
+
|
|
119
157
|
it("warns on direct assignment in non-production", () => {
|
|
120
158
|
class MyComp extends StatefulComponent {
|
|
121
159
|
render() { return null; }
|
|
@@ -158,16 +158,16 @@ describe("Virtual decorator", () => {
|
|
|
158
158
|
(instance as unknown as { _anchor: Comment })._anchor = anchor;
|
|
159
159
|
|
|
160
160
|
instance.onMount?.();
|
|
161
|
-
// First render -
|
|
162
|
-
instance.render();
|
|
161
|
+
// First render - sets up effects with the spacerTop/itemsSlot/spacerBottom elements
|
|
162
|
+
const outer = instance.render() as HTMLElement;
|
|
163
163
|
|
|
164
164
|
// Change scrollTop to trigger effect re-run (covers the while loop removing old nodes)
|
|
165
165
|
Object.defineProperty(container, "scrollTop", { configurable: true, value: 500 });
|
|
166
166
|
container.dispatchEvent(new Event("scroll"));
|
|
167
167
|
|
|
168
|
-
//
|
|
169
|
-
const
|
|
170
|
-
expect(
|
|
168
|
+
// buffer=0, itemHeight=50, scrollTop=500 → startIdx=10 → offsetTop=500px
|
|
169
|
+
const spacerTop = outer.children[0] as HTMLElement;
|
|
170
|
+
expect(spacerTop.getAttribute("style")).toContain("500px");
|
|
171
171
|
|
|
172
172
|
document.body.removeChild(container);
|
|
173
173
|
});
|
|
@@ -211,6 +211,22 @@ describe("Virtual decorator", () => {
|
|
|
211
211
|
expect(wrapper.children.length).toBe(0);
|
|
212
212
|
});
|
|
213
213
|
|
|
214
|
+
it("uses empty array when items is undefined (host.items ?? [])", () => {
|
|
215
|
+
class NoItems extends StatefulComponent {
|
|
216
|
+
// items is intentionally not defined
|
|
217
|
+
renderItem() { return document.createElement("div"); }
|
|
218
|
+
render() { return null; }
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const Wrapped = applyVirtual(NoItems as AnyConstructor, 50, 0);
|
|
222
|
+
const instance = new Wrapped();
|
|
223
|
+
// render() should work fine with 0 items
|
|
224
|
+
const result = instance.render() as HTMLElement;
|
|
225
|
+
expect(result).toBeInstanceOf(HTMLElement);
|
|
226
|
+
const style = result.getAttribute("style") ?? "";
|
|
227
|
+
expect(style).toContain("0px"); // totalH = 0 * 50 = 0
|
|
228
|
+
});
|
|
229
|
+
|
|
214
230
|
it("onUnmount without prior onMount does not throw", () => {
|
|
215
231
|
const Wrapped = applyVirtual(ListComp as AnyConstructor, 50);
|
|
216
232
|
const instance = new Wrapped();
|
|
@@ -230,12 +246,15 @@ describe("Virtual decorator", () => {
|
|
|
230
246
|
(instance as unknown as { _anchor: Comment })._anchor = anchor;
|
|
231
247
|
|
|
232
248
|
instance.onMount?.();
|
|
249
|
+
// Render to set up effects that observe scrollTop
|
|
250
|
+
const outer = instance.render() as HTMLElement;
|
|
233
251
|
|
|
234
252
|
Object.defineProperty(container, "scrollTop", { configurable: true, value: 200 });
|
|
235
253
|
container.dispatchEvent(new Event("scroll"));
|
|
236
254
|
|
|
237
|
-
|
|
238
|
-
|
|
255
|
+
// buffer=3, itemHeight=50, scrollTop=200 → startIdx=1 → offsetTop=50px
|
|
256
|
+
const spacerTop = outer.children[0] as HTMLElement;
|
|
257
|
+
expect(spacerTop.getAttribute("style")).toContain("50px");
|
|
239
258
|
document.body.removeChild(container);
|
|
240
259
|
});
|
|
241
260
|
});
|
|
@@ -1,25 +1,40 @@
|
|
|
1
1
|
import { StatelessComponent } from "@praxisjs/core";
|
|
2
|
+
import type { RootComponent } from "@praxisjs/core/internal";
|
|
2
3
|
import type {
|
|
3
4
|
ComponentConstructor,
|
|
4
5
|
ComponentInstance,
|
|
5
6
|
} from "@praxisjs/shared/internal";
|
|
6
7
|
|
|
8
|
+
import {
|
|
9
|
+
ClassBehavior,
|
|
10
|
+
createClassDecorator,
|
|
11
|
+
type ClassEnhancement,
|
|
12
|
+
} from "../create-class-decorator";
|
|
13
|
+
|
|
14
|
+
class ComponentBehavior extends ClassBehavior {
|
|
15
|
+
create(_instance: RootComponent): ClassEnhancement {
|
|
16
|
+
return {};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
20
|
+
initialize(Enhanced: new (...args: any[]) => unknown, original: new (...args: any[]) => unknown): void {
|
|
21
|
+
const isStateless = (original.prototype as object) instanceof StatelessComponent;
|
|
22
|
+
Object.defineProperty(Enhanced, "__isComponent", { value: true, configurable: true });
|
|
23
|
+
Object.defineProperty(Enhanced, "__isStateless", { value: isStateless, configurable: true });
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const behavior = new ComponentBehavior();
|
|
28
|
+
|
|
7
29
|
export function Component() {
|
|
8
30
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
9
31
|
return function <T extends new (...args: any[]) => ComponentInstance>(
|
|
10
32
|
constructor: T,
|
|
11
|
-
|
|
33
|
+
context: ClassDecoratorContext,
|
|
12
34
|
): T & ComponentConstructor {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
} as unknown as T & ComponentConstructor;
|
|
18
|
-
|
|
19
|
-
Object.defineProperty(Enhanced, "name", {
|
|
20
|
-
value: constructor.name,
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
return Enhanced;
|
|
35
|
+
return createClassDecorator(behavior)(
|
|
36
|
+
constructor as unknown as T & (new(...args: unknown[]) => RootComponent<Record<string, unknown>>),
|
|
37
|
+
context,
|
|
38
|
+
) as unknown as T & ComponentConstructor;
|
|
24
39
|
};
|
|
25
40
|
}
|
package/src/component/lazy.ts
CHANGED
|
@@ -1,60 +1,56 @@
|
|
|
1
1
|
import { type RootComponent, signal } from "@praxisjs/core/internal";
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
5
|
-
return function <T extends new (...args: any[]) => RootComponent<Record<string, any>>>(
|
|
6
|
-
constructor: T,
|
|
7
|
-
_context: ClassDecoratorContext,
|
|
8
|
-
): T {
|
|
9
|
-
return class LazyWrapper extends constructor {
|
|
10
|
-
private readonly _lazyVisible = signal(false);
|
|
11
|
-
private _observer?: IntersectionObserver;
|
|
12
|
-
private readonly _originalRender: () => Node | Node[] | null =
|
|
13
|
-
// Capture the *parent* class render, not LazyWrapper's own override,
|
|
14
|
-
// to avoid infinite recursion when render() delegates here.
|
|
15
|
-
(constructor.prototype as { render(): Node | Node[] | null }).render.bind(this);
|
|
3
|
+
import { ClassBehavior, createClassDecorator, type ClassEnhancement } from "../create-class-decorator";
|
|
16
4
|
|
|
17
|
-
|
|
18
|
-
|
|
5
|
+
class LazyBehavior extends ClassBehavior {
|
|
6
|
+
constructor(private readonly placeholder: number) {
|
|
7
|
+
super();
|
|
8
|
+
}
|
|
19
9
|
|
|
20
|
-
|
|
10
|
+
create(instance: RootComponent): ClassEnhancement {
|
|
11
|
+
const placeholder = this.placeholder;
|
|
12
|
+
const visible = signal(false);
|
|
13
|
+
let observer: IntersectionObserver | undefined;
|
|
14
|
+
|
|
15
|
+
return {
|
|
16
|
+
onMount() {
|
|
21
17
|
const el =
|
|
22
|
-
(
|
|
23
|
-
?.parentElement ?? null;
|
|
18
|
+
(instance as { _anchor?: Comment })._anchor?.parentElement ?? null;
|
|
24
19
|
if (!el) return;
|
|
25
20
|
|
|
26
21
|
if (!("IntersectionObserver" in window)) {
|
|
27
|
-
|
|
22
|
+
visible.set(true);
|
|
28
23
|
return;
|
|
29
24
|
}
|
|
30
25
|
|
|
31
|
-
if (!
|
|
32
|
-
el.style.minHeight = `${String(placeholder)}px`;
|
|
33
|
-
}
|
|
26
|
+
if (!visible()) el.style.minHeight = `${String(placeholder)}px`;
|
|
34
27
|
|
|
35
|
-
|
|
28
|
+
observer = new IntersectionObserver(
|
|
36
29
|
(entries) => {
|
|
37
30
|
if (entries[0]?.isIntersecting) {
|
|
38
|
-
|
|
31
|
+
visible.set(true);
|
|
39
32
|
el.style.minHeight = "";
|
|
40
|
-
|
|
33
|
+
observer?.disconnect();
|
|
41
34
|
}
|
|
42
35
|
},
|
|
43
36
|
{ rootMargin: "100px" },
|
|
44
37
|
);
|
|
45
38
|
|
|
46
|
-
|
|
47
|
-
}
|
|
39
|
+
observer.observe(el);
|
|
40
|
+
},
|
|
48
41
|
|
|
49
42
|
onUnmount() {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
43
|
+
observer?.disconnect();
|
|
44
|
+
},
|
|
45
|
+
|
|
46
|
+
render(originalRender) {
|
|
47
|
+
if (!visible()) return null;
|
|
48
|
+
return originalRender();
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function Lazy(placeholder = 200) {
|
|
55
|
+
return createClassDecorator(new LazyBehavior(placeholder));
|
|
60
56
|
}
|
|
@@ -1,81 +1,76 @@
|
|
|
1
1
|
import { computed, effect, type RootComponent, signal } from "@praxisjs/core/internal";
|
|
2
2
|
|
|
3
|
+
import { ClassBehavior, createClassDecorator, type ClassEnhancement } from "../create-class-decorator";
|
|
4
|
+
|
|
3
5
|
interface VirtualHost {
|
|
4
6
|
_anchor?: Comment;
|
|
5
7
|
items?: unknown[];
|
|
6
8
|
renderItem(item: unknown, index: number): Node | Node[] | null;
|
|
7
9
|
}
|
|
8
10
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
11
|
+
class VirtualBehavior extends ClassBehavior {
|
|
12
|
+
constructor(
|
|
13
|
+
private readonly itemHeight: number,
|
|
14
|
+
private readonly buffer: number,
|
|
15
|
+
) {
|
|
16
|
+
super();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
create(instance: RootComponent): ClassEnhancement {
|
|
20
|
+
const { itemHeight, buffer } = this;
|
|
21
|
+
const host = instance as unknown as VirtualHost;
|
|
22
|
+
const scrollTop = signal(0);
|
|
23
|
+
const viewHeight = signal(600);
|
|
24
|
+
let container: HTMLElement | undefined;
|
|
25
|
+
let scrollHandler: (() => void) | undefined;
|
|
26
|
+
const cleanups: Array<() => void> = [];
|
|
27
|
+
|
|
28
|
+
return {
|
|
22
29
|
onMount() {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
// Use the end anchor set by the runtime to locate the parent element
|
|
26
|
-
this._container =
|
|
27
|
-
(this as unknown as VirtualHost)._anchor?.parentElement ?? undefined;
|
|
28
|
-
|
|
29
|
-
if (!this._container) return;
|
|
30
|
+
container = host._anchor?.parentElement ?? undefined;
|
|
31
|
+
if (!container) return;
|
|
30
32
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
+
container.style.overflowY = "auto";
|
|
34
|
+
container.style.position = "relative";
|
|
35
|
+
viewHeight.set(container.clientHeight || 600);
|
|
33
36
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
this._scrollHandler = () => {
|
|
37
|
-
if (this._container) this._scrollTop.set(this._container.scrollTop);
|
|
37
|
+
scrollHandler = () => {
|
|
38
|
+
if (container) scrollTop.set(container.scrollTop);
|
|
38
39
|
};
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
}
|
|
40
|
+
container.addEventListener("scroll", scrollHandler);
|
|
41
|
+
},
|
|
42
42
|
|
|
43
43
|
onUnmount() {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
this._container.removeEventListener("scroll", this._scrollHandler);
|
|
44
|
+
if (container && scrollHandler) {
|
|
45
|
+
container.removeEventListener("scroll", scrollHandler);
|
|
47
46
|
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
}
|
|
47
|
+
cleanups.forEach((c) => { c(); });
|
|
48
|
+
cleanups.length = 0;
|
|
49
|
+
},
|
|
51
50
|
|
|
52
|
-
render() {
|
|
53
|
-
const host = this as unknown as VirtualHost;
|
|
51
|
+
render(originalRender) {
|
|
54
52
|
const items = host.items ?? [];
|
|
55
53
|
const total = items.length;
|
|
56
54
|
const totalH = total * itemHeight;
|
|
57
55
|
|
|
58
56
|
if (typeof host.renderItem !== "function") {
|
|
59
57
|
console.warn(
|
|
60
|
-
`[Virtual]
|
|
58
|
+
`[Virtual] component must implement renderItem(item, index)`,
|
|
61
59
|
);
|
|
62
|
-
return
|
|
60
|
+
return originalRender();
|
|
63
61
|
}
|
|
64
62
|
|
|
65
63
|
const renderItem = host.renderItem.bind(host);
|
|
66
64
|
|
|
67
|
-
const startIdx = computed(() =>
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
return Math.min(total - 1, end);
|
|
77
|
-
});
|
|
78
|
-
|
|
65
|
+
const startIdx = computed(() =>
|
|
66
|
+
Math.max(0, Math.floor(scrollTop() / itemHeight) - buffer),
|
|
67
|
+
);
|
|
68
|
+
const endIdx = computed(() =>
|
|
69
|
+
Math.min(
|
|
70
|
+
total - 1,
|
|
71
|
+
Math.ceil((scrollTop() + viewHeight()) / itemHeight) + buffer,
|
|
72
|
+
),
|
|
73
|
+
);
|
|
79
74
|
const visibleItems = computed(() => {
|
|
80
75
|
const result: Array<{ item: unknown; index: number }> = [];
|
|
81
76
|
for (let i = startIdx(); i <= endIdx(); i++) {
|
|
@@ -83,31 +78,23 @@ export function Virtual(itemHeight: number, buffer = 3) {
|
|
|
83
78
|
}
|
|
84
79
|
return result;
|
|
85
80
|
});
|
|
86
|
-
|
|
87
81
|
const offsetTop = computed(() => startIdx() * itemHeight);
|
|
88
|
-
const offsetBottom = computed(
|
|
89
|
-
() => (total - 1 - endIdx()) * itemHeight,
|
|
90
|
-
);
|
|
82
|
+
const offsetBottom = computed(() => (total - 1 - endIdx()) * itemHeight);
|
|
91
83
|
|
|
92
84
|
const outer = document.createElement("div");
|
|
93
|
-
outer.setAttribute(
|
|
94
|
-
"style",
|
|
95
|
-
`height:${String(totalH)}px; position:relative;`,
|
|
96
|
-
);
|
|
85
|
+
outer.setAttribute("style", `height:${String(totalH)}px; position:relative;`);
|
|
97
86
|
|
|
98
87
|
const spacerTop = document.createElement("div");
|
|
99
|
-
|
|
88
|
+
cleanups.push(
|
|
100
89
|
effect(() => {
|
|
101
90
|
spacerTop.setAttribute("style", `height:${String(offsetTop())}px;`);
|
|
102
91
|
}),
|
|
103
92
|
);
|
|
104
93
|
|
|
105
94
|
const itemsSlot = document.createElement("div");
|
|
106
|
-
|
|
95
|
+
cleanups.push(
|
|
107
96
|
effect(() => {
|
|
108
|
-
while (itemsSlot.firstChild)
|
|
109
|
-
itemsSlot.removeChild(itemsSlot.firstChild);
|
|
110
|
-
}
|
|
97
|
+
while (itemsSlot.firstChild) itemsSlot.removeChild(itemsSlot.firstChild);
|
|
111
98
|
visibleItems().forEach(({ item, index }) => {
|
|
112
99
|
const wrapper = document.createElement("div");
|
|
113
100
|
wrapper.setAttribute(
|
|
@@ -116,10 +103,9 @@ export function Virtual(itemHeight: number, buffer = 3) {
|
|
|
116
103
|
);
|
|
117
104
|
const rendered = renderItem(item, index);
|
|
118
105
|
if (rendered) {
|
|
119
|
-
|
|
120
|
-
|
|
106
|
+
(Array.isArray(rendered) ? rendered.flat() : [rendered]).forEach(
|
|
107
|
+
(n) => { wrapper.appendChild(n); },
|
|
121
108
|
);
|
|
122
|
-
nodes.forEach((n) => wrapper.appendChild(n));
|
|
123
109
|
}
|
|
124
110
|
itemsSlot.appendChild(wrapper);
|
|
125
111
|
});
|
|
@@ -127,12 +113,9 @@ export function Virtual(itemHeight: number, buffer = 3) {
|
|
|
127
113
|
);
|
|
128
114
|
|
|
129
115
|
const spacerBottom = document.createElement("div");
|
|
130
|
-
|
|
116
|
+
cleanups.push(
|
|
131
117
|
effect(() => {
|
|
132
|
-
spacerBottom.setAttribute(
|
|
133
|
-
"style",
|
|
134
|
-
`height:${String(offsetBottom())}px;`,
|
|
135
|
-
);
|
|
118
|
+
spacerBottom.setAttribute("style", `height:${String(offsetBottom())}px;`);
|
|
136
119
|
}),
|
|
137
120
|
);
|
|
138
121
|
|
|
@@ -141,7 +124,11 @@ export function Virtual(itemHeight: number, buffer = 3) {
|
|
|
141
124
|
outer.appendChild(spacerBottom);
|
|
142
125
|
|
|
143
126
|
return outer;
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
}
|
|
127
|
+
},
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function Virtual(itemHeight: number, buffer = 3) {
|
|
133
|
+
return createClassDecorator(new VirtualBehavior(itemHeight, buffer));
|
|
147
134
|
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { RootComponent } from "@praxisjs/core/internal";
|
|
2
|
+
|
|
3
|
+
export interface ClassEnhancement {
|
|
4
|
+
onMount?(): void;
|
|
5
|
+
onUnmount?(): void;
|
|
6
|
+
render?(
|
|
7
|
+
originalRender: () => Node | Node[] | null,
|
|
8
|
+
): Node | Node[] | null;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export abstract class ClassBehavior {
|
|
12
|
+
abstract create(instance: RootComponent): ClassEnhancement;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Called once after the enhanced class is created.
|
|
16
|
+
* Use this to set static properties on the class constructor itself.
|
|
17
|
+
*/
|
|
18
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
19
|
+
initialize?(_Enhanced: new (...args: any[]) => unknown, _original: new (...args: any[]) => unknown): void;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function createClassDecorator(behavior: ClassBehavior) {
|
|
23
|
+
return function <
|
|
24
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
25
|
+
T extends new (...args: any[]) => RootComponent<Record<string, any>>,
|
|
26
|
+
>(constructor: T, _context: ClassDecoratorContext): T {
|
|
27
|
+
const Enhanced = class extends constructor {
|
|
28
|
+
private readonly _enh: ClassEnhancement = behavior.create(
|
|
29
|
+
this as unknown as RootComponent,
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
onMount() {
|
|
33
|
+
super.onMount?.();
|
|
34
|
+
this._enh.onMount?.();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
onUnmount() {
|
|
38
|
+
super.onUnmount?.();
|
|
39
|
+
this._enh.onUnmount?.();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
render(...args: unknown[]): Node | Node[] | null {
|
|
43
|
+
const originalRender = () =>
|
|
44
|
+
(constructor.prototype as { render: (...a: unknown[]) => Node | Node[] | null }).render.apply(this, args);
|
|
45
|
+
if (this._enh.render) {
|
|
46
|
+
return this._enh.render(originalRender);
|
|
47
|
+
}
|
|
48
|
+
return originalRender();
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
// If the original class has no render, remove the wrapper so instances see `undefined`.
|
|
53
|
+
if (typeof (constructor.prototype as { render?: unknown }).render !== "function") {
|
|
54
|
+
delete (Enhanced.prototype as { render?: unknown }).render;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
Object.defineProperty(Enhanced, "name", { value: constructor.name });
|
|
58
|
+
behavior.initialize?.(Enhanced, constructor);
|
|
59
|
+
return Enhanced as unknown as T;
|
|
60
|
+
};
|
|
61
|
+
}
|