@nikala-ui/hooks 0.9.11 → 0.10.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 +25 -0
- package/package.json +1 -1
- package/src/create-form.ts +62 -14
package/README.md
CHANGED
|
@@ -80,6 +80,31 @@ const [value, setValue] = createControllableSignal({
|
|
|
80
80
|
});
|
|
81
81
|
```
|
|
82
82
|
|
|
83
|
+
### Form validation and submission
|
|
84
|
+
|
|
85
|
+
`createForm` supports change-, blur-, and submit-time validation, typed checkbox/select event values, stale async validation protection, and submit error state:
|
|
86
|
+
|
|
87
|
+
```tsx
|
|
88
|
+
const form = createForm({
|
|
89
|
+
initialValues: { email: "", marketing: false },
|
|
90
|
+
validateOn: "blur",
|
|
91
|
+
validate: (values) => ({
|
|
92
|
+
...(!values.email.includes("@") && { email: "Enter a valid email." }),
|
|
93
|
+
}),
|
|
94
|
+
onSubmit: async (values) => saveProfile(values),
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
<Form onSubmit={form.handleSubmit} loading={form.isSubmitting()}>
|
|
98
|
+
<Input
|
|
99
|
+
value={form.values().email}
|
|
100
|
+
onInput={form.handleChange("email")}
|
|
101
|
+
onBlur={form.handleBlur("email")}
|
|
102
|
+
/>
|
|
103
|
+
<FormMessage form={form} name="email" />
|
|
104
|
+
{form.submitError() && <p>Could not save changes.</p>}
|
|
105
|
+
</Form>
|
|
106
|
+
```
|
|
107
|
+
|
|
83
108
|
---
|
|
84
109
|
|
|
85
110
|
## Documentation & Links
|
package/package.json
CHANGED
package/src/create-form.ts
CHANGED
|
@@ -2,12 +2,22 @@ import { createSignal, createMemo, type Accessor } from "solid-js";
|
|
|
2
2
|
|
|
3
3
|
export type FormErrors<T> = Partial<Record<keyof T, string>>;
|
|
4
4
|
export type FormTouched<T> = Partial<Record<keyof T, boolean>>;
|
|
5
|
+
export type ValidateOn = "change" | "blur" | "submit";
|
|
6
|
+
|
|
7
|
+
export type FormInputEvent = Event & {
|
|
8
|
+
currentTarget:
|
|
9
|
+
| HTMLInputElement
|
|
10
|
+
| HTMLTextAreaElement
|
|
11
|
+
| HTMLSelectElement;
|
|
12
|
+
};
|
|
5
13
|
|
|
6
14
|
export interface CreateFormOptions<T extends Record<string, any>> {
|
|
7
15
|
/** Initial form field values object */
|
|
8
16
|
initialValues: T;
|
|
9
17
|
/** Custom validation function returning error messages object */
|
|
10
18
|
validate?: (values: T) => FormErrors<T> | Promise<FormErrors<T>>;
|
|
19
|
+
/** Event that triggers validation. Defaults to "change". */
|
|
20
|
+
validateOn?: ValidateOn;
|
|
11
21
|
/** Submit handler callback invoked when validation succeeds */
|
|
12
22
|
onSubmit?: (values: T) => void | Promise<void>;
|
|
13
23
|
}
|
|
@@ -21,6 +31,8 @@ export interface CreateFormReturn<T extends Record<string, any>> {
|
|
|
21
31
|
touched: Accessor<FormTouched<T>>;
|
|
22
32
|
/** Accessor indicating if form is currently submitting */
|
|
23
33
|
isSubmitting: Accessor<boolean>;
|
|
34
|
+
/** Accessor containing an exception thrown by onSubmit, if any */
|
|
35
|
+
submitError: Accessor<unknown>;
|
|
24
36
|
/** Accessor indicating if form has zero validation errors */
|
|
25
37
|
isValid: Accessor<boolean>;
|
|
26
38
|
/** Accessor indicating if form values differ from initial values */
|
|
@@ -32,11 +44,11 @@ export interface CreateFormReturn<T extends Record<string, any>> {
|
|
|
32
44
|
/** Set specific form field touched state */
|
|
33
45
|
setFieldTouched: <K extends keyof T>(field: K, isTouched?: boolean) => void;
|
|
34
46
|
/** Input change event listener helper factory function */
|
|
35
|
-
handleChange: <K extends keyof T>(field: K) => (e:
|
|
47
|
+
handleChange: <K extends keyof T>(field: K) => (e: FormInputEvent) => void;
|
|
36
48
|
/** Input blur event listener helper factory function */
|
|
37
49
|
handleBlur: <K extends keyof T>(field: K) => () => void;
|
|
38
50
|
/** Form onSubmit event handler */
|
|
39
|
-
handleSubmit: (e?: Event) => void
|
|
51
|
+
handleSubmit: (e?: Event) => Promise<void>;
|
|
40
52
|
/** Reset form values, errors, and touched states to initial values */
|
|
41
53
|
resetForm: () => void;
|
|
42
54
|
}
|
|
@@ -55,6 +67,9 @@ export function createForm<T extends Record<string, any>>(
|
|
|
55
67
|
const [errors, setErrors] = createSignal<FormErrors<T>>({});
|
|
56
68
|
const [touched, setTouched] = createSignal<FormTouched<T>>({});
|
|
57
69
|
const [isSubmitting, setIsSubmitting] = createSignal(false);
|
|
70
|
+
const [submitError, setSubmitError] = createSignal<unknown>();
|
|
71
|
+
const validateOn = options.validateOn ?? "change";
|
|
72
|
+
let validationSequence = 0;
|
|
58
73
|
|
|
59
74
|
const isDirty = createMemo(() => {
|
|
60
75
|
const current = values();
|
|
@@ -67,17 +82,27 @@ export function createForm<T extends Record<string, any>>(
|
|
|
67
82
|
});
|
|
68
83
|
|
|
69
84
|
const runValidation = async (currentValues: T): Promise<FormErrors<T>> => {
|
|
70
|
-
|
|
85
|
+
const sequence = ++validationSequence;
|
|
86
|
+
if (!options.validate) {
|
|
87
|
+
setErrors(() => ({}));
|
|
88
|
+
return {};
|
|
89
|
+
}
|
|
90
|
+
|
|
71
91
|
const result = await options.validate(currentValues);
|
|
72
92
|
const newErrors = result || {};
|
|
73
|
-
|
|
93
|
+
// Ignore stale async validations that finished after a newer request.
|
|
94
|
+
if (sequence === validationSequence) {
|
|
95
|
+
setErrors(() => newErrors);
|
|
96
|
+
}
|
|
74
97
|
return newErrors;
|
|
75
98
|
};
|
|
76
99
|
|
|
77
100
|
const setFieldValue = <K extends keyof T>(field: K, value: T[K]) => {
|
|
78
101
|
const next = { ...values(), [field]: value };
|
|
79
102
|
setValues(() => next);
|
|
80
|
-
|
|
103
|
+
if (validateOn === "change") {
|
|
104
|
+
void runValidation(next);
|
|
105
|
+
}
|
|
81
106
|
};
|
|
82
107
|
|
|
83
108
|
const setFieldError = <K extends keyof T>(field: K, error: string | undefined) => {
|
|
@@ -94,21 +119,37 @@ export function createForm<T extends Record<string, any>>(
|
|
|
94
119
|
};
|
|
95
120
|
|
|
96
121
|
const handleChange = <K extends keyof T>(field: K) => {
|
|
97
|
-
return (e:
|
|
98
|
-
const
|
|
99
|
-
|
|
122
|
+
return (e: FormInputEvent) => {
|
|
123
|
+
const target = e.currentTarget;
|
|
124
|
+
let value: unknown = target.value;
|
|
125
|
+
|
|
126
|
+
if (
|
|
127
|
+
"checked" in target &&
|
|
128
|
+
((target as HTMLInputElement).type === "checkbox" ||
|
|
129
|
+
(target as HTMLInputElement).type === "radio")
|
|
130
|
+
) {
|
|
131
|
+
value = (target as HTMLInputElement).checked;
|
|
132
|
+
} else if ("selectedOptions" in target && target.multiple) {
|
|
133
|
+
value = Array.from(target.selectedOptions, (option) => option.value);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
setFieldValue(field, value as T[K]);
|
|
100
137
|
};
|
|
101
138
|
};
|
|
102
139
|
|
|
103
140
|
const handleBlur = <K extends keyof T>(field: K) => {
|
|
104
141
|
return () => {
|
|
105
142
|
setFieldTouched(field, true);
|
|
143
|
+
if (validateOn === "blur") {
|
|
144
|
+
void runValidation(values());
|
|
145
|
+
}
|
|
106
146
|
};
|
|
107
147
|
};
|
|
108
148
|
|
|
109
149
|
const handleSubmit = async (e?: Event) => {
|
|
110
150
|
e?.preventDefault();
|
|
111
151
|
setIsSubmitting(true);
|
|
152
|
+
setSubmitError(undefined);
|
|
112
153
|
|
|
113
154
|
// Mark all fields as touched on submit
|
|
114
155
|
const allTouched = Object.keys(values()).reduce((acc, key) => {
|
|
@@ -117,14 +158,18 @@ export function createForm<T extends Record<string, any>>(
|
|
|
117
158
|
}, {} as FormTouched<T>);
|
|
118
159
|
setTouched(() => allTouched);
|
|
119
160
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
if (options.onSubmit) {
|
|
123
|
-
|
|
161
|
+
try {
|
|
162
|
+
const validationErrors = await runValidation(values());
|
|
163
|
+
if (Object.keys(validationErrors).length === 0 && options.onSubmit) {
|
|
164
|
+
try {
|
|
165
|
+
await options.onSubmit(values());
|
|
166
|
+
} catch (error) {
|
|
167
|
+
setSubmitError(() => error);
|
|
168
|
+
}
|
|
124
169
|
}
|
|
170
|
+
} finally {
|
|
171
|
+
setIsSubmitting(false);
|
|
125
172
|
}
|
|
126
|
-
|
|
127
|
-
setIsSubmitting(false);
|
|
128
173
|
};
|
|
129
174
|
|
|
130
175
|
const resetForm = () => {
|
|
@@ -132,6 +177,8 @@ export function createForm<T extends Record<string, any>>(
|
|
|
132
177
|
setErrors(() => ({}));
|
|
133
178
|
setTouched(() => ({}));
|
|
134
179
|
setIsSubmitting(false);
|
|
180
|
+
setSubmitError(undefined);
|
|
181
|
+
validationSequence += 1;
|
|
135
182
|
};
|
|
136
183
|
|
|
137
184
|
return {
|
|
@@ -139,6 +186,7 @@ export function createForm<T extends Record<string, any>>(
|
|
|
139
186
|
errors,
|
|
140
187
|
touched,
|
|
141
188
|
isSubmitting,
|
|
189
|
+
submitError,
|
|
142
190
|
isValid,
|
|
143
191
|
isDirty,
|
|
144
192
|
setFieldValue,
|