@lotics/ui 1.4.0 → 1.5.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/package.json +2 -1
- package/src/fonts.css +15 -13
- package/src/use_form.ts +285 -0
package/src/use_form.ts
ADDED
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
import { useCallback, useMemo, useReducer, useRef } from "react";
|
|
2
|
+
|
|
3
|
+
export interface UseFormProps<T> {
|
|
4
|
+
initialValues: T;
|
|
5
|
+
validate?: (
|
|
6
|
+
values: T,
|
|
7
|
+
) =>
|
|
8
|
+
| FormErrors<T>
|
|
9
|
+
| Promise<FormErrors<T>>
|
|
10
|
+
| undefined
|
|
11
|
+
| Promise<undefined>
|
|
12
|
+
| void
|
|
13
|
+
| Promise<void>;
|
|
14
|
+
onSubmit?: (values: T, helpers: FormSetters<T>) => Promise<void> | void;
|
|
15
|
+
onChange?: (values: T) => Promise<void> | void;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface FormHandler<T> extends FormSetters<T> {
|
|
19
|
+
submit: () => Promise<void>;
|
|
20
|
+
reset: () => void;
|
|
21
|
+
validate: () => void;
|
|
22
|
+
values: T;
|
|
23
|
+
errors: FormErrors<T>;
|
|
24
|
+
changes: FormChanged<T>;
|
|
25
|
+
submitting: boolean;
|
|
26
|
+
submitCount: number;
|
|
27
|
+
changed: boolean;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface FormSetters<T> {
|
|
31
|
+
setFieldError: (field: keyof T, message?: string) => void;
|
|
32
|
+
setFieldValue: {
|
|
33
|
+
(field: keyof T): (value: T[keyof T]) => void;
|
|
34
|
+
(field: keyof T, value: T[keyof T]): void;
|
|
35
|
+
};
|
|
36
|
+
setValues: (values: Partial<T>) => void;
|
|
37
|
+
setSubmitting: (submitting: boolean) => void;
|
|
38
|
+
setErrors: (errors: FormErrors<T>) => void;
|
|
39
|
+
reset: () => void;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
interface FormState<T> {
|
|
43
|
+
edits: Partial<T>;
|
|
44
|
+
errors: FormErrors<T>;
|
|
45
|
+
submitting: boolean;
|
|
46
|
+
submitCount: number;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export type FormErrors<T> = { [key in keyof T]?: string };
|
|
50
|
+
export type FormChanged<T> = { [key in keyof T]?: boolean };
|
|
51
|
+
|
|
52
|
+
type Action<T> =
|
|
53
|
+
| {
|
|
54
|
+
type: "SET_FIELD_VALUE";
|
|
55
|
+
field: keyof T;
|
|
56
|
+
value: T[keyof T];
|
|
57
|
+
}
|
|
58
|
+
| {
|
|
59
|
+
type: "SET_VALUES";
|
|
60
|
+
values: Partial<T>;
|
|
61
|
+
}
|
|
62
|
+
| {
|
|
63
|
+
type: "SET_FIELD_ERROR";
|
|
64
|
+
field: keyof T;
|
|
65
|
+
message?: string;
|
|
66
|
+
}
|
|
67
|
+
| {
|
|
68
|
+
type: "SET_ERRORS";
|
|
69
|
+
errors: FormErrors<T>;
|
|
70
|
+
}
|
|
71
|
+
| {
|
|
72
|
+
type: "SUBMIT";
|
|
73
|
+
}
|
|
74
|
+
| {
|
|
75
|
+
type: "SET_SUBMITTING";
|
|
76
|
+
submitting: boolean;
|
|
77
|
+
}
|
|
78
|
+
| {
|
|
79
|
+
type: "RESET";
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
function reducer<T>(prevState: FormState<T>, action: Action<T>): FormState<T> {
|
|
83
|
+
switch (action.type) {
|
|
84
|
+
case "SET_FIELD_VALUE": {
|
|
85
|
+
const errors = { ...prevState.errors };
|
|
86
|
+
delete errors[action.field];
|
|
87
|
+
return {
|
|
88
|
+
...prevState,
|
|
89
|
+
edits: { ...prevState.edits, [action.field]: action.value },
|
|
90
|
+
errors,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
case "SET_VALUES": {
|
|
94
|
+
const errors = { ...prevState.errors };
|
|
95
|
+
for (const key of Object.keys(action.values)) {
|
|
96
|
+
delete errors[key as keyof T];
|
|
97
|
+
}
|
|
98
|
+
return {
|
|
99
|
+
...prevState,
|
|
100
|
+
edits: { ...prevState.edits, ...action.values },
|
|
101
|
+
errors,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
case "SET_ERRORS":
|
|
105
|
+
return { ...prevState, errors: { ...action.errors } };
|
|
106
|
+
case "SET_FIELD_ERROR": {
|
|
107
|
+
const errors = { ...prevState.errors };
|
|
108
|
+
if (action.message === undefined) {
|
|
109
|
+
delete errors[action.field];
|
|
110
|
+
} else {
|
|
111
|
+
errors[action.field] = action.message;
|
|
112
|
+
}
|
|
113
|
+
return { ...prevState, errors };
|
|
114
|
+
}
|
|
115
|
+
case "SET_SUBMITTING":
|
|
116
|
+
if (prevState.submitting === action.submitting) return prevState;
|
|
117
|
+
return { ...prevState, submitting: action.submitting };
|
|
118
|
+
case "SUBMIT":
|
|
119
|
+
return {
|
|
120
|
+
...prevState,
|
|
121
|
+
errors: {},
|
|
122
|
+
submitting: false,
|
|
123
|
+
submitCount: prevState.submitCount + 1,
|
|
124
|
+
};
|
|
125
|
+
case "RESET":
|
|
126
|
+
return { edits: {}, errors: {}, submitting: false, submitCount: 0 };
|
|
127
|
+
default:
|
|
128
|
+
throw new Error("Action type not resolved");
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function createEmptyState<T>(): FormState<T> {
|
|
133
|
+
return { edits: {}, errors: {}, submitting: false, submitCount: 0 };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function useForm<T>(props: UseFormProps<T>): FormHandler<T> {
|
|
137
|
+
const { initialValues, validate, onSubmit, onChange } = props;
|
|
138
|
+
const [state, dispatch] = useReducer(reducer<T>, null, createEmptyState<T>);
|
|
139
|
+
const submittingRef = useRef(false);
|
|
140
|
+
|
|
141
|
+
// Values derived from props + user edits. When initialValues changes
|
|
142
|
+
// (e.g. SWR revalidation), non-edited fields automatically reflect
|
|
143
|
+
// the new server data. No synchronization needed.
|
|
144
|
+
const values = useMemo(
|
|
145
|
+
(): T => ({ ...initialValues, ...state.edits }),
|
|
146
|
+
[initialValues, state.edits],
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
const { errors, submitting, submitCount } = state;
|
|
150
|
+
|
|
151
|
+
const changes = useMemo((): FormChanged<T> => {
|
|
152
|
+
const result: FormChanged<T> = {};
|
|
153
|
+
for (const key of Object.keys(state.edits)) {
|
|
154
|
+
result[key as keyof T] = true;
|
|
155
|
+
}
|
|
156
|
+
return result;
|
|
157
|
+
}, [state.edits]);
|
|
158
|
+
|
|
159
|
+
const changed = Object.keys(state.edits).length > 0;
|
|
160
|
+
|
|
161
|
+
const setErrors = useCallback((nextErrors: FormErrors<T>) => {
|
|
162
|
+
dispatch({ type: "SET_ERRORS", errors: nextErrors });
|
|
163
|
+
}, []);
|
|
164
|
+
|
|
165
|
+
const setFieldError = useCallback((field: keyof T, message?: string): void => {
|
|
166
|
+
dispatch({ type: "SET_FIELD_ERROR", field, message });
|
|
167
|
+
}, []);
|
|
168
|
+
|
|
169
|
+
const setSubmitting = useCallback((newSubmitting: boolean) => {
|
|
170
|
+
dispatch({ type: "SET_SUBMITTING", submitting: newSubmitting });
|
|
171
|
+
}, []);
|
|
172
|
+
|
|
173
|
+
const setFieldValue = useCallback(
|
|
174
|
+
((...args: [field: keyof T, value?: T[keyof T]]) => {
|
|
175
|
+
const [field, value] = args;
|
|
176
|
+
|
|
177
|
+
if (args.length === 1) {
|
|
178
|
+
return (val: T[keyof T]) => {
|
|
179
|
+
dispatch({ type: "SET_FIELD_VALUE", field, value: val });
|
|
180
|
+
onChange?.({ ...values, [field]: val });
|
|
181
|
+
};
|
|
182
|
+
} else {
|
|
183
|
+
dispatch({
|
|
184
|
+
type: "SET_FIELD_VALUE",
|
|
185
|
+
field,
|
|
186
|
+
value: value as T[keyof T],
|
|
187
|
+
});
|
|
188
|
+
onChange?.({ ...values, [field]: value });
|
|
189
|
+
}
|
|
190
|
+
}) as {
|
|
191
|
+
(field: keyof T): (value: T[keyof T]) => void;
|
|
192
|
+
(field: keyof T, value: T[keyof T]): void;
|
|
193
|
+
},
|
|
194
|
+
[onChange, values],
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
const setValues = useCallback(
|
|
198
|
+
(newValues: Partial<T>) => {
|
|
199
|
+
dispatch({ type: "SET_VALUES", values: newValues });
|
|
200
|
+
onChange?.({ ...values, ...newValues });
|
|
201
|
+
},
|
|
202
|
+
[onChange, values],
|
|
203
|
+
);
|
|
204
|
+
|
|
205
|
+
const reset = useCallback(() => {
|
|
206
|
+
dispatch({ type: "RESET" });
|
|
207
|
+
}, []);
|
|
208
|
+
|
|
209
|
+
const handleValidate = useCallback(async () => {
|
|
210
|
+
if (validate !== undefined) {
|
|
211
|
+
setErrors((await validate(values)) || {});
|
|
212
|
+
}
|
|
213
|
+
}, [validate, values, setErrors]);
|
|
214
|
+
|
|
215
|
+
const formHelpers = useMemo(
|
|
216
|
+
(): FormSetters<T> => ({
|
|
217
|
+
setFieldError,
|
|
218
|
+
setFieldValue,
|
|
219
|
+
setValues,
|
|
220
|
+
setErrors,
|
|
221
|
+
setSubmitting,
|
|
222
|
+
reset,
|
|
223
|
+
}),
|
|
224
|
+
[setValues, setFieldError, setFieldValue, setErrors, setSubmitting, reset],
|
|
225
|
+
);
|
|
226
|
+
|
|
227
|
+
const submit = useCallback(async () => {
|
|
228
|
+
if (!onSubmit) return;
|
|
229
|
+
if (submittingRef.current) return;
|
|
230
|
+
|
|
231
|
+
if (validate !== undefined) {
|
|
232
|
+
const nextErrors = await validate(values);
|
|
233
|
+
|
|
234
|
+
if (nextErrors && Object.keys(nextErrors).length) {
|
|
235
|
+
setErrors(nextErrors);
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
submittingRef.current = true;
|
|
241
|
+
dispatch({ type: "SET_SUBMITTING", submitting: true });
|
|
242
|
+
try {
|
|
243
|
+
await onSubmit(values, formHelpers);
|
|
244
|
+
dispatch({ type: "SUBMIT" });
|
|
245
|
+
} finally {
|
|
246
|
+
submittingRef.current = false;
|
|
247
|
+
dispatch({ type: "SET_SUBMITTING", submitting: false });
|
|
248
|
+
}
|
|
249
|
+
}, [onSubmit, validate, setErrors, values, formHelpers]);
|
|
250
|
+
|
|
251
|
+
return useMemo(
|
|
252
|
+
() => ({
|
|
253
|
+
setValues,
|
|
254
|
+
setFieldValue,
|
|
255
|
+
setFieldError,
|
|
256
|
+
setErrors,
|
|
257
|
+
setSubmitting,
|
|
258
|
+
submit,
|
|
259
|
+
reset,
|
|
260
|
+
values,
|
|
261
|
+
errors,
|
|
262
|
+
changes,
|
|
263
|
+
submitting,
|
|
264
|
+
validate: handleValidate,
|
|
265
|
+
submitCount,
|
|
266
|
+
changed,
|
|
267
|
+
}),
|
|
268
|
+
[
|
|
269
|
+
setValues,
|
|
270
|
+
setFieldValue,
|
|
271
|
+
setFieldError,
|
|
272
|
+
setErrors,
|
|
273
|
+
setSubmitting,
|
|
274
|
+
submit,
|
|
275
|
+
reset,
|
|
276
|
+
values,
|
|
277
|
+
errors,
|
|
278
|
+
changes,
|
|
279
|
+
submitting,
|
|
280
|
+
handleValidate,
|
|
281
|
+
submitCount,
|
|
282
|
+
changed,
|
|
283
|
+
],
|
|
284
|
+
);
|
|
285
|
+
}
|