@praxisjs/decorators 1.1.1 → 1.3.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 +55 -0
- package/dist/__tests__/properties.test.js +303 -15
- package/dist/__tests__/properties.test.js.map +1 -1
- package/dist/create-accessor-decorator.d.ts +14 -0
- package/dist/create-accessor-decorator.d.ts.map +1 -0
- package/dist/create-accessor-decorator.js +27 -0
- package/dist/create-accessor-decorator.js.map +1 -0
- package/dist/create-getter-decorator.d.ts +14 -0
- package/dist/create-getter-decorator.d.ts.map +1 -1
- package/dist/create-getter-decorator.js +17 -0
- package/dist/create-getter-decorator.js.map +1 -1
- package/dist/functions/when.d.ts +1 -1
- package/dist/functions/when.d.ts.map +1 -1
- package/dist/functions/when.js +28 -8
- package/dist/functions/when.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/properties/computed.d.ts +11 -1
- package/dist/properties/computed.d.ts.map +1 -1
- package/dist/properties/computed.js +23 -11
- package/dist/properties/computed.js.map +1 -1
- package/package.json +3 -3
- package/src/__tests__/properties.test.ts +373 -15
- package/src/create-accessor-decorator.ts +45 -0
- package/src/create-getter-decorator.ts +39 -0
- package/src/functions/when.ts +32 -8
- package/src/index.ts +8 -0
- package/src/properties/computed.ts +48 -11
|
@@ -4,6 +4,8 @@ import { StatefulComponent } from "@praxisjs/core";
|
|
|
4
4
|
import { signal, computed } from "@praxisjs/core/internal";
|
|
5
5
|
|
|
6
6
|
import { createFieldDecorator } from "../create-field-decorator";
|
|
7
|
+
import { createGetterDecorator } from "../create-getter-decorator";
|
|
8
|
+
import { createAccessorDecorator } from "../create-accessor-decorator";
|
|
7
9
|
import { Watch } from "../functions/watch";
|
|
8
10
|
import { When } from "../functions/when";
|
|
9
11
|
import { Compose } from "../properties/compose";
|
|
@@ -152,39 +154,243 @@ describe("Prop decorator", () => {
|
|
|
152
154
|
|
|
153
155
|
describe("Computed decorator", () => {
|
|
154
156
|
it("wraps a getter in a computed — result is memoized", () => {
|
|
155
|
-
const
|
|
156
|
-
|
|
157
|
-
});
|
|
157
|
+
const s = signal(5);
|
|
158
|
+
const fn = vi.fn(function (this: object) { return s() * 2; });
|
|
158
159
|
|
|
159
|
-
const
|
|
160
|
-
|
|
160
|
+
const { ctx, run } = getterCtx("doubled");
|
|
161
|
+
Computed()(fn, ctx);
|
|
162
|
+
const instance = {} as unknown as StatefulComponent;
|
|
163
|
+
run(instance);
|
|
161
164
|
|
|
162
|
-
const result1 =
|
|
163
|
-
const result2 =
|
|
165
|
+
const result1 = (instance as unknown as Record<string, unknown>).doubled;
|
|
166
|
+
const result2 = (instance as unknown as Record<string, unknown>).doubled;
|
|
164
167
|
expect(result1).toBe(10);
|
|
165
168
|
expect(result2).toBe(10);
|
|
166
|
-
expect(fn).toHaveBeenCalledTimes(1);
|
|
169
|
+
expect(fn).toHaveBeenCalledTimes(1);
|
|
167
170
|
});
|
|
168
171
|
|
|
169
172
|
it("recomputes when underlying signal changes", () => {
|
|
170
173
|
const s = signal(3);
|
|
171
174
|
const getter = function (this: object) { return s() * 2; };
|
|
172
|
-
const
|
|
175
|
+
const { ctx, run } = getterCtx("val");
|
|
176
|
+
Computed()(getter, ctx);
|
|
173
177
|
const obj = {} as unknown as StatefulComponent;
|
|
174
|
-
|
|
178
|
+
run(obj);
|
|
179
|
+
|
|
180
|
+
expect((obj as unknown as Record<string, unknown>).val).toBe(6);
|
|
175
181
|
s.set(5);
|
|
176
|
-
expect(
|
|
182
|
+
expect((obj as unknown as Record<string, unknown>).val).toBe(10);
|
|
177
183
|
});
|
|
178
184
|
|
|
179
185
|
it("is per-instance — separate computed per object", () => {
|
|
180
186
|
const s = signal(1);
|
|
181
187
|
const getter = function (this: object) { return s(); };
|
|
182
|
-
const
|
|
188
|
+
const { ctx, run } = getterCtx("val");
|
|
189
|
+
Computed()(getter, ctx);
|
|
183
190
|
const a = {} as unknown as StatefulComponent;
|
|
184
191
|
const b = {} as unknown as StatefulComponent;
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
192
|
+
run(a);
|
|
193
|
+
run(b);
|
|
194
|
+
|
|
195
|
+
expect((a as unknown as Record<string, unknown>).val).toBe(1);
|
|
196
|
+
expect((b as unknown as Record<string, unknown>).val).toBe(1);
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
// ── Computed (writable) ───────────────────────────────────────────────────────
|
|
201
|
+
|
|
202
|
+
function getterCtx(name: string) {
|
|
203
|
+
const initializers: Array<(this: unknown) => void> = [];
|
|
204
|
+
return {
|
|
205
|
+
ctx: {
|
|
206
|
+
name,
|
|
207
|
+
kind: "getter" as const,
|
|
208
|
+
addInitializer(fn: (this: unknown) => void) {
|
|
209
|
+
initializers.push(fn);
|
|
210
|
+
},
|
|
211
|
+
} as unknown as ClassGetterDecoratorContext,
|
|
212
|
+
run(instance: unknown) {
|
|
213
|
+
initializers.forEach((fn) => { fn.call(instance); });
|
|
214
|
+
},
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
describe("Computed decorator — writable (set option)", () => {
|
|
219
|
+
it("setter is called when property is assigned", () => {
|
|
220
|
+
const written: number[] = [];
|
|
221
|
+
const { ctx, run } = getterCtx("double");
|
|
222
|
+
|
|
223
|
+
const s = signal(3);
|
|
224
|
+
const getter = function (this: object) { return s() * 2; };
|
|
225
|
+
Computed({ set: (v: unknown) => { written.push(v as number); } })(getter, ctx);
|
|
226
|
+
|
|
227
|
+
const instance = {} as unknown as StatefulComponent;
|
|
228
|
+
run(instance);
|
|
229
|
+
|
|
230
|
+
(instance as unknown as Record<string, unknown>).double = 42;
|
|
231
|
+
expect(written).toEqual([42]);
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it("setter receives the component instance as this", () => {
|
|
235
|
+
let capturedThis: unknown;
|
|
236
|
+
const { ctx, run } = getterCtx("val");
|
|
237
|
+
|
|
238
|
+
const getter = function (this: object) { return 0; };
|
|
239
|
+
Computed({ set(this: unknown, _v: unknown) { capturedThis = this; } })(getter, ctx);
|
|
240
|
+
|
|
241
|
+
const instance = { id: "target" } as unknown as StatefulComponent;
|
|
242
|
+
run(instance);
|
|
243
|
+
|
|
244
|
+
(instance as unknown as Record<string, unknown>).val = 99;
|
|
245
|
+
expect(capturedThis).toBe(instance);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it("getter still returns the computed value after set option is added", () => {
|
|
249
|
+
const s = signal(5);
|
|
250
|
+
const { ctx, run } = getterCtx("doubled");
|
|
251
|
+
|
|
252
|
+
const getter = function (this: object) { return s() * 2; };
|
|
253
|
+
Computed({ set: (_v: unknown) => { /* no-op */ } })(getter, ctx);
|
|
254
|
+
|
|
255
|
+
const instance = {} as unknown as StatefulComponent;
|
|
256
|
+
run(instance);
|
|
257
|
+
|
|
258
|
+
expect((instance as unknown as Record<string, unknown>).doubled).toBe(10);
|
|
259
|
+
s.set(7);
|
|
260
|
+
expect((instance as unknown as Record<string, unknown>).doubled).toBe(14);
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it("setter writes through to underlying signals, updating the getter", () => {
|
|
264
|
+
const s = signal(0);
|
|
265
|
+
const { ctx, run } = getterCtx("celsius");
|
|
266
|
+
|
|
267
|
+
const getter = function (this: object) { return s(); };
|
|
268
|
+
Computed({ set: (v: unknown) => { s.set(v as number); } })(getter, ctx);
|
|
269
|
+
|
|
270
|
+
const instance = {} as unknown as StatefulComponent;
|
|
271
|
+
run(instance);
|
|
272
|
+
|
|
273
|
+
(instance as unknown as Record<string, unknown>).celsius = 100;
|
|
274
|
+
expect((instance as unknown as Record<string, unknown>).celsius).toBe(100);
|
|
275
|
+
});
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
// ── Computed (accessor form) ──────────────────────────────────────────────────
|
|
279
|
+
|
|
280
|
+
describe("Computed decorator — accessor form ({ get, set })", () => {
|
|
281
|
+
function applyAccessorDecorator<T>(
|
|
282
|
+
options: { get: (this: object) => T; set: (this: object, value: T) => void },
|
|
283
|
+
instance: object,
|
|
284
|
+
): { get: () => T; set: (v: T) => void } {
|
|
285
|
+
const target = {
|
|
286
|
+
get(this: object) { return undefined as T; },
|
|
287
|
+
set(_v: T) { /* original */ },
|
|
288
|
+
};
|
|
289
|
+
const ctx = { kind: "accessor", name: "value" } as unknown as ClassAccessorDecoratorContext<object, T>;
|
|
290
|
+
const result = Computed(options)(target, ctx);
|
|
291
|
+
result.init!.call(instance, undefined as unknown as T);
|
|
292
|
+
return {
|
|
293
|
+
get: () => result.get!.call(instance),
|
|
294
|
+
set: (v: T) => result.set!.call(instance, v),
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
it("get function is wrapped in a computed — result is memoized", () => {
|
|
299
|
+
const s = signal(3);
|
|
300
|
+
const getFn = vi.fn(function (this: object) { return s() * 2; });
|
|
301
|
+
const instance = {};
|
|
302
|
+
const { get } = applyAccessorDecorator({ get: getFn, set: () => { /* no-op */ } }, instance);
|
|
303
|
+
|
|
304
|
+
expect(get()).toBe(6);
|
|
305
|
+
expect(get()).toBe(6);
|
|
306
|
+
expect(getFn).toHaveBeenCalledTimes(1);
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
it("recomputes when underlying signal changes", () => {
|
|
310
|
+
const s = signal(10);
|
|
311
|
+
const instance = {};
|
|
312
|
+
const { get } = applyAccessorDecorator({
|
|
313
|
+
get(this: object) { return s() + 1; },
|
|
314
|
+
set() { /* no-op */ },
|
|
315
|
+
}, instance);
|
|
316
|
+
|
|
317
|
+
expect(get()).toBe(11);
|
|
318
|
+
s.set(20);
|
|
319
|
+
expect(get()).toBe(21);
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
it("set function is called when the setter is invoked", () => {
|
|
323
|
+
const written: number[] = [];
|
|
324
|
+
const instance = {};
|
|
325
|
+
const { set } = applyAccessorDecorator({
|
|
326
|
+
get(this: object) { return 0; },
|
|
327
|
+
set(_v: number) { written.push(_v); },
|
|
328
|
+
}, instance);
|
|
329
|
+
|
|
330
|
+
set(42);
|
|
331
|
+
expect(written).toEqual([42]);
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
it("set function receives the component instance as this", () => {
|
|
335
|
+
let capturedThis: unknown;
|
|
336
|
+
const instance = { id: "target" };
|
|
337
|
+
const { set } = applyAccessorDecorator({
|
|
338
|
+
get(this: object) { return 0; },
|
|
339
|
+
set(this: unknown) { capturedThis = this; },
|
|
340
|
+
}, instance);
|
|
341
|
+
|
|
342
|
+
set(1);
|
|
343
|
+
expect(capturedThis).toBe(instance);
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
it("set writes through to underlying signals, updating the getter", () => {
|
|
347
|
+
const s = signal(0);
|
|
348
|
+
const instance = {};
|
|
349
|
+
const { get, set } = applyAccessorDecorator({
|
|
350
|
+
get(this: object) { return s(); },
|
|
351
|
+
set(this: object, v: number) { s.set(v); },
|
|
352
|
+
}, instance);
|
|
353
|
+
|
|
354
|
+
set(100);
|
|
355
|
+
expect(get()).toBe(100);
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
it("is per-instance — separate computed per object", () => {
|
|
359
|
+
const s = signal(5);
|
|
360
|
+
const options = {
|
|
361
|
+
get(this: object) { return s() * 2; },
|
|
362
|
+
set() { /* no-op */ },
|
|
363
|
+
};
|
|
364
|
+
const a = {};
|
|
365
|
+
const b = {};
|
|
366
|
+
const decA = applyAccessorDecorator(options, a);
|
|
367
|
+
const decB = applyAccessorDecorator(options, b);
|
|
368
|
+
|
|
369
|
+
expect(decA.get()).toBe(10);
|
|
370
|
+
expect(decB.get()).toBe(10);
|
|
371
|
+
s.set(7);
|
|
372
|
+
expect(decA.get()).toBe(14);
|
|
373
|
+
expect(decB.get()).toBe(14);
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
it("accessor form without set — setter is a no-op", () => {
|
|
377
|
+
const s = signal(5);
|
|
378
|
+
const instance = {};
|
|
379
|
+
const target = { get() { return undefined as unknown as number; }, set() { /* original */ } };
|
|
380
|
+
const ctx = { kind: "accessor", name: "value" } as unknown as ClassAccessorDecoratorContext<object, number>;
|
|
381
|
+
// Bypass overload constraint — optionsSet is undefined at runtime
|
|
382
|
+
type AccessorDecoFactory = (opts: { get: (this: object) => number }) => (
|
|
383
|
+
t: typeof target,
|
|
384
|
+
c: typeof ctx,
|
|
385
|
+
) => ClassAccessorDecoratorResult<object, number>;
|
|
386
|
+
const result = (Computed as unknown as AccessorDecoFactory)(
|
|
387
|
+
{ get(this: object) { return s() * 2; } },
|
|
388
|
+
)(target, ctx);
|
|
389
|
+
result.init!.call(instance, 0);
|
|
390
|
+
|
|
391
|
+
expect(result.get!.call(instance)).toBe(10);
|
|
392
|
+
expect(() => { result.set!.call(instance, 99); }).not.toThrow();
|
|
393
|
+
expect(result.get!.call(instance)).toBe(10); // unchanged — no setter
|
|
188
394
|
});
|
|
189
395
|
});
|
|
190
396
|
|
|
@@ -549,6 +755,78 @@ describe("When decorator", () => {
|
|
|
549
755
|
await Promise.resolve();
|
|
550
756
|
expect(handler).toHaveBeenCalledTimes(1);
|
|
551
757
|
});
|
|
758
|
+
|
|
759
|
+
it("fires immediately when the watched prop is a plain truthy value (non-signal)", () => {
|
|
760
|
+
const { ctx, run } = methodCtxFor("onPlain");
|
|
761
|
+
const handler = vi.fn();
|
|
762
|
+
When("plainProp")(handler, ctx as unknown as ClassMethodDecoratorContext);
|
|
763
|
+
|
|
764
|
+
const instance = new TestComponent();
|
|
765
|
+
(instance as unknown as Record<string, unknown>).plainProp = "hello";
|
|
766
|
+
run(instance);
|
|
767
|
+
(instance as unknown as { onMount: () => void }).onMount?.();
|
|
768
|
+
|
|
769
|
+
expect(handler).toHaveBeenCalledTimes(1);
|
|
770
|
+
expect(handler).toHaveBeenCalledWith("hello");
|
|
771
|
+
});
|
|
772
|
+
|
|
773
|
+
it("condition: fires when condition returns true", async () => {
|
|
774
|
+
const { ctx, run } = methodCtxFor("onHigh");
|
|
775
|
+
const handler = vi.fn();
|
|
776
|
+
When("count", (v: number) => v > 5)(handler, ctx as unknown as ClassMethodDecoratorContext);
|
|
777
|
+
|
|
778
|
+
const s = signal(0);
|
|
779
|
+
const instance = new TestComponent();
|
|
780
|
+
(instance as unknown as Record<string, unknown>).count = s;
|
|
781
|
+
run(instance);
|
|
782
|
+
(instance as unknown as { onMount: () => void }).onMount?.();
|
|
783
|
+
|
|
784
|
+
s.set(3); // condition false — should not fire
|
|
785
|
+
await Promise.resolve();
|
|
786
|
+
expect(handler).not.toHaveBeenCalled();
|
|
787
|
+
|
|
788
|
+
s.set(7); // condition true — should fire
|
|
789
|
+
await Promise.resolve();
|
|
790
|
+
expect(handler).toHaveBeenCalledTimes(1);
|
|
791
|
+
expect(handler).toHaveBeenCalledWith(7);
|
|
792
|
+
});
|
|
793
|
+
|
|
794
|
+
it("condition: does not fire when value is truthy but condition returns false", async () => {
|
|
795
|
+
const { ctx, run } = methodCtxFor("onPositive");
|
|
796
|
+
const handler = vi.fn();
|
|
797
|
+
When("n", (v: number) => v > 100)(handler, ctx as unknown as ClassMethodDecoratorContext);
|
|
798
|
+
|
|
799
|
+
const s = signal(0);
|
|
800
|
+
const instance = new TestComponent();
|
|
801
|
+
(instance as unknown as Record<string, unknown>).n = s;
|
|
802
|
+
run(instance);
|
|
803
|
+
(instance as unknown as { onMount: () => void }).onMount?.();
|
|
804
|
+
|
|
805
|
+
s.set(1); // truthy but condition fails
|
|
806
|
+
s.set(50); // truthy but condition fails
|
|
807
|
+
await Promise.resolve();
|
|
808
|
+
expect(handler).not.toHaveBeenCalled();
|
|
809
|
+
});
|
|
810
|
+
|
|
811
|
+
it("condition: fires on the first value satisfying the condition, then stops", async () => {
|
|
812
|
+
const { ctx, run } = methodCtxFor("onFirst");
|
|
813
|
+
const handler = vi.fn();
|
|
814
|
+
When("v", (v: number) => v >= 10)(handler, ctx as unknown as ClassMethodDecoratorContext);
|
|
815
|
+
|
|
816
|
+
const s = signal(0);
|
|
817
|
+
const instance = new TestComponent();
|
|
818
|
+
(instance as unknown as Record<string, unknown>).v = s;
|
|
819
|
+
run(instance);
|
|
820
|
+
(instance as unknown as { onMount: () => void }).onMount?.();
|
|
821
|
+
|
|
822
|
+
s.set(10); // first match — fires
|
|
823
|
+
await Promise.resolve();
|
|
824
|
+
expect(handler).toHaveBeenCalledTimes(1);
|
|
825
|
+
|
|
826
|
+
s.set(20); // when() already disposed — should not fire again
|
|
827
|
+
await Promise.resolve();
|
|
828
|
+
expect(handler).toHaveBeenCalledTimes(1);
|
|
829
|
+
});
|
|
552
830
|
});
|
|
553
831
|
|
|
554
832
|
// ── Prop (additional branches) ────────────────────────────────────────────────
|
|
@@ -1085,3 +1363,83 @@ describe("createFieldDecorator — additional property", () => {
|
|
|
1085
1363
|
expect((instance as unknown as Record<string, unknown>).extraProp).toBe("extra");
|
|
1086
1364
|
});
|
|
1087
1365
|
});
|
|
1366
|
+
|
|
1367
|
+
// ── createGetterDecorator ─────────────────────────────────────────────────────
|
|
1368
|
+
|
|
1369
|
+
describe("createGetterDecorator", () => {
|
|
1370
|
+
it("calls behavior.wrap on access and returns the wrapped value", () => {
|
|
1371
|
+
const wrap = vi.fn((original: (this: object) => unknown, instance: object) => {
|
|
1372
|
+
return () => (original.call(instance) as number) * 2;
|
|
1373
|
+
});
|
|
1374
|
+
const decorator = createGetterDecorator({ wrap });
|
|
1375
|
+
const originalGetter = function (this: object) { return 5 as unknown; };
|
|
1376
|
+
const ctx = { kind: "getter" } as ClassGetterDecoratorContext<object, number>;
|
|
1377
|
+
const wrapped = decorator(originalGetter as (this: object) => number, ctx);
|
|
1378
|
+
|
|
1379
|
+
const instance = {};
|
|
1380
|
+
expect(wrapped.call(instance)).toBe(10);
|
|
1381
|
+
expect(wrap).toHaveBeenCalledWith(originalGetter, instance);
|
|
1382
|
+
});
|
|
1383
|
+
|
|
1384
|
+
it("passes the correct instance as this to behavior.wrap", () => {
|
|
1385
|
+
let capturedInstance: unknown;
|
|
1386
|
+
const decorator = createGetterDecorator({
|
|
1387
|
+
wrap(_original, instance) {
|
|
1388
|
+
capturedInstance = instance;
|
|
1389
|
+
return () => 0;
|
|
1390
|
+
},
|
|
1391
|
+
});
|
|
1392
|
+
const getter = function (this: object) { return 0 as unknown; };
|
|
1393
|
+
const ctx = { kind: "getter" } as ClassGetterDecoratorContext<object, number>;
|
|
1394
|
+
const wrapped = decorator(getter as (this: object) => number, ctx);
|
|
1395
|
+
|
|
1396
|
+
const instance = { id: "test-instance" };
|
|
1397
|
+
wrapped.call(instance);
|
|
1398
|
+
expect(capturedInstance).toBe(instance);
|
|
1399
|
+
});
|
|
1400
|
+
});
|
|
1401
|
+
|
|
1402
|
+
// ── createAccessorDecorator — init ────────────────────────────────────────────
|
|
1403
|
+
|
|
1404
|
+
describe("createAccessorDecorator — init", () => {
|
|
1405
|
+
it("init stores the initial value that bind receives on first get", () => {
|
|
1406
|
+
let capturedInitialValue: unknown = "NOT_SET";
|
|
1407
|
+
const behavior = {
|
|
1408
|
+
bind(_inst: object, _name: string | symbol, initialValue: unknown) {
|
|
1409
|
+
capturedInitialValue = initialValue;
|
|
1410
|
+
return { get: () => initialValue };
|
|
1411
|
+
},
|
|
1412
|
+
};
|
|
1413
|
+
const decorator = createAccessorDecorator(behavior);
|
|
1414
|
+
const target = {
|
|
1415
|
+
get() { return 0; },
|
|
1416
|
+
set() { /* original */ },
|
|
1417
|
+
} as ClassAccessorDecoratorTarget<object, number>;
|
|
1418
|
+
const ctx = { kind: "accessor", name: "val" } as unknown as ClassAccessorDecoratorContext<object, number>;
|
|
1419
|
+
const result = decorator(target, ctx);
|
|
1420
|
+
|
|
1421
|
+
const instance = {};
|
|
1422
|
+
const returned = result.init!.call(instance, 42);
|
|
1423
|
+
result.get!.call(instance); // triggers bind
|
|
1424
|
+
expect(returned).toBe(42);
|
|
1425
|
+
expect(capturedInitialValue).toBe(42);
|
|
1426
|
+
});
|
|
1427
|
+
|
|
1428
|
+
it("init returns the initial value unchanged", () => {
|
|
1429
|
+
const behavior = {
|
|
1430
|
+
bind(_inst: object, _name: string | symbol, initialValue: unknown) {
|
|
1431
|
+
return { get: () => initialValue };
|
|
1432
|
+
},
|
|
1433
|
+
};
|
|
1434
|
+
const decorator = createAccessorDecorator(behavior);
|
|
1435
|
+
const target = {
|
|
1436
|
+
get() { return ""; },
|
|
1437
|
+
set() { /* original */ },
|
|
1438
|
+
} as ClassAccessorDecoratorTarget<object, string>;
|
|
1439
|
+
const ctx = { kind: "accessor", name: "s" } as unknown as ClassAccessorDecoratorContext<object, string>;
|
|
1440
|
+
const result = decorator(target, ctx);
|
|
1441
|
+
|
|
1442
|
+
const instance = {};
|
|
1443
|
+
expect(result.init!.call(instance, "hello")).toBe("hello");
|
|
1444
|
+
});
|
|
1445
|
+
});
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
export interface AccessorBinding {
|
|
2
|
+
get: () => unknown;
|
|
3
|
+
set?: (value: unknown) => void;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export interface AccessorBehavior {
|
|
7
|
+
/**
|
|
8
|
+
* Called once per instance on first access. `initialValue` is the value
|
|
9
|
+
* provided by the `accessor` field initializer (e.g. `accessor count = 0`).
|
|
10
|
+
* Returns the get/set pair used by the accessor.
|
|
11
|
+
*/
|
|
12
|
+
bind(instance: object, name: string | symbol, initialValue: unknown): AccessorBinding;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function createAccessorDecorator(behavior: AccessorBehavior) {
|
|
16
|
+
return function <This extends object, V>(
|
|
17
|
+
_target: ClassAccessorDecoratorTarget<This, V>,
|
|
18
|
+
context: ClassAccessorDecoratorContext<This, V>,
|
|
19
|
+
): ClassAccessorDecoratorResult<This, V> {
|
|
20
|
+
const bindings = new WeakMap<object, AccessorBinding>();
|
|
21
|
+
const initValues = new WeakMap<object, unknown>();
|
|
22
|
+
|
|
23
|
+
function getBinding(instance: object): AccessorBinding {
|
|
24
|
+
let b = bindings.get(instance);
|
|
25
|
+
if (!b) {
|
|
26
|
+
b = behavior.bind(instance, context.name, initValues.get(instance));
|
|
27
|
+
bindings.set(instance, b);
|
|
28
|
+
}
|
|
29
|
+
return b;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
init(this: This, value: V): V {
|
|
34
|
+
initValues.set(this as object, value);
|
|
35
|
+
return value;
|
|
36
|
+
},
|
|
37
|
+
get(this: This): V {
|
|
38
|
+
return getBinding(this as object).get() as V;
|
|
39
|
+
},
|
|
40
|
+
set(this: This, value: V): void {
|
|
41
|
+
getBinding(this as object).set?.(value as unknown);
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
};
|
|
45
|
+
}
|
|
@@ -23,6 +23,45 @@ export function createGetterDecorator(behavior: GetterBehavior) {
|
|
|
23
23
|
};
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
export interface WritableGetterBinding {
|
|
27
|
+
get: () => unknown;
|
|
28
|
+
set?: (value: unknown) => void;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface WritableGetterBehavior {
|
|
32
|
+
/**
|
|
33
|
+
* Called once per instance during construction. Returns the get/set pair
|
|
34
|
+
* that will be installed on the instance via `Object.defineProperty`.
|
|
35
|
+
* If `set` is omitted, the property is read-only at runtime (though TypeScript
|
|
36
|
+
* may still see a setter if one is declared in the class body).
|
|
37
|
+
*/
|
|
38
|
+
bind(
|
|
39
|
+
instance: object,
|
|
40
|
+
name: string | symbol,
|
|
41
|
+
original: (this: object) => unknown,
|
|
42
|
+
): WritableGetterBinding;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function createWritableGetterDecorator(behavior: WritableGetterBehavior) {
|
|
46
|
+
return function <This, T>(
|
|
47
|
+
target: (this: This) => T,
|
|
48
|
+
context: ClassGetterDecoratorContext<This, T>,
|
|
49
|
+
): (this: This) => T {
|
|
50
|
+
context.addInitializer(function (this: unknown) {
|
|
51
|
+
const instance = this as object;
|
|
52
|
+
const binding = behavior.bind(instance, context.name, target as (this: object) => unknown);
|
|
53
|
+
const descriptor: PropertyDescriptor = {
|
|
54
|
+
get: binding.get,
|
|
55
|
+
configurable: true,
|
|
56
|
+
enumerable: false,
|
|
57
|
+
};
|
|
58
|
+
if (binding.set !== undefined) descriptor.set = binding.set;
|
|
59
|
+
Object.defineProperty(instance, context.name, descriptor);
|
|
60
|
+
});
|
|
61
|
+
return target;
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
26
65
|
export interface GetterObserverBehavior {
|
|
27
66
|
/**
|
|
28
67
|
* Called once per instance when the class is initialized.
|
package/src/functions/when.ts
CHANGED
|
@@ -1,17 +1,41 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { effect } from "@praxisjs/core/internal";
|
|
2
2
|
import type { Computed, Signal } from "@praxisjs/shared";
|
|
3
|
-
import { isComputed } from "@praxisjs/shared/internal";
|
|
3
|
+
import { isComputed, isSignal } from "@praxisjs/shared/internal";
|
|
4
4
|
|
|
5
5
|
import { createLifecycleMethodDecorator } from "../create-lifecycle-method-decorator";
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
/** Read the current value of a property, unwrapping Signal or Computed if needed. */
|
|
8
|
+
function readProp(instance: Record<string, unknown>, key: string): unknown {
|
|
9
|
+
const raw = instance[key];
|
|
10
|
+
if (isComputed(raw)) return (raw as Computed<unknown>)();
|
|
11
|
+
if (isSignal(raw)) return (raw as Signal<unknown>)();
|
|
12
|
+
return raw;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function When<T = unknown>(
|
|
16
|
+
propName: string,
|
|
17
|
+
condition?: (value: NonNullable<T>) => boolean,
|
|
18
|
+
) {
|
|
8
19
|
return createLifecycleMethodDecorator({
|
|
9
20
|
register(callback, instance) {
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
21
|
+
const inst = instance as unknown as Record<string, unknown>;
|
|
22
|
+
let fired = false;
|
|
23
|
+
|
|
24
|
+
return effect(() => {
|
|
25
|
+
if (fired) return;
|
|
26
|
+
// Reading via readProp subscribes this effect to the underlying signal,
|
|
27
|
+
// whether the property is a raw Signal/Computed or an @State getter.
|
|
28
|
+
const val = readProp(inst, propName) as T;
|
|
29
|
+
|
|
30
|
+
const meetsCondition = condition
|
|
31
|
+
? val != null && condition(val)
|
|
32
|
+
: val != null && Boolean(val);
|
|
33
|
+
|
|
34
|
+
if (meetsCondition) {
|
|
35
|
+
fired = true;
|
|
36
|
+
callback(val as NonNullable<T>);
|
|
37
|
+
}
|
|
38
|
+
});
|
|
15
39
|
},
|
|
16
40
|
});
|
|
17
41
|
}
|
package/src/index.ts
CHANGED
|
@@ -54,6 +54,14 @@ export {
|
|
|
54
54
|
export {
|
|
55
55
|
createGetterDecorator,
|
|
56
56
|
type GetterBehavior,
|
|
57
|
+
createWritableGetterDecorator,
|
|
58
|
+
type WritableGetterBehavior,
|
|
59
|
+
type WritableGetterBinding,
|
|
57
60
|
createGetterObserverDecorator,
|
|
58
61
|
type GetterObserverBehavior,
|
|
59
62
|
} from "./create-getter-decorator";
|
|
63
|
+
export {
|
|
64
|
+
createAccessorDecorator,
|
|
65
|
+
type AccessorBehavior,
|
|
66
|
+
type AccessorBinding,
|
|
67
|
+
} from "./create-accessor-decorator";
|
|
@@ -1,18 +1,55 @@
|
|
|
1
1
|
import { computed } from "@praxisjs/core/internal";
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { createAccessorDecorator } from "../create-accessor-decorator";
|
|
4
|
+
import { createWritableGetterDecorator } from "../create-getter-decorator";
|
|
4
5
|
|
|
5
|
-
export
|
|
6
|
-
|
|
6
|
+
export interface ComputedOptions<T = unknown> {
|
|
7
|
+
get?: (this: object) => T;
|
|
8
|
+
set?: (this: object, value: T) => void;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// Accessor form — @Computed({ get, set }) on an `accessor` field.
|
|
12
|
+
// TypeScript sees the accessor as writable; no cast needed when assigning.
|
|
13
|
+
// The `Owner` generic is inferred from the `this` type of the provided functions.
|
|
14
|
+
export function Computed<T, Owner extends object = object>(options: {
|
|
15
|
+
get: (this: Owner) => T;
|
|
16
|
+
set: (this: Owner, value: T) => void;
|
|
17
|
+
}): <This extends object>(
|
|
18
|
+
target: ClassAccessorDecoratorTarget<This, T>,
|
|
19
|
+
context: ClassAccessorDecoratorContext<This, T>,
|
|
20
|
+
) => ClassAccessorDecoratorResult<This, T>;
|
|
21
|
+
|
|
22
|
+
// Getter form — @Computed() or @Computed({ set }) on a `get` accessor.
|
|
23
|
+
export function Computed(options?: {
|
|
24
|
+
set?: (this: object, value: unknown) => void;
|
|
25
|
+
}): <This, R>(
|
|
26
|
+
target: (this: This) => R,
|
|
27
|
+
context: ClassGetterDecoratorContext<This, R>,
|
|
28
|
+
) => (this: This) => R;
|
|
29
|
+
|
|
30
|
+
export function Computed<T = unknown>(options?: ComputedOptions<T>): unknown {
|
|
31
|
+
const optionsGet = options?.get as ((this: object) => unknown) | undefined;
|
|
32
|
+
const optionsSet = options?.set as ((this: object, value: unknown) => void) | undefined;
|
|
33
|
+
|
|
34
|
+
if (optionsGet) {
|
|
35
|
+
return createAccessorDecorator({
|
|
36
|
+
bind(instance, _name, _initialValue) {
|
|
37
|
+
const c = computed(() => optionsGet.call(instance));
|
|
38
|
+
return {
|
|
39
|
+
get: c,
|
|
40
|
+
set: optionsSet ? (value) => { optionsSet.call(instance, value); } : undefined,
|
|
41
|
+
};
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
}
|
|
7
45
|
|
|
8
|
-
return
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
c
|
|
13
|
-
|
|
14
|
-
}
|
|
15
|
-
return c;
|
|
46
|
+
return createWritableGetterDecorator({
|
|
47
|
+
bind(instance, _name, original) {
|
|
48
|
+
const c = computed(() => original.call(instance));
|
|
49
|
+
return {
|
|
50
|
+
get: c,
|
|
51
|
+
set: optionsSet ? (value) => { optionsSet.call(instance, value); } : undefined,
|
|
52
|
+
};
|
|
16
53
|
},
|
|
17
54
|
});
|
|
18
55
|
}
|