@nikala-ui/hooks 0.9.12 → 0.10.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/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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nikala-ui/hooks",
3
- "version": "0.9.12",
3
+ "version": "0.10.1",
4
4
  "description": "Reactive SolidJS primitives and primitives for Nikala UI",
5
5
  "type": "module",
6
6
  "main": "./src/index.ts",
@@ -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: Event & { currentTarget: HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement }) => void;
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
- if (!options.validate) return {};
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
- setErrors(() => newErrors);
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
- runValidation(next);
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: Event & { currentTarget: HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement }) => {
98
- const val = e.currentTarget.value;
99
- setFieldValue(field, val as any);
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
- const validationErrors = await runValidation(values());
121
- if (Object.keys(validationErrors).length === 0) {
122
- if (options.onSubmit) {
123
- await options.onSubmit(values());
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,