@praxisjs/decorators 0.4.2 → 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.
Files changed (190) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/dist/__tests__/component.test.d.ts +2 -0
  3. package/dist/__tests__/component.test.d.ts.map +1 -0
  4. package/dist/__tests__/component.test.js +146 -0
  5. package/dist/__tests__/component.test.js.map +1 -0
  6. package/dist/__tests__/compose.test.d.ts +2 -0
  7. package/dist/__tests__/compose.test.d.ts.map +1 -0
  8. package/dist/__tests__/compose.test.js +171 -0
  9. package/dist/__tests__/compose.test.js.map +1 -0
  10. package/dist/__tests__/events.test.d.ts +2 -0
  11. package/dist/__tests__/events.test.d.ts.map +1 -0
  12. package/dist/__tests__/events.test.js +216 -0
  13. package/dist/__tests__/events.test.js.map +1 -0
  14. package/dist/__tests__/functions.test.js +283 -31
  15. package/dist/__tests__/functions.test.js.map +1 -1
  16. package/dist/__tests__/lazy.test.d.ts +2 -0
  17. package/dist/__tests__/lazy.test.d.ts.map +1 -0
  18. package/dist/__tests__/lazy.test.js +163 -0
  19. package/dist/__tests__/lazy.test.js.map +1 -0
  20. package/dist/__tests__/memo.test.js +71 -22
  21. package/dist/__tests__/memo.test.js.map +1 -1
  22. package/dist/__tests__/properties.test.js +191 -1
  23. package/dist/__tests__/properties.test.js.map +1 -1
  24. package/dist/__tests__/resource.test.d.ts +2 -0
  25. package/dist/__tests__/resource.test.d.ts.map +1 -0
  26. package/dist/__tests__/resource.test.js +103 -0
  27. package/dist/__tests__/resource.test.js.map +1 -0
  28. package/dist/__tests__/slot.test.d.ts +2 -0
  29. package/dist/__tests__/slot.test.d.ts.map +1 -0
  30. package/dist/__tests__/slot.test.js +147 -0
  31. package/dist/__tests__/slot.test.js.map +1 -0
  32. package/dist/__tests__/virtual.test.d.ts +2 -0
  33. package/dist/__tests__/virtual.test.d.ts.map +1 -0
  34. package/dist/__tests__/virtual.test.js +242 -0
  35. package/dist/__tests__/virtual.test.js.map +1 -0
  36. package/dist/component/component.d.ts +1 -1
  37. package/dist/component/component.d.ts.map +1 -1
  38. package/dist/component/component.js +15 -10
  39. package/dist/component/component.js.map +1 -1
  40. package/dist/component/lazy.d.ts +1 -1
  41. package/dist/component/lazy.d.ts.map +1 -1
  42. package/dist/component/lazy.js +29 -27
  43. package/dist/component/lazy.js.map +1 -1
  44. package/dist/component/virtual.d.ts +1 -1
  45. package/dist/component/virtual.d.ts.map +1 -1
  46. package/dist/component/virtual.js +46 -50
  47. package/dist/component/virtual.js.map +1 -1
  48. package/dist/create-class-decorator.d.ts +16 -0
  49. package/dist/create-class-decorator.d.ts.map +1 -0
  50. package/dist/create-class-decorator.js +35 -0
  51. package/dist/create-class-decorator.js.map +1 -0
  52. package/dist/create-field-decorator.d.ts +14 -0
  53. package/dist/create-field-decorator.d.ts.map +1 -0
  54. package/dist/create-field-decorator.js +37 -0
  55. package/dist/create-field-decorator.js.map +1 -0
  56. package/dist/create-getter-decorator.d.ts +18 -0
  57. package/dist/create-getter-decorator.d.ts.map +1 -0
  58. package/dist/create-getter-decorator.js +15 -0
  59. package/dist/create-getter-decorator.js.map +1 -0
  60. package/dist/create-lifecycle-method-decorator.d.ts +6 -0
  61. package/dist/create-lifecycle-method-decorator.d.ts.map +1 -0
  62. package/dist/create-lifecycle-method-decorator.js +22 -0
  63. package/dist/create-lifecycle-method-decorator.js.map +1 -0
  64. package/dist/create-method-decorator.d.ts +6 -0
  65. package/dist/create-method-decorator.d.ts.map +1 -0
  66. package/dist/create-method-decorator.js +14 -0
  67. package/dist/create-method-decorator.js.map +1 -0
  68. package/dist/events/emit.d.ts +1 -1
  69. package/dist/events/emit.d.ts.map +1 -1
  70. package/dist/events/emit.js +24 -29
  71. package/dist/events/emit.js.map +1 -1
  72. package/dist/events/on-command.d.ts +1 -2
  73. package/dist/events/on-command.d.ts.map +1 -1
  74. package/dist/events/on-command.js +15 -32
  75. package/dist/events/on-command.js.map +1 -1
  76. package/dist/functions/bind.d.ts +1 -2
  77. package/dist/functions/bind.d.ts.map +1 -1
  78. package/dist/functions/bind.js +4 -11
  79. package/dist/functions/bind.js.map +1 -1
  80. package/dist/functions/debounce.d.ts +1 -2
  81. package/dist/functions/debounce.d.ts.map +1 -1
  82. package/dist/functions/debounce.js +12 -20
  83. package/dist/functions/debounce.js.map +1 -1
  84. package/dist/functions/log.d.ts +1 -2
  85. package/dist/functions/log.d.ts.map +1 -1
  86. package/dist/functions/log.js +40 -38
  87. package/dist/functions/log.js.map +1 -1
  88. package/dist/functions/memo.d.ts +1 -2
  89. package/dist/functions/memo.d.ts.map +1 -1
  90. package/dist/functions/memo.js +15 -27
  91. package/dist/functions/memo.js.map +1 -1
  92. package/dist/functions/once.d.ts +1 -2
  93. package/dist/functions/once.d.ts.map +1 -1
  94. package/dist/functions/once.js +14 -12
  95. package/dist/functions/once.js.map +1 -1
  96. package/dist/functions/retry.d.ts +1 -2
  97. package/dist/functions/retry.d.ts.map +1 -1
  98. package/dist/functions/retry.js +23 -20
  99. package/dist/functions/retry.js.map +1 -1
  100. package/dist/functions/throttle.d.ts +1 -2
  101. package/dist/functions/throttle.d.ts.map +1 -1
  102. package/dist/functions/throttle.js +9 -16
  103. package/dist/functions/throttle.js.map +1 -1
  104. package/dist/functions/watch.d.ts +1 -1
  105. package/dist/functions/watch.d.ts.map +1 -1
  106. package/dist/functions/watch.js +25 -31
  107. package/dist/functions/watch.js.map +1 -1
  108. package/dist/functions/when.d.ts +1 -2
  109. package/dist/functions/when.d.ts.map +1 -1
  110. package/dist/functions/when.js +10 -26
  111. package/dist/functions/when.js.map +1 -1
  112. package/dist/index.d.ts +6 -1
  113. package/dist/index.d.ts.map +1 -1
  114. package/dist/index.js +6 -1
  115. package/dist/index.js.map +1 -1
  116. package/dist/properties/compose.d.ts +3 -0
  117. package/dist/properties/compose.d.ts.map +1 -0
  118. package/dist/properties/compose.js +40 -0
  119. package/dist/properties/compose.js.map +1 -0
  120. package/dist/properties/computed.d.ts +1 -2
  121. package/dist/properties/computed.d.ts.map +1 -1
  122. package/dist/properties/computed.js +10 -9
  123. package/dist/properties/computed.js.map +1 -1
  124. package/dist/properties/history.d.ts.map +1 -1
  125. package/dist/properties/history.js +37 -32
  126. package/dist/properties/history.js.map +1 -1
  127. package/dist/properties/index.d.ts +2 -0
  128. package/dist/properties/index.d.ts.map +1 -1
  129. package/dist/properties/index.js +2 -0
  130. package/dist/properties/index.js.map +1 -1
  131. package/dist/properties/persisted.d.ts +1 -2
  132. package/dist/properties/persisted.d.ts.map +1 -1
  133. package/dist/properties/persisted.js +13 -29
  134. package/dist/properties/persisted.js.map +1 -1
  135. package/dist/properties/prop.d.ts +1 -2
  136. package/dist/properties/prop.d.ts.map +1 -1
  137. package/dist/properties/prop.js +21 -25
  138. package/dist/properties/prop.js.map +1 -1
  139. package/dist/properties/resource.d.ts +14 -0
  140. package/dist/properties/resource.d.ts.map +1 -0
  141. package/dist/properties/resource.js +27 -0
  142. package/dist/properties/resource.js.map +1 -0
  143. package/dist/properties/slot.d.ts +1 -2
  144. package/dist/properties/slot.d.ts.map +1 -1
  145. package/dist/properties/slot.js +15 -20
  146. package/dist/properties/slot.js.map +1 -1
  147. package/dist/properties/state.d.ts +1 -2
  148. package/dist/properties/state.d.ts.map +1 -1
  149. package/dist/properties/state.js +13 -18
  150. package/dist/properties/state.js.map +1 -1
  151. package/package.json +2 -2
  152. package/src/__tests__/component.test.ts +166 -0
  153. package/src/__tests__/compose.test.ts +202 -0
  154. package/src/__tests__/events.test.ts +268 -0
  155. package/src/__tests__/functions.test.ts +368 -30
  156. package/src/__tests__/lazy.test.ts +218 -0
  157. package/src/__tests__/memo.test.ts +89 -22
  158. package/src/__tests__/properties.test.ts +249 -1
  159. package/src/__tests__/resource.test.ts +113 -0
  160. package/src/__tests__/slot.test.ts +179 -0
  161. package/src/__tests__/virtual.test.ts +260 -0
  162. package/src/component/component.ts +27 -12
  163. package/src/component/lazy.ts +33 -35
  164. package/src/component/virtual.tsx +62 -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 +11 -26
  174. package/src/functions/log.ts +40 -43
  175. package/src/functions/memo.ts +15 -45
  176. package/src/functions/once.ts +13 -16
  177. package/src/functions/retry.ts +21 -22
  178. package/src/functions/throttle.ts +9 -23
  179. package/src/functions/watch.ts +27 -34
  180. package/src/functions/when.ts +11 -36
  181. package/src/index.ts +28 -0
  182. package/src/properties/compose.ts +53 -0
  183. package/src/properties/computed.ts +11 -13
  184. package/src/properties/history.ts +42 -41
  185. package/src/properties/index.ts +2 -0
  186. package/src/properties/persisted.ts +17 -58
  187. package/src/properties/prop.ts +21 -31
  188. package/src/properties/resource.ts +34 -0
  189. package/src/properties/slot.ts +17 -30
  190. package/src/properties/state.ts +14 -24
@@ -1,58 +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>(
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
- this.render.bind(this);
3
+ import { ClassBehavior, createClassDecorator, type ClassEnhancement } from "../create-class-decorator";
14
4
 
15
- onMount() {
16
- super.onMount?.();
5
+ class LazyBehavior extends ClassBehavior {
6
+ constructor(private readonly placeholder: number) {
7
+ super();
8
+ }
17
9
 
18
- // 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() {
19
17
  const el =
20
- (this as unknown as { _anchor?: Comment })._anchor
21
- ?.parentElement ?? null;
18
+ (instance as { _anchor?: Comment })._anchor?.parentElement ?? null;
22
19
  if (!el) return;
23
20
 
24
21
  if (!("IntersectionObserver" in window)) {
25
- this._lazyVisible.set(true);
22
+ visible.set(true);
26
23
  return;
27
24
  }
28
25
 
29
- if (!this._lazyVisible()) {
30
- el.style.minHeight = `${String(placeholder)}px`;
31
- }
26
+ if (!visible()) el.style.minHeight = `${String(placeholder)}px`;
32
27
 
33
- this._observer = new IntersectionObserver(
28
+ observer = new IntersectionObserver(
34
29
  (entries) => {
35
30
  if (entries[0]?.isIntersecting) {
36
- this._lazyVisible.set(true);
31
+ visible.set(true);
37
32
  el.style.minHeight = "";
38
- this._observer?.disconnect();
33
+ observer?.disconnect();
39
34
  }
40
35
  },
41
36
  { rootMargin: "100px" },
42
37
  );
43
38
 
44
- this._observer.observe(el);
45
- }
39
+ observer.observe(el);
40
+ },
46
41
 
47
42
  onUnmount() {
48
- super.onUnmount?.();
49
- this._observer?.disconnect();
50
- }
51
-
52
- render(): Node | Node[] | null {
53
- if (!this._lazyVisible()) return null;
54
- return this._originalRender();
55
- }
56
- } as unknown as T;
57
- };
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));
58
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
- 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>(
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
+ container = host._anchor?.parentElement ?? undefined;
31
+ if (!container) return;
30
32
 
31
- this._container.style.overflowY = "auto";
32
- this._container.style.position = "relative";
33
+ container.style.overflowY = "auto";
34
+ container.style.position = "relative";
35
+ viewHeight.set(container.clientHeight || 600);
33
36
 
34
- this._viewHeight.set(this._container.clientHeight || 600);
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
- this._container.addEventListener("scroll", this._scrollHandler);
41
- }
40
+ container.addEventListener("scroll", scrollHandler);
41
+ },
42
42
 
43
43
  onUnmount() {
44
- super.onUnmount?.();
45
- if (this._container && this._scrollHandler) {
46
- this._container.removeEventListener("scroll", this._scrollHandler);
44
+ if (container && scrollHandler) {
45
+ container.removeEventListener("scroll", scrollHandler);
47
46
  }
48
- this._cleanups.forEach((c) => { c(); });
49
- this._cleanups = [];
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] ${constructor.name} must implement renderItem(item, index)`,
58
+ `[Virtual] component must implement renderItem(item, index)`,
61
59
  );
62
- return null;
60
+ return originalRender();
63
61
  }
64
62
 
65
63
  const renderItem = host.renderItem.bind(host);
66
64
 
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
-
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
- this._cleanups.push(
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
- this._cleanups.push(
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
- const nodes = (
120
- Array.isArray(rendered) ? rendered.flat() : [rendered]
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
- this._cleanups.push(
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
- } as unknown as T;
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
+ }
@@ -0,0 +1,66 @@
1
+ import type { StatefulComponent } from "@praxisjs/core";
2
+
3
+ export interface FieldBinding {
4
+ /** Replaces the decorated property. If absent, the original property is left untouched. */
5
+ descriptor?: PropertyDescriptor;
6
+ /** Extra properties to define on the instance (e.g. companion properties like `nameHistory`). */
7
+ additional?: Record<string, PropertyDescriptor>;
8
+ onMount?: () => void;
9
+ onUnmount?: () => void;
10
+ }
11
+
12
+ export interface FieldBehavior {
13
+ bind(
14
+ instance: StatefulComponent,
15
+ name: string,
16
+ initialValue: unknown,
17
+ ): FieldBinding;
18
+ }
19
+
20
+ export function createFieldDecorator(behavior: FieldBehavior) {
21
+ return function (
22
+ _value: undefined,
23
+ context: ClassFieldDecoratorContext<StatefulComponent>,
24
+ ): void {
25
+ context.addInitializer(function (this: unknown) {
26
+ const instance = this as StatefulComponent & Record<string, unknown>;
27
+ const name = context.name as string;
28
+ const initialValue = instance[name];
29
+
30
+ const { descriptor, additional, onMount, onUnmount } = behavior.bind(
31
+ instance,
32
+ name,
33
+ initialValue,
34
+ );
35
+
36
+ if (descriptor) {
37
+ Reflect.deleteProperty(instance, name);
38
+ Object.defineProperty(instance, name, {
39
+ enumerable: true,
40
+ configurable: true,
41
+ ...descriptor,
42
+ });
43
+ }
44
+
45
+ for (const [key, desc] of Object.entries(additional ?? {})) {
46
+ Object.defineProperty(instance, key, {
47
+ configurable: true,
48
+ ...desc,
49
+ });
50
+ }
51
+
52
+ if (onMount ?? onUnmount) {
53
+ const prevMount = instance.onMount?.bind(instance);
54
+ const prevUnmount = instance.onUnmount?.bind(instance);
55
+ instance.onMount = () => {
56
+ prevMount?.();
57
+ onMount?.();
58
+ };
59
+ instance.onUnmount = () => {
60
+ prevUnmount?.();
61
+ onUnmount?.();
62
+ };
63
+ }
64
+ });
65
+ };
66
+ }
@@ -0,0 +1,52 @@
1
+ export interface GetterBehavior {
2
+ /**
3
+ * Given the original getter and the current instance, returns a function
4
+ * that produces the getter value. Called on every property access.
5
+ */
6
+ wrap(
7
+ original: (this: object) => unknown,
8
+ instance: object,
9
+ ): () => unknown;
10
+ }
11
+
12
+ export function createGetterDecorator(behavior: GetterBehavior) {
13
+ return function <This, T>(
14
+ target: (this: This) => T,
15
+ _context: ClassGetterDecoratorContext<This, T>,
16
+ ): (this: This) => T {
17
+ return function (this: This): T {
18
+ return behavior.wrap(
19
+ target as (this: object) => unknown,
20
+ this as object,
21
+ )() as T;
22
+ };
23
+ };
24
+ }
25
+
26
+ export interface GetterObserverBehavior {
27
+ /**
28
+ * Called once per instance when the class is initialized.
29
+ * Use this to set up observation side-effects on the getter without
30
+ * changing its return value (e.g. tracking changes for devtools).
31
+ */
32
+ observe(
33
+ getter: (this: object) => unknown,
34
+ instance: object,
35
+ name: string,
36
+ ): void;
37
+ }
38
+
39
+ export function createGetterObserverDecorator(behavior: GetterObserverBehavior) {
40
+ return function <This, T>(
41
+ target: (this: This) => T,
42
+ context: ClassGetterDecoratorContext<This, T>,
43
+ ): void {
44
+ context.addInitializer(function (this: unknown) {
45
+ behavior.observe(
46
+ target as (this: object) => unknown,
47
+ this as object,
48
+ context.name as string,
49
+ );
50
+ });
51
+ };
52
+ }
@@ -0,0 +1,38 @@
1
+ import type { StatefulComponent } from "@praxisjs/core";
2
+
3
+ export interface LifecycleMethodBehavior<
4
+ T extends StatefulComponent = StatefulComponent,
5
+ > {
6
+ register(
7
+ callback: (...args: unknown[]) => void,
8
+ instance: T,
9
+ ): (() => void) | undefined;
10
+ }
11
+
12
+ export function createLifecycleMethodDecorator<
13
+ T extends StatefulComponent = StatefulComponent,
14
+ >(behavior: LifecycleMethodBehavior<T>) {
15
+ return function (
16
+ value: (this: T, ...args: unknown[]) => void,
17
+ _context: ClassMethodDecoratorContext<T>,
18
+ ): void {
19
+ _context.addInitializer(function (this: unknown) {
20
+ const instance = this as T;
21
+ const prevMount = instance.onMount?.bind(instance);
22
+ const prevUnmount = instance.onUnmount?.bind(instance);
23
+ let cleanup: (() => void) | undefined;
24
+
25
+ instance.onMount = function (this: T) {
26
+ prevMount?.call(this);
27
+ const result = behavior.register(value.bind(this), this);
28
+ if (result) cleanup = result;
29
+ };
30
+
31
+ instance.onUnmount = function (this: T) {
32
+ prevUnmount?.call(this);
33
+ cleanup?.();
34
+ cleanup = undefined;
35
+ };
36
+ });
37
+ };
38
+ }
@@ -0,0 +1,26 @@
1
+ import type { StatefulComponent } from "@praxisjs/core";
2
+
3
+ export interface MethodBehavior {
4
+ wrap(
5
+ original: (...args: unknown[]) => unknown,
6
+ instance: object,
7
+ name: string,
8
+ ): (...args: unknown[]) => unknown;
9
+ }
10
+
11
+ export function createMethodDecorator(behavior: MethodBehavior) {
12
+ return function (
13
+ value: (this: object, ...args: unknown[]) => unknown,
14
+ context: ClassMethodDecoratorContext<StatefulComponent>,
15
+ ): void {
16
+ context.addInitializer(function (this: unknown) {
17
+ const instance = this as object;
18
+ const name = context.name as string;
19
+ Object.defineProperty(instance, name, {
20
+ value: behavior.wrap(value, instance, name),
21
+ configurable: true,
22
+ writable: true,
23
+ });
24
+ });
25
+ };
26
+ }
@@ -1,42 +1,31 @@
1
1
  import type { StatefulComponent } from "@praxisjs/core";
2
2
 
3
+ import { createMethodDecorator } from "../create-method-decorator";
3
4
  import { readProp } from "./helper";
4
5
 
5
6
  export function Emit(propName: string) {
6
- return function (
7
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
8
- value: (this: StatefulComponent, ...args: any[]) => any,
9
- context: ClassMethodDecoratorContext<StatefulComponent>,
10
- ): void {
11
- const wrapped = function (
12
- this: StatefulComponent,
13
- ...args: unknown[]
14
- ): unknown {
15
- // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
16
- const result = value.apply(this, args);
7
+ return createMethodDecorator({
8
+ wrap(original, instance) {
9
+ const comp = instance as StatefulComponent;
10
+ return (...args: unknown[]) => {
11
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-explicit-any
12
+ const result = original.apply(instance, args) as any;
17
13
 
18
- const callback = readProp(this, propName);
19
- if (typeof callback !== "function") return result;
14
+ const callback = readProp(comp, propName);
15
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-return
16
+ if (typeof callback !== "function") return result;
20
17
 
21
- if (result !== undefined) {
22
- (callback as (v: unknown) => void)(result);
23
- } else if (args.length > 0) {
24
- (callback as (...a: unknown[]) => void)(...args);
25
- } else {
26
- (callback as () => void)();
27
- }
18
+ if (result !== undefined) {
19
+ (callback as (v: unknown) => void)(result);
20
+ } else if (args.length > 0) {
21
+ (callback as (...a: unknown[]) => void)(...args);
22
+ } else {
23
+ (callback as () => void)();
24
+ }
28
25
 
29
- return result;
30
- };
31
-
32
- context.addInitializer(function (this: unknown) {
33
- const name = context.name as string;
34
- const bound = wrapped.bind(this as StatefulComponent);
35
- Object.defineProperty(this as object, name, {
36
- value: bound,
37
- configurable: true,
38
- writable: true,
39
- });
40
- });
41
- };
26
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-return
27
+ return result;
28
+ };
29
+ },
30
+ });
42
31
  }