@praxisjs/decorators 0.4.3 → 0.6.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.
Files changed (192) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/dist/__tests__/component.test.js +10 -1
  3. package/dist/__tests__/component.test.js.map +1 -1
  4. package/dist/__tests__/compose.test.d.ts +2 -0
  5. package/dist/__tests__/compose.test.d.ts.map +1 -0
  6. package/dist/__tests__/compose.test.js +171 -0
  7. package/dist/__tests__/compose.test.js.map +1 -0
  8. package/dist/__tests__/events.test.js +26 -0
  9. package/dist/__tests__/events.test.js.map +1 -1
  10. package/dist/__tests__/functions.test.js +205 -47
  11. package/dist/__tests__/functions.test.js.map +1 -1
  12. package/dist/__tests__/lazy.test.js +5 -7
  13. package/dist/__tests__/lazy.test.js.map +1 -1
  14. package/dist/__tests__/memo.test.js +114 -25
  15. package/dist/__tests__/memo.test.js.map +1 -1
  16. package/dist/__tests__/properties.test.js +193 -0
  17. package/dist/__tests__/properties.test.js.map +1 -1
  18. package/dist/__tests__/resource.test.d.ts +2 -0
  19. package/dist/__tests__/resource.test.d.ts.map +1 -0
  20. package/dist/__tests__/resource.test.js +127 -0
  21. package/dist/__tests__/resource.test.js.map +1 -0
  22. package/dist/__tests__/slot.test.js +32 -0
  23. package/dist/__tests__/slot.test.js.map +1 -1
  24. package/dist/__tests__/until.test.d.ts +2 -0
  25. package/dist/__tests__/until.test.d.ts.map +1 -0
  26. package/dist/__tests__/until.test.js +88 -0
  27. package/dist/__tests__/until.test.js.map +1 -0
  28. package/dist/__tests__/virtual.test.js +164 -15
  29. package/dist/__tests__/virtual.test.js.map +1 -1
  30. package/dist/component/component.d.ts +1 -1
  31. package/dist/component/component.d.ts.map +1 -1
  32. package/dist/component/component.js +15 -10
  33. package/dist/component/component.js.map +1 -1
  34. package/dist/component/lazy.d.ts.map +1 -1
  35. package/dist/component/lazy.js +29 -30
  36. package/dist/component/lazy.js.map +1 -1
  37. package/dist/component/virtual.d.ts.map +1 -1
  38. package/dist/component/virtual.js +49 -50
  39. package/dist/component/virtual.js.map +1 -1
  40. package/dist/create-class-decorator.d.ts +16 -0
  41. package/dist/create-class-decorator.d.ts.map +1 -0
  42. package/dist/create-class-decorator.js +35 -0
  43. package/dist/create-class-decorator.js.map +1 -0
  44. package/dist/create-field-decorator.d.ts +14 -0
  45. package/dist/create-field-decorator.d.ts.map +1 -0
  46. package/dist/create-field-decorator.js +37 -0
  47. package/dist/create-field-decorator.js.map +1 -0
  48. package/dist/create-getter-decorator.d.ts +18 -0
  49. package/dist/create-getter-decorator.d.ts.map +1 -0
  50. package/dist/create-getter-decorator.js +15 -0
  51. package/dist/create-getter-decorator.js.map +1 -0
  52. package/dist/create-lifecycle-method-decorator.d.ts +6 -0
  53. package/dist/create-lifecycle-method-decorator.d.ts.map +1 -0
  54. package/dist/create-lifecycle-method-decorator.js +22 -0
  55. package/dist/create-lifecycle-method-decorator.js.map +1 -0
  56. package/dist/create-method-decorator.d.ts +6 -0
  57. package/dist/create-method-decorator.d.ts.map +1 -0
  58. package/dist/create-method-decorator.js +14 -0
  59. package/dist/create-method-decorator.js.map +1 -0
  60. package/dist/events/emit.d.ts +1 -1
  61. package/dist/events/emit.d.ts.map +1 -1
  62. package/dist/events/emit.js +24 -29
  63. package/dist/events/emit.js.map +1 -1
  64. package/dist/events/on-command.d.ts +1 -2
  65. package/dist/events/on-command.d.ts.map +1 -1
  66. package/dist/events/on-command.js +15 -32
  67. package/dist/events/on-command.js.map +1 -1
  68. package/dist/functions/bind.d.ts +1 -2
  69. package/dist/functions/bind.d.ts.map +1 -1
  70. package/dist/functions/bind.js +4 -11
  71. package/dist/functions/bind.js.map +1 -1
  72. package/dist/functions/debounce.d.ts.map +1 -1
  73. package/dist/functions/debounce.js +21 -20
  74. package/dist/functions/debounce.js.map +1 -1
  75. package/dist/functions/index.d.ts +1 -0
  76. package/dist/functions/index.d.ts.map +1 -1
  77. package/dist/functions/index.js +1 -0
  78. package/dist/functions/index.js.map +1 -1
  79. package/dist/functions/log.d.ts +1 -2
  80. package/dist/functions/log.d.ts.map +1 -1
  81. package/dist/functions/log.js +40 -38
  82. package/dist/functions/log.js.map +1 -1
  83. package/dist/functions/memo.d.ts +1 -2
  84. package/dist/functions/memo.d.ts.map +1 -1
  85. package/dist/functions/memo.js +29 -27
  86. package/dist/functions/memo.js.map +1 -1
  87. package/dist/functions/once.d.ts +1 -2
  88. package/dist/functions/once.d.ts.map +1 -1
  89. package/dist/functions/once.js +14 -12
  90. package/dist/functions/once.js.map +1 -1
  91. package/dist/functions/retry.d.ts +1 -2
  92. package/dist/functions/retry.d.ts.map +1 -1
  93. package/dist/functions/retry.js +23 -20
  94. package/dist/functions/retry.js.map +1 -1
  95. package/dist/functions/throttle.d.ts +1 -2
  96. package/dist/functions/throttle.d.ts.map +1 -1
  97. package/dist/functions/throttle.js +11 -17
  98. package/dist/functions/throttle.js.map +1 -1
  99. package/dist/functions/until.d.ts +2 -0
  100. package/dist/functions/until.d.ts.map +1 -0
  101. package/dist/functions/until.js +17 -0
  102. package/dist/functions/until.js.map +1 -0
  103. package/dist/functions/watch.d.ts +1 -1
  104. package/dist/functions/watch.d.ts.map +1 -1
  105. package/dist/functions/watch.js +25 -39
  106. package/dist/functions/watch.js.map +1 -1
  107. package/dist/functions/when.d.ts +1 -2
  108. package/dist/functions/when.d.ts.map +1 -1
  109. package/dist/functions/when.js +10 -26
  110. package/dist/functions/when.js.map +1 -1
  111. package/dist/index.d.ts +7 -2
  112. package/dist/index.d.ts.map +1 -1
  113. package/dist/index.js +7 -2
  114. package/dist/index.js.map +1 -1
  115. package/dist/properties/compose.d.ts +3 -0
  116. package/dist/properties/compose.d.ts.map +1 -0
  117. package/dist/properties/compose.js +40 -0
  118. package/dist/properties/compose.js.map +1 -0
  119. package/dist/properties/computed.d.ts +1 -2
  120. package/dist/properties/computed.d.ts.map +1 -1
  121. package/dist/properties/computed.js +10 -9
  122. package/dist/properties/computed.js.map +1 -1
  123. package/dist/properties/history.d.ts.map +1 -1
  124. package/dist/properties/history.js +37 -32
  125. package/dist/properties/history.js.map +1 -1
  126. package/dist/properties/index.d.ts +2 -0
  127. package/dist/properties/index.d.ts.map +1 -1
  128. package/dist/properties/index.js +2 -0
  129. package/dist/properties/index.js.map +1 -1
  130. package/dist/properties/persisted.d.ts +1 -2
  131. package/dist/properties/persisted.d.ts.map +1 -1
  132. package/dist/properties/persisted.js +13 -29
  133. package/dist/properties/persisted.js.map +1 -1
  134. package/dist/properties/prop.d.ts +1 -2
  135. package/dist/properties/prop.d.ts.map +1 -1
  136. package/dist/properties/prop.js +21 -25
  137. package/dist/properties/prop.js.map +1 -1
  138. package/dist/properties/resource.d.ts +14 -0
  139. package/dist/properties/resource.d.ts.map +1 -0
  140. package/dist/properties/resource.js +27 -0
  141. package/dist/properties/resource.js.map +1 -0
  142. package/dist/properties/slot.d.ts +1 -2
  143. package/dist/properties/slot.d.ts.map +1 -1
  144. package/dist/properties/slot.js +15 -20
  145. package/dist/properties/slot.js.map +1 -1
  146. package/dist/properties/state.d.ts +1 -2
  147. package/dist/properties/state.d.ts.map +1 -1
  148. package/dist/properties/state.js +13 -18
  149. package/dist/properties/state.js.map +1 -1
  150. package/package.json +2 -2
  151. package/src/__tests__/component.test.ts +10 -0
  152. package/src/__tests__/compose.test.ts +202 -0
  153. package/src/__tests__/events.test.ts +32 -0
  154. package/src/__tests__/functions.test.ts +257 -46
  155. package/src/__tests__/lazy.test.ts +3 -5
  156. package/src/__tests__/memo.test.ts +143 -25
  157. package/src/__tests__/properties.test.ts +233 -0
  158. package/src/__tests__/resource.test.ts +141 -0
  159. package/src/__tests__/slot.test.ts +38 -0
  160. package/src/__tests__/until.test.ts +113 -0
  161. package/src/__tests__/virtual.test.ts +135 -7
  162. package/src/component/component.ts +27 -12
  163. package/src/component/lazy.ts +33 -37
  164. package/src/component/virtual.tsx +67 -75
  165. package/src/create-class-decorator.ts +61 -0
  166. package/src/create-field-decorator.ts +66 -0
  167. package/src/create-getter-decorator.ts +52 -0
  168. package/src/create-lifecycle-method-decorator.ts +38 -0
  169. package/src/create-method-decorator.ts +26 -0
  170. package/src/events/emit.ts +22 -33
  171. package/src/events/on-command.ts +22 -47
  172. package/src/functions/bind.ts +4 -15
  173. package/src/functions/debounce.ts +21 -23
  174. package/src/functions/index.ts +1 -0
  175. package/src/functions/log.ts +40 -43
  176. package/src/functions/memo.ts +28 -42
  177. package/src/functions/once.ts +13 -16
  178. package/src/functions/retry.ts +21 -22
  179. package/src/functions/throttle.ts +11 -24
  180. package/src/functions/until.ts +19 -0
  181. package/src/functions/watch.ts +27 -43
  182. package/src/functions/when.ts +11 -36
  183. package/src/index.ts +29 -0
  184. package/src/properties/compose.ts +53 -0
  185. package/src/properties/computed.ts +11 -13
  186. package/src/properties/history.ts +42 -41
  187. package/src/properties/index.ts +2 -0
  188. package/src/properties/persisted.ts +17 -58
  189. package/src/properties/prop.ts +21 -31
  190. package/src/properties/resource.ts +34 -0
  191. package/src/properties/slot.ts +17 -30
  192. package/src/properties/state.ts +14 -24
@@ -0,0 +1,113 @@
1
+ import { describe, it, expect } from "vitest";
2
+
3
+ import { signal, computed } from "@praxisjs/core/internal";
4
+
5
+ import { Until } from "../functions/until";
6
+
7
+ // Simulates TC39 decorator context for a method decorator
8
+ function mockMethodContext(name: string) {
9
+ const initializers: Array<(this: object) => void> = [];
10
+ const ctx = {
11
+ name,
12
+ kind: "method" as const,
13
+ addInitializer(fn: (this: object) => void) {
14
+ initializers.push(fn);
15
+ },
16
+ runInitializers(instance: object) {
17
+ initializers.forEach((fn) => { fn.call(instance); });
18
+ },
19
+ };
20
+ return ctx;
21
+ }
22
+
23
+ // ── @Until ────────────────────────────────────────────────────────────────────
24
+
25
+ describe("Until", () => {
26
+ it("resolves immediately when the signal is already truthy", async () => {
27
+ const s = signal(42);
28
+ const obj: Record<string, unknown> = { count: s };
29
+
30
+ const ctx = mockMethodContext("wait");
31
+ Until("count")(() => {}, ctx as unknown as ClassMethodDecoratorContext);
32
+ ctx.runInitializers(obj);
33
+
34
+ const result = await (obj.wait as () => Promise<number>)();
35
+ expect(result).toBe(42);
36
+ });
37
+
38
+ it("resolves when the signal becomes truthy", async () => {
39
+ const s = signal<number>(0);
40
+ const obj: Record<string, unknown> = { count: s };
41
+
42
+ const ctx = mockMethodContext("wait");
43
+ Until("count")(() => {}, ctx as unknown as ClassMethodDecoratorContext);
44
+ ctx.runInitializers(obj);
45
+
46
+ const promise = (obj.wait as () => Promise<number>)();
47
+ s.set(5);
48
+ expect(await promise).toBe(5);
49
+ });
50
+
51
+ it("resolves with a computed signal", async () => {
52
+ const s = signal(0);
53
+ const doubled = computed(() => s() * 2);
54
+ const obj: Record<string, unknown> = { doubled };
55
+
56
+ const ctx = mockMethodContext("waitDoubled");
57
+ Until("doubled")(() => {}, ctx as unknown as ClassMethodDecoratorContext);
58
+ ctx.runInitializers(obj);
59
+
60
+ const promise = (obj.waitDoubled as () => Promise<number>)();
61
+ s.set(3); // doubled becomes 6 (truthy)
62
+ expect(await promise).toBe(6);
63
+ });
64
+
65
+ it("each call returns a new promise", async () => {
66
+ const s = signal<string>("");
67
+ const obj: Record<string, unknown> = { name: s };
68
+
69
+ const ctx = mockMethodContext("waitName");
70
+ Until("name")(() => {}, ctx as unknown as ClassMethodDecoratorContext);
71
+ ctx.runInitializers(obj);
72
+
73
+ const wait = obj.waitName as () => Promise<string>;
74
+
75
+ const p1 = wait();
76
+ const p2 = wait();
77
+ expect(p1).not.toBe(p2);
78
+
79
+ s.set("alice");
80
+ expect(await p1).toBe("alice");
81
+ expect(await p2).toBe("alice");
82
+ });
83
+
84
+ it("does not resolve when signal stays falsy", async () => {
85
+ const s = signal<number>(0);
86
+ const obj: Record<string, unknown> = { count: s };
87
+
88
+ const ctx = mockMethodContext("wait");
89
+ Until("count")(() => {}, ctx as unknown as ClassMethodDecoratorContext);
90
+ ctx.runInitializers(obj);
91
+
92
+ let resolved = false;
93
+ (obj.wait as () => Promise<number>)().then(() => { resolved = true; });
94
+
95
+ await new Promise((res) => { setTimeout(res, 10); });
96
+ expect(resolved).toBe(false);
97
+ });
98
+
99
+ it("ignores the original method body", async () => {
100
+ const s = signal("hello");
101
+ const obj: Record<string, unknown> = { value: s };
102
+
103
+ let bodyCalled = false;
104
+ const original = () => { bodyCalled = true; };
105
+
106
+ const ctx = mockMethodContext("wait");
107
+ Until("value")(original, ctx as unknown as ClassMethodDecoratorContext);
108
+ ctx.runInitializers(obj);
109
+
110
+ await (obj.wait as () => Promise<string>)();
111
+ expect(bodyCalled).toBe(false);
112
+ });
113
+ });
@@ -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 - populates the itemsSlot
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
- // Verify scrollTop was updated (effect should have re-run)
169
- const scrollTop = (instance as unknown as { _scrollTop: () => number })._scrollTop;
170
- expect(scrollTop()).toBe(500);
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,124 @@ 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
- const scrollTop = (instance as unknown as { _scrollTop: () => number })._scrollTop;
238
- expect(scrollTop()).toBe(200);
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");
258
+ document.body.removeChild(container);
259
+ });
260
+
261
+ it("itemHeight = 0 throws a descriptive error on onMount", () => {
262
+ class ZeroHeightList extends StatefulComponent {
263
+ items = ["a", "b"];
264
+ renderItem(item: unknown) {
265
+ const el = document.createElement("div");
266
+ el.textContent = String(item);
267
+ return el;
268
+ }
269
+ render() { return null; }
270
+ }
271
+
272
+ const Wrapped = applyVirtual(ZeroHeightList as AnyConstructor, 0);
273
+ const instance = new Wrapped();
274
+
275
+ const container = document.createElement("div");
276
+ document.body.appendChild(container);
277
+ const anchor = document.createComment("end");
278
+ container.appendChild(anchor);
279
+ (instance as unknown as { _anchor: Comment })._anchor = anchor;
280
+
281
+ expect(() => instance.onMount?.()).toThrow(/itemHeight must be a positive number/);
282
+ document.body.removeChild(container);
283
+ });
284
+
285
+ it("itemHeight = 0 in render() does not divide by zero (no container)", () => {
286
+ class ZeroHeightList2 extends StatefulComponent {
287
+ items = ["a", "b"];
288
+ renderItem(item: unknown) {
289
+ return document.createElement("div");
290
+ }
291
+ render() { return null; }
292
+ }
293
+ // render() is called before onMount, so no throw yet — but it still computes total height
294
+ const Wrapped = applyVirtual(ZeroHeightList2 as AnyConstructor, 0);
295
+ const instance = new Wrapped();
296
+ // Render without mounting — no onMount guard triggered, but division happens in computed
297
+ // The guard is in onMount, so render still runs. Verify it doesn't produce NaN/Infinity
298
+ expect(() => instance.render()).not.toThrow();
299
+ });
300
+
301
+ it("renderItem() that throws — does not crash the entire component", () => {
302
+ class ThrowingList extends StatefulComponent {
303
+ items = ["x"];
304
+ renderItem(): Node {
305
+ throw new Error("render item failed");
306
+ }
307
+ render() { return null; }
308
+ }
309
+
310
+ const Wrapped = applyVirtual(ThrowingList as AnyConstructor, 50, 0);
311
+ const instance = new Wrapped();
312
+ // Render triggers the effect which calls renderItem — it should propagate the error
313
+ expect(() => instance.render()).toThrow("render item failed");
314
+ });
315
+
316
+ it("scrollTop > totalHeight renders last visible items without crash", () => {
317
+ class ShortList extends StatefulComponent {
318
+ items = [1, 2, 3]; // only 3 items × 50px = 150px total
319
+ renderItem(item: unknown) {
320
+ const el = document.createElement("div");
321
+ el.textContent = String(item);
322
+ return el;
323
+ }
324
+ render() { return null; }
325
+ }
326
+
327
+ const Wrapped = applyVirtual(ShortList as AnyConstructor, 50, 0);
328
+ const instance = new Wrapped();
329
+
330
+ const container = document.createElement("div");
331
+ document.body.appendChild(container);
332
+ const anchor = document.createComment("end");
333
+ container.appendChild(anchor);
334
+ (instance as unknown as { _anchor: Comment })._anchor = anchor;
335
+
336
+ instance.onMount?.();
337
+ const outer = instance.render() as HTMLElement;
338
+
339
+ // Simulate scrollTop way past end of list (e.g., 9999px for a 150px list)
340
+ Object.defineProperty(container, "scrollTop", { configurable: true, value: 9999 });
341
+ expect(() => container.dispatchEvent(new Event("scroll"))).not.toThrow();
342
+
343
+ // The outer container should still be present
344
+ expect(outer).toBeInstanceOf(HTMLElement);
239
345
  document.body.removeChild(container);
240
346
  });
347
+
348
+ it("buffer applied correctly — renders extra items beyond visible area", () => {
349
+ class BufferList extends StatefulComponent {
350
+ items = Array.from({ length: 20 }, (_, i) => i);
351
+ renderItem(item: unknown) {
352
+ const el = document.createElement("div");
353
+ el.textContent = String(item);
354
+ return el;
355
+ }
356
+ render() { return null; }
357
+ }
358
+
359
+ // itemHeight=100, buffer=2, viewHeight defaults to 600 → visible=6 items + 2 buffer each side
360
+ const Wrapped = applyVirtual(BufferList as AnyConstructor, 100, 2);
361
+ const instance = new Wrapped();
362
+ const outer = instance.render() as HTMLElement;
363
+
364
+ // With scrollTop=0, buffer=2: startIdx=max(0, 0-2)=0, endIdx=min(19, ceil(600/100)+2)=8
365
+ // So 9 items should be rendered (indices 0..8)
366
+ const itemsSlot = outer.children[1] as HTMLElement;
367
+ expect(itemsSlot.children.length).toBe(9);
368
+ });
241
369
  });
@@ -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
- _context: ClassDecoratorContext,
33
+ context: ClassDecoratorContext,
12
34
  ): T & ComponentConstructor {
13
- const isStateless = constructor.prototype instanceof StatelessComponent;
14
- const Enhanced = class extends constructor {
15
- static __isComponent = true as const;
16
- static __isStateless = isStateless;
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
  }
@@ -1,60 +1,56 @@
1
1
  import { type RootComponent, signal } from "@praxisjs/core/internal";
2
2
 
3
- export function Lazy(placeholder = 200) {
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
- onMount() {
18
- super.onMount?.();
5
+ class LazyBehavior extends ClassBehavior {
6
+ constructor(private readonly placeholder: number) {
7
+ super();
8
+ }
19
9
 
20
- // Use the end anchor to find the parent element
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
- (this as unknown as { _anchor?: Comment })._anchor
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
- this._lazyVisible.set(true);
22
+ visible.set(true);
28
23
  return;
29
24
  }
30
25
 
31
- if (!this._lazyVisible()) {
32
- el.style.minHeight = `${String(placeholder)}px`;
33
- }
26
+ if (!visible()) el.style.minHeight = `${String(placeholder)}px`;
34
27
 
35
- this._observer = new IntersectionObserver(
28
+ observer = new IntersectionObserver(
36
29
  (entries) => {
37
30
  if (entries[0]?.isIntersecting) {
38
- this._lazyVisible.set(true);
31
+ visible.set(true);
39
32
  el.style.minHeight = "";
40
- this._observer?.disconnect();
33
+ observer?.disconnect();
41
34
  }
42
35
  },
43
36
  { rootMargin: "100px" },
44
37
  );
45
38
 
46
- this._observer.observe(el);
47
- }
39
+ observer.observe(el);
40
+ },
48
41
 
49
42
  onUnmount() {
50
- super.onUnmount?.();
51
- this._observer?.disconnect();
52
- }
53
-
54
- render(): Node | Node[] | null {
55
- if (!this._lazyVisible()) return null;
56
- return this._originalRender();
57
- }
58
- } as unknown as T;
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,81 @@
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
- export function Virtual(itemHeight: number, buffer = 3) {
10
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
11
- return function <T extends new (...args: any[]) => RootComponent<Record<string, any>>>(
12
- constructor: T,
13
- _context: ClassDecoratorContext,
14
- ): T {
15
- return class VirtualWrapper extends constructor {
16
- private readonly _scrollTop = signal(0);
17
- private readonly _viewHeight = signal(600);
18
- private _container?: HTMLElement;
19
- private _scrollHandler?: () => void;
20
- private _cleanups: Array<() => void> = [];
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
- super.onMount?.();
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
-
31
- this._container.style.overflowY = "auto";
32
- this._container.style.position = "relative";
30
+ if (itemHeight <= 0) {
31
+ throw new Error(
32
+ `[Virtual] itemHeight must be a positive number, got ${String(itemHeight)}`,
33
+ );
34
+ }
35
+ container = host._anchor?.parentElement ?? undefined;
36
+ if (!container) return;
33
37
 
34
- this._viewHeight.set(this._container.clientHeight || 600);
38
+ container.style.overflowY = "auto";
39
+ container.style.position = "relative";
40
+ viewHeight.set(container.clientHeight || 600);
35
41
 
36
- this._scrollHandler = () => {
37
- if (this._container) this._scrollTop.set(this._container.scrollTop);
42
+ scrollHandler = () => {
43
+ if (container) scrollTop.set(container.scrollTop);
38
44
  };
39
-
40
- this._container.addEventListener("scroll", this._scrollHandler);
41
- }
45
+ container.addEventListener("scroll", scrollHandler);
46
+ },
42
47
 
43
48
  onUnmount() {
44
- super.onUnmount?.();
45
- if (this._container && this._scrollHandler) {
46
- this._container.removeEventListener("scroll", this._scrollHandler);
49
+ if (container && scrollHandler) {
50
+ container.removeEventListener("scroll", scrollHandler);
47
51
  }
48
- this._cleanups.forEach((c) => { c(); });
49
- this._cleanups = [];
50
- }
52
+ cleanups.forEach((c) => { c(); });
53
+ cleanups.length = 0;
54
+ },
51
55
 
52
- render() {
53
- const host = this as unknown as VirtualHost;
56
+ render(originalRender) {
54
57
  const items = host.items ?? [];
55
58
  const total = items.length;
56
59
  const totalH = total * itemHeight;
57
60
 
58
61
  if (typeof host.renderItem !== "function") {
59
62
  console.warn(
60
- `[Virtual] ${constructor.name} must implement renderItem(item, index)`,
63
+ `[Virtual] component must implement renderItem(item, index)`,
61
64
  );
62
- return null;
65
+ return originalRender();
63
66
  }
64
67
 
65
68
  const renderItem = host.renderItem.bind(host);
66
69
 
67
- const startIdx = computed(() => {
68
- const start = Math.floor(this._scrollTop() / itemHeight) - buffer;
69
- return Math.max(0, start);
70
- });
71
-
72
- const endIdx = computed(() => {
73
- const end =
74
- Math.ceil((this._scrollTop() + this._viewHeight()) / itemHeight) +
75
- buffer;
76
- return Math.min(total - 1, end);
77
- });
78
-
70
+ const startIdx = computed(() =>
71
+ Math.max(0, Math.floor(scrollTop() / itemHeight) - buffer),
72
+ );
73
+ const endIdx = computed(() =>
74
+ Math.min(
75
+ total - 1,
76
+ Math.ceil((scrollTop() + viewHeight()) / itemHeight) + buffer,
77
+ ),
78
+ );
79
79
  const visibleItems = computed(() => {
80
80
  const result: Array<{ item: unknown; index: number }> = [];
81
81
  for (let i = startIdx(); i <= endIdx(); i++) {
@@ -83,31 +83,23 @@ export function Virtual(itemHeight: number, buffer = 3) {
83
83
  }
84
84
  return result;
85
85
  });
86
-
87
86
  const offsetTop = computed(() => startIdx() * itemHeight);
88
- const offsetBottom = computed(
89
- () => (total - 1 - endIdx()) * itemHeight,
90
- );
87
+ const offsetBottom = computed(() => (total - 1 - endIdx()) * itemHeight);
91
88
 
92
89
  const outer = document.createElement("div");
93
- outer.setAttribute(
94
- "style",
95
- `height:${String(totalH)}px; position:relative;`,
96
- );
90
+ outer.setAttribute("style", `height:${String(totalH)}px; position:relative;`);
97
91
 
98
92
  const spacerTop = document.createElement("div");
99
- this._cleanups.push(
93
+ cleanups.push(
100
94
  effect(() => {
101
95
  spacerTop.setAttribute("style", `height:${String(offsetTop())}px;`);
102
96
  }),
103
97
  );
104
98
 
105
99
  const itemsSlot = document.createElement("div");
106
- this._cleanups.push(
100
+ cleanups.push(
107
101
  effect(() => {
108
- while (itemsSlot.firstChild) {
109
- itemsSlot.removeChild(itemsSlot.firstChild);
110
- }
102
+ while (itemsSlot.firstChild) itemsSlot.removeChild(itemsSlot.firstChild);
111
103
  visibleItems().forEach(({ item, index }) => {
112
104
  const wrapper = document.createElement("div");
113
105
  wrapper.setAttribute(
@@ -116,10 +108,9 @@ export function Virtual(itemHeight: number, buffer = 3) {
116
108
  );
117
109
  const rendered = renderItem(item, index);
118
110
  if (rendered) {
119
- const nodes = (
120
- Array.isArray(rendered) ? rendered.flat() : [rendered]
111
+ (Array.isArray(rendered) ? rendered.flat() : [rendered]).forEach(
112
+ (n) => { wrapper.appendChild(n); },
121
113
  );
122
- nodes.forEach((n) => wrapper.appendChild(n));
123
114
  }
124
115
  itemsSlot.appendChild(wrapper);
125
116
  });
@@ -127,12 +118,9 @@ export function Virtual(itemHeight: number, buffer = 3) {
127
118
  );
128
119
 
129
120
  const spacerBottom = document.createElement("div");
130
- this._cleanups.push(
121
+ cleanups.push(
131
122
  effect(() => {
132
- spacerBottom.setAttribute(
133
- "style",
134
- `height:${String(offsetBottom())}px;`,
135
- );
123
+ spacerBottom.setAttribute("style", `height:${String(offsetBottom())}px;`);
136
124
  }),
137
125
  );
138
126
 
@@ -141,7 +129,11 @@ export function Virtual(itemHeight: number, buffer = 3) {
141
129
  outer.appendChild(spacerBottom);
142
130
 
143
131
  return outer;
144
- }
145
- } as unknown as T;
146
- };
132
+ },
133
+ };
134
+ }
135
+ }
136
+
137
+ export function Virtual(itemHeight: number, buffer = 3) {
138
+ return createClassDecorator(new VirtualBehavior(itemHeight, buffer));
147
139
  }