@praxisjs/decorators 1.4.0 → 1.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 (53) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/dist/__tests__/component.test.js +2 -1
  3. package/dist/__tests__/component.test.js.map +1 -1
  4. package/dist/__tests__/events.test.js +4 -3
  5. package/dist/__tests__/events.test.js.map +1 -1
  6. package/dist/__tests__/lazy.test.js +17 -17
  7. package/dist/__tests__/lazy.test.js.map +1 -1
  8. package/dist/__tests__/properties.test.js +45 -8
  9. package/dist/__tests__/properties.test.js.map +1 -1
  10. package/dist/component/lazy.d.ts.map +1 -1
  11. package/dist/component/lazy.js +2 -2
  12. package/dist/component/lazy.js.map +1 -1
  13. package/dist/events/helper.d.ts +2 -2
  14. package/dist/events/helper.d.ts.map +1 -1
  15. package/dist/events/helper.js +3 -2
  16. package/dist/events/helper.js.map +1 -1
  17. package/dist/index.d.ts +2 -2
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +2 -1
  20. package/dist/index.js.map +1 -1
  21. package/dist/properties/deep-state.js +2 -2
  22. package/dist/properties/deep-state.js.map +1 -1
  23. package/dist/properties/function-prop.d.ts +2 -0
  24. package/dist/properties/function-prop.d.ts.map +1 -0
  25. package/dist/properties/function-prop.js +24 -0
  26. package/dist/properties/function-prop.js.map +1 -0
  27. package/dist/properties/index.d.ts +1 -0
  28. package/dist/properties/index.d.ts.map +1 -1
  29. package/dist/properties/index.js +1 -0
  30. package/dist/properties/index.js.map +1 -1
  31. package/dist/properties/prop.d.ts.map +1 -1
  32. package/dist/properties/prop.js +5 -4
  33. package/dist/properties/prop.js.map +1 -1
  34. package/dist/properties/state.js +2 -2
  35. package/dist/properties/state.js.map +1 -1
  36. package/dist/reactive-host.d.ts +7 -3
  37. package/dist/reactive-host.d.ts.map +1 -1
  38. package/dist/reactive-host.js +1 -1
  39. package/dist/reactive-host.js.map +1 -1
  40. package/package.json +3 -3
  41. package/src/__tests__/component.test.ts +2 -1
  42. package/src/__tests__/events.test.ts +4 -3
  43. package/src/__tests__/lazy.test.ts +17 -17
  44. package/src/__tests__/properties.test.ts +61 -8
  45. package/src/component/lazy.ts +6 -3
  46. package/src/events/helper.ts +8 -4
  47. package/src/index.ts +2 -1
  48. package/src/properties/deep-state.ts +2 -2
  49. package/src/properties/function-prop.ts +29 -0
  50. package/src/properties/index.ts +1 -0
  51. package/src/properties/prop.ts +10 -4
  52. package/src/properties/state.ts +2 -2
  53. package/src/reactive-host.ts +7 -3
@@ -2,6 +2,7 @@
2
2
  import { beforeEach, afterEach, describe, it, expect, vi } from "vitest";
3
3
 
4
4
  import { StatefulComponent } from "@praxisjs/core";
5
+ import { setComponentAnchor } from "@praxisjs/core/internal";
5
6
  import { Lazy } from "../component/lazy";
6
7
  import type { LazyOptions } from "../component/lazy";
7
8
 
@@ -55,7 +56,7 @@ describe("Lazy decorator", () => {
55
56
  expect(resolveRender(instance)).toBeNull();
56
57
  });
57
58
 
58
- it("sets _lazyVisible to true when IntersectionObserver is not supported", () => {
59
+ it("sets lazy visibility to true when IntersectionObserver is not supported", () => {
59
60
  // Remove IntersectionObserver to simulate unsupported environment
60
61
  // @ts-expect-error – removing test stub
61
62
  delete globalThis.IntersectionObserver;
@@ -68,7 +69,7 @@ describe("Lazy decorator", () => {
68
69
  document.body.appendChild(parent);
69
70
  const anchor = document.createComment("end");
70
71
  parent.appendChild(anchor);
71
- (instance as unknown as { _anchor: Comment })._anchor = anchor;
72
+ setComponentAnchor(instance, anchor);
72
73
 
73
74
  instance.onMount?.();
74
75
  // Without IntersectionObserver, component should immediately become visible
@@ -84,7 +85,7 @@ describe("Lazy decorator", () => {
84
85
  document.body.appendChild(parent);
85
86
  const anchor = document.createComment("end");
86
87
  parent.appendChild(anchor);
87
- (instance as unknown as { _anchor: Comment })._anchor = anchor;
88
+ setComponentAnchor(instance, anchor);
88
89
 
89
90
  instance.onMount?.();
90
91
  expect(parent.style.minHeight).toBe("300px");
@@ -99,7 +100,7 @@ describe("Lazy decorator", () => {
99
100
  document.body.appendChild(parent);
100
101
  const anchor = document.createComment("end");
101
102
  parent.appendChild(anchor);
102
- (instance as unknown as { _anchor: Comment })._anchor = anchor;
103
+ setComponentAnchor(instance, anchor);
103
104
 
104
105
  instance.onMount?.();
105
106
  expect(resolveRender(instance)).toBeNull();
@@ -115,12 +116,12 @@ describe("Lazy decorator", () => {
115
116
  document.body.removeChild(parent);
116
117
  });
117
118
 
118
- it("does nothing when _anchor has no parent", () => {
119
+ it("does nothing when the anchor has no parent", () => {
119
120
  const Wrapped = applyLazy(BaseComp as AnyConstructor);
120
121
  const instance = new Wrapped();
121
122
 
122
123
  const anchor = document.createComment("end");
123
- (instance as unknown as { _anchor: Comment })._anchor = anchor;
124
+ setComponentAnchor(instance, anchor);
124
125
 
125
126
  // Should not throw even though anchor has no parent
126
127
  expect(() => instance.onMount?.()).not.toThrow();
@@ -134,7 +135,7 @@ describe("Lazy decorator", () => {
134
135
  document.body.appendChild(parent);
135
136
  const anchor = document.createComment("end");
136
137
  parent.appendChild(anchor);
137
- (instance as unknown as { _anchor: Comment })._anchor = anchor;
138
+ setComponentAnchor(instance, anchor);
138
139
 
139
140
  instance.onMount?.();
140
141
  instance.onUnmount?.();
@@ -142,10 +143,9 @@ describe("Lazy decorator", () => {
142
143
  document.body.removeChild(parent);
143
144
  });
144
145
 
145
- it("onMount does nothing when _anchor is undefined", () => {
146
+ it("onMount does nothing when anchor is undefined", () => {
146
147
  const Wrapped = applyLazy(BaseComp as AnyConstructor);
147
148
  const instance = new Wrapped();
148
- // _anchor is undefined by default
149
149
  expect(() => instance.onMount?.()).not.toThrow();
150
150
  });
151
151
 
@@ -157,7 +157,7 @@ describe("Lazy decorator", () => {
157
157
  document.body.appendChild(parent);
158
158
  const anchor = document.createComment("end");
159
159
  parent.appendChild(anchor);
160
- (instance as unknown as { _anchor: Comment })._anchor = anchor;
160
+ setComponentAnchor(instance, anchor);
161
161
 
162
162
  instance.onMount?.();
163
163
  expect(resolveRender(instance)).toBeNull(); // not yet visible
@@ -181,7 +181,7 @@ describe("Lazy decorator", () => {
181
181
  document.body.appendChild(parent);
182
182
  const anchor = document.createComment("end");
183
183
  parent.appendChild(anchor);
184
- (instance as unknown as { _anchor: Comment })._anchor = anchor;
184
+ setComponentAnchor(instance, anchor);
185
185
 
186
186
  instance.onMount?.();
187
187
 
@@ -205,7 +205,7 @@ describe("Lazy decorator", () => {
205
205
  document.body.appendChild(parent);
206
206
  const anchor = document.createComment("end");
207
207
  parent.appendChild(anchor);
208
- (instance as unknown as { _anchor: Comment })._anchor = anchor;
208
+ setComponentAnchor(instance, anchor);
209
209
 
210
210
  instance.onMount?.();
211
211
  expect(parent.style.minHeight).toBe("150px");
@@ -233,7 +233,7 @@ describe("Lazy decorator", () => {
233
233
  document.body.appendChild(parent);
234
234
  const anchor = document.createComment("end");
235
235
  parent.appendChild(anchor);
236
- (instance as unknown as { _anchor: Comment })._anchor = anchor;
236
+ setComponentAnchor(instance, anchor);
237
237
 
238
238
  instance.onMount?.();
239
239
  expect(capturedInit?.root).toBe(rootEl);
@@ -260,7 +260,7 @@ describe("Lazy decorator", () => {
260
260
  document.body.appendChild(parent);
261
261
  const anchor = document.createComment("end");
262
262
  parent.appendChild(anchor);
263
- (instance as unknown as { _anchor: Comment })._anchor = anchor;
263
+ setComponentAnchor(instance, anchor);
264
264
 
265
265
  instance.onMount?.();
266
266
  expect(capturedInit?.root).toBeNull();
@@ -276,7 +276,7 @@ describe("Lazy decorator", () => {
276
276
  document.body.appendChild(parent);
277
277
  const anchor = document.createComment("end");
278
278
  parent.appendChild(anchor);
279
- (instance as unknown as { _anchor: Comment })._anchor = anchor;
279
+ setComponentAnchor(instance, anchor);
280
280
 
281
281
  // First mount — become visible
282
282
  instance.onMount?.();
@@ -286,10 +286,10 @@ describe("Lazy decorator", () => {
286
286
  );
287
287
  instance.onUnmount?.();
288
288
 
289
- // Second mount — _lazyVisible is already true
289
+ // Second mount — lazy visibility is already true
290
290
  instance.onMount?.();
291
291
 
292
- // minHeight must NOT be re-applied because _lazyVisible() is true
292
+ // minHeight must NOT be re-applied because lazy visibility is true
293
293
  expect(parent.style.minHeight).toBe("");
294
294
  document.body.removeChild(parent);
295
295
  });
@@ -1,7 +1,12 @@
1
1
  import { describe, it, expect, vi } from "vitest";
2
2
 
3
3
  import { StatefulComponent } from "@praxisjs/core";
4
- import { signal, computed } from "@praxisjs/core/internal";
4
+ import {
5
+ computed,
6
+ isStateDirty,
7
+ setStateDirty,
8
+ signal,
9
+ } from "@praxisjs/core/internal";
5
10
 
6
11
  import { createFieldDecorator } from "../create-field-decorator";
7
12
  import { createGetterDecorator } from "../create-getter-decorator";
@@ -11,6 +16,7 @@ import { When } from "../functions/when";
11
16
  import { Compose } from "../properties/compose";
12
17
  import { Computed } from "../properties/computed";
13
18
  import { History } from "../properties/history";
19
+ import { FunctionProp } from "../properties/function-prop";
14
20
  import { Prop } from "../properties/prop";
15
21
  import { State } from "../properties/state";
16
22
 
@@ -68,7 +74,7 @@ describe("State decorator", () => {
68
74
  expect((instance as unknown as Record<string, unknown>).count).toBe(20);
69
75
  });
70
76
 
71
- it("sets _stateDirty on write", () => {
77
+ it("sets state dirty on write", () => {
72
78
  const { ctx, run } = fieldCtx("value");
73
79
  State()(undefined, ctx);
74
80
 
@@ -76,9 +82,9 @@ describe("State decorator", () => {
76
82
  (instance as unknown as Record<string, unknown>).value = "hello";
77
83
  run(instance);
78
84
 
79
- instance._stateDirty = false;
85
+ setStateDirty(instance, false);
80
86
  (instance as unknown as Record<string, unknown>).value = "world";
81
- expect(instance._stateDirty).toBe(true);
87
+ expect(isStateDirty(instance)).toBe(true);
82
88
  });
83
89
 
84
90
  it("accepts null as initial value", () => {
@@ -115,7 +121,7 @@ describe("State decorator", () => {
115
121
  // ── Prop ──────────────────────────────────────────────────────────────────────
116
122
 
117
123
  describe("Prop decorator", () => {
118
- it("reads from _rawProps when set by parent", () => {
124
+ it("reads from raw props when set by parent", () => {
119
125
  const { ctx, run } = fieldCtx("label");
120
126
  Prop()(undefined, ctx);
121
127
 
@@ -125,7 +131,7 @@ describe("Prop decorator", () => {
125
131
  expect((instance as unknown as Record<string, unknown>).label).toBe("hello");
126
132
  });
127
133
 
128
- it("falls back to default value when not in _rawProps", () => {
134
+ it("falls back to default value when not in raw props", () => {
129
135
  const { ctx, run } = fieldCtx("size");
130
136
  Prop()(undefined, ctx);
131
137
 
@@ -150,6 +156,53 @@ describe("Prop decorator", () => {
150
156
  });
151
157
  });
152
158
 
159
+ // ── FunctionProp ──────────────────────────────────────────────────────────────
160
+
161
+ describe("FunctionProp decorator", () => {
162
+ it("reads function values from raw props without invoking them", () => {
163
+ const { ctx, run } = fieldCtx("filter");
164
+ FunctionProp()(undefined, ctx);
165
+
166
+ const filter = vi.fn((value: number) => value > 1);
167
+ const instance = new TestComponent({ filter });
168
+ run(instance);
169
+
170
+ const received = (instance as unknown as Record<string, unknown>).filter;
171
+ expect(received).toBe(filter);
172
+ expect(filter).not.toHaveBeenCalled();
173
+ expect((received as (value: number) => boolean)(2)).toBe(true);
174
+ expect(filter).toHaveBeenCalledWith(2);
175
+ });
176
+
177
+ it("falls back to the default function when not in raw props", () => {
178
+ const { ctx, run } = fieldCtx("filter");
179
+ FunctionProp()(undefined, ctx);
180
+
181
+ const defaultFilter = vi.fn((value: number) => value % 2 === 0);
182
+ const instance = new TestComponent();
183
+ (instance as unknown as Record<string, unknown>).filter = defaultFilter;
184
+ run(instance);
185
+
186
+ const received = (instance as unknown as Record<string, unknown>).filter;
187
+ expect(received).toBe(defaultFilter);
188
+ expect((received as (value: number) => boolean)(4)).toBe(true);
189
+ expect(defaultFilter).toHaveBeenCalledWith(4);
190
+ });
191
+
192
+ it("set() updates the default function used without a raw prop", () => {
193
+ const { ctx, run } = fieldCtx("filter");
194
+ FunctionProp()(undefined, ctx);
195
+
196
+ const instance = new TestComponent();
197
+ run(instance);
198
+
199
+ const nextFilter = (value: number) => value < 10;
200
+ (instance as unknown as Record<string, unknown>).filter = nextFilter;
201
+
202
+ expect((instance as unknown as Record<string, unknown>).filter).toBe(nextFilter);
203
+ });
204
+ });
205
+
153
206
  // ── Computed ──────────────────────────────────────────────────────────────────
154
207
 
155
208
  describe("Computed decorator", () => {
@@ -832,7 +885,7 @@ describe("When decorator", () => {
832
885
  // ── Prop (additional branches) ────────────────────────────────────────────────
833
886
 
834
887
  describe("Prop decorator — additional branches", () => {
835
- it("set() updates the default value (used when no _rawProps entry)", () => {
888
+ it("set() updates the default value (used when no raw props entry)", () => {
836
889
  const { ctx, run } = fieldCtx("size");
837
890
  Prop()(undefined, ctx);
838
891
 
@@ -840,7 +893,7 @@ describe("Prop decorator — additional branches", () => {
840
893
  (instance as unknown as Record<string, unknown>).size = "sm";
841
894
  run(instance);
842
895
 
843
- // Setter path: updates _defaults
896
+ // Setter path: updates defaults.
844
897
  (instance as unknown as Record<string, unknown>).size = "lg";
845
898
  expect((instance as unknown as Record<string, unknown>).size).toBe("lg");
846
899
  });
@@ -1,4 +1,8 @@
1
- import { type RootComponent, signal } from "@praxisjs/core/internal";
1
+ import {
2
+ getComponentAnchor,
3
+ type RootComponent,
4
+ signal,
5
+ } from "@praxisjs/core/internal";
2
6
 
3
7
  import { ClassBehavior, createClassDecorator, type ClassEnhancement } from "../create-class-decorator";
4
8
 
@@ -27,8 +31,7 @@ class LazyBehavior extends ClassBehavior {
27
31
 
28
32
  return {
29
33
  onMount() {
30
- const el =
31
- (instance as { _anchor?: Comment })._anchor?.parentElement ?? null;
34
+ const el = getComponentAnchor(instance)?.parentElement ?? null;
32
35
  if (!el) return;
33
36
 
34
37
  if (!("IntersectionObserver" in window)) {
@@ -1,10 +1,14 @@
1
- import type { StatefulComponent } from "@praxisjs/core";
1
+ import {
2
+ getComponentDefault,
3
+ getComponentRawProp,
4
+ type RootComponent,
5
+ } from "@praxisjs/core/internal";
2
6
 
3
7
  export function readProp(
4
- instance: StatefulComponent,
8
+ instance: RootComponent<Record<string, unknown>>,
5
9
  propName: string,
6
10
  ): unknown {
7
- const fromParent = instance._rawProps[propName];
11
+ const fromParent = getComponentRawProp(instance, propName);
8
12
  if (fromParent !== undefined) return fromParent;
9
- return instance._defaults[propName];
13
+ return getComponentDefault(instance, propName);
10
14
  }
package/src/index.ts CHANGED
@@ -15,6 +15,7 @@ export {
15
15
  } from "./functions";
16
16
  export {
17
17
  Prop,
18
+ FunctionProp,
18
19
  State,
19
20
  Computed,
20
21
  Persisted,
@@ -39,7 +40,7 @@ export {
39
40
  type FieldBehavior,
40
41
  type FieldBinding,
41
42
  } from "./create-field-decorator";
42
- export { type ReactiveHost } from "./reactive-host";
43
+ export { reactiveHostType, type ReactiveHost } from "./reactive-host";
43
44
  export {
44
45
  createClassDecorator,
45
46
  ClassBehavior,
@@ -1,4 +1,4 @@
1
- import { signal } from "@praxisjs/core/internal";
1
+ import { markStateDirty, signal } from "@praxisjs/core/internal";
2
2
 
3
3
  import { createFieldDecorator } from "../create-field-decorator";
4
4
 
@@ -37,7 +37,7 @@ export function DeepState() {
37
37
  : current;
38
38
 
39
39
  function notify() {
40
- instance._stateDirty = true;
40
+ markStateDirty(instance);
41
41
  version.update((v) => v + 1);
42
42
  }
43
43
 
@@ -0,0 +1,29 @@
1
+ import {
2
+ getComponentDefault,
3
+ getComponentRawProp,
4
+ setComponentDefault,
5
+ } from "@praxisjs/core/internal";
6
+
7
+ import { createFieldDecorator } from "../create-field-decorator";
8
+
9
+ export function FunctionProp() {
10
+ return createFieldDecorator({
11
+ bind(instance, name, initialValue) {
12
+ setComponentDefault(instance, name, initialValue);
13
+ return {
14
+ descriptor: {
15
+ get() {
16
+ const fromParent = getComponentRawProp(instance, name);
17
+ if (fromParent !== undefined) {
18
+ return fromParent;
19
+ }
20
+ return getComponentDefault(instance, name);
21
+ },
22
+ set(value: unknown) {
23
+ setComponentDefault(instance, name, value);
24
+ },
25
+ },
26
+ };
27
+ },
28
+ });
29
+ }
@@ -1,4 +1,5 @@
1
1
  export { Prop } from "./prop";
2
+ export { FunctionProp } from "./function-prop";
2
3
  export { State } from "./state";
3
4
  export { Computed } from "./computed";
4
5
  export { Persisted } from "./persisted";
@@ -1,22 +1,28 @@
1
+ import {
2
+ getComponentDefault,
3
+ getComponentRawProp,
4
+ setComponentDefault,
5
+ } from "@praxisjs/core/internal";
6
+
1
7
  import { createFieldDecorator } from "../create-field-decorator";
2
8
 
3
9
  export function Prop() {
4
10
  return createFieldDecorator({
5
11
  bind(instance, name, initialValue) {
6
- instance._defaults[name] = initialValue;
12
+ setComponentDefault(instance, name, initialValue);
7
13
  return {
8
14
  descriptor: {
9
15
  get() {
10
- const fromParent = (instance._rawProps)[name];
16
+ const fromParent = getComponentRawProp(instance, name);
11
17
  if (fromParent !== undefined) {
12
18
  return typeof fromParent === "function"
13
19
  ? (fromParent as () => unknown)()
14
20
  : fromParent;
15
21
  }
16
- return instance._defaults[name];
22
+ return getComponentDefault(instance, name);
17
23
  },
18
24
  set(value: unknown) {
19
- instance._defaults[name] = value;
25
+ setComponentDefault(instance, name, value);
20
26
  },
21
27
  },
22
28
  };
@@ -1,4 +1,4 @@
1
- import { signal } from "@praxisjs/core/internal";
1
+ import { markStateDirty, signal } from "@praxisjs/core/internal";
2
2
 
3
3
  import { createFieldDecorator } from "../create-field-decorator";
4
4
 
@@ -12,7 +12,7 @@ export function State() {
12
12
  descriptor: {
13
13
  get: () => sig(),
14
14
  set: (value: unknown) => {
15
- instance._stateDirty = true;
15
+ markStateDirty(instance);
16
16
  sig.set(value);
17
17
  },
18
18
  },
@@ -1,3 +1,7 @@
1
- export interface ReactiveHost {
2
- _stateDirty: boolean;
3
- }
1
+ import type { componentPropsType } from "@praxisjs/core/internal";
2
+
3
+ export const reactiveHostType: unique symbol = Symbol("praxis.reactiveHostType");
4
+
5
+ export type ReactiveHost =
6
+ | { readonly [componentPropsType]: object }
7
+ | { readonly [reactiveHostType]: true };