@fumari/stf 1.0.0 → 1.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.
package/dist/index.d.ts CHANGED
@@ -75,6 +75,10 @@ interface DataEngineListener {
75
75
  * when `field` is specified, this is also fired when parent is deleted.
76
76
  */
77
77
  onDelete?: (key: FieldKey, ctx: OnDeleteContext) => void;
78
+ /**
79
+ * For listener attached to a namespace's engine, this is fired when the namespace is removed.
80
+ */
81
+ onUnmount?: () => void;
78
82
  }
79
83
  interface OnUpdateContext {
80
84
  /**
@@ -94,8 +98,7 @@ interface OnInitContext {
94
98
  }
95
99
  declare class DataEngine {
96
100
  private data;
97
- /** namespace -> engine */
98
- private namespaces;
101
+ private readonly namespaces;
99
102
  private readonly listeners;
100
103
  constructor(defaultValues?: DefaultValue<NonNullable<object>>);
101
104
  listen(listener: DataEngineListener): void;
@@ -119,7 +122,8 @@ declare class DataEngine {
119
122
  * create an isolated data engine
120
123
  */
121
124
  namespace(namespace: string, initialValue?: DefaultValue<NonNullable<object>>): DataEngine;
122
- reset(data: Record<string, unknown>): void;
125
+ reset(data: NonNullable<object>): void;
126
+ clearNamespaces(): void;
123
127
  }
124
128
  declare function useFieldValue<V = unknown>(key: FieldKey, options?: {
125
129
  stf?: Stf | DataEngine;
@@ -135,5 +139,10 @@ declare function useFieldValue<V = unknown>(key: FieldKey, options?: {
135
139
  declare function useListener(listener: DataEngineListener & {
136
140
  stf?: Stf | DataEngine;
137
141
  }): void;
142
+ declare function useNamespace(options: {
143
+ namespace: string;
144
+ initial?: DefaultValue<NonNullable<object>>;
145
+ stf?: Stf | DataEngine;
146
+ }): DataEngine;
138
147
  //#endregion
139
- export { ArrayItemInfo, DataEngine, DataEngineListener, DefaultValue, FieldKey, OnDeleteContext, OnInitContext, OnUpdateContext, PropertyItemInfo, Stf, StfProvider, useArray, useDataEngine, useFieldValue, useListener, useObject, useStf };
148
+ export { ArrayItemInfo, DataEngine, DataEngineListener, DefaultValue, FieldKey, OnDeleteContext, OnInitContext, OnUpdateContext, PropertyItemInfo, Stf, StfProvider, useArray, useDataEngine, useFieldValue, useListener, useNamespace, useObject, useStf };
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  'use client';
2
2
 
3
- import { deepEqual, fieldKeyStartsWith, objectGet, objectSet, stringifyFieldKey } from "./lib/utils.js";
3
+ import { deepEqual, fieldKeyStartsWith, isPlainObject, 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
 
@@ -160,6 +160,10 @@ var ListenerManager = class {
160
160
  for (const v of listeners) v.onDelete?.(field, ctx);
161
161
  }
162
162
  }
163
+ onUnmount() {
164
+ for (const v of this.unindexed) v.onUnmount?.();
165
+ for (const listeners of this.indexed.values()) for (const v of listeners) v.onUnmount?.();
166
+ }
163
167
  };
164
168
  var DataEngine = class DataEngine {
165
169
  constructor(defaultValues = {}) {
@@ -184,28 +188,33 @@ var DataEngine = class DataEngine {
184
188
  */
185
189
  init(key, defaultValue, ctx = {}) {
186
190
  if (key.length === 0) return this.data;
187
- let cur = this.data;
188
- const currentKey = [];
191
+ const parentKey = [];
189
192
  const parentUpdateCtx = {
190
193
  swallow: true,
191
194
  ...ctx
192
195
  };
196
+ let cur = this.data;
193
197
  for (let i = 0; i < key.length; i++) {
194
198
  const propKey = key[i];
195
199
  const propValue = cur[propKey];
196
200
  if (i === key.length - 1) {
197
201
  if (propValue !== void 0) return propValue;
198
202
  cur[propKey] = getDefaultValue(defaultValue);
199
- this.listeners.onUpdate(currentKey, parentUpdateCtx);
203
+ this.listeners.onUpdate(parentKey, parentUpdateCtx);
200
204
  this.listeners.onInit(key, ctx);
201
205
  return cur[propKey];
202
- } else if (typeof propValue === "object" && propValue !== null) cur = propValue;
203
- else {
204
- if (propValue !== void 0) console.warn(`the original value of field ${currentKey.join(".")} is overidden, this might be unexpected.`);
206
+ } else if (isPlainObject(propValue)) {
207
+ cur = propValue;
208
+ parentKey.push(propKey);
209
+ } else {
205
210
  cur = cur[propKey] = {};
206
- this.listeners.onUpdate(currentKey, parentUpdateCtx);
211
+ this.listeners.onUpdate(parentKey, parentUpdateCtx);
212
+ parentKey.push(propKey);
213
+ if (propValue !== void 0) {
214
+ console.warn(`the original value of field ${parentKey.join(".")} is overidden, this might be unexpected.`);
215
+ this.listeners.onUpdate(parentKey, parentUpdateCtx);
216
+ }
207
217
  }
208
- currentKey.push(propKey);
209
218
  }
210
219
  }
211
220
  delete(key, ctx = {}) {
@@ -222,7 +231,7 @@ var DataEngine = class DataEngine {
222
231
  ...ctx
223
232
  });
224
233
  return deleted[0];
225
- } else if (typeof parent === "object" && parent !== null) {
234
+ } else if (isPlainObject(parent)) {
226
235
  const temp = parent[prop];
227
236
  delete parent[prop];
228
237
  this.listeners.onDelete(key, ctx);
@@ -264,17 +273,27 @@ var DataEngine = class DataEngine {
264
273
  return child;
265
274
  }
266
275
  reset(data) {
267
- this.namespaces.clear();
268
276
  this.update([], data);
269
277
  }
278
+ clearNamespaces() {
279
+ for (const [name, engine] of this.namespaces) {
280
+ this.namespaces.delete(name);
281
+ engine.listeners.onUnmount();
282
+ }
283
+ }
270
284
  };
271
285
  function useFieldValue(key, options = {}) {
272
- const engine = useDataEngine(options.stf);
273
- const { compute = (v) => v, defaultValue, isChanged = (a, b) => a !== b } = options;
286
+ const { stf, compute = (v) => v, defaultValue, isChanged = (a, b) => a !== b } = options;
287
+ const engine = useDataEngine(stf);
274
288
  const [value, setValue] = useState(() => compute(engine.init(key, defaultValue)));
289
+ const prevEngineRef = useRef(engine);
290
+ if (prevEngineRef.current !== engine) {
291
+ setValue(compute(engine.init(key, defaultValue)));
292
+ prevEngineRef.current = engine;
293
+ }
275
294
  useListener({
276
295
  field: key,
277
- stf: options.stf,
296
+ stf,
278
297
  onInit() {
279
298
  const computed = compute(engine.get(key));
280
299
  setValue((prev) => isChanged(prev, computed) ? computed : prev);
@@ -297,6 +316,9 @@ function useListener(listener) {
297
316
  useEffect(() => {
298
317
  const internal = {
299
318
  field: listener.field,
319
+ onUnmount(...args) {
320
+ return listenerRef.current.onUnmount?.(...args);
321
+ },
300
322
  onDelete(...args) {
301
323
  return listenerRef.current.onDelete?.(...args);
302
324
  },
@@ -313,6 +335,18 @@ function useListener(listener) {
313
335
  };
314
336
  }, [engine, listener.field]);
315
337
  }
338
+ function useNamespace(options) {
339
+ const { namespace, stf, initial } = options;
340
+ const engine = useDataEngine(stf);
341
+ const [value, setValue] = useState(() => engine.namespace(namespace, initial));
342
+ useListener({
343
+ stf: value,
344
+ onUnmount() {
345
+ setValue(engine.namespace(namespace, initial));
346
+ }
347
+ });
348
+ return value;
349
+ }
316
350
 
317
351
  //#endregion
318
- export { DataEngine, StfProvider, useArray, useDataEngine, useFieldValue, useListener, useObject, useStf };
352
+ export { DataEngine, StfProvider, useArray, useDataEngine, useFieldValue, useListener, useNamespace, useObject, useStf };
@@ -17,5 +17,6 @@ declare function stringifyFieldKey(fieldKey: FieldKey): string;
17
17
  * @returns if `a` starts with `b`.
18
18
  */
19
19
  declare function fieldKeyStartsWith(a: string, b: string): boolean;
20
+ declare function isPlainObject(value: unknown): value is Record<string, unknown>;
20
21
  //#endregion
21
- export { deepEqual, fieldKeyStartsWith, objectGet, objectSet, stringifyFieldKey };
22
+ export { deepEqual, fieldKeyStartsWith, isPlainObject, objectGet, objectSet, stringifyFieldKey };
package/dist/lib/utils.js CHANGED
@@ -2,7 +2,7 @@
2
2
  function objectGet(obj, key) {
3
3
  let cur = obj;
4
4
  for (const prop of key) {
5
- if (typeof cur !== "object" || cur === null || !(prop in cur)) return;
5
+ if (!isPlainObject(cur) || !(prop in cur)) return;
6
6
  cur = cur[prop];
7
7
  }
8
8
  return cur;
@@ -15,7 +15,7 @@ function objectGet(obj, key) {
15
15
  function objectSet(obj, key, value) {
16
16
  if (key.length === 0) return value;
17
17
  const parent = objectGet(obj, key.slice(0, -1));
18
- if (typeof parent !== "object" || parent === null) throw new Error("missing parent object");
18
+ if (!isPlainObject(parent)) throw new Error("missing parent object");
19
19
  parent[key[key.length - 1]] = value;
20
20
  return obj;
21
21
  }
@@ -31,13 +31,14 @@ function deepEqual(a, b) {
31
31
  return a.every((item, index) => deepEqual(item, b[index]));
32
32
  }
33
33
  if (Array.isArray(a) || Array.isArray(b)) return false;
34
+ if (!isPlainObject(a) || !isPlainObject(b)) return false;
34
35
  const keysA = Object.keys(a);
35
36
  const keysB = Object.keys(b);
36
37
  if (keysA.length !== keysB.length) return false;
37
- return keysA.every((key) => Object.prototype.hasOwnProperty.call(b, key) && deepEqual(a[key], b[key]));
38
+ return keysA.every((key) => key in b && deepEqual(a[key], b[key]));
38
39
  }
39
40
  function stringifyFieldKey(fieldKey) {
40
- return fieldKey.map((v) => `${typeof v}:${v}`).join(".");
41
+ return fieldKey.map((v) => typeof v === "string" ? `_${v}` : `n${v}`).join(".");
41
42
  }
42
43
  /**
43
44
  * @returns if `a` starts with `b`.
@@ -45,6 +46,11 @@ function stringifyFieldKey(fieldKey) {
45
46
  function fieldKeyStartsWith(a, b) {
46
47
  return b.length === 0 || a === b || a.startsWith(b + ".");
47
48
  }
49
+ function isPlainObject(value) {
50
+ if (typeof value !== "object" || value === null) return false;
51
+ const prototype = Object.getPrototypeOf(value);
52
+ return prototype === null || prototype === Object.prototype || Object.getPrototypeOf(prototype) === null;
53
+ }
48
54
 
49
55
  //#endregion
50
- export { deepEqual, fieldKeyStartsWith, objectGet, objectSet, stringifyFieldKey };
56
+ export { deepEqual, fieldKeyStartsWith, isPlainObject, objectGet, objectSet, stringifyFieldKey };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fumari/stf",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "Schema to Form.",
5
5
  "keywords": [],
6
6
  "license": "MIT",