@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 +30 -18
- package/dist/index.js +62 -42
- package/dist/lib/utils.d.ts +5 -5
- package/dist/lib/utils.js +7 -9
- package/package.json +3 -3
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(
|
|
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
|
-
*
|
|
69
|
+
* fired on `init(field)`.
|
|
70
70
|
*/
|
|
71
|
-
onInit?: (key: FieldKey) => void;
|
|
71
|
+
onInit?: (key: FieldKey, ctx: OnInitContext) => void;
|
|
72
72
|
/**
|
|
73
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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(
|
|
21
|
-
if (
|
|
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 (
|
|
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
|
-
|
|
156
|
-
const
|
|
157
|
-
|
|
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.
|
|
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,
|
|
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,
|
|
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
|
|
210
|
-
|
|
211
|
-
this.listeners.onDelete(key);
|
|
212
|
-
|
|
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.
|
|
217
|
-
this.listeners.
|
|
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, {
|
|
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
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
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.
|
|
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
|
-
|
|
284
|
+
setValue((prev) => isChanged(prev, computed) ? computed : prev);
|
|
265
285
|
},
|
|
266
286
|
onDelete() {
|
|
267
287
|
const computed = compute(void 0);
|
|
268
|
-
|
|
288
|
+
setValue((prev) => isChanged(prev, computed) ? computed : prev);
|
|
269
289
|
}
|
|
270
290
|
});
|
|
271
291
|
return [value, (newValue) => engine.update(key, newValue)];
|
package/dist/lib/utils.d.ts
CHANGED
|
@@ -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 {
|
|
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 {
|
|
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
|
+
"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.
|
|
23
|
-
"@types/react": "^19.2.
|
|
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"
|