solid-tiny-form 0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 solid tiny
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,96 @@
1
+ # solid-tiny-form
2
+
3
+ A tiny SolidJS form state manager inspired by `react-hook-form`, but designed around Solid's fine-grained reactivity and `createStore`.
4
+
5
+ ## Features
6
+
7
+ - `createForm` API for registering fields, setting values, submitting, resetting, and validating.
8
+ - Form state is stored in a Solid store from `solid-js/store`.
9
+ - Pluggable resolver interface for schema validation libraries.
10
+ - Built-in ArkType resolver via `arktypeResolver`.
11
+ - Lightweight field-level rules for common cases.
12
+
13
+ ## Installation
14
+
15
+ ```sh
16
+ pnpm add solid-tiny-form solid-js
17
+ ```
18
+
19
+ Install ArkType when using the optional ArkType resolver:
20
+
21
+ ```sh
22
+ pnpm add arktype
23
+ ```
24
+
25
+ The ArkType adapter supports `arktype@^2.1.27` and is exposed from `solid-tiny-form/resolvers/arktype`.
26
+
27
+ ## Usage
28
+
29
+ ```tsx
30
+ import { type } from "arktype";
31
+ import { createForm } from "solid-tiny-form";
32
+ import { arktypeResolver } from "solid-tiny-form/resolvers/arktype";
33
+
34
+ const schema = type({
35
+ email: "string.email",
36
+ age: "number >= 18",
37
+ });
38
+
39
+ function Example() {
40
+ const form = createForm({
41
+ defaultValues: {
42
+ email: "",
43
+ age: 18,
44
+ },
45
+ resolver: arktypeResolver(schema),
46
+ mode: "onBlur",
47
+ });
48
+
49
+ const onSubmit = form.handleSubmit((values) => {
50
+ console.log(values);
51
+ });
52
+
53
+ return (
54
+ <form onSubmit={onSubmit}>
55
+ <input {...form.register("email")} />
56
+ <input type="number" {...form.register("age", { valueAsNumber: true })} />
57
+ <button disabled={form.formState.isSubmitting}>Submit</button>
58
+ </form>
59
+ );
60
+ }
61
+ ```
62
+
63
+ ## API
64
+
65
+ ### `createForm(options)`
66
+
67
+ Creates a form controller.
68
+
69
+ ```ts
70
+ const form = createForm({
71
+ defaultValues,
72
+ resolver,
73
+ context,
74
+ mode: "onSubmit",
75
+ });
76
+ ```
77
+
78
+ Validation modes are `onSubmit`, `onBlur`, `onChange`, and `all`.
79
+
80
+ ### Resolver interface
81
+
82
+ Resolvers receive current values and return parsed values plus field errors.
83
+
84
+ ```ts
85
+ type Resolver<TValues, TContext> = (
86
+ values: TValues,
87
+ context: {
88
+ name?: string;
89
+ context?: TContext;
90
+ }
91
+ ) =>
92
+ | { values: TValues; errors: Record<string, FieldError> }
93
+ | Promise<{ values: TValues; errors: Record<string, FieldError> }>;
94
+ ```
95
+
96
+ This keeps the core form independent from any validation library.
@@ -0,0 +1,7 @@
1
+ import { a as FieldPath, c as FieldValues, d as RegisterOptions, f as Resolver, g as ValidationMode, h as SetValueOptions, i as FieldFlags, l as Form, m as ResolverResult, n as FieldError, o as FieldPathValue, p as ResolverContext, r as FieldErrors, s as FieldRegistration, t as CreateFormOptions, u as FormState } from "./types-CzHtMISe.mjs";
2
+
3
+ //#region src/create-form.d.ts
4
+ /** Create a Solid-first form controller backed by createStore. */
5
+ declare function createForm<TValues extends FieldValues = FieldValues, TContext = unknown>(options?: CreateFormOptions<TValues, TContext>): Form<TValues>;
6
+ //#endregion
7
+ export { type CreateFormOptions, type FieldError, type FieldErrors, type FieldFlags, type FieldPath, type FieldPathValue, type FieldRegistration, type FieldValues, type Form, type FormState, type RegisterOptions, type Resolver, type ResolverContext, type ResolverResult, type SetValueOptions, type ValidationMode, createForm };
package/dist/index.mjs ADDED
@@ -0,0 +1,329 @@
1
+ import { createStore, produce, reconcile, unwrap } from "solid-js/store";
2
+ //#region src/create-form/async.ts
3
+ /** Run a promise from a synchronous event handler and surface failures globally. */
4
+ function runAsync(task) {
5
+ task.catch((error) => {
6
+ queueMicrotask(() => {
7
+ throw error;
8
+ });
9
+ });
10
+ }
11
+ //#endregion
12
+ //#region src/utils.ts
13
+ /** Return a dot path split into store path segments. */
14
+ function toPath(name) {
15
+ return name.split(".").filter(Boolean);
16
+ }
17
+ /** Read a nested value from an object by dot path. */
18
+ function getPath(source, name) {
19
+ let value = source;
20
+ for (const key of toPath(name)) {
21
+ if (value == null || typeof value !== "object") return;
22
+ value = value[key];
23
+ }
24
+ return value;
25
+ }
26
+ /** Set a nested value on a cloned object by dot path. */
27
+ function setPath(source, name, value) {
28
+ const root = cloneValue(source);
29
+ const path = toPath(name);
30
+ let cursor = root;
31
+ for (const [index, key] of path.entries()) {
32
+ if (index === path.length - 1) {
33
+ cursor[key] = value;
34
+ break;
35
+ }
36
+ const next = cursor[key];
37
+ if (next == null || typeof next !== "object" || Array.isArray(next)) cursor[key] = {};
38
+ cursor = cursor[key];
39
+ }
40
+ return root;
41
+ }
42
+ /** Create a JSON-compatible deep clone. */
43
+ function cloneValue(value) {
44
+ if (value == null || typeof value !== "object") return value;
45
+ return JSON.parse(JSON.stringify(value));
46
+ }
47
+ /** Compare two JSON-compatible values. */
48
+ function isEqual(left, right) {
49
+ return JSON.stringify(left) === JSON.stringify(right);
50
+ }
51
+ /** Return true when an object has no own enumerable keys. */
52
+ function isEmptyObject(value) {
53
+ return Object.keys(value).length === 0;
54
+ }
55
+ /** Extract a form value from a DOM event or raw value. */
56
+ function getEventValue(valueOrEvent) {
57
+ if (!isEventLike(valueOrEvent)) return valueOrEvent;
58
+ const target = valueOrEvent.currentTarget ?? valueOrEvent.target;
59
+ if (!isValueTarget(target)) return;
60
+ if (target.type === "checkbox") return Boolean(target.checked);
61
+ if (target.type === "number") return target.value === "" ? void 0 : Number(target.value);
62
+ return target.value;
63
+ }
64
+ function isEventLike(value) {
65
+ return value != null && typeof value === "object" && ("target" in value || "currentTarget" in value);
66
+ }
67
+ function isValueTarget(target) {
68
+ return target != null && typeof target === "object" && ("value" in target || "checked" in target);
69
+ }
70
+ //#endregion
71
+ //#region src/create-form/state.ts
72
+ /** Create the initial form state object. */
73
+ function createInitialFormState(defaultValues) {
74
+ return {
75
+ defaultValues,
76
+ dirtyFields: {},
77
+ errors: {},
78
+ isDirty: false,
79
+ isSubmitted: false,
80
+ isSubmitting: false,
81
+ isValid: true,
82
+ isValidating: false,
83
+ submitCount: 0,
84
+ touchedFields: {},
85
+ values: cloneValue(defaultValues)
86
+ };
87
+ }
88
+ /** Merge full or single-field validation errors into current form errors. */
89
+ function mergeValidationErrors(currentErrors, name, errors) {
90
+ if (!name) return errors;
91
+ const nextErrors = {
92
+ ...unwrap(currentErrors),
93
+ [name]: errors[name]
94
+ };
95
+ if (!errors[name]) delete nextErrors[name];
96
+ return nextErrors;
97
+ }
98
+ /** Calculate the next dirty field map after one field value changes. */
99
+ function createNextDirtyFields(dirtyFields, values, defaultValues, name) {
100
+ const nextDirtyFields = { ...unwrap(dirtyFields) };
101
+ if (!isEqual(getPath(values, name), getPath(defaultValues, name))) nextDirtyFields[name] = true;
102
+ else delete nextDirtyFields[name];
103
+ return {
104
+ dirtyFields: nextDirtyFields,
105
+ isDirty: !isEmptyObject(nextDirtyFields)
106
+ };
107
+ }
108
+ //#endregion
109
+ //#region src/validation/rules.ts
110
+ /** Validate one field with built-in registration rules. */
111
+ async function validateField(name, value, values, options) {
112
+ return validateRequired(name, value, options) ?? validateMin(name, value, options) ?? validateMax(name, value, options) ?? validatePattern(name, value, options) ?? await validateCustom(name, value, values, options);
113
+ }
114
+ function validateRequired(name, value, options) {
115
+ if (!(options.required && isEmpty(value))) return;
116
+ return {
117
+ message: typeof options.required === "string" ? options.required : `${name} is required`,
118
+ type: "required"
119
+ };
120
+ }
121
+ function validateMin(name, value, options) {
122
+ const min = readRule(options.min);
123
+ if (!min || isEmpty(value) || Number(value) >= min.value) return;
124
+ return {
125
+ message: min.message ?? `${name} must be at least ${min.value}`,
126
+ type: "min"
127
+ };
128
+ }
129
+ function validateMax(name, value, options) {
130
+ const max = readRule(options.max);
131
+ if (!max || isEmpty(value) || Number(value) <= max.value) return;
132
+ return {
133
+ message: max.message ?? `${name} must be at most ${max.value}`,
134
+ type: "max"
135
+ };
136
+ }
137
+ function validatePattern(name, value, options) {
138
+ const pattern = readPattern(options.pattern);
139
+ if (!pattern || typeof value !== "string" || pattern.value.test(value)) return;
140
+ return {
141
+ message: pattern.message ?? `${name} is invalid`,
142
+ type: "pattern"
143
+ };
144
+ }
145
+ async function validateCustom(name, value, values, options) {
146
+ if (!options.validate) return;
147
+ const result = await options.validate(value, values);
148
+ if (result === true) return;
149
+ if (typeof result === "string") return {
150
+ message: result,
151
+ type: "validate"
152
+ };
153
+ if (typeof result === "boolean") return {
154
+ message: `${name} is invalid`,
155
+ type: "validate"
156
+ };
157
+ return result;
158
+ }
159
+ function isEmpty(value) {
160
+ return value == null || value === "" || Array.isArray(value) && value.length === 0;
161
+ }
162
+ function readRule(rule) {
163
+ if (typeof rule === "number") return {
164
+ value: rule,
165
+ message: void 0
166
+ };
167
+ return rule;
168
+ }
169
+ function readPattern(rule) {
170
+ if (rule instanceof RegExp) return {
171
+ value: rule,
172
+ message: void 0
173
+ };
174
+ return rule;
175
+ }
176
+ //#endregion
177
+ //#region src/create-form.ts
178
+ /** Create a Solid-first form controller backed by createStore. */
179
+ function createForm(options = {}) {
180
+ const mode = options.mode ?? "onSubmit";
181
+ const fieldOptions = /* @__PURE__ */ new Map();
182
+ const [formState, setFormState] = createStore(createInitialFormState(cloneValue(options.defaultValues ?? {})));
183
+ function register(name, options) {
184
+ if (options) fieldOptions.set(name, options);
185
+ return {
186
+ name,
187
+ get value() {
188
+ return getValue(name);
189
+ },
190
+ get checked() {
191
+ return Boolean(getValue(name));
192
+ },
193
+ onInput(event) {
194
+ runAsync(setValue(name, readInputValue(name, event), { shouldValidate: mode === "onChange" || mode === "all" }));
195
+ },
196
+ onChange(event) {
197
+ runAsync(setValue(name, readInputValue(name, event), { shouldValidate: mode === "onChange" || mode === "all" }));
198
+ },
199
+ onBlur() {
200
+ setFormState("touchedFields", produce((touchedFields) => {
201
+ touchedFields[name] = true;
202
+ }));
203
+ if (mode === "onBlur" || mode === "all") runAsync(trigger(name));
204
+ }
205
+ };
206
+ }
207
+ function getValues() {
208
+ return unwrap(formState.values);
209
+ }
210
+ function getValue(name) {
211
+ return getPath(formState.values, name);
212
+ }
213
+ async function setValue(name, value, setValueOptions = {}) {
214
+ setFormState("values", setPath(getValues(), name, value));
215
+ if (setValueOptions.shouldTouch) setFormState("touchedFields", produce((touchedFields) => {
216
+ touchedFields[name] = true;
217
+ }));
218
+ if (setValueOptions.shouldDirty ?? true) updateDirtyField(name);
219
+ if (setValueOptions.shouldValidate) await trigger(name);
220
+ }
221
+ function setError(name, error) {
222
+ setFormState("errors", produce((errors) => {
223
+ errors[name] = error;
224
+ }));
225
+ setFormState("isValid", false);
226
+ }
227
+ function clearErrors(name) {
228
+ if (name) setFormState("errors", produce((errors) => {
229
+ delete errors[name];
230
+ }));
231
+ else setFormState("errors", reconcile({}));
232
+ setFormState("isValid", isEmptyObject(formState.errors));
233
+ }
234
+ async function trigger(name) {
235
+ setFormState("isValidating", true);
236
+ const errors = await collectErrors(name, getValues());
237
+ const nextErrors = mergeValidationErrors(formState.errors, name, errors);
238
+ setFormState("errors", reconcile(nextErrors));
239
+ setFormState("isValid", isEmptyObject(nextErrors));
240
+ setFormState("isValidating", false);
241
+ return name ? !nextErrors[name] : isEmptyObject(errors);
242
+ }
243
+ async function collectErrors(name, values) {
244
+ const errors = {};
245
+ await collectRegisteredErrors(errors, name, values);
246
+ await collectResolverErrors(errors, name, values);
247
+ return errors;
248
+ }
249
+ async function collectRegisteredErrors(errors, name, values) {
250
+ for (const [fieldName, registerOptions] of fieldOptions) {
251
+ if (name && fieldName !== name) continue;
252
+ const error = await validateField(fieldName, getPath(values, fieldName), values, registerOptions);
253
+ if (error) errors[fieldName] = error;
254
+ }
255
+ }
256
+ async function collectResolverErrors(errors, name, values) {
257
+ if (!options.resolver) return;
258
+ const result = await options.resolver(values, {
259
+ name,
260
+ context: options.context
261
+ });
262
+ Object.assign(errors, name ? pickFieldErrors(result.errors, name) : result.errors);
263
+ if (isEmptyObject(result.errors)) setFormState("values", cloneValue(result.values));
264
+ }
265
+ function reset(values) {
266
+ const nextDefaultValues = cloneValue(values ?? options.defaultValues ?? {});
267
+ setFormState({
268
+ values: cloneValue(nextDefaultValues),
269
+ defaultValues: nextDefaultValues,
270
+ errors: {},
271
+ touchedFields: {},
272
+ dirtyFields: {},
273
+ isDirty: false,
274
+ isValid: true,
275
+ isValidating: false,
276
+ isSubmitting: false,
277
+ isSubmitted: false,
278
+ submitCount: 0
279
+ });
280
+ }
281
+ function handleSubmit(onValid, onInvalid) {
282
+ return async (event) => {
283
+ event?.preventDefault();
284
+ setFormState("submitCount", (count) => count + 1);
285
+ setFormState("isSubmitted", true);
286
+ setFormState("isSubmitting", true);
287
+ const isValid = await trigger();
288
+ try {
289
+ if (isValid) await onValid(getValues(), event);
290
+ else await onInvalid?.(formState.errors, event);
291
+ } finally {
292
+ setFormState("isSubmitting", false);
293
+ }
294
+ };
295
+ }
296
+ function updateDirtyField(name) {
297
+ const nextDirty = createNextDirtyFields(formState.dirtyFields, formState.values, formState.defaultValues, name);
298
+ setFormState("dirtyFields", reconcile(nextDirty.dirtyFields));
299
+ setFormState("isDirty", nextDirty.isDirty);
300
+ }
301
+ function readInputValue(name, event) {
302
+ return transformValue(name, getEventValue(event));
303
+ }
304
+ function transformValue(name, value) {
305
+ const options = fieldOptions.get(name);
306
+ if (options?.setValueAs) return options.setValueAs(value);
307
+ if (options?.valueAsNumber) return value === "" || value == null ? void 0 : Number(value);
308
+ return value;
309
+ }
310
+ return {
311
+ formState,
312
+ setFormState,
313
+ register,
314
+ getValues,
315
+ getValue,
316
+ setValue,
317
+ setError,
318
+ clearErrors,
319
+ trigger,
320
+ reset,
321
+ handleSubmit
322
+ };
323
+ }
324
+ function pickFieldErrors(errors, name) {
325
+ if (!errors[name]) return {};
326
+ return { [name]: errors[name] };
327
+ }
328
+ //#endregion
329
+ export { createForm };
@@ -0,0 +1,10 @@
1
+ import { c as FieldValues, f as Resolver } from "../types-CzHtMISe.mjs";
2
+ import { Type } from "arktype";
3
+
4
+ //#region src/resolvers/arktype.d.ts
5
+ /** A supported ArkType schema. */
6
+ type ArkTypeSchema<TValues extends FieldValues = FieldValues> = Type<TValues>;
7
+ /** Create a resolver for ArkType schemas. */
8
+ declare function arktypeResolver<TValues extends FieldValues>(schema: ArkTypeSchema<TValues>): Resolver<TValues>;
9
+ //#endregion
10
+ export { ArkTypeSchema, arktypeResolver };
@@ -0,0 +1,41 @@
1
+ //#region src/resolvers/arktype.ts
2
+ /** Create a resolver for ArkType schemas. */
3
+ function arktypeResolver(schema) {
4
+ return (values) => {
5
+ const result = schema(values);
6
+ const errors = readArkTypeErrors(result);
7
+ return {
8
+ errors: errors ?? {},
9
+ values: errors ? values : result
10
+ };
11
+ };
12
+ }
13
+ function readArkTypeErrors(result) {
14
+ if (!isArkErrors(result)) return;
15
+ const errors = {};
16
+ collectByPath(errors, result.byPath);
17
+ if (Object.keys(errors).length === 0) errors.root = {
18
+ message: result.summary ?? result.toString(),
19
+ type: "arktype"
20
+ };
21
+ return errors;
22
+ }
23
+ function isArkErrors(value) {
24
+ return Array.isArray(value) && "byPath" in value && typeof value.summary === "string";
25
+ }
26
+ function collectByPath(errors, byPath) {
27
+ for (const [path, value] of Object.entries(byPath)) errors[path || "root"] = {
28
+ message: readMessage(value),
29
+ type: "arktype"
30
+ };
31
+ }
32
+ function readMessage(value) {
33
+ if (typeof value === "string") return value;
34
+ if (value && typeof value === "object") {
35
+ const candidate = value;
36
+ return candidate.message ?? candidate.summary ?? candidate.toString?.() ?? "Invalid field value";
37
+ }
38
+ return "Invalid field value";
39
+ }
40
+ //#endregion
41
+ export { arktypeResolver };
@@ -0,0 +1,153 @@
1
+ import { SetStoreFunction, Store } from "solid-js/store";
2
+
3
+ //#region src/types.d.ts
4
+ /** The object shape managed by a form. */
5
+ type FieldValues = object;
6
+ /** A dot-separated field path. */
7
+ type FieldPath<TValues extends FieldValues = FieldValues> = keyof TValues extends never ? string : ObjectPath<TValues>;
8
+ /** The value type represented by a dot-separated field path. */
9
+ type FieldPathValue<TValues extends FieldValues, TName extends FieldPath<TValues>> = PathValue<TValues, TName>;
10
+ type ObjectPath<TValue> = TValue extends FieldValues ? { [TKey in Extract<keyof TValue, string>]: TValue[TKey] extends FieldValues ? TKey | `${TKey}.${ObjectPath<TValue[TKey]>}` : TKey }[Extract<keyof TValue, string>] : never;
11
+ type PathValue<TValue, TName> = TName extends `${infer TKey}.${infer TRest}` ? TKey extends keyof TValue ? PathValue<TValue[TKey], TRest> : unknown : TName extends keyof TValue ? TValue[TName] : unknown;
12
+ /** A validation error associated with a field path. */
13
+ interface FieldError {
14
+ /** A human-readable error message. */
15
+ message: string;
16
+ /** The validation rule or source that produced the error. */
17
+ type?: string;
18
+ }
19
+ /** Validation errors keyed by dot-separated field path. */
20
+ type FieldErrors<_TValues extends FieldValues = FieldValues> = Record<string, FieldError | undefined>;
21
+ /** Field flags keyed by dot-separated field path. */
22
+ type FieldFlags<_TValues extends FieldValues = FieldValues> = Record<string, boolean | undefined>;
23
+ /** Current form state stored in a Solid store. */
24
+ interface FormState<TValues extends FieldValues = FieldValues> {
25
+ /** Initial values used to calculate dirty fields and reset the form. */
26
+ defaultValues: TValues;
27
+ /** Fields whose value differs from the default value. */
28
+ dirtyFields: FieldFlags<TValues>;
29
+ /** Current field errors. */
30
+ errors: FieldErrors<TValues>;
31
+ /** Whether any field is dirty. */
32
+ isDirty: boolean;
33
+ /** Whether a submit has been attempted. */
34
+ isSubmitted: boolean;
35
+ /** Whether submit handling is currently running. */
36
+ isSubmitting: boolean;
37
+ /** Whether the latest validation result contains no errors. */
38
+ isValid: boolean;
39
+ /** Whether validation is currently running. */
40
+ isValidating: boolean;
41
+ /** Number of submit attempts. */
42
+ submitCount: number;
43
+ /** Fields that have received a blur event. */
44
+ touchedFields: FieldFlags<TValues>;
45
+ /** Current field values. */
46
+ values: TValues;
47
+ }
48
+ /** Data returned from a validation resolver. */
49
+ interface ResolverResult<TValues extends FieldValues = FieldValues> {
50
+ /** Validation errors keyed by dot-separated field path. */
51
+ errors: FieldErrors<TValues>;
52
+ /** Parsed or transformed values. Use the input values when no transform is needed. */
53
+ values: TValues;
54
+ }
55
+ /** Context passed to a validation resolver. */
56
+ interface ResolverContext<TValues extends FieldValues = FieldValues, TContext = unknown> {
57
+ /** User-provided context from createForm options. */
58
+ context?: TContext;
59
+ /** Optional field name that triggered validation. */
60
+ name?: FieldPath<TValues>;
61
+ }
62
+ /** Pluggable validation function for schema libraries or custom logic. */
63
+ type Resolver<TValues extends FieldValues = FieldValues, TContext = unknown> = (values: TValues, context: ResolverContext<TValues, TContext>) => ResolverResult<TValues> | Promise<ResolverResult<TValues>>;
64
+ /** Built-in field-level validation rules. */
65
+ interface RegisterOptions<TValue = unknown, TValues extends FieldValues = FieldValues> {
66
+ /** Maximum string length or numeric value. */
67
+ max?: number | {
68
+ value: number;
69
+ message: string;
70
+ };
71
+ /** Minimum string length or numeric value. */
72
+ min?: number | {
73
+ value: number;
74
+ message: string;
75
+ };
76
+ /** Pattern that a string value must match. */
77
+ pattern?: RegExp | {
78
+ value: RegExp;
79
+ message: string;
80
+ };
81
+ /** Whether the field is required. */
82
+ required?: boolean | string;
83
+ /** Custom value transform before storing. */
84
+ setValueAs?: (value: unknown) => TValue;
85
+ /** Custom synchronous or asynchronous validation rule. */
86
+ validate?: (value: TValue, values: TValues) => boolean | string | FieldError | Promise<boolean | string | FieldError>;
87
+ /** Convert input values to numbers before storing them. */
88
+ valueAsNumber?: boolean;
89
+ }
90
+ /** Props and helpers returned by register. */
91
+ interface FieldRegistration<TValue = unknown> {
92
+ /** Reactive checked getter for checkbox inputs. */
93
+ readonly checked: boolean;
94
+ /** Field name. */
95
+ name: string;
96
+ /** Solid blur handler that marks the field as touched. */
97
+ onBlur: () => void;
98
+ /** Solid change handler that updates form state. */
99
+ onChange: (event: Event) => void;
100
+ /** Solid input handler that updates form state. */
101
+ onInput: (event: Event) => void;
102
+ /** Reactive value getter suitable for spreading into Solid JSX. */
103
+ readonly value: TValue;
104
+ }
105
+ /** Validation trigger mode. */
106
+ type ValidationMode = "onSubmit" | "onBlur" | "onChange" | "all";
107
+ /** Options used to create a form. */
108
+ interface CreateFormOptions<TValues extends FieldValues = FieldValues, TContext = unknown> {
109
+ /** Extra context passed to the resolver. */
110
+ context?: TContext;
111
+ /** Initial form values. */
112
+ defaultValues?: Partial<TValues>;
113
+ /** When validation should run automatically. Defaults to onSubmit. */
114
+ mode?: ValidationMode;
115
+ /** Schema or custom validation resolver. */
116
+ resolver?: Resolver<TValues, TContext>;
117
+ }
118
+ /** Public form object returned by createForm. */
119
+ interface Form<TValues extends FieldValues = FieldValues> {
120
+ /** Clear all errors or one field error. */
121
+ clearErrors: (name?: FieldPath<TValues>) => void;
122
+ /** Reactive form state. */
123
+ formState: Store<FormState<TValues>>;
124
+ /** Read a single field value. */
125
+ getValue: <TName extends FieldPath<TValues>>(name: TName) => FieldPathValue<TValues, TName>;
126
+ /** Read a field value or all values. */
127
+ getValues: () => TValues;
128
+ /** Submit helper that validates before calling the valid handler. */
129
+ handleSubmit: (onValid: (values: TValues, event?: Event) => void | Promise<void>, onInvalid?: (errors: FieldErrors<TValues>, event?: Event) => void | Promise<void>) => (event?: Event) => Promise<void>;
130
+ /** Register a field and receive Solid-friendly input props. */
131
+ register: <TName extends FieldPath<TValues>>(name: TName, options?: RegisterOptions<FieldPathValue<TValues, TName>, TValues>) => FieldRegistration<FieldPathValue<TValues, TName>>;
132
+ /** Reset values and form state. */
133
+ reset: (values?: Partial<TValues>) => void;
134
+ /** Set a field error. */
135
+ setError: (name: FieldPath<TValues>, error: FieldError) => void;
136
+ /** Low-level setter for the form state store. */
137
+ setFormState: SetStoreFunction<FormState<TValues>>;
138
+ /** Set a field value. */
139
+ setValue: <TName extends FieldPath<TValues>>(name: TName, value: FieldPathValue<TValues, TName>, options?: SetValueOptions) => Promise<void>;
140
+ /** Validate all fields or a single field. */
141
+ trigger: (name?: FieldPath<TValues>) => Promise<boolean>;
142
+ }
143
+ /** Options for setValue. */
144
+ interface SetValueOptions {
145
+ /** Recalculate dirty state. Defaults to true. */
146
+ shouldDirty?: boolean;
147
+ /** Mark the field as touched. */
148
+ shouldTouch?: boolean;
149
+ /** Trigger validation after the value is set. */
150
+ shouldValidate?: boolean;
151
+ }
152
+ //#endregion
153
+ export { FieldPath as a, FieldValues as c, RegisterOptions as d, Resolver as f, ValidationMode as g, SetValueOptions as h, FieldFlags as i, Form as l, ResolverResult as m, FieldError as n, FieldPathValue as o, ResolverContext as p, FieldErrors as r, FieldRegistration as s, CreateFormOptions as t, FormState as u };
package/package.json ADDED
@@ -0,0 +1,72 @@
1
+ {
2
+ "devDependencies": {
3
+ "@biomejs/biome": "2.4.16",
4
+ "@solidjs/testing-library": "^0.8.10",
5
+ "@testing-library/jest-dom": "^6.9.1",
6
+ "@types/node": "^25.9.2",
7
+ "@typescript/native-preview": "7.0.0-dev.20260608.1",
8
+ "@vitest/ui": "^4.1.8",
9
+ "arktype": "^2.2.0",
10
+ "bumpp": "^11.1.0",
11
+ "jsdom": "^29.1.1",
12
+ "tsdown": "^0.22.2",
13
+ "typescript": "^5.9.3",
14
+ "ultracite": "7.8.2",
15
+ "vite": "^8.0.16",
16
+ "vite-plugin-solid": "^2.11.12",
17
+ "vitest": "^4.1.8"
18
+ },
19
+ "name": "solid-tiny-form",
20
+ "peerDependencies": {
21
+ "arktype": "^2.1.27",
22
+ "solid-js": "^1.9.7"
23
+ },
24
+ "type": "module",
25
+ "version": "0.1.0",
26
+ "peerDependenciesMeta": {
27
+ "arktype": {
28
+ "optional": true
29
+ }
30
+ },
31
+ "description": "A tiny SolidJS form state manager with pluggable validation",
32
+ "author": "solid tiny",
33
+ "license": "MIT",
34
+ "homepage": "https://github.com/sxl-cc/solid-tiny-form#readme",
35
+ "repository": {
36
+ "type": "git",
37
+ "url": "git+https://github.com/sxl-cc/solid-tiny-form.git"
38
+ },
39
+ "bugs": {
40
+ "url": "https://github.com/sxl-cc/solid-tiny-form/issues"
41
+ },
42
+ "files": [
43
+ "dist"
44
+ ],
45
+ "sideEffects": false,
46
+ "keywords": [
47
+ "solidjs",
48
+ "solid",
49
+ "form",
50
+ "reactive",
51
+ "validation",
52
+ "arktype"
53
+ ],
54
+ "types": "./dist/index.d.mts",
55
+ "exports": {
56
+ ".": "./dist/index.mjs",
57
+ "./resolvers/arktype": "./dist/resolvers/arktype.mjs",
58
+ "./package.json": "./package.json"
59
+ },
60
+ "scripts": {
61
+ "build": "tsdown",
62
+ "dev": "vite -c ./playground/vite.config.ts",
63
+ "test": "vitest run",
64
+ "test:ui": "vitest --ui",
65
+ "tc": "tsgo --noEmit --skipLibCheck",
66
+ "bump": "pnpm bumpp --no-push",
67
+ "prepublish": "pnpm build",
68
+ "check": "ultracite check",
69
+ "check:error": "ultracite check --diagnostic-level error",
70
+ "fix": "ultracite fix"
71
+ }
72
+ }