@homebound/form-state 2.33.0 → 3.1.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/README.md +6 -6
- package/dist/index.d.ts +450 -9
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1118 -14
- package/dist/index.js.map +1 -0
- package/package.json +42 -50
- package/skills/form-state/SKILL.md +9 -9
- package/dist/FormStateApp.d.ts +0 -5
- package/dist/FormStateApp.js +0 -54
- package/dist/FormStateApp.test.d.ts +0 -1
- package/dist/FormStateApp.test.js +0 -26
- package/dist/config.d.ts +0 -78
- package/dist/config.js +0 -2
- package/dist/configBuilders.d.ts +0 -67
- package/dist/configBuilders.js +0 -115
- package/dist/configBuilders.test.d.ts +0 -1
- package/dist/configBuilders.test.js +0 -65
- package/dist/fields/deepEquals.d.ts +0 -7
- package/dist/fields/deepEquals.js +0 -53
- package/dist/fields/fragmentField.d.ts +0 -4
- package/dist/fields/fragmentField.js +0 -58
- package/dist/fields/listField.d.ts +0 -11
- package/dist/fields/listField.js +0 -355
- package/dist/fields/objectField.d.ts +0 -63
- package/dist/fields/objectField.js +0 -277
- package/dist/fields/valueField.d.ts +0 -81
- package/dist/fields/valueField.js +0 -244
- package/dist/formState.stories.d.ts +0 -4
- package/dist/formState.stories.js +0 -12
- package/dist/formState.test.d.ts +0 -11
- package/dist/formState.test.js +0 -2109
- package/dist/formStateDomain.d.ts +0 -47
- package/dist/formStateDomain.js +0 -28
- package/dist/mobx-behavior.test.d.ts +0 -1
- package/dist/mobx-behavior.test.js +0 -51
- package/dist/proxies.d.ts +0 -5
- package/dist/proxies.js +0 -20
- package/dist/rules.d.ts +0 -10
- package/dist/rules.js +0 -11
- package/dist/setupTests.d.ts +0 -1
- package/dist/setupTests.js +0 -15
- package/dist/stackoverlfow.d.ts +0 -17
- package/dist/stackoverlfow.js +0 -17
- package/dist/temporal.d.ts +0 -6
- package/dist/temporal.js +0 -34
- package/dist/temporal.test.d.ts +0 -1
- package/dist/temporal.test.js +0 -97
- package/dist/useFormState.d.ts +0 -82
- package/dist/useFormState.js +0 -158
- package/dist/useFormState.test.d.ts +0 -1
- package/dist/useFormState.test.js +0 -787
- package/dist/useFormStates.d.ts +0 -68
- package/dist/useFormStates.js +0 -101
- package/dist/useFormStates.test.d.ts +0 -1
- package/dist/useFormStates.test.js +0 -223
- package/dist/utils.d.ts +0 -52
- package/dist/utils.js +0 -198
- package/dist/utils.test.d.ts +0 -1
- package/dist/utils.test.js +0 -102
package/README.md
CHANGED
|
@@ -138,12 +138,12 @@ For submit forms, the expectation is that you should:
|
|
|
138
138
|
If you do this, you should not have to call `commitChanges` manually, because code like:
|
|
139
139
|
|
|
140
140
|
```ts
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
141
|
+
onClick: async () => {
|
|
142
|
+
const input = formState.changedValue;
|
|
143
|
+
await saveAuthor(input);
|
|
144
|
+
// checks if formState.dirty is true before closing
|
|
145
|
+
closeModal();
|
|
146
|
+
};
|
|
147
147
|
```
|
|
148
148
|
|
|
149
149
|
Will "just work" because the control flow will be:
|
package/dist/index.d.ts
CHANGED
|
@@ -1,9 +1,450 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
1
|
+
import { Temporal } from "temporal-polyfill";
|
|
2
|
+
//#region src/fields/fragmentField.d.ts
|
|
3
|
+
interface FragmentField<V> {
|
|
4
|
+
value: V;
|
|
5
|
+
}
|
|
6
|
+
//#endregion
|
|
7
|
+
//#region src/rules.d.ts
|
|
8
|
+
/** A validation rule, given the value and name, return the error string if valid, or undefined if valid. */
|
|
9
|
+
type Rule<V> = (opts: {
|
|
10
|
+
value: V;
|
|
11
|
+
key: string;
|
|
12
|
+
originalValue: V;
|
|
13
|
+
}) => string | undefined;
|
|
14
|
+
/** A rule that validates `value` is not `undefined`, `null`, or empty string. */
|
|
15
|
+
export declare const required: <V>({ value: v }: {
|
|
16
|
+
value: V;
|
|
17
|
+
}) => string | undefined;
|
|
18
|
+
//#endregion
|
|
19
|
+
//#region src/fields/valueField.d.ts
|
|
20
|
+
/**
|
|
21
|
+
* Form state for a primitive field in the form, i.e. its value but also touched/validation/etc. state.
|
|
22
|
+
*
|
|
23
|
+
* This API also provides hooks for form elements to call into, i.e. `blur()` and `set(...)` that will
|
|
24
|
+
* update the field state and re-render, i.e. when including in an `ObjectState`-typed literal that is
|
|
25
|
+
* an mobx `useLocalObservable`/observable.
|
|
26
|
+
*
|
|
27
|
+
* Note that `V` will always have `null | undefined` added to it by `FieldStates`, b/c most form fields
|
|
28
|
+
* i.e. text boxes, can always be cleared out/deleted.
|
|
29
|
+
*/
|
|
30
|
+
interface FieldState<V> {
|
|
31
|
+
/** The key in the parent object, i.e. `firstName` in `author: { firstName: string }`. */
|
|
32
|
+
readonly key: string;
|
|
33
|
+
/** The current value of the field. */
|
|
34
|
+
value: V;
|
|
35
|
+
readonly originalValue: V;
|
|
36
|
+
touched: boolean;
|
|
37
|
+
readOnly: boolean;
|
|
38
|
+
loading: boolean;
|
|
39
|
+
readonly required: boolean;
|
|
40
|
+
readonly dirty: boolean;
|
|
41
|
+
readonly valid: boolean;
|
|
42
|
+
readonly focused: boolean;
|
|
43
|
+
readonly isNewEntity: boolean;
|
|
44
|
+
rules: Rule<V>[];
|
|
45
|
+
readonly errors: string[];
|
|
46
|
+
/**
|
|
47
|
+
* Returns a subset of V with only the changed values.
|
|
48
|
+
*
|
|
49
|
+
* Reading this also records that the value is probably being put on the wire, so that a later
|
|
50
|
+
* `refreshing` set can tell a server ack of this value apart from a stale cache refresh.
|
|
51
|
+
*/
|
|
52
|
+
readonly changedValue: V;
|
|
53
|
+
/** Focuses the field. Disables changes from `ObjectState.set` calls. */
|
|
54
|
+
focus(): void;
|
|
55
|
+
/** Blur marks the field as touched and triggers an auto-save. */
|
|
56
|
+
blur(): void;
|
|
57
|
+
/** Triggers an auto-save; the caller (i.e. useFormState) should still dirty & valid check. */
|
|
58
|
+
maybeAutoSave(): void;
|
|
59
|
+
set(value: V, opts?: SetOpts): void;
|
|
60
|
+
/** Reverts back to the original value and resets dirty/touched. */
|
|
61
|
+
revertChanges(): void;
|
|
62
|
+
/**
|
|
63
|
+
* Accepts the current changed value (if any) as the original and resets dirty/touched.
|
|
64
|
+
*
|
|
65
|
+
* @deprecated Use `formState.update(ackedServerValue)` instead. `commitChanges` marks all current
|
|
66
|
+
* changes as saved, which risks discarding a user's in-flight edits. Updating from the server ack gives
|
|
67
|
+
* form-state a field-by-field audit of which changes were successfully saved.
|
|
68
|
+
*/
|
|
69
|
+
commitChanges(): void;
|
|
70
|
+
/** Creates a new FieldState with a transformation of the value, i.e. string to int, or feet to inches. */
|
|
71
|
+
adapt<V2>(adapter: ValueAdapter<V, V2>): FieldState<V2>;
|
|
72
|
+
/**
|
|
73
|
+
* Releases any mobx reactions this field created.
|
|
74
|
+
*
|
|
75
|
+
* This only matters when wrapping a mobx observable (i.e. a store or class instance): each field
|
|
76
|
+
* subscribes to its slot on the store so that external mutations can trigger auto-save, and that
|
|
77
|
+
* subscription keeps the field alive until it is disposed. Plain objects create no reactions.
|
|
78
|
+
*/
|
|
79
|
+
dispose(): void;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Allows changing a type in the formState (like a string) to a different type in the UI (like a number).
|
|
83
|
+
*
|
|
84
|
+
* Or doing unit of measure conversions within the same type, like from meters to feet.
|
|
85
|
+
*/
|
|
86
|
+
interface ValueAdapter<V, V2 = V> {
|
|
87
|
+
/** Converts the original FieldState's value `V` into new `V2` type. */
|
|
88
|
+
toValue(value: V): V2;
|
|
89
|
+
/** Converts the adapted FieldState's value `V2` back into the original `V` type. */
|
|
90
|
+
fromValue(value: V2): V;
|
|
91
|
+
}
|
|
92
|
+
/** Public options for our `set` command. */
|
|
93
|
+
interface SetOpts {
|
|
94
|
+
/** Whether this `set` should trigger an auto-save; defaults to true. */
|
|
95
|
+
autoSave?: boolean;
|
|
96
|
+
}
|
|
97
|
+
//#endregion
|
|
98
|
+
//#region src/fields/listField.d.ts
|
|
99
|
+
/** Form state for list of children, i.e. `U` is a `Book` in a form with a `books: Book[]`. */
|
|
100
|
+
interface ListFieldState<U> extends FieldState<U[]> {
|
|
101
|
+
readonly rows: ReadonlyArray<ObjectState<U>>;
|
|
102
|
+
add(value: U, index?: number): void;
|
|
103
|
+
remove(indexOrValue: number | U): void;
|
|
104
|
+
}
|
|
105
|
+
//#endregion
|
|
106
|
+
//#region src/temporal.d.ts
|
|
107
|
+
type SupportedTemporal = Temporal.PlainDate | Temporal.ZonedDateTime;
|
|
108
|
+
//#endregion
|
|
109
|
+
//#region src/useFormState.d.ts
|
|
110
|
+
type Query<I> = {
|
|
111
|
+
data: I;
|
|
112
|
+
loading: boolean;
|
|
113
|
+
error?: any;
|
|
114
|
+
};
|
|
115
|
+
type InputAndMap<T, I> = {
|
|
116
|
+
input: I;
|
|
117
|
+
map?: (input: Exclude<I, null | undefined>) => T;
|
|
118
|
+
ifUndefined?: T;
|
|
119
|
+
onlyOnce?: boolean;
|
|
120
|
+
};
|
|
121
|
+
type QueryAndMap<T, I> = {
|
|
122
|
+
query: Query<I>;
|
|
123
|
+
map: (input: Exclude<I, null | undefined>) => T;
|
|
124
|
+
ifUndefined?: T;
|
|
125
|
+
};
|
|
126
|
+
/**
|
|
127
|
+
* The opts has for `useFormState`.
|
|
128
|
+
*
|
|
129
|
+
* @typeparam T the form type, which is usually as close as possible to your *GraphQL input*
|
|
130
|
+
* @typeparam I the *form input* type, which is usually the *GraphQL output* type, i.e. the type of the response from your GraphQL query
|
|
131
|
+
*/
|
|
132
|
+
type UseFormStateOpts<T, I> = {
|
|
133
|
+
/** The form configuration, should be a module-level const or useMemo'd. */
|
|
134
|
+
config: ObjectConfig<T>;
|
|
135
|
+
/**
|
|
136
|
+
* Provides the form's initial value.
|
|
137
|
+
*
|
|
138
|
+
* User's can either:
|
|
139
|
+
*
|
|
140
|
+
* - Provide no initial value (don't set `init` at all)
|
|
141
|
+
* - Provide an initial value that already matches the form type `T` (i.e. set `init: data`)
|
|
142
|
+
* - Provide an initial value from an object that _almost_ matches the form type `T`,
|
|
143
|
+
* but needs to be mapped from it's input type `I` to the form type
|
|
144
|
+
* (i.e. set `init: { input: data, map: (data) => ...}`).
|
|
145
|
+
*
|
|
146
|
+
* The value of using the 3rd option is that: a) we internally `useMemo` on the identity of the
|
|
147
|
+
* `init.input` (i.e. a response from an Apollo hook) and don't require the `map` function
|
|
148
|
+
* to have a stable identity, and also b) we will null-check/undefined-check `init.input` and
|
|
149
|
+
* only call `init.map` if it's set, otherwise we'll use `init.ifDefined` or `{}`, saving you
|
|
150
|
+
* from having to null check within your `init.map` function.
|
|
151
|
+
*/
|
|
152
|
+
init?: InputAndMap<T, I> | QueryAndMap<T, I>;
|
|
153
|
+
/**
|
|
154
|
+
* A hook to add custom, cross-field validation rules that can be difficult to setup directly in the config DSL.
|
|
155
|
+
*
|
|
156
|
+
* Does not need to be stable/useMemo'd.
|
|
157
|
+
*/
|
|
158
|
+
addRules?: (state: ObjectState<T>) => void;
|
|
159
|
+
/** Whether the form should be read only, when changed it won't re-create the whole form. */
|
|
160
|
+
readOnly?: boolean;
|
|
161
|
+
/**
|
|
162
|
+
* Whether the form is loading.
|
|
163
|
+
*
|
|
164
|
+
* Note that we also will infer loading from `init.input === undefined` or `init.query.loading`,
|
|
165
|
+
* so you only need to set this directly if you're not using either of those conventions.
|
|
166
|
+
*/
|
|
167
|
+
loading?: boolean;
|
|
168
|
+
/**
|
|
169
|
+
* Fired when the form should auto-save, i.e. after a) blur and b) all fields are valid.
|
|
170
|
+
*
|
|
171
|
+
* Does not need to be stable/useMemo'd.
|
|
172
|
+
*/
|
|
173
|
+
autoSave?: (state: ObjectState<T>) => Promise<unknown>;
|
|
174
|
+
};
|
|
175
|
+
type FormObjectState<T, I> = ObjectState<T> & {
|
|
176
|
+
/**
|
|
177
|
+
* Refreshes the form with a server-acked value, i.e. `form.update(savedAuthor)`.
|
|
178
|
+
*
|
|
179
|
+
* This is intended for save callbacks that need `form.dirty` to become `false` immediately, before the
|
|
180
|
+
* normal query/cache refresh has a chance to rerender `useFormState`. Pass the value returned by the
|
|
181
|
+
* server so the form can map it through `init.map` and then do a field-by-field refresh of only the
|
|
182
|
+
* changes the server has acknowledged.
|
|
183
|
+
*/
|
|
184
|
+
update(input: Exclude<I, null | undefined>): void;
|
|
185
|
+
};
|
|
186
|
+
/**
|
|
187
|
+
* Creates a formState instance for editing in a form.
|
|
188
|
+
*/
|
|
189
|
+
export declare function useFormState<T, I>(opts: UseFormStateOpts<T, I>): FormObjectState<T, I>;
|
|
190
|
+
//#endregion
|
|
191
|
+
//#region src/utils.d.ts
|
|
192
|
+
type Builtin = Date | Function | SupportedTemporal | Uint8Array | string | number | boolean;
|
|
193
|
+
type OmitIf<Base, Condition> = Pick<Base, { [Key in keyof Base]: Base[Key] extends Condition ? never : Key; }[keyof Base]>;
|
|
194
|
+
//#endregion
|
|
195
|
+
//#region src/fields/objectField.d.ts
|
|
196
|
+
/**
|
|
197
|
+
* Wraps a given input/on-the-wire type `T` for editing in a form.
|
|
198
|
+
*
|
|
199
|
+
* We basically mimic every field in `T` (i.e. `firstName`, `lastName`, etc.) but decorate them
|
|
200
|
+
* with form-specific state like `touched`, `dirty`, `errors`, etc.
|
|
201
|
+
*
|
|
202
|
+
* The intent is that, after ensuring all fields are `valid`/etc., callers can take the
|
|
203
|
+
* result of this `objectState.value` (or `objectState.originalValue` for the non-proxy version) and
|
|
204
|
+
* have exactly the on-the-wire type `T` that they need to submit to the backend, without doing the
|
|
205
|
+
* manual mapping of "data that was in the form controls" into "data that the backend wants".
|
|
206
|
+
*
|
|
207
|
+
* Note that this can be hierarchical by having a field of `ListFieldState` that
|
|
208
|
+
* themselves each wrap an `ObjectState`, i.e.:
|
|
209
|
+
*
|
|
210
|
+
* ```
|
|
211
|
+
* ObjectState for author
|
|
212
|
+
* - firstName: FieldState
|
|
213
|
+
* - lastName: FieldState
|
|
214
|
+
* - rows: ListFieldState
|
|
215
|
+
* - [0]: ObjectState for book 1
|
|
216
|
+
* - title: FieldState
|
|
217
|
+
* - [1]: ObjectState for book 2
|
|
218
|
+
* - title: FieldState
|
|
219
|
+
* ```
|
|
220
|
+
*/
|
|
221
|
+
type ObjectState<T> = FieldStates<T> & FieldState<T> & {
|
|
222
|
+
/** Sets the state of fields in `state`. */
|
|
223
|
+
set(state: Partial<T>, opts?: SetOpts): void;
|
|
224
|
+
/** Returns whether the object can be saved, i.e. is valid, but also as a side-effect marks touched. */
|
|
225
|
+
canSave(): boolean;
|
|
226
|
+
};
|
|
227
|
+
declare const fragmentSym: unique symbol;
|
|
228
|
+
type Fragment<V> = V & {
|
|
229
|
+
[fragmentSym]: true;
|
|
230
|
+
};
|
|
231
|
+
export declare function fragment<V>(value: V): Fragment<V>;
|
|
232
|
+
/** For a given input type `T`, decorate each field into the "field state" type that holds our form-relevant state, i.e. valid/touched/etc. */
|
|
233
|
+
type FieldStates<T> = { [K in keyof T]-?: T[K] extends Fragment<infer V> ? FragmentField<V> : T[K] extends Array<infer U> | null | undefined ? [U] extends [Builtin] ? FieldState<T[K]> : ListFieldState<U> : T[K] extends Builtin | null | undefined ? FieldState<T[K]> : ObjectState<T[K]>; };
|
|
234
|
+
/**
|
|
235
|
+
* Creates a new `ObjectState` for a given form object `T` given config rules in `config`.
|
|
236
|
+
*
|
|
237
|
+
* The returned `ObjectState` can be used in a mobx `useLocalObservable` to drive an
|
|
238
|
+
* interactive form that tracks the current valid/touched/etc. state of both each
|
|
239
|
+
* individual fields as well as the top-level form/object itself.
|
|
240
|
+
*/
|
|
241
|
+
export declare function createObjectState<T>(config: ObjectConfig<T>, instance: T, opts?: {
|
|
242
|
+
maybeAutoSave?: () => void;
|
|
243
|
+
}): ObjectState<T>;
|
|
244
|
+
//#endregion
|
|
245
|
+
//#region src/config.d.ts
|
|
246
|
+
/**
|
|
247
|
+
* Config rules for each field in `T` that we're editing in a form.
|
|
248
|
+
*
|
|
249
|
+
* Basically every field is either a value/primitive or a list, and this `ObjectConfig` lets
|
|
250
|
+
* the caller define field-specific behavior, i.e. validation rules.
|
|
251
|
+
*/
|
|
252
|
+
type ObjectConfig<T> = { [P in keyof OmitIf<T, Function>]: T[P] extends Fragment<infer V> ? FragmentFieldConfig : T[P] extends Array<infer U> | null | undefined ? U extends Builtin ? ValueFieldConfig<T[P]> : ListFieldConfig<U> : ValueFieldConfig<T[P]> | ObjectFieldConfig<T[P]>; };
|
|
253
|
+
/** Field configuration for an opaque value that we don't actually want to include. */
|
|
254
|
+
type FragmentFieldConfig = {
|
|
255
|
+
type: "fragment";
|
|
256
|
+
};
|
|
257
|
+
/** Field configuration for primitive values, i.e. strings/numbers/Dates/user-defined types. */
|
|
258
|
+
type ValueFieldConfig<V> = {
|
|
259
|
+
type: "value";
|
|
260
|
+
rules?: Rule<V | null | undefined>[];
|
|
261
|
+
/**
|
|
262
|
+
* If true, marks this field as the id, which will be used for things like "always include in changedValue".
|
|
263
|
+
*
|
|
264
|
+
* Defaults to true for fields named `id`.
|
|
265
|
+
*/
|
|
266
|
+
isIdKey?: boolean;
|
|
267
|
+
/** If true, and this value is used on an entity in a list, the entity won't count towards the list validity. */
|
|
268
|
+
isDeleteKey?: boolean;
|
|
269
|
+
/** If true, the entity that contains this value will be treated as read only. */
|
|
270
|
+
isReadOnlyKey?: boolean;
|
|
271
|
+
/** If true, we ignore field in dirty checks. */
|
|
272
|
+
isLocalOnly?: boolean;
|
|
273
|
+
/**
|
|
274
|
+
* Marks a field as being backed by a mobx class computed field.
|
|
275
|
+
*
|
|
276
|
+
* Note that it might still be settable (some computed have setters), but we do
|
|
277
|
+
* exclude from the `reset` operation, i.e. we assume resetting other non-computed fields
|
|
278
|
+
* will effectively reset this field as well.
|
|
279
|
+
*/
|
|
280
|
+
computed?: boolean;
|
|
281
|
+
/** Marks a field as being initially read-only, i.e. `field.readOnly = true/false` can change this default. */
|
|
282
|
+
readOnly?: boolean;
|
|
283
|
+
/** Marks an array field to be order agnostic when determining changed/dirty states, defaults true. */
|
|
284
|
+
strictOrder?: false;
|
|
285
|
+
};
|
|
286
|
+
/** Field configuration for list values, i.e. `U` is `Book` in a form with `books: Book[]`. */
|
|
287
|
+
type ListFieldConfig<U> = {
|
|
288
|
+
type: "list";
|
|
289
|
+
/** Rules that can run on the full list of children. */
|
|
290
|
+
rules?: Rule<readonly ObjectState<U>[]>[];
|
|
291
|
+
/** Config for each child's form state, i.e. each book. */
|
|
292
|
+
config: ObjectConfig<U>;
|
|
293
|
+
/**
|
|
294
|
+
* What the server-side update behavior is for this collection.
|
|
295
|
+
*
|
|
296
|
+
* When exhaustive, we include all rows in `changedValue` so that we
|
|
297
|
+
* don't orhan unchanged rows.
|
|
298
|
+
*
|
|
299
|
+
* When incremental, we only include changed rows in `changedValue`.
|
|
300
|
+
*
|
|
301
|
+
* When deep-exhaustive, we include all rows in `changedValue` and also
|
|
302
|
+
* all fields in the child entities, i.e. not just placeholder `{ id: "b:1" }`
|
|
303
|
+
* values for otherwise-unchanged children.
|
|
304
|
+
*
|
|
305
|
+
* Defaults to `exhaustive` b/c that is the safest and also Joist's
|
|
306
|
+
* default behavior.
|
|
307
|
+
*/
|
|
308
|
+
update?: "exhaustive" | "incremental" | "deep-exhaustive";
|
|
309
|
+
/** Set to not consider the order of the list when evaluating changed/dirty states, defaults true. */
|
|
310
|
+
strictOrder?: false;
|
|
311
|
+
};
|
|
312
|
+
type ObjectFieldConfig<U> = {
|
|
313
|
+
type: "object";
|
|
314
|
+
/** Marks an object as a reference, which means we'll only include it's `id` in `changedValue` output. */
|
|
315
|
+
reference?: boolean;
|
|
316
|
+
/** Config for the child's form state, i.e. each book. */
|
|
317
|
+
config: ObjectConfig<U>;
|
|
318
|
+
};
|
|
319
|
+
//#endregion
|
|
320
|
+
//#region src/configBuilders.d.ts
|
|
321
|
+
/**
|
|
322
|
+
* Provides a Zod-ish API for building form configs.
|
|
323
|
+
*/
|
|
324
|
+
export declare const f: {
|
|
325
|
+
/** Creates the top-level config for a form, like an author or book. */
|
|
326
|
+
config<T>(fields: ObjectConfigBuilderFields<T>): ObjectConfig<T>;
|
|
327
|
+
/** Creates the config DSL for an object, like an author or book. */
|
|
328
|
+
object<T>(fields: ObjectConfigBuilderFields<T>): ObjectConfigBuilder<T>;
|
|
329
|
+
/** Creates the config DSL for an list of objects, like an author's list of books. */
|
|
330
|
+
list<U>(fields: ObjectConfigBuilderFields<U>): ListFieldConfigBuilder<U>;
|
|
331
|
+
/** Creates the config DSL for a primitive value, like a first name or phone number. */
|
|
332
|
+
value<V>(): ValueFieldConfigBuilder<V>;
|
|
333
|
+
/** A shortcut for creating a child object with a single `id` key. */
|
|
334
|
+
reference<V extends {
|
|
335
|
+
id?: unknown;
|
|
336
|
+
}>(fields?: ObjectConfigBuilderFields<V>): ObjectConfigBuilder<V>;
|
|
337
|
+
/** A shorthand for creating a computed value. */
|
|
338
|
+
computed<V>(): ValueFieldConfigBuilder<V>;
|
|
339
|
+
};
|
|
340
|
+
/**
|
|
341
|
+
* Defines the fields that should be passed into `f.object({ ... })` for fhe form type `T`.
|
|
342
|
+
*
|
|
343
|
+
* This is basically a map of `key` in `T` to `f.value()`, `f.list()`, or `f.object()`
|
|
344
|
+
* DSL objects that define the runtime structure/fields of the form.
|
|
345
|
+
*/
|
|
346
|
+
type ObjectConfigBuilderFields<T> = { [P in keyof OmitIf<T, Function>]: T[P] extends Fragment<infer V> ? FragmentFieldConfigBuilder : T[P] extends Array<infer U> | null | undefined ? U extends Builtin ? ValueFieldConfigBuilder<T[P]> : ListFieldConfigBuilder<U> : ValueFieldConfigBuilder<T[P]> | ObjectConfigBuilder<T[P]>; };
|
|
347
|
+
interface FragmentFieldConfigBuilder {
|
|
348
|
+
build(): FragmentFieldConfig;
|
|
349
|
+
}
|
|
350
|
+
/** Provides a fluent DSL for building up an object's config. */
|
|
351
|
+
declare class ObjectConfigBuilder<T> {
|
|
352
|
+
private config;
|
|
353
|
+
constructor(fields: ObjectConfigBuilderFields<T>);
|
|
354
|
+
ref(): this;
|
|
355
|
+
build(): ObjectFieldConfig<T>;
|
|
356
|
+
}
|
|
357
|
+
/** Provides a fluent DSL for building up a value's config. */
|
|
358
|
+
declare class ValueFieldConfigBuilder<V> {
|
|
359
|
+
private config;
|
|
360
|
+
/** Marks the field as required, keeping any rules that were already added. */
|
|
361
|
+
req(): this;
|
|
362
|
+
/** Marks the field as read only. */
|
|
363
|
+
readOnly(): this;
|
|
364
|
+
/** Marks the field as local only. */
|
|
365
|
+
localOnly(): this;
|
|
366
|
+
/** Appends `rules` to the field's validation rules. */
|
|
367
|
+
rules(rules: Rule<V | null | undefined>[]): this;
|
|
368
|
+
/** Marks the field as computed. */
|
|
369
|
+
computed(): this;
|
|
370
|
+
build(): ValueFieldConfig<V>;
|
|
371
|
+
}
|
|
372
|
+
/** Provides a fluent DSL for building up a list's config. */
|
|
373
|
+
declare class ListFieldConfigBuilder<U> {
|
|
374
|
+
private config;
|
|
375
|
+
constructor(fields: ObjectConfigBuilderFields<U>);
|
|
376
|
+
/** Appends `rule` to the field's validation rules. */
|
|
377
|
+
rule(rule: Rule<readonly ObjectState<U>[]>): this;
|
|
378
|
+
/** Appends `rules` to the field's validation rules. */
|
|
379
|
+
rules(rules: Rule<readonly ObjectState<U>[]>[]): this;
|
|
380
|
+
build(): ListFieldConfig<U>;
|
|
381
|
+
}
|
|
382
|
+
//#endregion
|
|
383
|
+
//#region src/useFormStates.d.ts
|
|
384
|
+
/**
|
|
385
|
+
* The opts has for `useFormStates`.
|
|
386
|
+
*
|
|
387
|
+
* @typeparam T the form type, which is usually as close as possible to your *GraphQL input*
|
|
388
|
+
* @typeparam I the *form input* type, which is usually the *GraphQL output* type, i.e. the type of the response from your GraphQL query
|
|
389
|
+
*/
|
|
390
|
+
type UseFormStatesOpts<T, I> = {
|
|
391
|
+
/**
|
|
392
|
+
* The config to use for each form state.
|
|
393
|
+
*
|
|
394
|
+
* Should be stable/useMemo'd.
|
|
395
|
+
*/
|
|
396
|
+
config: ObjectConfig<T>;
|
|
397
|
+
/**
|
|
398
|
+
* Fired for each individual `ObjectState` when it's had a value change.
|
|
399
|
+
*
|
|
400
|
+
* Does not need to be stable/useMemo'd.
|
|
401
|
+
*/
|
|
402
|
+
autoSave?: (state: ObjectState<T>) => Promise<void>;
|
|
403
|
+
/**
|
|
404
|
+
* A hook to add custom, cross-field validation rules that can be difficult to setup directly in the config DSL.
|
|
405
|
+
*
|
|
406
|
+
* This will be called once-per `ObjectState` instance, and so is effectively a `useEffect` hook with
|
|
407
|
+
* a `[config, objectState]` dependency.
|
|
408
|
+
*
|
|
409
|
+
* Does not need to be stable/useMemo'd.
|
|
410
|
+
*/
|
|
411
|
+
addRules?: (state: ObjectState<T>) => void;
|
|
412
|
+
/**
|
|
413
|
+
* Given an input to `getFormState`, returns the identity value that we'll cache that value's form state on.
|
|
414
|
+
*
|
|
415
|
+
* Does not need to be stable/useMemo'd.
|
|
416
|
+
*/
|
|
417
|
+
getId: (v: I) => string;
|
|
418
|
+
/**
|
|
419
|
+
* Maps an input to `getFormState` to the actual form shape `T`.
|
|
420
|
+
*
|
|
421
|
+
* Does not need to be stable/useMemo'd.
|
|
422
|
+
*/
|
|
423
|
+
map?: (input: Exclude<I, null | undefined>) => T;
|
|
424
|
+
/**
|
|
425
|
+
* Sets all `ObjectState`s to readOnly.
|
|
426
|
+
*/
|
|
427
|
+
readOnly?: boolean;
|
|
428
|
+
};
|
|
429
|
+
type UseFormStatesHook<T, I> = {
|
|
430
|
+
getFormState: (input: I, opts?: {
|
|
431
|
+
readOnly?: boolean;
|
|
432
|
+
}) => ObjectState<T>;
|
|
433
|
+
};
|
|
434
|
+
/**
|
|
435
|
+
* A hook to manage many "mini-forms" on a single page, typically one form per row
|
|
436
|
+
* in a table.
|
|
437
|
+
*
|
|
438
|
+
* This hook basically provides the page/table with a cache, so each table row naively ask "what's
|
|
439
|
+
* the form state for this given row's data?" and get back a new-or-existing `ObjectState` instance
|
|
440
|
+
* that, if already existing, still has any of the user's WIP changes.
|
|
441
|
+
*
|
|
442
|
+
* Each mini-form/row can have its own autoSave calls, independent of the other rows.
|
|
443
|
+
*
|
|
444
|
+
* @typeparam T the form type, which is usually as close as possible to your *GraphQL input*
|
|
445
|
+
* @typeparam I the *form input* type, which is usually the *GraphQL output* type, i.e. the type of the response from your GraphQL query
|
|
446
|
+
*/
|
|
447
|
+
export declare function useFormStates<T, I = T>(opts: UseFormStatesOpts<T, I>): UseFormStatesHook<T, I>;
|
|
448
|
+
//#endregion
|
|
449
|
+
export type { FieldState, FormObjectState, Fragment, FragmentField, ListFieldState, ObjectConfig, ObjectState, Rule, ValueAdapter };
|
|
450
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","names":[],"sources":["../src/fields/fragmentField.ts","../src/rules.ts","../src/fields/valueField.ts","../src/fields/listField.ts","../src/temporal.ts","../src/useFormState.ts","../src/utils.ts","../src/fields/objectField.ts","../src/config.ts","../src/configBuilders.ts","../src/useFormStates.ts"],"mappings":"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"}
|