@praxisjs/decorators 1.7.0 → 2.0.1

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 (266) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/dist/component/component.d.ts +1 -1
  3. package/dist/component/component.d.ts.map +1 -1
  4. package/dist/component/component.js +8 -15
  5. package/dist/component/component.js.map +1 -1
  6. package/dist/component/lazy.d.ts +1 -1
  7. package/dist/component/lazy.d.ts.map +1 -1
  8. package/dist/component/lazy.js +44 -36
  9. package/dist/component/lazy.js.map +1 -1
  10. package/dist/create-class-decorator.d.ts +27 -5
  11. package/dist/create-class-decorator.d.ts.map +1 -1
  12. package/dist/create-class-decorator.js +53 -18
  13. package/dist/create-class-decorator.js.map +1 -1
  14. package/dist/create-field-decorator.d.ts +42 -3
  15. package/dist/create-field-decorator.d.ts.map +1 -1
  16. package/dist/create-field-decorator.js +63 -27
  17. package/dist/create-field-decorator.js.map +1 -1
  18. package/dist/create-getter-decorator.d.ts +81 -16
  19. package/dist/create-getter-decorator.d.ts.map +1 -1
  20. package/dist/create-getter-decorator.js +127 -15
  21. package/dist/create-getter-decorator.js.map +1 -1
  22. package/dist/create-lifecycle-method-decorator.d.ts +21 -3
  23. package/dist/create-lifecycle-method-decorator.d.ts.map +1 -1
  24. package/dist/create-lifecycle-method-decorator.js +25 -18
  25. package/dist/create-lifecycle-method-decorator.js.map +1 -1
  26. package/dist/create-method-decorator.d.ts +43 -3
  27. package/dist/create-method-decorator.d.ts.map +1 -1
  28. package/dist/create-method-decorator.js +49 -8
  29. package/dist/create-method-decorator.js.map +1 -1
  30. package/dist/events/command.d.ts +2 -1
  31. package/dist/events/command.d.ts.map +1 -1
  32. package/dist/events/command.js +10 -8
  33. package/dist/events/command.js.map +1 -1
  34. package/dist/events/emit.d.ts +2 -2
  35. package/dist/events/emit.d.ts.map +1 -1
  36. package/dist/events/emit.js +18 -24
  37. package/dist/events/emit.js.map +1 -1
  38. package/dist/events/on-command.d.ts +2 -1
  39. package/dist/events/on-command.d.ts.map +1 -1
  40. package/dist/events/on-command.js +12 -14
  41. package/dist/events/on-command.js.map +1 -1
  42. package/dist/field.d.ts +51 -0
  43. package/dist/field.d.ts.map +1 -0
  44. package/dist/field.js +82 -0
  45. package/dist/field.js.map +1 -0
  46. package/dist/functions/bind.d.ts +2 -1
  47. package/dist/functions/bind.d.ts.map +1 -1
  48. package/dist/functions/bind.js +4 -4
  49. package/dist/functions/bind.js.map +1 -1
  50. package/dist/functions/debounce.d.ts +2 -2
  51. package/dist/functions/debounce.d.ts.map +1 -1
  52. package/dist/functions/debounce.js +25 -23
  53. package/dist/functions/debounce.js.map +1 -1
  54. package/dist/functions/log.d.ts +2 -1
  55. package/dist/functions/log.d.ts.map +1 -1
  56. package/dist/functions/log.js +38 -40
  57. package/dist/functions/log.js.map +1 -1
  58. package/dist/functions/memo.d.ts +2 -1
  59. package/dist/functions/memo.d.ts.map +1 -1
  60. package/dist/functions/memo.js +41 -40
  61. package/dist/functions/memo.js.map +1 -1
  62. package/dist/functions/once.d.ts +2 -1
  63. package/dist/functions/once.d.ts.map +1 -1
  64. package/dist/functions/once.js +10 -14
  65. package/dist/functions/once.js.map +1 -1
  66. package/dist/functions/retry.d.ts +2 -1
  67. package/dist/functions/retry.d.ts.map +1 -1
  68. package/dist/functions/retry.js +21 -23
  69. package/dist/functions/retry.js.map +1 -1
  70. package/dist/functions/throttle.d.ts +2 -1
  71. package/dist/functions/throttle.d.ts.map +1 -1
  72. package/dist/functions/throttle.js +12 -14
  73. package/dist/functions/throttle.js.map +1 -1
  74. package/dist/functions/until.d.ts +2 -1
  75. package/dist/functions/until.d.ts.map +1 -1
  76. package/dist/functions/until.js +13 -14
  77. package/dist/functions/until.js.map +1 -1
  78. package/dist/functions/watch.d.ts +1 -1
  79. package/dist/functions/watch.d.ts.map +1 -1
  80. package/dist/functions/watch.js +36 -34
  81. package/dist/functions/watch.js.map +1 -1
  82. package/dist/functions/when.d.ts +1 -1
  83. package/dist/functions/when.d.ts.map +1 -1
  84. package/dist/functions/when.js +19 -21
  85. package/dist/functions/when.js.map +1 -1
  86. package/dist/index.d.ts +5 -5
  87. package/dist/index.d.ts.map +1 -1
  88. package/dist/index.js +2 -2
  89. package/dist/index.js.map +1 -1
  90. package/dist/lifecycle.d.ts +24 -0
  91. package/dist/lifecycle.d.ts.map +1 -0
  92. package/dist/lifecycle.js +55 -0
  93. package/dist/lifecycle.js.map +1 -0
  94. package/dist/properties/compose.d.ts +3 -2
  95. package/dist/properties/compose.d.ts.map +1 -1
  96. package/dist/properties/compose.js +33 -19
  97. package/dist/properties/compose.js.map +1 -1
  98. package/dist/properties/computed.d.ts +12 -10
  99. package/dist/properties/computed.d.ts.map +1 -1
  100. package/dist/properties/computed.js +26 -21
  101. package/dist/properties/computed.js.map +1 -1
  102. package/dist/properties/deep-state.d.ts +2 -1
  103. package/dist/properties/deep-state.d.ts.map +1 -1
  104. package/dist/properties/deep-state.js +66 -60
  105. package/dist/properties/deep-state.js.map +1 -1
  106. package/dist/properties/function-prop.d.ts +2 -1
  107. package/dist/properties/function-prop.d.ts.map +1 -1
  108. package/dist/properties/function-prop.js +20 -22
  109. package/dist/properties/function-prop.js.map +1 -1
  110. package/dist/properties/history.d.ts +2 -2
  111. package/dist/properties/history.d.ts.map +1 -1
  112. package/dist/properties/history.js +34 -33
  113. package/dist/properties/history.js.map +1 -1
  114. package/dist/properties/index.d.ts +1 -0
  115. package/dist/properties/index.d.ts.map +1 -1
  116. package/dist/properties/index.js +1 -0
  117. package/dist/properties/index.js.map +1 -1
  118. package/dist/properties/persisted.d.ts +2 -1
  119. package/dist/properties/persisted.d.ts.map +1 -1
  120. package/dist/properties/persisted.js +24 -18
  121. package/dist/properties/persisted.js.map +1 -1
  122. package/dist/properties/prop.d.ts +2 -1
  123. package/dist/properties/prop.d.ts.map +1 -1
  124. package/dist/properties/prop.js +26 -22
  125. package/dist/properties/prop.js.map +1 -1
  126. package/dist/properties/ref.d.ts +2 -1
  127. package/dist/properties/ref.d.ts.map +1 -1
  128. package/dist/properties/ref.js +11 -8
  129. package/dist/properties/ref.js.map +1 -1
  130. package/dist/properties/resource.d.ts.map +1 -1
  131. package/dist/properties/resource.js +21 -16
  132. package/dist/properties/resource.js.map +1 -1
  133. package/dist/properties/selector.d.ts +12 -0
  134. package/dist/properties/selector.d.ts.map +1 -0
  135. package/dist/properties/selector.js +25 -0
  136. package/dist/properties/selector.js.map +1 -0
  137. package/dist/properties/slot.d.ts +2 -1
  138. package/dist/properties/slot.d.ts.map +1 -1
  139. package/dist/properties/slot.js +22 -20
  140. package/dist/properties/slot.js.map +1 -1
  141. package/dist/properties/state.d.ts.map +1 -1
  142. package/dist/properties/state.js +27 -16
  143. package/dist/properties/state.js.map +1 -1
  144. package/dist/properties/synced.d.ts +2 -1
  145. package/dist/properties/synced.d.ts.map +1 -1
  146. package/dist/properties/synced.js +27 -19
  147. package/dist/properties/synced.js.map +1 -1
  148. package/dist/shared-method.d.ts +14 -0
  149. package/dist/shared-method.d.ts.map +1 -0
  150. package/dist/shared-method.js +87 -0
  151. package/dist/shared-method.js.map +1 -0
  152. package/package.json +14 -5
  153. package/src/component/component.ts +8 -27
  154. package/src/component/lazy.ts +51 -41
  155. package/src/create-class-decorator.ts +90 -31
  156. package/src/create-field-decorator.ts +120 -43
  157. package/src/create-getter-decorator.ts +263 -66
  158. package/src/create-lifecycle-method-decorator.ts +50 -28
  159. package/src/create-method-decorator.ts +119 -13
  160. package/src/events/command.ts +11 -9
  161. package/src/events/emit.ts +16 -22
  162. package/src/events/on-command.ts +17 -18
  163. package/src/field.ts +116 -0
  164. package/src/functions/bind.ts +4 -4
  165. package/src/functions/debounce.ts +25 -23
  166. package/src/functions/log.ts +44 -46
  167. package/src/functions/memo.ts +39 -37
  168. package/src/functions/once.ts +10 -13
  169. package/src/functions/retry.ts +19 -21
  170. package/src/functions/throttle.ts +12 -13
  171. package/src/functions/until.ts +14 -14
  172. package/src/functions/watch.ts +37 -36
  173. package/src/functions/when.ts +18 -20
  174. package/src/index.ts +12 -5
  175. package/src/lifecycle.ts +74 -0
  176. package/src/properties/compose.ts +33 -20
  177. package/src/properties/computed.ts +35 -45
  178. package/src/properties/deep-state.ts +66 -64
  179. package/src/properties/function-prop.ts +25 -27
  180. package/src/properties/history.ts +34 -35
  181. package/src/properties/index.ts +1 -0
  182. package/src/properties/persisted.ts +30 -20
  183. package/src/properties/prop.ts +30 -22
  184. package/src/properties/ref.ts +12 -10
  185. package/src/properties/resource.ts +22 -17
  186. package/src/properties/selector.ts +45 -0
  187. package/src/properties/slot.ts +28 -22
  188. package/src/properties/state.ts +32 -16
  189. package/src/properties/synced.ts +31 -19
  190. package/src/shared-method.ts +101 -0
  191. package/dist/__tests__/component.test.d.ts +0 -2
  192. package/dist/__tests__/component.test.d.ts.map +0 -1
  193. package/dist/__tests__/component.test.js +0 -147
  194. package/dist/__tests__/component.test.js.map +0 -1
  195. package/dist/__tests__/compose.test.d.ts +0 -2
  196. package/dist/__tests__/compose.test.d.ts.map +0 -1
  197. package/dist/__tests__/compose.test.js +0 -164
  198. package/dist/__tests__/compose.test.js.map +0 -1
  199. package/dist/__tests__/deep-state.test.d.ts +0 -2
  200. package/dist/__tests__/deep-state.test.d.ts.map +0 -1
  201. package/dist/__tests__/deep-state.test.js +0 -221
  202. package/dist/__tests__/deep-state.test.js.map +0 -1
  203. package/dist/__tests__/events.test.d.ts +0 -2
  204. package/dist/__tests__/events.test.d.ts.map +0 -1
  205. package/dist/__tests__/events.test.js +0 -295
  206. package/dist/__tests__/events.test.js.map +0 -1
  207. package/dist/__tests__/functions.test.d.ts +0 -2
  208. package/dist/__tests__/functions.test.d.ts.map +0 -1
  209. package/dist/__tests__/functions.test.js +0 -618
  210. package/dist/__tests__/functions.test.js.map +0 -1
  211. package/dist/__tests__/lazy.test.d.ts +0 -2
  212. package/dist/__tests__/lazy.test.d.ts.map +0 -1
  213. package/dist/__tests__/lazy.test.js +0 -227
  214. package/dist/__tests__/lazy.test.js.map +0 -1
  215. package/dist/__tests__/memo.test.d.ts +0 -2
  216. package/dist/__tests__/memo.test.d.ts.map +0 -1
  217. package/dist/__tests__/memo.test.js +0 -249
  218. package/dist/__tests__/memo.test.js.map +0 -1
  219. package/dist/__tests__/properties-browser.test.d.ts +0 -2
  220. package/dist/__tests__/properties-browser.test.d.ts.map +0 -1
  221. package/dist/__tests__/properties-browser.test.js +0 -227
  222. package/dist/__tests__/properties-browser.test.js.map +0 -1
  223. package/dist/__tests__/properties.test.d.ts +0 -2
  224. package/dist/__tests__/properties.test.d.ts.map +0 -1
  225. package/dist/__tests__/properties.test.js +0 -1253
  226. package/dist/__tests__/properties.test.js.map +0 -1
  227. package/dist/__tests__/ref.test.d.ts +0 -2
  228. package/dist/__tests__/ref.test.d.ts.map +0 -1
  229. package/dist/__tests__/ref.test.js +0 -106
  230. package/dist/__tests__/ref.test.js.map +0 -1
  231. package/dist/__tests__/resource.test.d.ts +0 -2
  232. package/dist/__tests__/resource.test.d.ts.map +0 -1
  233. package/dist/__tests__/resource.test.js +0 -217
  234. package/dist/__tests__/resource.test.js.map +0 -1
  235. package/dist/__tests__/slot.test.d.ts +0 -2
  236. package/dist/__tests__/slot.test.d.ts.map +0 -1
  237. package/dist/__tests__/slot.test.js +0 -147
  238. package/dist/__tests__/slot.test.js.map +0 -1
  239. package/dist/__tests__/synced.test.d.ts +0 -2
  240. package/dist/__tests__/synced.test.d.ts.map +0 -1
  241. package/dist/__tests__/synced.test.js +0 -161
  242. package/dist/__tests__/synced.test.js.map +0 -1
  243. package/dist/__tests__/until.test.d.ts +0 -2
  244. package/dist/__tests__/until.test.d.ts.map +0 -1
  245. package/dist/__tests__/until.test.js +0 -88
  246. package/dist/__tests__/until.test.js.map +0 -1
  247. package/dist/create-accessor-decorator.d.ts +0 -14
  248. package/dist/create-accessor-decorator.d.ts.map +0 -1
  249. package/dist/create-accessor-decorator.js +0 -27
  250. package/dist/create-accessor-decorator.js.map +0 -1
  251. package/src/__tests__/component.test.ts +0 -167
  252. package/src/__tests__/compose.test.ts +0 -202
  253. package/src/__tests__/deep-state.test.ts +0 -265
  254. package/src/__tests__/events.test.ts +0 -366
  255. package/src/__tests__/functions.test.ts +0 -803
  256. package/src/__tests__/lazy.test.ts +0 -296
  257. package/src/__tests__/memo.test.ts +0 -293
  258. package/src/__tests__/properties-browser.test.ts +0 -288
  259. package/src/__tests__/properties.test.ts +0 -1556
  260. package/src/__tests__/ref.test.ts +0 -131
  261. package/src/__tests__/resource.test.ts +0 -246
  262. package/src/__tests__/slot.test.ts +0 -179
  263. package/src/__tests__/synced.test.ts +0 -193
  264. package/src/__tests__/until.test.ts +0 -113
  265. package/src/create-accessor-decorator.ts +0 -45
  266. package/tsconfig.json +0 -9
@@ -1,45 +1,44 @@
1
- import type { StatefulComponent } from "@praxisjs/core";
2
1
  import { type HistoryElement, history } from "@praxisjs/core/internal";
3
2
  import type { Signal } from "@praxisjs/shared";
4
3
 
5
- import { createFieldDecorator } from "../create-field-decorator";
4
+ import { type FieldDecorator, type FieldHost, sharedField } from "../field";
5
+ import { addLifecycleHooks } from "../lifecycle";
6
6
 
7
7
  export type HistoryOf<C, K extends keyof C> = HistoryElement<C[K]>;
8
8
 
9
- export function History(fieldName: string, limit = 50) {
10
- return createFieldDecorator({
11
- bind(instance: StatefulComponent, _name: string) {
12
- const inst = instance as unknown as Record<string, unknown>;
13
- let h: HistoryElement<unknown> | undefined;
14
-
15
- const source = () => inst[fieldName];
9
+ function createHistory(host: FieldHost, fieldName: string, limit: number): HistoryElement<unknown> {
10
+ const h = history((() => host[fieldName]) as Signal<unknown>, limit);
11
+ const undo = h.undo.bind(h);
12
+ const redo = h.redo.bind(h);
13
+ h.undo = () => {
14
+ const prev = h.values()[h.values().length - 2];
15
+ if (prev === undefined) return;
16
+ undo();
17
+ host[fieldName] = prev;
18
+ };
19
+ h.redo = () => {
20
+ redo();
21
+ host[fieldName] = h.current();
22
+ };
23
+ return h;
24
+ }
16
25
 
17
- return {
18
- descriptor: {
19
- get(): HistoryElement<unknown> {
20
- if (!h) {
21
- const hInst = history(source as Signal<unknown>, limit);
22
- const _undo = hInst.undo.bind(hInst);
23
- const _redo = hInst.redo.bind(hInst);
24
- hInst.undo = () => {
25
- const prev = hInst.values()[hInst.values().length - 2];
26
- if (prev === undefined) return;
27
- _undo();
28
- inst[fieldName] = prev;
29
- };
30
- hInst.redo = () => {
31
- _redo();
32
- inst[fieldName] = hInst.current();
33
- };
34
- h = hInst;
35
- }
36
- return h;
37
- },
38
- enumerable: false,
26
+ export function History(fieldName: string, limit = 50): FieldDecorator {
27
+ return sharedField((name) => {
28
+ const slot = Symbol(`praxis.history.${name}`);
29
+ // The tracking effect would otherwise keep recording a source that outlives the component.
30
+ const destroy = (instance: object) => { ((instance as FieldHost)[slot] as HistoryElement<unknown> | undefined)?.destroy(); };
31
+ return {
32
+ descriptor: {
33
+ enumerable: false,
34
+ // Created on first read, so an instance whose history is never read records nothing.
35
+ get(this: FieldHost) {
36
+ return (this[slot] ??= createHistory(this, fieldName, limit));
39
37
  },
40
- // The tracking effect would otherwise keep recording a source that outlives the component.
41
- onUnmount() { h?.destroy(); },
42
- };
43
- },
38
+ },
39
+ init(instance) {
40
+ addLifecycleHooks(instance, undefined, destroy);
41
+ },
42
+ };
44
43
  });
45
44
  }
@@ -10,3 +10,4 @@ export { Resource, invalidateResource, type ResourceInstance, type ResourceOptio
10
10
  export { Synced } from "./synced";
11
11
  export { DeepState } from "./deep-state";
12
12
  export { Ref, createRef } from "./ref";
13
+ export { Selector, type SelectorOptions } from "./selector";
@@ -4,21 +4,24 @@ import {
4
4
  type PersistedSignalOptions,
5
5
  } from "@praxisjs/core/internal";
6
6
 
7
- import { createFieldDecorator } from "../create-field-decorator";
7
+ import { type FieldDecorator, type FieldHost, sharedField } from "../field";
8
+ import { addLifecycleHooks } from "../lifecycle";
8
9
 
9
- const signalMap = new WeakMap<object, Map<string, PersistedSignal<unknown>>>();
10
+ // Fields of one instance that persist under the same key share one signal. Only read while the
11
+ // instance is built; each field then reads its signal from its own slot.
12
+ const SIGNALS: unique symbol = Symbol("praxis.persistedSignals");
13
+
14
+ interface PersistedHost {
15
+ [SIGNALS]?: Map<string, PersistedSignal<unknown>>;
16
+ }
10
17
 
11
18
  function getOrCreateSignal<T>(
12
- instance: object,
19
+ instance: PersistedHost,
13
20
  storageKey: string,
14
21
  initialValue: T,
15
22
  options: PersistedSignalOptions<T>,
16
23
  ): PersistedSignal<T> {
17
- let map = signalMap.get(instance);
18
- if (!map) {
19
- map = new Map();
20
- signalMap.set(instance, map);
21
- }
24
+ const map = (instance[SIGNALS] ??= new Map<string, PersistedSignal<unknown>>());
22
25
  let sig = map.get(storageKey);
23
26
  if (!sig) {
24
27
  sig = persistedSignal(storageKey, initialValue, options) as PersistedSignal<unknown>;
@@ -30,18 +33,25 @@ function getOrCreateSignal<T>(
30
33
  export function Persisted<T>(
31
34
  key?: string,
32
35
  options: PersistedSignalOptions<T> = {},
33
- ) {
34
- return createFieldDecorator({
35
- bind(instance, name, initialValue) {
36
- const sig = getOrCreateSignal(instance, key ?? name, initialValue as T, options);
37
- return {
38
- descriptor: {
39
- get: () => sig(),
40
- set: (value: T) => { sig.set(value); },
36
+ ): FieldDecorator {
37
+ return sharedField((name) => {
38
+ const slot = Symbol(`praxis.persisted.${name}`);
39
+ const storageKey = key ?? name;
40
+ // Without this every mounted instance would leave a window "storage" listener behind.
41
+ const close = (instance: object) => { ((instance as FieldHost)[slot] as PersistedSignal<T>).close(); };
42
+ return {
43
+ descriptor: {
44
+ get(this: FieldHost) {
45
+ return (this[slot] as PersistedSignal<T>)();
46
+ },
47
+ set(this: FieldHost, value: T) {
48
+ (this[slot] as PersistedSignal<T>).set(value);
41
49
  },
42
- // Without this every mounted instance would leave a window "storage" listener behind.
43
- onUnmount() { sig.close(); },
44
- };
45
- },
50
+ },
51
+ init(instance, initialValue) {
52
+ instance[slot] = getOrCreateSignal(instance, storageKey, initialValue as T, options);
53
+ addLifecycleHooks(instance, undefined, close);
54
+ },
55
+ };
46
56
  });
47
57
  }
@@ -1,31 +1,39 @@
1
+ import type { StatefulComponent } from "@praxisjs/core";
1
2
  import {
2
3
  getComponentDefault,
3
4
  getComponentRawProp,
4
5
  setComponentDefault,
5
6
  } from "@praxisjs/core/internal";
6
7
 
7
- import { createFieldDecorator } from "../create-field-decorator";
8
+ import { addClaimableInitializer, assertFieldContext } from "../field";
8
9
 
9
10
  export function Prop() {
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 typeof fromParent === "function"
19
- ? (fromParent as () => unknown)()
20
- : fromParent;
21
- }
22
- return getComponentDefault(instance, name);
23
- },
24
- set(value: unknown) {
25
- setComponentDefault(instance, name, value);
26
- },
27
- },
28
- };
29
- },
30
- });
11
+ return function <This extends StatefulComponent, Value = unknown>(
12
+ _value: undefined,
13
+ context: ClassFieldDecoratorContext<This, Value>,
14
+ ): void {
15
+ assertFieldContext(context, "@Prop()");
16
+ const name = context.name as string;
17
+ // Shared by every instance, like @State's accessors.
18
+ const descriptor: PropertyDescriptor = {
19
+ enumerable: true,
20
+ configurable: true,
21
+ get(this: object) {
22
+ const fromParent = getComponentRawProp(this, name);
23
+ if (fromParent !== undefined) {
24
+ return typeof fromParent === "function"
25
+ ? (fromParent as () => unknown)()
26
+ : fromParent;
27
+ }
28
+ return getComponentDefault(this, name);
29
+ },
30
+ set(this: object, value: unknown) {
31
+ setComponentDefault(this, name, value);
32
+ },
33
+ };
34
+ addClaimableInitializer(context, function (value) {
35
+ setComponentDefault(this, name, value);
36
+ Object.defineProperty(this, name, descriptor);
37
+ });
38
+ };
31
39
  }
@@ -1,11 +1,10 @@
1
- import { createFieldDecorator } from "../create-field-decorator";
1
+ import { type FieldDecorator, type FieldHost, sharedField } from "../field";
2
2
 
3
3
  export type Ref<T extends Element = Element> = ((el: T | null) => void) & {
4
4
  current: T | null;
5
5
  };
6
6
 
7
7
  /** Creates a standalone ref object for use outside of class fields (e.g. module-level or passed to @Lazy). */
8
-
9
8
  export function createRef<T extends Element = Element>(): Ref<T> {
10
9
  function refFn(el: T | null): void {
11
10
  (refFn as Ref<T>).current = el;
@@ -15,13 +14,16 @@ export function createRef<T extends Element = Element>(): Ref<T> {
15
14
  }
16
15
 
17
16
  // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters
18
- export function Ref<T extends Element = Element>() {
19
- return createFieldDecorator({
20
- bind(_instance, _name, _initialValue) {
21
- const ref = createRef<T>();
22
- return {
23
- descriptor: { get: () => ref },
24
- };
25
- },
17
+ export function Ref<T extends Element = Element>(): FieldDecorator {
18
+ return sharedField((name) => {
19
+ const ref = Symbol(`praxis.ref.${name}`);
20
+ return {
21
+ descriptor: {
22
+ // Created on first read: a ref that is never handed to an element costs nothing.
23
+ get(this: FieldHost) {
24
+ return (this[ref] ??= createRef<T>());
25
+ },
26
+ },
27
+ };
26
28
  });
27
29
  }
@@ -4,7 +4,8 @@ import {
4
4
  type ResourceOptions,
5
5
  } from "@praxisjs/core/internal";
6
6
 
7
- import { createFieldDecorator, type FieldBinding } from "../create-field-decorator";
7
+ import { type FieldHost, sharedField } from "../field";
8
+ import { addLifecycleHooks } from "../lifecycle";
8
9
 
9
10
  export { invalidateResource } from "@praxisjs/core/internal";
10
11
  export type { ResourceInstance, ResourceOptions };
@@ -30,22 +31,26 @@ export function Resource<T>(
30
31
  fetcher: (() => Promise<T>) | ((self: any) => Promise<T>),
31
32
  options: ResourceOptions<T> = {},
32
33
  ): FieldDecoratorReturn {
33
- return createFieldDecorator({
34
- bind(instance, _name, _initialValue): FieldBinding {
35
- const bound =
36
- fetcher.length > 0
37
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
38
- ? () => (fetcher as (self: any) => Promise<T>)(instance)
39
- : (fetcher as () => Promise<T>);
40
- const r = resource(bound, options);
41
- return {
42
- descriptor: {
43
- get(): ResourceInstance<T> { return r; },
44
- // eslint-disable-next-line @typescript-eslint/no-empty-function
45
- set(): void {},
34
+ return sharedField((name) => {
35
+ const slot = Symbol(`praxis.resource.${name}`);
36
+ const destroy = (instance: object) => { ((instance as FieldHost)[slot] as ResourceInstance<T>).destroy(); };
37
+ return {
38
+ descriptor: {
39
+ get(this: FieldHost): ResourceInstance<T> {
40
+ return this[slot] as ResourceInstance<T>;
46
41
  },
47
- onUnmount() { r.destroy(); },
48
- };
49
- },
42
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
43
+ set(): void {},
44
+ },
45
+ init(instance) {
46
+ const bound =
47
+ fetcher.length > 0
48
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
49
+ ? () => (fetcher as (self: any) => Promise<T>)(instance)
50
+ : (fetcher as () => Promise<T>);
51
+ instance[slot] = resource(bound, options);
52
+ addLifecycleHooks(instance, undefined, destroy);
53
+ },
54
+ };
50
55
  }) as unknown as FieldDecoratorReturn;
51
56
  }
@@ -0,0 +1,45 @@
1
+ import { selector, type Selector as SelectorFn, type SelectorEquals } from "@praxisjs/core/internal";
2
+ import type { Computed } from "@praxisjs/shared";
3
+ import { isComputed } from "@praxisjs/shared/internal";
4
+
5
+ import { type FieldHost, sharedField } from "../field";
6
+
7
+ export type Selector<K> = SelectorFn<K>;
8
+
9
+ export interface SelectorOptions<K, T> {
10
+ /**
11
+ * Decides whether `key` is selected for the source value `value`. Defaults to a strict
12
+ * comparison (SameValueZero), which notifies only the previous and next keys; a custom function
13
+ * is evaluated against every key in use whenever the source changes.
14
+ */
15
+ equals?: SelectorEquals<K, T>;
16
+ }
17
+
18
+ function createSelector<K, T>(host: FieldHost, source: string, equals: SelectorEquals<K, T> | undefined): SelectorFn<K> {
19
+ const read = () => {
20
+ const raw = host[source];
21
+ return (isComputed(raw) ? (raw as Computed<unknown>)() : raw) as T;
22
+ };
23
+ return equals ? selector(read, equals) : (selector(read) as unknown as SelectorFn<K>);
24
+ }
25
+
26
+ export function Selector<const Name extends string, K = unknown, T = unknown>(
27
+ source: Name,
28
+ options: SelectorOptions<K, T> = {},
29
+ ) {
30
+ const decorator = sharedField<object>((name) => {
31
+ const slot = Symbol(`praxis.selector.${name}`);
32
+ return {
33
+ descriptor: {
34
+ // Created on first read, so the source field may be declared after this one.
35
+ get(this: FieldHost) {
36
+ return (this[slot] ??= createSelector(this, source, options.equals));
37
+ },
38
+ },
39
+ };
40
+ });
41
+ return decorator as <This extends Record<Name, unknown>, Value extends Selector<never>>(
42
+ value: undefined,
43
+ context: ClassFieldDecoratorContext<This, Value>,
44
+ ) => void;
45
+ }
@@ -1,6 +1,11 @@
1
- import { createFieldDecorator } from "../create-field-decorator";
1
+ import { type FieldDecorator, type FieldHost, sharedField } from "../field";
2
2
 
3
- const slotsMap = new WeakMap<object, Map<string, unknown[]>>();
3
+ // On the instance rather than in a WeakMap: every slot field read goes through here.
4
+ const SLOTS: unique symbol = Symbol("praxis.slots");
5
+
6
+ interface SlotHost {
7
+ [SLOTS]?: Map<string, unknown[]>;
8
+ }
4
9
 
5
10
  function resolveSlots(children: unknown): Map<string, unknown[]> {
6
11
  const slots = new Map<string, unknown[]>();
@@ -18,8 +23,9 @@ function resolveSlots(children: unknown): Map<string, unknown[]> {
18
23
  const slotName = child.getAttribute("slot");
19
24
  if (slotName) {
20
25
  child.removeAttribute("slot");
21
- if (!slots.has(slotName)) slots.set(slotName, []);
22
- (slots.get(slotName) as unknown[]).push(child);
26
+ let named = slots.get(slotName);
27
+ if (!named) slots.set(slotName, (named = []));
28
+ named.push(child);
23
29
  continue;
24
30
  }
25
31
  }
@@ -30,29 +36,29 @@ function resolveSlots(children: unknown): Map<string, unknown[]> {
30
36
  }
31
37
 
32
38
  export function initSlots(instance: object, children: unknown): void {
33
- slotsMap.set(instance, resolveSlots(children));
39
+ (instance as SlotHost)[SLOTS] = resolveSlots(children);
34
40
  }
35
41
 
36
42
  export function getSlot(instance: object, name: string): unknown[] {
37
- return slotsMap.get(instance)?.get(name) ?? [];
43
+ return (instance as SlotHost)[SLOTS]?.get(name) ?? [];
38
44
  }
39
45
 
40
- export function Slot(name?: string) {
41
- return createFieldDecorator({
42
- bind(instance, propName, _initialValue) {
43
- const slotName = name ?? (propName === "default" ? "default" : propName);
44
- return {
45
- descriptor: {
46
- get: () => getSlot(instance, slotName),
47
- set: (_value: unknown) => {
48
- if (process.env.NODE_ENV !== "production") {
49
- console.warn(
50
- `[Slot] "${propName}" is a slot and cannot be assigned directly. Slots are filled by the parent component.`,
51
- );
52
- }
53
- },
46
+ export function Slot(name?: string): FieldDecorator {
47
+ return sharedField((propName) => {
48
+ const slotName = name ?? propName;
49
+ return {
50
+ descriptor: {
51
+ get(this: FieldHost) {
52
+ return getSlot(this, slotName);
53
+ },
54
+ set(_value: unknown) {
55
+ if (process.env.NODE_ENV !== "production") {
56
+ console.warn(
57
+ `[Slot] "${propName}" is a slot and cannot be assigned directly. Slots are filled by the parent component.`,
58
+ );
59
+ }
54
60
  },
55
- };
56
- },
61
+ },
62
+ };
57
63
  });
58
64
  }
@@ -1,22 +1,38 @@
1
- import { markStateDirty, signal } from "@praxisjs/core/internal";
1
+ import { markStateDirty, readSignal, SignalNode, writeSignal } from "@praxisjs/core/internal";
2
2
 
3
- import { createFieldDecorator } from "../create-field-decorator";
3
+ import { addClaimableInitializer, assertFieldContext } from "../field";
4
4
 
5
5
  import type { ReactiveHost } from "../reactive-host";
6
6
 
7
+ type StateHost = Record<PropertyKey, unknown>;
8
+
7
9
  export function State() {
8
- return createFieldDecorator<ReactiveHost>({
9
- bind(instance, _name, initialValue) {
10
- const sig = signal(initialValue);
11
- return {
12
- descriptor: {
13
- get: () => sig(),
14
- set: (value: unknown) => {
15
- markStateDirty(instance);
16
- sig.set(value);
17
- },
18
- },
19
- };
20
- },
21
- });
10
+ return function <This extends ReactiveHost, Value = unknown>(
11
+ _value: undefined,
12
+ context: ClassFieldDecoratorContext<This, Value>,
13
+ ): void {
14
+ assertFieldContext(context, "@State()");
15
+ const name = context.name;
16
+ // One accessor pair per decorated field, shared by every instance: per-instance closures would
17
+ // give each instance its own hidden class, turning every field read into a megamorphic access.
18
+ // It stays an own property of each instance, so decorators stacked above @State (and custom ones
19
+ // built with createFieldDecorator) find and wrap it, and a Proxy around the instance works.
20
+ const node = Symbol(`praxis.state.${String(name)}`);
21
+ const descriptor: PropertyDescriptor = {
22
+ enumerable: true,
23
+ configurable: true,
24
+ get(this: StateHost) {
25
+ return readSignal(this[node] as SignalNode);
26
+ },
27
+ set(this: StateHost, value: unknown) {
28
+ markStateDirty(this);
29
+ writeSignal(this[node] as SignalNode, value);
30
+ },
31
+ };
32
+ addClaimableInitializer(context, function (value) {
33
+ const instance = this as unknown as StateHost;
34
+ instance[node] = new SignalNode(value);
35
+ Object.defineProperty(instance, name, descriptor);
36
+ });
37
+ };
22
38
  }
@@ -1,28 +1,40 @@
1
1
  import { syncedSignal, type SyncedSignal } from "@praxisjs/core/internal";
2
2
 
3
- import { createFieldDecorator } from "../create-field-decorator";
3
+ import { type FieldDecorator, type FieldHost, sharedField } from "../field";
4
+ import { addLifecycleHooks } from "../lifecycle";
4
5
 
5
- const signalMap = new WeakMap<object, Map<string, SyncedSignal<unknown>>>();
6
+ // Fields of one instance synced over the same channel share one signal. Only read while the
7
+ // instance is built; each field then reads its signal from its own slot.
8
+ const SIGNALS: unique symbol = Symbol("praxis.syncedSignals");
6
9
 
7
- export function Synced(channelName?: string) {
8
- return createFieldDecorator({
9
- bind(instance, name, initialValue) {
10
- const key = channelName ?? name;
11
-
12
- if (!signalMap.has(instance)) signalMap.set(instance, new Map());
13
- const map = signalMap.get(instance) as Map<string, SyncedSignal<unknown>>;
14
- if (!map.has(key)) map.set(key, syncedSignal(key, initialValue));
15
- const sig = map.get(key) as SyncedSignal<unknown>;
10
+ interface SyncedHost {
11
+ [SIGNALS]?: Map<string, SyncedSignal<unknown>>;
12
+ }
16
13
 
17
- return {
18
- descriptor: {
19
- get: () => sig(),
20
- set: (value: unknown) => { sig.set(value); },
14
+ export function Synced(channelName?: string): FieldDecorator {
15
+ return sharedField((name) => {
16
+ const slot = Symbol(`praxis.synced.${name}`);
17
+ const key = channelName ?? name;
18
+ const close = (instance: object) => { ((instance as FieldHost)[slot] as SyncedSignal<unknown>).close(); };
19
+ return {
20
+ descriptor: {
21
+ get(this: FieldHost) {
22
+ return (this[slot] as SyncedSignal<unknown>)();
21
23
  },
22
- onUnmount() {
23
- sig.close();
24
+ set(this: FieldHost, value: unknown) {
25
+ (this[slot] as SyncedSignal<unknown>).set(value);
24
26
  },
25
- };
26
- },
27
+ },
28
+ init(instance, initialValue) {
29
+ const map = ((instance as SyncedHost)[SIGNALS] ??= new Map<string, SyncedSignal<unknown>>());
30
+ let sig = map.get(key);
31
+ if (!sig) {
32
+ sig = syncedSignal(key, initialValue);
33
+ map.set(key, sig);
34
+ }
35
+ instance[slot] = sig;
36
+ addLifecycleHooks(instance, undefined, close);
37
+ },
38
+ };
27
39
  });
28
40
  }
@@ -0,0 +1,101 @@
1
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
2
+ export type AnyMethod = (this: any, ...args: any[]) => any;
3
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
4
+ export type MethodContext = ClassMethodDecoratorContext<any, any>;
5
+
6
+ // Replacements installed through sharedMethod, and the ones an outer decorator wrapped again. Only
7
+ // read while decorating and while building the first instance of a class.
8
+ const shared = new WeakSet<AnyMethod>();
9
+ const superseded = new WeakSet<AnyMethod>();
10
+
11
+ function isPrototype(object: object): boolean {
12
+ // Class prototypes own their `constructor`; instances inherit it.
13
+ return Object.hasOwn(object, "constructor");
14
+ }
15
+
16
+ function ownerOf(object: object, name: PropertyKey): object | null {
17
+ let owner: object | null = object;
18
+ while (owner !== null && !Object.hasOwn(owner, name)) owner = Object.getPrototypeOf(owner) as object | null;
19
+ return owner;
20
+ }
21
+
22
+ function defineOwnMethod(object: object, name: PropertyKey, value: unknown): void {
23
+ Object.defineProperty(object, name, { value, configurable: true, writable: true });
24
+ }
25
+
26
+ /**
27
+ * Returns `method`, the decorator's replacement for `value`, set up so that reading it from an
28
+ * instance returns it bound to that instance, the same function on every read: detached calls
29
+ * (`onClick={this.save}`) keep working. `method` is shared by every instance and keeps any
30
+ * per-instance state in symbol slots.
31
+ *
32
+ * Rather than defining a bound wrapper on each instance as it is built, the first instance turns
33
+ * the method's prototype entry into an accessor: an instance pays for its bound copy only when the
34
+ * method is first read from it, and nothing if it never is.
35
+ */
36
+ export function sharedMethod<F extends AnyMethod>(
37
+ context: MethodContext,
38
+ value: AnyMethod,
39
+ method: F = value as F,
40
+ ): F {
41
+ if (value === method) {
42
+ // @Bind over a decorator that already binds: nothing left to do.
43
+ if (shared.has(method)) return method;
44
+ } else {
45
+ // The inner decorator's replacement now only runs through this one, which does the binding.
46
+ superseded.add(value);
47
+ }
48
+ shared.add(method);
49
+ // `this.#method` can't be read detached through a property, so there is nothing to bind.
50
+ if (context.private) return method;
51
+
52
+ const name = context.name;
53
+ let installed = false;
54
+ let home: object | undefined;
55
+
56
+ const accessor: PropertyDescriptor = {
57
+ configurable: true,
58
+ enumerable: false,
59
+ get(this: object | undefined) {
60
+ // Read from a prototype, or through `super.method()` from a subclass override: caching a
61
+ // bound copy on the instance would then shadow the override. Read with no receiver at all,
62
+ // as `vi.spyOn(instance, name)` does with a getter it finds on the prototype: the spy then
63
+ // wraps the method itself, which takes `this` from each call.
64
+ if (this == null || isPrototype(this) || ownerOf(this, name) !== home) return method;
65
+ const bound = method.bind(this);
66
+ defineOwnMethod(this, name, bound);
67
+ return bound;
68
+ },
69
+ set(this: object, next: unknown) {
70
+ defineOwnMethod(this, name, next);
71
+ },
72
+ };
73
+
74
+ context.addInitializer(function (this: unknown) {
75
+ if (installed) return;
76
+ const target = this as object;
77
+ if (superseded.has(method)) {
78
+ installed = true;
79
+ return;
80
+ }
81
+ if (!context.static) {
82
+ for (
83
+ let proto = Object.getPrototypeOf(target) as object | null;
84
+ proto !== null;
85
+ proto = Object.getPrototypeOf(proto) as object | null
86
+ ) {
87
+ if (Object.getOwnPropertyDescriptor(proto, name)?.value === method) {
88
+ installed = true;
89
+ home = proto;
90
+ Object.defineProperty(proto, name, accessor);
91
+ return;
92
+ }
93
+ }
94
+ }
95
+ // A static method (bound to its class, as before), or a decorator applied by hand whose
96
+ // replacement never reached a prototype: bind it on the object itself.
97
+ defineOwnMethod(target, name, method.bind(target));
98
+ });
99
+
100
+ return method;
101
+ }
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=component.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"component.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/component.test.ts"],"names":[],"mappings":""}