@fumari/stf 0.0.3 → 1.0.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/dist/index.d.ts CHANGED
@@ -19,7 +19,7 @@ declare function useStf(options: {
19
19
  */
20
20
  defaultValues?: DefaultValue<Record<string, unknown>>;
21
21
  }): Stf;
22
- declare function useDataEngine(stf?: Stf): DataEngine;
22
+ declare function useDataEngine(instance?: Stf | DataEngine): DataEngine;
23
23
  interface ArrayItemInfo {
24
24
  field: FieldKey;
25
25
  index: number;
@@ -62,27 +62,40 @@ interface DataEngineListener {
62
62
  */
63
63
  field?: FieldKey;
64
64
  /**
65
- * when field value is changed
65
+ * when field value is changed.
66
66
  */
67
67
  onUpdate?: (key: FieldKey, ctx: OnUpdateContext) => void;
68
68
  /**
69
- * when `init(field)` is called
69
+ * fired on `init(field)`.
70
70
  */
71
- onInit?: (key: FieldKey) => void;
71
+ onInit?: (key: FieldKey, ctx: OnInitContext) => void;
72
72
  /**
73
- * when `delete(field)` is called
73
+ * fired on `delete(field)`.
74
+ *
75
+ * when `field` is specified, this is also fired when parent is deleted.
74
76
  */
75
- onDelete?: (key: FieldKey) => void;
77
+ onDelete?: (key: FieldKey, ctx: OnDeleteContext) => void;
76
78
  }
77
79
  interface OnUpdateContext {
78
80
  /**
79
81
  * An update is swallow if the change doesn't affect the values of children fields.
80
82
  */
81
83
  swallow: boolean;
84
+ /** custom data */
85
+ custom?: Record<string, unknown>;
86
+ }
87
+ interface OnDeleteContext {
88
+ /** custom data */
89
+ custom?: Record<string, unknown>;
90
+ }
91
+ interface OnInitContext {
92
+ /** custom data */
93
+ custom?: Record<string, unknown>;
82
94
  }
83
95
  declare class DataEngine {
84
96
  private data;
85
- private attachedDataMap;
97
+ /** namespace -> engine */
98
+ private namespaces;
86
99
  private readonly listeners;
87
100
  constructor(defaultValues?: DefaultValue<NonNullable<object>>);
88
101
  listen(listener: DataEngineListener): void;
@@ -94,23 +107,22 @@ declare class DataEngine {
94
107
  * @param defaultValue the initial value, the field is also created for `undefined`
95
108
  * @returns the value of initialized field, or the current value of field if already initialized
96
109
  */
97
- init(key: FieldKey, defaultValue?: DefaultValue): unknown;
98
- delete(key: FieldKey): unknown | undefined;
110
+ init(key: FieldKey, defaultValue?: DefaultValue, ctx?: OnInitContext): unknown;
111
+ delete(key: FieldKey, ctx?: OnDeleteContext): unknown | undefined;
99
112
  get(key: FieldKey): unknown;
100
113
  /**
101
114
  * update the value of field if it exists
102
115
  * @returns if the field is updated
103
116
  */
104
- update(key: FieldKey, value: unknown): boolean;
105
- attachedData<T>(namespace: string): {
106
- get: (field: FieldKey) => T | undefined;
107
- set: (field: FieldKey, value: T) => void;
108
- delete: (field?: FieldKey) => void;
109
- };
117
+ update(key: FieldKey, value: unknown, ctx?: Partial<OnUpdateContext>): boolean;
118
+ /**
119
+ * create an isolated data engine
120
+ */
121
+ namespace(namespace: string, initialValue?: DefaultValue<NonNullable<object>>): DataEngine;
110
122
  reset(data: Record<string, unknown>): void;
111
123
  }
112
124
  declare function useFieldValue<V = unknown>(key: FieldKey, options?: {
113
- stf?: Stf;
125
+ stf?: Stf | DataEngine;
114
126
  defaultValue?: DefaultValue;
115
127
  /**
116
128
  * compute value from the actual field value.
@@ -121,7 +133,7 @@ declare function useFieldValue<V = unknown>(key: FieldKey, options?: {
121
133
  isChanged?: (prev: V, next: V) => boolean;
122
134
  }): readonly [V, (newValue: unknown) => boolean];
123
135
  declare function useListener(listener: DataEngineListener & {
124
- stf?: Stf;
136
+ stf?: Stf | DataEngine;
125
137
  }): void;
126
138
  //#endregion
127
- export { ArrayItemInfo, DataEngine, DataEngineListener, DefaultValue, FieldKey, PropertyItemInfo, Stf, StfProvider, useArray, useDataEngine, useFieldValue, useListener, useObject, useStf };
139
+ export { ArrayItemInfo, DataEngine, DataEngineListener, DefaultValue, FieldKey, OnDeleteContext, OnInitContext, OnUpdateContext, PropertyItemInfo, Stf, StfProvider, useArray, useDataEngine, useFieldValue, useListener, useObject, useStf };
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  'use client';
2
2
 
3
- import { deepEqual, objectGet, objectSet, stringifyFieldKey } from "./lib/utils.js";
3
+ import { deepEqual, fieldKeyStartsWith, objectGet, objectSet, stringifyFieldKey } from "./lib/utils.js";
4
4
  import { createContext, use, useEffect, useMemo, useRef, useState } from "react";
5
5
  import { jsx } from "react/jsx-runtime";
6
6
 
@@ -17,8 +17,9 @@ function useStf(options) {
17
17
  const dataEngine = useMemo(() => new DataEngine(defaultValues), []);
18
18
  return useMemo(() => ({ dataEngine }), [dataEngine]);
19
19
  }
20
- function useDataEngine(stf) {
21
- if (stf) return stf.dataEngine;
20
+ function useDataEngine(instance) {
21
+ if (instance instanceof DataEngine) return instance;
22
+ if (instance) return instance.dataEngine;
22
23
  return use(Context).dataEngine;
23
24
  }
24
25
  function useArray(field, options = {}) {
@@ -142,24 +143,27 @@ var ListenerManager = class {
142
143
  const set = this.indexed.get(updatedKey);
143
144
  if (set) for (const v of set) v.onUpdate?.(field, ctx);
144
145
  } else for (const [k, listeners] of this.indexed.entries()) {
145
- if (updatedKey.length !== 0 && k !== updatedKey && !k.startsWith(updatedKey + ".")) continue;
146
+ if (!fieldKeyStartsWith(k, updatedKey)) continue;
146
147
  for (const v of listeners) v.onUpdate?.(field, ctx);
147
148
  }
148
149
  }
149
- onInit(field) {
150
- for (const v of this.unindexed) v.onInit?.(field);
150
+ onInit(field, ctx) {
151
+ for (const v of this.unindexed) v.onInit?.(field, ctx);
151
152
  const set = this.indexed.get(stringifyFieldKey(field));
152
- if (set) for (const v of set) v.onInit?.(field);
153
+ if (set) for (const v of set) v.onInit?.(field, ctx);
153
154
  }
154
- onDelete(field) {
155
- for (const v of this.unindexed) v.onDelete?.(field);
156
- const set = this.indexed.get(stringifyFieldKey(field));
157
- if (set) for (const v of set) v.onDelete?.(field);
155
+ onDelete(field, ctx) {
156
+ const fieldKey = stringifyFieldKey(field);
157
+ for (const v of this.unindexed) v.onDelete?.(field, ctx);
158
+ for (const [k, listeners] of this.indexed.entries()) {
159
+ if (!fieldKeyStartsWith(k, fieldKey)) continue;
160
+ for (const v of listeners) v.onDelete?.(field, ctx);
161
+ }
158
162
  }
159
163
  };
160
- var DataEngine = class {
164
+ var DataEngine = class DataEngine {
161
165
  constructor(defaultValues = {}) {
162
- this.attachedDataMap = /* @__PURE__ */ new Map();
166
+ this.namespaces = /* @__PURE__ */ new Map();
163
167
  this.listeners = new ListenerManager();
164
168
  this.data = getDefaultValue(defaultValues);
165
169
  }
@@ -178,43 +182,54 @@ var DataEngine = class {
178
182
  * @param defaultValue the initial value, the field is also created for `undefined`
179
183
  * @returns the value of initialized field, or the current value of field if already initialized
180
184
  */
181
- init(key, defaultValue) {
185
+ init(key, defaultValue, ctx = {}) {
182
186
  if (key.length === 0) return this.data;
183
187
  let cur = this.data;
184
188
  const currentKey = [];
189
+ const parentUpdateCtx = {
190
+ swallow: true,
191
+ ...ctx
192
+ };
185
193
  for (let i = 0; i < key.length; i++) {
186
194
  const propKey = key[i];
187
195
  const propValue = cur[propKey];
188
196
  if (i === key.length - 1) {
189
197
  if (propValue !== void 0) return propValue;
190
198
  cur[propKey] = getDefaultValue(defaultValue);
191
- this.listeners.onUpdate(currentKey, { swallow: true });
192
- this.listeners.onInit(key);
199
+ this.listeners.onUpdate(currentKey, parentUpdateCtx);
200
+ this.listeners.onInit(key, ctx);
193
201
  return cur[propKey];
194
202
  } else if (typeof propValue === "object" && propValue !== null) cur = propValue;
195
203
  else {
196
204
  if (propValue !== void 0) console.warn(`the original value of field ${currentKey.join(".")} is overidden, this might be unexpected.`);
197
205
  cur = cur[propKey] = {};
198
- this.listeners.onUpdate(currentKey, { swallow: true });
206
+ this.listeners.onUpdate(currentKey, parentUpdateCtx);
199
207
  }
200
208
  currentKey.push(propKey);
201
209
  }
202
210
  }
203
- delete(key) {
211
+ delete(key, ctx = {}) {
204
212
  if (key.length === 0) return;
205
213
  const parentKey = key.slice(0, -1);
206
214
  const prop = key[key.length - 1];
207
215
  const parent = this.get(parentKey);
208
216
  if (Array.isArray(parent) && typeof prop === "number") {
209
- const [deleted] = parent.splice(prop, 1);
210
- this.listeners.onUpdate(parentKey, { swallow: false });
211
- this.listeners.onDelete(key);
212
- return deleted;
217
+ const deleted = parent.splice(prop, 1);
218
+ if (deleted.length === 0) return;
219
+ this.listeners.onDelete(key, ctx);
220
+ this.listeners.onUpdate(parentKey, {
221
+ swallow: false,
222
+ ...ctx
223
+ });
224
+ return deleted[0];
213
225
  } else if (typeof parent === "object" && parent !== null) {
214
226
  const temp = parent[prop];
215
227
  delete parent[prop];
216
- this.listeners.onUpdate(parentKey, { swallow: true });
217
- this.listeners.onDelete(key);
228
+ this.listeners.onDelete(key, ctx);
229
+ this.listeners.onUpdate(parentKey, {
230
+ swallow: true,
231
+ ...ctx
232
+ });
218
233
  return temp;
219
234
  }
220
235
  }
@@ -225,31 +240,31 @@ var DataEngine = class {
225
240
  * update the value of field if it exists
226
241
  * @returns if the field is updated
227
242
  */
228
- update(key, value) {
243
+ update(key, value, ctx) {
229
244
  try {
230
245
  this.data = objectSet(this.data, key, value);
231
- this.listeners.onUpdate(key, { swallow: false });
246
+ this.listeners.onUpdate(key, {
247
+ swallow: false,
248
+ ...ctx
249
+ });
232
250
  return true;
233
251
  } catch {
234
252
  return false;
235
253
  }
236
254
  }
237
- attachedData(namespace) {
238
- return {
239
- get: (field) => {
240
- return this.attachedDataMap.get(`${namespace}:${stringifyFieldKey(field)}`);
241
- },
242
- set: (field, value) => {
243
- this.attachedDataMap.set(`${namespace}:${stringifyFieldKey(field)}`, value);
244
- },
245
- delete: (field) => {
246
- if (field) this.attachedDataMap.delete(`${namespace}:${stringifyFieldKey(field)}`);
247
- else for (const key of this.attachedDataMap.keys()) if (key.startsWith(`${namespace}:`)) this.attachedDataMap.delete(key);
248
- }
249
- };
255
+ /**
256
+ * create an isolated data engine
257
+ */
258
+ namespace(namespace, initialValue) {
259
+ let child = this.namespaces.get(namespace);
260
+ if (!child) {
261
+ child = new DataEngine(initialValue);
262
+ this.namespaces.set(namespace, child);
263
+ }
264
+ return child;
250
265
  }
251
266
  reset(data) {
252
- this.attachedDataMap.clear();
267
+ this.namespaces.clear();
253
268
  this.update([], data);
254
269
  }
255
270
  };
@@ -259,13 +274,18 @@ function useFieldValue(key, options = {}) {
259
274
  const [value, setValue] = useState(() => compute(engine.init(key, defaultValue)));
260
275
  useListener({
261
276
  field: key,
277
+ stf: options.stf,
278
+ onInit() {
279
+ const computed = compute(engine.get(key));
280
+ setValue((prev) => isChanged(prev, computed) ? computed : prev);
281
+ },
262
282
  onUpdate() {
263
283
  const computed = compute(engine.get(key));
264
- if (isChanged(value, computed)) setValue(computed);
284
+ setValue((prev) => isChanged(prev, computed) ? computed : prev);
265
285
  },
266
286
  onDelete() {
267
287
  const computed = compute(void 0);
268
- if (isChanged(value, computed)) setValue(computed);
288
+ setValue((prev) => isChanged(prev, computed) ? computed : prev);
269
289
  }
270
290
  });
271
291
  return [value, (newValue) => engine.update(key, newValue)];
@@ -1,10 +1,6 @@
1
1
  import { t as FieldKey } from "../types-Do_aTV5I.js";
2
2
 
3
3
  //#region src/lib/utils.d.ts
4
- /**
5
- * test if array a starts with array b, only compare values via `===`.
6
- */
7
- declare function arrayStartsWith(a: unknown[], b: unknown[]): boolean;
8
4
  declare function objectGet(obj: unknown, key: (string | number)[]): unknown | undefined;
9
5
  /**
10
6
  * set the value of field if it exists (in place)
@@ -17,5 +13,9 @@ declare function objectSet(obj: unknown, key: FieldKey, value: unknown): unknown
17
13
  */
18
14
  declare function deepEqual(a: unknown, b: unknown): boolean;
19
15
  declare function stringifyFieldKey(fieldKey: FieldKey): string;
16
+ /**
17
+ * @returns if `a` starts with `b`.
18
+ */
19
+ declare function fieldKeyStartsWith(a: string, b: string): boolean;
20
20
  //#endregion
21
- export { arrayStartsWith, deepEqual, objectGet, objectSet, stringifyFieldKey };
21
+ export { deepEqual, fieldKeyStartsWith, objectGet, objectSet, stringifyFieldKey };
package/dist/lib/utils.js CHANGED
@@ -1,12 +1,4 @@
1
1
  //#region src/lib/utils.ts
2
- /**
3
- * test if array a starts with array b, only compare values via `===`.
4
- */
5
- function arrayStartsWith(a, b) {
6
- if (b.length > a.length) return false;
7
- for (let i = 0; i < b.length; i++) if (a[i] !== b[i]) return false;
8
- return true;
9
- }
10
2
  function objectGet(obj, key) {
11
3
  let cur = obj;
12
4
  for (const prop of key) {
@@ -47,6 +39,12 @@ function deepEqual(a, b) {
47
39
  function stringifyFieldKey(fieldKey) {
48
40
  return fieldKey.map((v) => `${typeof v}:${v}`).join(".");
49
41
  }
42
+ /**
43
+ * @returns if `a` starts with `b`.
44
+ */
45
+ function fieldKeyStartsWith(a, b) {
46
+ return b.length === 0 || a === b || a.startsWith(b + ".");
47
+ }
50
48
 
51
49
  //#endregion
52
- export { arrayStartsWith, deepEqual, objectGet, objectSet, stringifyFieldKey };
50
+ export { deepEqual, fieldKeyStartsWith, objectGet, objectSet, stringifyFieldKey };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fumari/stf",
3
- "version": "0.0.3",
3
+ "version": "1.0.0",
4
4
  "description": "Schema to Form.",
5
5
  "keywords": [],
6
6
  "license": "MIT",
@@ -19,8 +19,8 @@
19
19
  "access": "public"
20
20
  },
21
21
  "devDependencies": {
22
- "@types/node": "25.2.1",
23
- "@types/react": "^19.2.13",
22
+ "@types/node": "25.2.3",
23
+ "@types/react": "^19.2.14",
24
24
  "tsdown": "^0.20.3",
25
25
  "eslint-config-custom": "0.0.0",
26
26
  "tsconfig": "0.0.0"