@conform-to/react 1.9.0 → 1.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 CHANGED
@@ -7,23 +7,23 @@
7
7
  ╚══════╝ ╚═════╝ ╚═╝ ╚══╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝
8
8
  ```
9
9
 
10
- Version 1.9.0 / License MIT / Copyright (c) 2024 Edmund Hung
10
+ Version 1.10.0 / License MIT / Copyright (c) 2025 Edmund Hung
11
11
 
12
- A type-safe form validation library utilizing web fundamentals to progressively enhance HTML Forms with full support for server frameworks like Remix and Next.js.
12
+ Progressively enhance HTML forms with React. Build resilient, type-safe forms with no hassle using web standards.
13
13
 
14
- # Getting Started
14
+ ## Getting Started
15
15
 
16
16
  Check out the overview and tutorial at our website https://conform.guide
17
17
 
18
- # Features
18
+ ## Features
19
19
 
20
- - Progressive enhancement first APIs
21
- - Type-safe field inference
22
- - Fine-grained subscription
23
- - Built-in accessibility helpers
24
- - Automatic type coercion with Zod
20
+ - Full type safety with schema field inference
21
+ - Standard Schema support with enhanced Zod and Valibot integration
22
+ - Progressive enhancement first design with built-in accessibility features
23
+ - Native Server Actions support for Remix and Next.js
24
+ - Built on web standards for flexible composition with other tools
25
25
 
26
- # Documentation
26
+ ## Documentation
27
27
 
28
28
  - Validation: https://conform.guide/validation
29
29
  - Nested object and Array: https://conform.guide/complex-structures
@@ -31,6 +31,6 @@ Check out the overview and tutorial at our website https://conform.guide
31
31
  - Intent button: https://conform.guide/intent-button
32
32
  - Accessibility Guide: https://conform.guide/accessibility
33
33
 
34
- # Support
34
+ ## Support
35
35
 
36
36
  To report a bug, please open an issue on the repository at https://github.com/edmundhung/conform. For feature requests and questions, you can post them in the Discussions section.
@@ -1,6 +1,6 @@
1
1
  import { type Serialize, type SubmissionResult, serialize } from '@conform-to/dom/future';
2
2
  import { useEffect } from 'react';
3
- import type { FormContext, DefaultFieldMetadata, IntentDispatcher, FormMetadata, Fieldset, FormOptions, FieldName, Field, Control, Selector, UseFormDataOptions, ValidateHandler, ErrorHandler, SubmitHandler, FormState, FormRef } from './types';
3
+ import type { FormContext, DefaultMetadata, IntentDispatcher, FormMetadata, Fieldset, FormOptions, FieldName, FieldMetadata, Control, Selector, UseFormDataOptions, ValidateHandler, ErrorHandler, SubmitHandler, FormState, FormRef } from './types';
4
4
  export declare const FormConfig: import("react").Context<{
5
5
  intentName: string;
6
6
  observer: {
@@ -66,7 +66,7 @@ export declare function useConform<ErrorShape, Value = undefined>(formRef: FormR
66
66
  */
67
67
  export declare function useForm<FormShape extends Record<string, any> = Record<string, any>, ErrorShape = string, Value = undefined>(options: FormOptions<FormShape, ErrorShape, Value>): {
68
68
  form: FormMetadata<ErrorShape>;
69
- fields: Fieldset<FormShape, DefaultFieldMetadata<ErrorShape>>;
69
+ fields: Fieldset<FormShape, DefaultMetadata<ErrorShape>>;
70
70
  intent: IntentDispatcher;
71
71
  };
72
72
  /**
@@ -79,7 +79,7 @@ export declare function useForm<FormShape extends Record<string, any> = Record<s
79
79
  * function ErrorSummary() {
80
80
  * const form = useFormMetadata();
81
81
  *
82
- * if (!form.invalid) return null;
82
+ * if (form.valid) return null;
83
83
  *
84
84
  * return (
85
85
  * <div>Please fix {Object.keys(form.fieldErrors).length} errors</div>
@@ -112,7 +112,7 @@ export declare function useFormMetadata<ErrorShape = string[]>(options?: {
112
112
  */
113
113
  export declare function useField<FieldShape = any>(name: FieldName<FieldShape>, options?: {
114
114
  formId?: string;
115
- }): Field<FieldShape>;
115
+ }): FieldMetadata<FieldShape>;
116
116
  /**
117
117
  * A React hook that provides an intent dispatcher for programmatic form actions.
118
118
  * Intent dispatchers allow you to trigger form operations like validation, field updates,
@@ -127,7 +127,7 @@ function useConform(formRef, options) {
127
127
  }
128
128
  }, [formRef, state$1.resetKey]);
129
129
  react.useEffect(() => {
130
- if (!state$1.intendedValue) {
130
+ if (!state$1.clientIntendedValue) {
131
131
  return;
132
132
  }
133
133
  var formElement = dom.getFormElement(formRef);
@@ -136,9 +136,9 @@ function useConform(formRef, options) {
136
136
  console.error('Failed to update form value; No form element found');
137
137
  return;
138
138
  }
139
- dom.updateFormValue(formElement, state$1.intendedValue, optionsRef.current.serialize);
139
+ dom.updateFormValue(formElement, state$1.clientIntendedValue, optionsRef.current.serialize);
140
140
  lastIntentedValueRef.current = undefined;
141
- }, [formRef, state$1.intendedValue, optionsRef]);
141
+ }, [formRef, state$1.clientIntendedValue, optionsRef]);
142
142
  var handleSubmit = react.useCallback(event => {
143
143
  var _abortControllerRef$c2, _lastAsyncResultRef$c;
144
144
  var abortController = new AbortController();
@@ -172,9 +172,8 @@ function useConform(formRef, options) {
172
172
  }
173
173
 
174
174
  // Override submission value if the last intended value is not applied yet (i.e. batch updates)
175
- if (typeof lastIntentedValueRef.current !== 'undefined') {
176
- var _lastIntentedValueRef;
177
- submission.payload = (_lastIntentedValueRef = lastIntentedValueRef.current) !== null && _lastIntentedValueRef !== void 0 ? _lastIntentedValueRef : {};
175
+ if (lastIntentedValueRef.current != null) {
176
+ submission.payload = lastIntentedValueRef.current;
178
177
  }
179
178
  var intendedValue = intent.applyIntent(submission);
180
179
 
@@ -432,7 +431,7 @@ function useForm(options) {
432
431
  * function ErrorSummary() {
433
432
  * const form = useFormMetadata();
434
433
  *
435
- * if (!form.invalid) return null;
434
+ * if (form.valid) return null;
436
435
  *
437
436
  * return (
438
437
  * <div>Please fix {Object.keys(form.fieldErrors).length} errors</div>
@@ -123,7 +123,7 @@ function useConform(formRef, options) {
123
123
  }
124
124
  }, [formRef, state.resetKey]);
125
125
  useEffect(() => {
126
- if (!state.intendedValue) {
126
+ if (!state.clientIntendedValue) {
127
127
  return;
128
128
  }
129
129
  var formElement = getFormElement(formRef);
@@ -132,9 +132,9 @@ function useConform(formRef, options) {
132
132
  console.error('Failed to update form value; No form element found');
133
133
  return;
134
134
  }
135
- updateFormValue(formElement, state.intendedValue, optionsRef.current.serialize);
135
+ updateFormValue(formElement, state.clientIntendedValue, optionsRef.current.serialize);
136
136
  lastIntentedValueRef.current = undefined;
137
- }, [formRef, state.intendedValue, optionsRef]);
137
+ }, [formRef, state.clientIntendedValue, optionsRef]);
138
138
  var handleSubmit = useCallback(event => {
139
139
  var _abortControllerRef$c2, _lastAsyncResultRef$c;
140
140
  var abortController = new AbortController();
@@ -168,9 +168,8 @@ function useConform(formRef, options) {
168
168
  }
169
169
 
170
170
  // Override submission value if the last intended value is not applied yet (i.e. batch updates)
171
- if (typeof lastIntentedValueRef.current !== 'undefined') {
172
- var _lastIntentedValueRef;
173
- submission.payload = (_lastIntentedValueRef = lastIntentedValueRef.current) !== null && _lastIntentedValueRef !== void 0 ? _lastIntentedValueRef : {};
171
+ if (lastIntentedValueRef.current != null) {
172
+ submission.payload = lastIntentedValueRef.current;
174
173
  }
175
174
  var intendedValue = applyIntent(submission);
176
175
 
@@ -428,7 +427,7 @@ function useForm(options) {
428
427
  * function ErrorSummary() {
429
428
  * const form = useFormMetadata();
430
429
  *
431
- * if (!form.invalid) return null;
430
+ * if (form.valid) return null;
432
431
  *
433
432
  * return (
434
433
  * <div>Please fix {Object.keys(form.fieldErrors).length} errors</div>
@@ -1,5 +1,6 @@
1
- export type { FormValue, FormError } from '@conform-to/dom/future';
1
+ export type { FormError, FormValue, Submission, SubmissionResult, } from '@conform-to/dom/future';
2
2
  export { parseSubmission, report, isDirty } from '@conform-to/dom/future';
3
- export type { Control, FormOptions, Fieldset, DefaultValue } from './types';
3
+ export type { Control, DefaultValue, FormContext, FormMetadata, FormOptions, FormRef, FieldMetadata, FieldName, Fieldset, IntentDispatcher, } from './types';
4
4
  export { FormProvider, useControl, useField, useForm, useFormData, useFormMetadata, useIntent, } from './hooks';
5
+ export { memoize } from './memoize';
5
6
  //# sourceMappingURL=index.d.ts.map
@@ -4,6 +4,7 @@ Object.defineProperty(exports, '__esModule', { value: true });
4
4
 
5
5
  var future = require('@conform-to/dom/future');
6
6
  var hooks = require('./hooks.js');
7
+ var memoize = require('./memoize.js');
7
8
 
8
9
 
9
10
 
@@ -26,3 +27,4 @@ exports.useForm = hooks.useForm;
26
27
  exports.useFormData = hooks.useFormData;
27
28
  exports.useFormMetadata = hooks.useFormMetadata;
28
29
  exports.useIntent = hooks.useIntent;
30
+ exports.memoize = memoize.memoize;
@@ -1,2 +1,3 @@
1
1
  export { isDirty, parseSubmission, report } from '@conform-to/dom/future';
2
2
  export { FormProvider, useControl, useField, useForm, useFormData, useFormMetadata, useIntent } from './hooks.mjs';
3
+ export { memoize } from './memoize.mjs';
@@ -0,0 +1,49 @@
1
+ /**
2
+ * A memoized function with cache clearing capability.
3
+ */
4
+ export type Memoized<T extends (...args: any) => any> = {
5
+ (this: ThisParameterType<T>, ...args: Parameters<T>): ReturnType<T>;
6
+ /** Clears the memoization cache */
7
+ clearCache: () => void;
8
+ };
9
+ /**
10
+ * Default equality check that compares arguments using Object.is().
11
+ *
12
+ * @param prevArgs - Previous function arguments
13
+ * @param nextArgs - Current function arguments
14
+ * @returns True if all arguments are equal
15
+ */
16
+ export declare function defaultEqualityCheck(prevArgs: any[], nextArgs: any[]): boolean;
17
+ /**
18
+ * Memoizes function calls, caching only the most recent result to prevent redundant async validations.
19
+ *
20
+ * Built-in implementation based on memoize-one with enhanced async support.
21
+ * Can be replaced with other memoization libraries if needed.
22
+ *
23
+ * @param fn - The function to memoize
24
+ * @param isEqual - Custom equality function to compare arguments (defaults to shallow comparison)
25
+ * @returns Memoized function with cache clearing capability
26
+ *
27
+ * @example
28
+ * ```ts
29
+ * // Async validation with API call
30
+ * const validateUsername = useMemo(
31
+ * () => memoize(async function isUnique(username: string) {
32
+ * const response = await fetch(`/api/users/${username}`);
33
+ * return response.ok ? null : ['Username is already taken'];
34
+ * }),
35
+ * []
36
+ * );
37
+ *
38
+ * // Usage in form validation
39
+ * async onValidate({ payload, error }) {
40
+ * if (payload.username && !error.fieldErrors.username) {
41
+ * const messages = await validateUsername(value.username);
42
+ * if (messages) error.fieldErrors.username = messages;
43
+ * }
44
+ * return error;
45
+ * }
46
+ * ```
47
+ */
48
+ export declare function memoize<T extends (...args: any) => any>(fn: T, isEqual?: (prevArgs: Parameters<T>, nextArgs: Parameters<T>) => boolean): Memoized<T>;
49
+ //# sourceMappingURL=memoize.d.ts.map
@@ -0,0 +1,96 @@
1
+ 'use strict';
2
+
3
+ Object.defineProperty(exports, '__esModule', { value: true });
4
+
5
+ /**
6
+ * A memoized function with cache clearing capability.
7
+ */
8
+
9
+ /**
10
+ * Default equality check that compares arguments using Object.is().
11
+ *
12
+ * @param prevArgs - Previous function arguments
13
+ * @param nextArgs - Current function arguments
14
+ * @returns True if all arguments are equal
15
+ */
16
+ function defaultEqualityCheck(prevArgs, nextArgs) {
17
+ if (prevArgs.length !== nextArgs.length) {
18
+ return false;
19
+ }
20
+ for (var i = 0; i < prevArgs.length; i++) {
21
+ if (!Object.is(prevArgs[i], nextArgs[i])) {
22
+ return false;
23
+ }
24
+ }
25
+ return true;
26
+ }
27
+
28
+ /**
29
+ * Memoizes function calls, caching only the most recent result to prevent redundant async validations.
30
+ *
31
+ * Built-in implementation based on memoize-one with enhanced async support.
32
+ * Can be replaced with other memoization libraries if needed.
33
+ *
34
+ * @param fn - The function to memoize
35
+ * @param isEqual - Custom equality function to compare arguments (defaults to shallow comparison)
36
+ * @returns Memoized function with cache clearing capability
37
+ *
38
+ * @example
39
+ * ```ts
40
+ * // Async validation with API call
41
+ * const validateUsername = useMemo(
42
+ * () => memoize(async function isUnique(username: string) {
43
+ * const response = await fetch(`/api/users/${username}`);
44
+ * return response.ok ? null : ['Username is already taken'];
45
+ * }),
46
+ * []
47
+ * );
48
+ *
49
+ * // Usage in form validation
50
+ * async onValidate({ payload, error }) {
51
+ * if (payload.username && !error.fieldErrors.username) {
52
+ * const messages = await validateUsername(value.username);
53
+ * if (messages) error.fieldErrors.username = messages;
54
+ * }
55
+ * return error;
56
+ * }
57
+ * ```
58
+ */
59
+ function memoize(fn) {
60
+ var isEqual = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : defaultEqualityCheck;
61
+ var cache = null;
62
+ function memoized() {
63
+ for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {
64
+ args[_key] = arguments[_key];
65
+ }
66
+ // Check if new arguments match last arguments including the context (this)
67
+ if (cache && cache.this === this && isEqual(cache.args, args)) {
68
+ return cache.result;
69
+ }
70
+ var result = fn.apply(this, args);
71
+ if (result instanceof Promise) {
72
+ result = result.catch(e => {
73
+ // If the promise is rejected, clear the cache so that the next call will re-invoke fn
74
+ cache = null;
75
+
76
+ // Re-throw the exception so that it can be handled by the caller
77
+ throw e;
78
+ });
79
+ }
80
+
81
+ // Update the cache
82
+ cache = {
83
+ this: this,
84
+ args,
85
+ result
86
+ };
87
+ return result;
88
+ }
89
+ memoized.clearCache = function clearCache() {
90
+ cache = null;
91
+ };
92
+ return memoized;
93
+ }
94
+
95
+ exports.defaultEqualityCheck = defaultEqualityCheck;
96
+ exports.memoize = memoize;
@@ -0,0 +1,91 @@
1
+ /**
2
+ * A memoized function with cache clearing capability.
3
+ */
4
+
5
+ /**
6
+ * Default equality check that compares arguments using Object.is().
7
+ *
8
+ * @param prevArgs - Previous function arguments
9
+ * @param nextArgs - Current function arguments
10
+ * @returns True if all arguments are equal
11
+ */
12
+ function defaultEqualityCheck(prevArgs, nextArgs) {
13
+ if (prevArgs.length !== nextArgs.length) {
14
+ return false;
15
+ }
16
+ for (var i = 0; i < prevArgs.length; i++) {
17
+ if (!Object.is(prevArgs[i], nextArgs[i])) {
18
+ return false;
19
+ }
20
+ }
21
+ return true;
22
+ }
23
+
24
+ /**
25
+ * Memoizes function calls, caching only the most recent result to prevent redundant async validations.
26
+ *
27
+ * Built-in implementation based on memoize-one with enhanced async support.
28
+ * Can be replaced with other memoization libraries if needed.
29
+ *
30
+ * @param fn - The function to memoize
31
+ * @param isEqual - Custom equality function to compare arguments (defaults to shallow comparison)
32
+ * @returns Memoized function with cache clearing capability
33
+ *
34
+ * @example
35
+ * ```ts
36
+ * // Async validation with API call
37
+ * const validateUsername = useMemo(
38
+ * () => memoize(async function isUnique(username: string) {
39
+ * const response = await fetch(`/api/users/${username}`);
40
+ * return response.ok ? null : ['Username is already taken'];
41
+ * }),
42
+ * []
43
+ * );
44
+ *
45
+ * // Usage in form validation
46
+ * async onValidate({ payload, error }) {
47
+ * if (payload.username && !error.fieldErrors.username) {
48
+ * const messages = await validateUsername(value.username);
49
+ * if (messages) error.fieldErrors.username = messages;
50
+ * }
51
+ * return error;
52
+ * }
53
+ * ```
54
+ */
55
+ function memoize(fn) {
56
+ var isEqual = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : defaultEqualityCheck;
57
+ var cache = null;
58
+ function memoized() {
59
+ for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {
60
+ args[_key] = arguments[_key];
61
+ }
62
+ // Check if new arguments match last arguments including the context (this)
63
+ if (cache && cache.this === this && isEqual(cache.args, args)) {
64
+ return cache.result;
65
+ }
66
+ var result = fn.apply(this, args);
67
+ if (result instanceof Promise) {
68
+ result = result.catch(e => {
69
+ // If the promise is rejected, clear the cache so that the next call will re-invoke fn
70
+ cache = null;
71
+
72
+ // Re-throw the exception so that it can be handled by the caller
73
+ throw e;
74
+ });
75
+ }
76
+
77
+ // Update the cache
78
+ cache = {
79
+ this: this,
80
+ args,
81
+ result
82
+ };
83
+ return result;
84
+ }
85
+ memoized.clearCache = function clearCache() {
86
+ cache = null;
87
+ };
88
+ return memoized;
89
+ }
90
+
91
+ export { defaultEqualityCheck, memoize };
@@ -0,0 +1,56 @@
1
+ /** The Standard Schema interface. */
2
+ export interface StandardSchemaV1<Input = unknown, Output = Input> {
3
+ /** The Standard Schema properties. */
4
+ readonly '~standard': StandardSchemaV1.Props<Input, Output>;
5
+ }
6
+ export declare namespace StandardSchemaV1 {
7
+ /** The Standard Schema properties interface. */
8
+ interface Props<Input = unknown, Output = Input> {
9
+ /** The version number of the standard. */
10
+ readonly version: 1;
11
+ /** The vendor name of the schema library. */
12
+ readonly vendor: string;
13
+ /** Validates unknown input values. */
14
+ readonly validate: (value: unknown) => Result<Output> | Promise<Result<Output>>;
15
+ /** Inferred types associated with the schema. */
16
+ readonly types?: Types<Input, Output> | undefined;
17
+ }
18
+ /** The result interface of the validate function. */
19
+ type Result<Output> = SuccessResult<Output> | FailureResult;
20
+ /** The result interface if validation succeeds. */
21
+ interface SuccessResult<Output> {
22
+ /** The typed output value. */
23
+ readonly value: Output;
24
+ /** The non-existent issues. */
25
+ readonly issues?: undefined;
26
+ }
27
+ /** The result interface if validation fails. */
28
+ interface FailureResult {
29
+ /** The issues of failed validation. */
30
+ readonly issues: ReadonlyArray<Issue>;
31
+ }
32
+ /** The issue interface of the failure output. */
33
+ interface Issue {
34
+ /** The error message of the issue. */
35
+ readonly message: string;
36
+ /** The path of the issue, if any. */
37
+ readonly path?: ReadonlyArray<PropertyKey | PathSegment> | undefined;
38
+ }
39
+ /** The path segment interface of the issue. */
40
+ interface PathSegment {
41
+ /** The key representing a path segment. */
42
+ readonly key: PropertyKey;
43
+ }
44
+ /** The Standard Schema types interface. */
45
+ interface Types<Input = unknown, Output = Input> {
46
+ /** The input type of the schema. */
47
+ readonly input: Input;
48
+ /** The output type of the schema. */
49
+ readonly output: Output;
50
+ }
51
+ /** Infers the input type of a Standard Schema. */
52
+ type InferInput<Schema extends StandardSchemaV1> = NonNullable<Schema['~standard']['types']>['input'];
53
+ /** Infers the output type of a Standard Schema. */
54
+ type InferOutput<Schema extends StandardSchemaV1> = NonNullable<Schema['~standard']['types']>['output'];
55
+ }
56
+ //# sourceMappingURL=standard-schema.d.ts.map
@@ -1,5 +1,5 @@
1
1
  import { type ValidationAttributes, type Serialize } from '@conform-to/dom/future';
2
- import type { DefaultFieldMetadata, Field, FieldName, Fieldset, FormContext, FormMetadata, FormState, FormAction, UnknownIntent, ActionHandler } from './types';
2
+ import type { DefaultMetadata, FieldMetadata, FieldName, Fieldset, FormContext, FormMetadata, FormState, FormAction, UnknownIntent, ActionHandler } from './types';
3
3
  export declare function initializeState<ErrorShape>(): FormState<ErrorShape>;
4
4
  /**
5
5
  * Updates form state based on action type:
@@ -23,7 +23,9 @@ export declare function isDefaultChecked(context: FormContext<any>, name: string
23
23
  export declare function isTouched(state: FormState<any>, name?: string): boolean;
24
24
  export declare function getDefaultListKey(prefix: string, initialValue: Record<string, unknown> | null, name: string): string[];
25
25
  export declare function getListKey(context: FormContext<any>, name: string): string[];
26
- export declare function getError<ErrorShape>(state: FormState<ErrorShape>, name?: string): ErrorShape[] | undefined;
26
+ export declare function getErrors<ErrorShape>(state: FormState<ErrorShape>, name?: string): ErrorShape[] | undefined;
27
+ export declare function getFieldErrors<ErrorShape>(state: FormState<ErrorShape>, name?: string): Record<string, ErrorShape[]>;
28
+ export declare function isValid(state: FormState<any>, name?: string): boolean;
27
29
  /**
28
30
  * Gets validation constraint for a field, with fallback to parent array patterns.
29
31
  * e.g. "array[0].key" falls back to "array[].key" if specific constraint not found.
@@ -31,26 +33,26 @@ export declare function getError<ErrorShape>(state: FormState<ErrorShape>, name?
31
33
  export declare function getConstraint(context: FormContext<any>, name: string): ValidationAttributes | undefined;
32
34
  export declare function getFormMetadata<ErrorShape>(context: FormContext<ErrorShape>, options: {
33
35
  serialize: Serialize;
34
- }): FormMetadata<ErrorShape, DefaultFieldMetadata<ErrorShape>>;
36
+ }): FormMetadata<ErrorShape, DefaultMetadata<ErrorShape>>;
35
37
  export declare function getField<FieldShape, ErrorShape = string>(context: FormContext<ErrorShape>, options: {
36
38
  name: FieldName<FieldShape>;
37
39
  serialize: Serialize;
38
40
  key?: string;
39
- }): Field<FieldShape, DefaultFieldMetadata<ErrorShape>>;
41
+ }): FieldMetadata<FieldShape, DefaultMetadata<ErrorShape>>;
40
42
  /**
41
43
  * Creates a proxy that dynamically generates field objects when properties are accessed.
42
44
  */
43
45
  export declare function getFieldset<FieldShape = Record<string, any>, ErrorShape = string>(context: FormContext<ErrorShape>, options: {
44
46
  name?: FieldName<FieldShape>;
45
47
  serialize: Serialize;
46
- }): Fieldset<FieldShape, DefaultFieldMetadata<ErrorShape>>;
48
+ }): Fieldset<FieldShape, DefaultMetadata<ErrorShape>>;
47
49
  /**
48
50
  * Creates an array of field objects for list/array inputs
49
51
  */
50
52
  export declare function getFieldList<FieldShape = Array<any>, ErrorShape = string>(context: FormContext<ErrorShape>, options: {
51
53
  name: FieldName<FieldShape>;
52
54
  serialize: Serialize;
53
- }): Field<[
55
+ }): FieldMetadata<[
54
56
  FieldShape
55
- ] extends [Array<infer ItemShape> | null | undefined] ? ItemShape : unknown, DefaultFieldMetadata<ErrorShape>>[];
57
+ ] extends [Array<infer ItemShape> | null | undefined] ? ItemShape : unknown, DefaultMetadata<ErrorShape>>[];
56
58
  //# sourceMappingURL=state.d.ts.map
@@ -10,8 +10,8 @@ function initializeState() {
10
10
  return {
11
11
  resetKey: util.generateUniqueKey(),
12
12
  listKeys: {},
13
- intendedValue: null,
14
- serverValidatedValue: null,
13
+ clientIntendedValue: null,
14
+ serverIntendedValue: null,
15
15
  serverError: null,
16
16
  clientError: null,
17
17
  touchedFields: []
@@ -33,20 +33,20 @@ function updateState(state, action) {
33
33
 
34
34
  // Apply the form error and intended value from the result first
35
35
  state = action.type === 'client' ? util.merge(state, {
36
- intendedValue: !action.intent ? value : (_action$intendedValue2 = action.intendedValue) !== null && _action$intendedValue2 !== void 0 ? _action$intendedValue2 : state.intendedValue,
36
+ clientIntendedValue: (_action$intendedValue2 = action.intendedValue) !== null && _action$intendedValue2 !== void 0 ? _action$intendedValue2 : state.clientIntendedValue,
37
+ serverIntendedValue: action.intendedValue ? null : state.serverIntendedValue,
37
38
  // Update client error only if the error is different from the previous one to minimize unnecessary re-renders
38
39
  clientError: typeof action.error !== 'undefined' && !future.deepEqual(state.clientError, action.error) ? action.error : state.clientError,
39
40
  // Reset server error if form value is changed
40
- serverError: typeof action.error !== 'undefined' && !future.deepEqual(state.serverValidatedValue, value) ? null : state.serverError
41
+ serverError: typeof action.error !== 'undefined' && !future.deepEqual(state.serverIntendedValue, value) ? null : state.serverError
41
42
  }) : util.merge(state, {
42
- intendedValue: action.type === 'initialize' ? value : state.intendedValue,
43
43
  // Clear client error to avoid showing stale errors
44
44
  clientError: null,
45
45
  // Update server error if the error is defined.
46
46
  // There is no need to check if the error is different as we are updating other states as well
47
47
  serverError: typeof action.error !== 'undefined' ? action.error : state.serverError,
48
48
  // Keep track of the value that the serverError is based on
49
- serverValidatedValue: typeof action.error !== 'undefined' ? value : state.serverValidatedValue
49
+ serverIntendedValue: !future.deepEqual(state.serverIntendedValue, value) ? value : state.serverIntendedValue
50
50
  });
51
51
  if (action.type !== 'server' && typeof action.intent !== 'undefined') {
52
52
  var _action$intent, _action$ctx$handlers;
@@ -70,19 +70,19 @@ function updateState(state, action) {
70
70
  return state;
71
71
  }
72
72
  function getDefaultValue(context, name) {
73
- var _ref, _context$state$intend;
73
+ var _ref, _context$state$server;
74
74
  var serialize = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : future.serialize;
75
- var value = future.getValueAtPath((_ref = (_context$state$intend = context.state.intendedValue) !== null && _context$state$intend !== void 0 ? _context$state$intend : context.defaultValue) !== null && _ref !== void 0 ? _ref : {}, name);
75
+ var value = future.getValueAtPath((_ref = (_context$state$server = context.state.serverIntendedValue) !== null && _context$state$server !== void 0 ? _context$state$server : context.state.clientIntendedValue) !== null && _ref !== void 0 ? _ref : context.defaultValue, name);
76
76
  var serializedValue = serialize(value);
77
77
  if (typeof serializedValue === 'string') {
78
78
  return serializedValue;
79
79
  }
80
80
  }
81
81
  function getDefaultOptions(context, name) {
82
- var _ref2, _context$state$intend2;
82
+ var _ref2, _context$state$server2;
83
83
  var serialize = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : future.serialize;
84
- var value = future.getValueAtPath((_ref2 = (_context$state$intend2 = context.state.intendedValue) !== null && _context$state$intend2 !== void 0 ? _context$state$intend2 : context.defaultValue) !== null && _ref2 !== void 0 ? _ref2 : {}, name);
85
- var serializedValue = typeof value !== 'undefined' ? serialize(value) : undefined;
84
+ var value = future.getValueAtPath((_ref2 = (_context$state$server2 = context.state.serverIntendedValue) !== null && _context$state$server2 !== void 0 ? _context$state$server2 : context.state.clientIntendedValue) !== null && _ref2 !== void 0 ? _ref2 : context.defaultValue, name);
85
+ var serializedValue = serialize(value);
86
86
  if (Array.isArray(serializedValue) && serializedValue.every(item => typeof item === 'string')) {
87
87
  return serializedValue;
88
88
  }
@@ -91,10 +91,10 @@ function getDefaultOptions(context, name) {
91
91
  }
92
92
  }
93
93
  function isDefaultChecked(context, name) {
94
- var _ref3, _context$state$intend3;
94
+ var _ref3, _context$state$server3;
95
95
  var serialize = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : future.serialize;
96
- var value = future.getValueAtPath((_ref3 = (_context$state$intend3 = context.state.intendedValue) !== null && _context$state$intend3 !== void 0 ? _context$state$intend3 : context.defaultValue) !== null && _ref3 !== void 0 ? _ref3 : {}, name);
97
- var serializedValue = typeof value !== 'undefined' ? serialize(value) : undefined;
96
+ var value = future.getValueAtPath((_ref3 = (_context$state$server3 = context.state.serverIntendedValue) !== null && _context$state$server3 !== void 0 ? _context$state$server3 : context.state.clientIntendedValue) !== null && _ref3 !== void 0 ? _ref3 : context.defaultValue, name);
97
+ var serializedValue = serialize(value);
98
98
  return serializedValue === 'on';
99
99
  }
100
100
 
@@ -116,10 +116,10 @@ function getDefaultListKey(prefix, initialValue, name) {
116
116
  return util.getArrayAtPath(initialValue, name).map((_, index) => "".concat(prefix, "-").concat(future.appendPathSegment(name, index)));
117
117
  }
118
118
  function getListKey(context, name) {
119
- var _context$state$listKe, _context$state$listKe2, _context$state$intend4;
120
- return (_context$state$listKe = (_context$state$listKe2 = context.state.listKeys) === null || _context$state$listKe2 === void 0 ? void 0 : _context$state$listKe2[name]) !== null && _context$state$listKe !== void 0 ? _context$state$listKe : getDefaultListKey(context.state.resetKey, (_context$state$intend4 = context.state.intendedValue) !== null && _context$state$intend4 !== void 0 ? _context$state$intend4 : context.defaultValue, name);
119
+ var _context$state$listKe, _context$state$listKe2, _ref4, _context$state$server4;
120
+ return (_context$state$listKe = (_context$state$listKe2 = context.state.listKeys) === null || _context$state$listKe2 === void 0 ? void 0 : _context$state$listKe2[name]) !== null && _context$state$listKe !== void 0 ? _context$state$listKe : getDefaultListKey(context.state.resetKey, (_ref4 = (_context$state$server4 = context.state.serverIntendedValue) !== null && _context$state$server4 !== void 0 ? _context$state$server4 : context.state.clientIntendedValue) !== null && _ref4 !== void 0 ? _ref4 : context.defaultValue, name);
121
121
  }
122
- function getError(state, name) {
122
+ function getErrors(state, name) {
123
123
  var _state$serverError;
124
124
  var error = (_state$serverError = state.serverError) !== null && _state$serverError !== void 0 ? _state$serverError : state.clientError;
125
125
  if (!error || !isTouched(state, name)) {
@@ -130,6 +130,55 @@ function getError(state, name) {
130
130
  return errors;
131
131
  }
132
132
  }
133
+ function getFieldErrors(state, name) {
134
+ var _state$serverError2;
135
+ var result = {};
136
+ var error = (_state$serverError2 = state.serverError) !== null && _state$serverError2 !== void 0 ? _state$serverError2 : state.clientError;
137
+ if (error) {
138
+ var basePath = future.getPathSegments(name);
139
+ for (var field of Object.keys(error.fieldErrors)) {
140
+ var relativePath = future.getRelativePath(field, basePath);
141
+
142
+ // Only include errors for specified field's children
143
+ if (!relativePath || relativePath.length === 0) {
144
+ continue;
145
+ }
146
+ var _error = getErrors(state, field);
147
+ if (typeof _error !== 'undefined') {
148
+ result[future.formatPathSegments(relativePath)] = _error;
149
+ }
150
+ }
151
+ }
152
+ return result;
153
+ }
154
+ function isValid(state, name) {
155
+ var _state$serverError3;
156
+ var error = (_state$serverError3 = state.serverError) !== null && _state$serverError3 !== void 0 ? _state$serverError3 : state.clientError;
157
+
158
+ // If there is no error, it must be valid
159
+ if (!error) {
160
+ return true;
161
+ }
162
+ var basePath = future.getPathSegments(name);
163
+ for (var field of Object.keys(error.fieldErrors)) {
164
+ // When checking a specific field, only check that field and its children
165
+ if (name && !future.getRelativePath(field, basePath)) {
166
+ continue;
167
+ }
168
+
169
+ // If the field is not touched, we don't consider its error
170
+ var _error2 = getErrors(state, field);
171
+ if (_error2) {
172
+ return false;
173
+ }
174
+ }
175
+
176
+ // Make sure there is no form error when checking the whole form
177
+ if (!name) {
178
+ return !getErrors(state);
179
+ }
180
+ return true;
181
+ }
133
182
 
134
183
  /**
135
184
  * Gets validation constraint for a field, with fallback to parent array patterns.
@@ -160,29 +209,24 @@ function getConstraint(context, name) {
160
209
  }
161
210
  function getFormMetadata(context, options) {
162
211
  return {
212
+ key: context.state.resetKey,
163
213
  id: context.formId,
214
+ errorId: "".concat(context.formId, "-form-error"),
215
+ descriptionId: "".concat(context.formId, "-form-description"),
164
216
  get errors() {
165
- return getError(context.state);
217
+ return getErrors(context.state);
166
218
  },
167
219
  get fieldErrors() {
168
- var result = {};
169
- for (var name of context.state.touchedFields) {
170
- if (!name) {
171
- // Skip form-level errors
172
- continue;
173
- }
174
- var error = getError(context.state, name);
175
- if (typeof error !== 'undefined') {
176
- result[name] = error;
177
- }
178
- }
179
- return result;
220
+ return getFieldErrors(context.state);
180
221
  },
181
222
  get touched() {
182
223
  return isTouched(context.state);
183
224
  },
225
+ get valid() {
226
+ return isValid(context.state);
227
+ },
184
228
  get invalid() {
185
- return typeof getError(context.state) !== 'undefined';
229
+ return !this.valid;
186
230
  },
187
231
  props: {
188
232
  id: context.formId,
@@ -210,12 +254,13 @@ function getFormMetadata(context, options) {
210
254
  };
211
255
  }
212
256
  function getField(context, options) {
213
- var id = "".concat(context.formId, "-").concat(options.name);
257
+ var id = "".concat(context.formId, "-field-").concat(options.name.replace(/[^a-zA-Z0-9._-]/g, '_'));
214
258
  var constraint = getConstraint(context, options.name);
215
259
  var metadata = {
216
260
  id: id,
217
261
  descriptionId: "".concat(id, "-description"),
218
262
  errorId: "".concat(id, "-error"),
263
+ formId: context.formId,
219
264
  required: constraint === null || constraint === void 0 ? void 0 : constraint.required,
220
265
  minLength: constraint === null || constraint === void 0 ? void 0 : constraint.minLength,
221
266
  maxLength: constraint === null || constraint === void 0 ? void 0 : constraint.maxLength,
@@ -236,11 +281,17 @@ function getField(context, options) {
236
281
  get touched() {
237
282
  return isTouched(context.state, options.name);
238
283
  },
284
+ get valid() {
285
+ return isValid(context.state, options.name);
286
+ },
239
287
  get invalid() {
240
- return typeof getError(context.state, options.name) !== 'undefined';
288
+ return !this.valid;
241
289
  },
242
290
  get errors() {
243
- return getError(context.state, options.name);
291
+ return getErrors(context.state, options.name);
292
+ },
293
+ get fieldErrors() {
294
+ return getFieldErrors(context.state, options.name);
244
295
  }
245
296
  };
246
297
  return Object.assign(metadata, {
@@ -288,8 +339,9 @@ exports.getConstraint = getConstraint;
288
339
  exports.getDefaultListKey = getDefaultListKey;
289
340
  exports.getDefaultOptions = getDefaultOptions;
290
341
  exports.getDefaultValue = getDefaultValue;
291
- exports.getError = getError;
342
+ exports.getErrors = getErrors;
292
343
  exports.getField = getField;
344
+ exports.getFieldErrors = getFieldErrors;
293
345
  exports.getFieldList = getFieldList;
294
346
  exports.getFieldset = getFieldset;
295
347
  exports.getFormMetadata = getFormMetadata;
@@ -297,4 +349,5 @@ exports.getListKey = getListKey;
297
349
  exports.initializeState = initializeState;
298
350
  exports.isDefaultChecked = isDefaultChecked;
299
351
  exports.isTouched = isTouched;
352
+ exports.isValid = isValid;
300
353
  exports.updateState = updateState;
@@ -6,8 +6,8 @@ function initializeState() {
6
6
  return {
7
7
  resetKey: generateUniqueKey(),
8
8
  listKeys: {},
9
- intendedValue: null,
10
- serverValidatedValue: null,
9
+ clientIntendedValue: null,
10
+ serverIntendedValue: null,
11
11
  serverError: null,
12
12
  clientError: null,
13
13
  touchedFields: []
@@ -29,20 +29,20 @@ function updateState(state, action) {
29
29
 
30
30
  // Apply the form error and intended value from the result first
31
31
  state = action.type === 'client' ? merge(state, {
32
- intendedValue: !action.intent ? value : (_action$intendedValue2 = action.intendedValue) !== null && _action$intendedValue2 !== void 0 ? _action$intendedValue2 : state.intendedValue,
32
+ clientIntendedValue: (_action$intendedValue2 = action.intendedValue) !== null && _action$intendedValue2 !== void 0 ? _action$intendedValue2 : state.clientIntendedValue,
33
+ serverIntendedValue: action.intendedValue ? null : state.serverIntendedValue,
33
34
  // Update client error only if the error is different from the previous one to minimize unnecessary re-renders
34
35
  clientError: typeof action.error !== 'undefined' && !deepEqual(state.clientError, action.error) ? action.error : state.clientError,
35
36
  // Reset server error if form value is changed
36
- serverError: typeof action.error !== 'undefined' && !deepEqual(state.serverValidatedValue, value) ? null : state.serverError
37
+ serverError: typeof action.error !== 'undefined' && !deepEqual(state.serverIntendedValue, value) ? null : state.serverError
37
38
  }) : merge(state, {
38
- intendedValue: action.type === 'initialize' ? value : state.intendedValue,
39
39
  // Clear client error to avoid showing stale errors
40
40
  clientError: null,
41
41
  // Update server error if the error is defined.
42
42
  // There is no need to check if the error is different as we are updating other states as well
43
43
  serverError: typeof action.error !== 'undefined' ? action.error : state.serverError,
44
44
  // Keep track of the value that the serverError is based on
45
- serverValidatedValue: typeof action.error !== 'undefined' ? value : state.serverValidatedValue
45
+ serverIntendedValue: !deepEqual(state.serverIntendedValue, value) ? value : state.serverIntendedValue
46
46
  });
47
47
  if (action.type !== 'server' && typeof action.intent !== 'undefined') {
48
48
  var _action$intent, _action$ctx$handlers;
@@ -66,19 +66,19 @@ function updateState(state, action) {
66
66
  return state;
67
67
  }
68
68
  function getDefaultValue(context, name) {
69
- var _ref, _context$state$intend;
69
+ var _ref, _context$state$server;
70
70
  var serialize$1 = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : serialize;
71
- var value = getValueAtPath((_ref = (_context$state$intend = context.state.intendedValue) !== null && _context$state$intend !== void 0 ? _context$state$intend : context.defaultValue) !== null && _ref !== void 0 ? _ref : {}, name);
71
+ var value = getValueAtPath((_ref = (_context$state$server = context.state.serverIntendedValue) !== null && _context$state$server !== void 0 ? _context$state$server : context.state.clientIntendedValue) !== null && _ref !== void 0 ? _ref : context.defaultValue, name);
72
72
  var serializedValue = serialize$1(value);
73
73
  if (typeof serializedValue === 'string') {
74
74
  return serializedValue;
75
75
  }
76
76
  }
77
77
  function getDefaultOptions(context, name) {
78
- var _ref2, _context$state$intend2;
78
+ var _ref2, _context$state$server2;
79
79
  var serialize$1 = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : serialize;
80
- var value = getValueAtPath((_ref2 = (_context$state$intend2 = context.state.intendedValue) !== null && _context$state$intend2 !== void 0 ? _context$state$intend2 : context.defaultValue) !== null && _ref2 !== void 0 ? _ref2 : {}, name);
81
- var serializedValue = typeof value !== 'undefined' ? serialize$1(value) : undefined;
80
+ var value = getValueAtPath((_ref2 = (_context$state$server2 = context.state.serverIntendedValue) !== null && _context$state$server2 !== void 0 ? _context$state$server2 : context.state.clientIntendedValue) !== null && _ref2 !== void 0 ? _ref2 : context.defaultValue, name);
81
+ var serializedValue = serialize$1(value);
82
82
  if (Array.isArray(serializedValue) && serializedValue.every(item => typeof item === 'string')) {
83
83
  return serializedValue;
84
84
  }
@@ -87,10 +87,10 @@ function getDefaultOptions(context, name) {
87
87
  }
88
88
  }
89
89
  function isDefaultChecked(context, name) {
90
- var _ref3, _context$state$intend3;
90
+ var _ref3, _context$state$server3;
91
91
  var serialize$1 = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : serialize;
92
- var value = getValueAtPath((_ref3 = (_context$state$intend3 = context.state.intendedValue) !== null && _context$state$intend3 !== void 0 ? _context$state$intend3 : context.defaultValue) !== null && _ref3 !== void 0 ? _ref3 : {}, name);
93
- var serializedValue = typeof value !== 'undefined' ? serialize$1(value) : undefined;
92
+ var value = getValueAtPath((_ref3 = (_context$state$server3 = context.state.serverIntendedValue) !== null && _context$state$server3 !== void 0 ? _context$state$server3 : context.state.clientIntendedValue) !== null && _ref3 !== void 0 ? _ref3 : context.defaultValue, name);
93
+ var serializedValue = serialize$1(value);
94
94
  return serializedValue === 'on';
95
95
  }
96
96
 
@@ -112,10 +112,10 @@ function getDefaultListKey(prefix, initialValue, name) {
112
112
  return getArrayAtPath(initialValue, name).map((_, index) => "".concat(prefix, "-").concat(appendPathSegment(name, index)));
113
113
  }
114
114
  function getListKey(context, name) {
115
- var _context$state$listKe, _context$state$listKe2, _context$state$intend4;
116
- return (_context$state$listKe = (_context$state$listKe2 = context.state.listKeys) === null || _context$state$listKe2 === void 0 ? void 0 : _context$state$listKe2[name]) !== null && _context$state$listKe !== void 0 ? _context$state$listKe : getDefaultListKey(context.state.resetKey, (_context$state$intend4 = context.state.intendedValue) !== null && _context$state$intend4 !== void 0 ? _context$state$intend4 : context.defaultValue, name);
115
+ var _context$state$listKe, _context$state$listKe2, _ref4, _context$state$server4;
116
+ return (_context$state$listKe = (_context$state$listKe2 = context.state.listKeys) === null || _context$state$listKe2 === void 0 ? void 0 : _context$state$listKe2[name]) !== null && _context$state$listKe !== void 0 ? _context$state$listKe : getDefaultListKey(context.state.resetKey, (_ref4 = (_context$state$server4 = context.state.serverIntendedValue) !== null && _context$state$server4 !== void 0 ? _context$state$server4 : context.state.clientIntendedValue) !== null && _ref4 !== void 0 ? _ref4 : context.defaultValue, name);
117
117
  }
118
- function getError(state, name) {
118
+ function getErrors(state, name) {
119
119
  var _state$serverError;
120
120
  var error = (_state$serverError = state.serverError) !== null && _state$serverError !== void 0 ? _state$serverError : state.clientError;
121
121
  if (!error || !isTouched(state, name)) {
@@ -126,6 +126,55 @@ function getError(state, name) {
126
126
  return errors;
127
127
  }
128
128
  }
129
+ function getFieldErrors(state, name) {
130
+ var _state$serverError2;
131
+ var result = {};
132
+ var error = (_state$serverError2 = state.serverError) !== null && _state$serverError2 !== void 0 ? _state$serverError2 : state.clientError;
133
+ if (error) {
134
+ var basePath = getPathSegments(name);
135
+ for (var field of Object.keys(error.fieldErrors)) {
136
+ var relativePath = getRelativePath(field, basePath);
137
+
138
+ // Only include errors for specified field's children
139
+ if (!relativePath || relativePath.length === 0) {
140
+ continue;
141
+ }
142
+ var _error = getErrors(state, field);
143
+ if (typeof _error !== 'undefined') {
144
+ result[formatPathSegments(relativePath)] = _error;
145
+ }
146
+ }
147
+ }
148
+ return result;
149
+ }
150
+ function isValid(state, name) {
151
+ var _state$serverError3;
152
+ var error = (_state$serverError3 = state.serverError) !== null && _state$serverError3 !== void 0 ? _state$serverError3 : state.clientError;
153
+
154
+ // If there is no error, it must be valid
155
+ if (!error) {
156
+ return true;
157
+ }
158
+ var basePath = getPathSegments(name);
159
+ for (var field of Object.keys(error.fieldErrors)) {
160
+ // When checking a specific field, only check that field and its children
161
+ if (name && !getRelativePath(field, basePath)) {
162
+ continue;
163
+ }
164
+
165
+ // If the field is not touched, we don't consider its error
166
+ var _error2 = getErrors(state, field);
167
+ if (_error2) {
168
+ return false;
169
+ }
170
+ }
171
+
172
+ // Make sure there is no form error when checking the whole form
173
+ if (!name) {
174
+ return !getErrors(state);
175
+ }
176
+ return true;
177
+ }
129
178
 
130
179
  /**
131
180
  * Gets validation constraint for a field, with fallback to parent array patterns.
@@ -156,29 +205,24 @@ function getConstraint(context, name) {
156
205
  }
157
206
  function getFormMetadata(context, options) {
158
207
  return {
208
+ key: context.state.resetKey,
159
209
  id: context.formId,
210
+ errorId: "".concat(context.formId, "-form-error"),
211
+ descriptionId: "".concat(context.formId, "-form-description"),
160
212
  get errors() {
161
- return getError(context.state);
213
+ return getErrors(context.state);
162
214
  },
163
215
  get fieldErrors() {
164
- var result = {};
165
- for (var name of context.state.touchedFields) {
166
- if (!name) {
167
- // Skip form-level errors
168
- continue;
169
- }
170
- var error = getError(context.state, name);
171
- if (typeof error !== 'undefined') {
172
- result[name] = error;
173
- }
174
- }
175
- return result;
216
+ return getFieldErrors(context.state);
176
217
  },
177
218
  get touched() {
178
219
  return isTouched(context.state);
179
220
  },
221
+ get valid() {
222
+ return isValid(context.state);
223
+ },
180
224
  get invalid() {
181
- return typeof getError(context.state) !== 'undefined';
225
+ return !this.valid;
182
226
  },
183
227
  props: {
184
228
  id: context.formId,
@@ -206,12 +250,13 @@ function getFormMetadata(context, options) {
206
250
  };
207
251
  }
208
252
  function getField(context, options) {
209
- var id = "".concat(context.formId, "-").concat(options.name);
253
+ var id = "".concat(context.formId, "-field-").concat(options.name.replace(/[^a-zA-Z0-9._-]/g, '_'));
210
254
  var constraint = getConstraint(context, options.name);
211
255
  var metadata = {
212
256
  id: id,
213
257
  descriptionId: "".concat(id, "-description"),
214
258
  errorId: "".concat(id, "-error"),
259
+ formId: context.formId,
215
260
  required: constraint === null || constraint === void 0 ? void 0 : constraint.required,
216
261
  minLength: constraint === null || constraint === void 0 ? void 0 : constraint.minLength,
217
262
  maxLength: constraint === null || constraint === void 0 ? void 0 : constraint.maxLength,
@@ -232,11 +277,17 @@ function getField(context, options) {
232
277
  get touched() {
233
278
  return isTouched(context.state, options.name);
234
279
  },
280
+ get valid() {
281
+ return isValid(context.state, options.name);
282
+ },
235
283
  get invalid() {
236
- return typeof getError(context.state, options.name) !== 'undefined';
284
+ return !this.valid;
237
285
  },
238
286
  get errors() {
239
- return getError(context.state, options.name);
287
+ return getErrors(context.state, options.name);
288
+ },
289
+ get fieldErrors() {
290
+ return getFieldErrors(context.state, options.name);
240
291
  }
241
292
  };
242
293
  return Object.assign(metadata, {
@@ -280,4 +331,4 @@ function getFieldList(context, options) {
280
331
  });
281
332
  }
282
333
 
283
- export { getConstraint, getDefaultListKey, getDefaultOptions, getDefaultValue, getError, getField, getFieldList, getFieldset, getFormMetadata, getListKey, initializeState, isDefaultChecked, isTouched, updateState };
334
+ export { getConstraint, getDefaultListKey, getDefaultOptions, getDefaultValue, getErrors, getField, getFieldErrors, getFieldList, getFieldset, getFormMetadata, getListKey, initializeState, isDefaultChecked, isTouched, isValid, updateState };
@@ -1,5 +1,5 @@
1
1
  import type { FormError, FormValue, SubmissionResult, ValidationAttributes } from '@conform-to/dom/future';
2
- import { StandardSchemaV1 } from '@standard-schema/spec';
2
+ import type { StandardSchemaV1 } from './standard-schema';
3
3
  export type Prettify<T> = {
4
4
  [K in keyof T]: T[K];
5
5
  } & {};
@@ -70,10 +70,10 @@ export type DefaultValue<FormShape> = FormShape extends string | number | boolea
70
70
  export type FormState<ErrorShape> = {
71
71
  /** Unique identifier that changes on form reset to trigger reset side effects */
72
72
  resetKey: string;
73
- /** Form values from user intent actions (validate, update, insert, remove, etc.) */
74
- intendedValue: Record<string, unknown> | null;
75
- /** Form values that have been validated on the server */
76
- serverValidatedValue: Record<string, unknown> | null;
73
+ /** Form values from client actions that will be synced to the DOM */
74
+ clientIntendedValue: Record<string, unknown> | null;
75
+ /** Form values from server actions, or submitted values when no server intent exists */
76
+ serverIntendedValue: Record<string, unknown> | null;
77
77
  /** Validation errors from server-side processing */
78
78
  serverError: FormError<ErrorShape> | null;
79
79
  /** Validation errors from client-side validation */
@@ -241,7 +241,7 @@ export type Combine<T> = {
241
241
  [K in keyof BaseCombine<T>]: BaseCombine<T>[K];
242
242
  };
243
243
  /** Field metadata object containing field state, validation attributes, and nested field access methods. */
244
- export type Field<FieldShape, Metadata extends Record<string, unknown> = DefaultFieldMetadata<unknown>> = Readonly<Metadata & {
244
+ export type FieldMetadata<FieldShape, Metadata extends Record<string, unknown> = DefaultMetadata<unknown>> = Readonly<Metadata & {
245
245
  /** Unique key for React list rendering (for array fields). */
246
246
  key: string | undefined;
247
247
  /** The field name path exactly as provided. */
@@ -251,26 +251,34 @@ export type Field<FieldShape, Metadata extends Record<string, unknown> = Default
251
251
  FieldShape
252
252
  ] extends [Record<string, unknown> | null | undefined] ? FieldShape : unknown, Metadata>;
253
253
  /** Method to get array of fields for list/array fields under this field. */
254
- getFieldList(): Array<Field<[
254
+ getFieldList(): Array<FieldMetadata<[
255
255
  FieldShape
256
256
  ] extends [Array<infer ItemShape> | null | undefined] ? ItemShape : unknown, Metadata>>;
257
257
  }>;
258
258
  /** Fieldset object containing all form fields as properties with their respective field metadata. */
259
259
  export type Fieldset<FieldShape, // extends Record<string, unknown>,
260
- FieldMetadata extends Record<string, unknown>> = {
261
- [Key in keyof Combine<FieldShape>]-?: Field<Combine<FieldShape>[Key], FieldMetadata>;
260
+ Metadata extends Record<string, unknown>> = {
261
+ [Key in keyof Combine<FieldShape>]-?: FieldMetadata<Combine<FieldShape>[Key], Metadata>;
262
262
  };
263
263
  /** Form-level metadata and state object containing validation status, errors, and field access methods. */
264
- export type FormMetadata<ErrorShape, FieldMetadata extends Record<string, unknown> = DefaultFieldMetadata<ErrorShape>> = Readonly<{
264
+ export type FormMetadata<ErrorShape, Metadata extends Record<string, unknown> = DefaultMetadata<ErrorShape>> = Readonly<{
265
+ /** Unique identifier that changes on form reset */
266
+ key: string;
265
267
  /** The form's unique identifier. */
266
268
  id: string;
269
+ /** Auto-generated ID for associating form descriptions via aria-describedby. */
270
+ descriptionId: string;
271
+ /** Auto-generated ID for associating form errors via aria-describedby. */
272
+ errorId: string;
267
273
  /** Whether any field in the form has been touched (through intent.validate() or the shouldValidate option). */
268
274
  touched: boolean;
269
- /** Whether the form currently has any validation errors. */
275
+ /** Whether the form currently has no validation errors. */
276
+ valid: boolean;
277
+ /** @deprecated Use `.valid` instead. This was not an intentionl breaking change and would be removed in the next minor version soon */
270
278
  invalid: boolean;
271
279
  /** Form-level validation errors, if any exist. */
272
280
  errors: ErrorShape[] | undefined;
273
- /** Object containing field-specific validation errors for all validated fields. */
281
+ /** Object containing errors for all touched fields. */
274
282
  fieldErrors: Record<string, ErrorShape[]>;
275
283
  /** Form props object for spreading onto the <form> element. */
276
284
  props: Readonly<{
@@ -283,24 +291,26 @@ export type FormMetadata<ErrorShape, FieldMetadata extends Record<string, unknow
283
291
  /** The current state of the form */
284
292
  context: FormContext<ErrorShape>;
285
293
  /** Method to get metadata for a specific field by name. */
286
- getField<FieldShape>(name: FieldName<FieldShape>): Field<FieldShape, FieldMetadata>;
294
+ getField<FieldShape>(name: FieldName<FieldShape>): FieldMetadata<FieldShape, Metadata>;
287
295
  /** Method to get a fieldset object for nested object fields. */
288
296
  getFieldset<FieldShape>(name: FieldName<FieldShape>): Fieldset<[
289
297
  FieldShape
290
- ] extends [Record<string, unknown> | null | undefined] ? FieldShape : unknown, FieldMetadata>;
298
+ ] extends [Record<string, unknown> | null | undefined] ? FieldShape : unknown, Metadata>;
291
299
  /** Method to get an array of field objects for array fields. */
292
- getFieldList<FieldShape>(name: FieldName<FieldShape>): Array<Field<[
300
+ getFieldList<FieldShape>(name: FieldName<FieldShape>): Array<FieldMetadata<[
293
301
  FieldShape
294
- ] extends [Array<infer ItemShape> | null | undefined] ? ItemShape : unknown, FieldMetadata>>;
302
+ ] extends [Array<infer ItemShape> | null | undefined] ? ItemShape : unknown, Metadata>>;
295
303
  }>;
296
304
  /** Default field metadata object containing field state, validation attributes, and accessibility IDs. */
297
- export type DefaultFieldMetadata<ErrorShape> = Readonly<ValidationAttributes & {
298
- /** The field's unique identifier, automatically generated as {formId}-{fieldName}. */
305
+ export type DefaultMetadata<ErrorShape> = Readonly<ValidationAttributes & {
306
+ /** The field's unique identifier, automatically generated as {formId}-field-{fieldName}. */
299
307
  id: string;
300
308
  /** Auto-generated ID for associating field descriptions via aria-describedby. */
301
309
  descriptionId: string;
302
310
  /** Auto-generated ID for associating field errors via aria-describedby. */
303
311
  errorId: string;
312
+ /** The form's unique identifier for associating field via the `form` attribute. */
313
+ formId: string;
304
314
  /** The field's default value as a string. */
305
315
  defaultValue: string | undefined;
306
316
  /** Default selected options for multi-select fields or checkbox group. */
@@ -309,10 +319,14 @@ export type DefaultFieldMetadata<ErrorShape> = Readonly<ValidationAttributes & {
309
319
  defaultChecked: boolean | undefined;
310
320
  /** Whether this field has been touched (through intent.validate() or the shouldValidate option). */
311
321
  touched: boolean;
312
- /** Whether this field currently has validation errors. */
322
+ /** Whether this field currently has no validation errors. */
323
+ valid: boolean;
324
+ /** @deprecated Use `.valid` instead. This was not an intentionl breaking change and would be removed in the next minor version soon */
313
325
  invalid: boolean;
314
326
  /** Array of validation error messages for this field. */
315
327
  errors: ErrorShape[] | undefined;
328
+ /** Object containing errors for all touched subfields. */
329
+ fieldErrors: Record<string, ErrorShape[]>;
316
330
  }>;
317
331
  export type ValidateResult<ErrorShape, Value> = FormError<ErrorShape> | null | {
318
332
  error: FormError<ErrorShape> | null;
@@ -1,5 +1,5 @@
1
1
  import type { FormError } from '@conform-to/dom/future';
2
- import { StandardSchemaV1 } from '@standard-schema/spec';
2
+ import type { StandardSchemaV1 } from './standard-schema';
3
3
  import { ValidateHandler, ValidateResult } from './types';
4
4
  export declare function isUndefined(value: unknown): value is undefined;
5
5
  export declare function isString(value: unknown): value is string;
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "description": "Conform view adapter for react",
4
4
  "homepage": "https://conform.guide",
5
5
  "license": "MIT",
6
- "version": "1.9.0",
6
+ "version": "1.10.0",
7
7
  "main": "./dist/index.js",
8
8
  "module": "./dist/index.mjs",
9
9
  "types": "./dist/index.d.ts",
@@ -41,7 +41,7 @@
41
41
  "url": "https://github.com/edmundhung/conform/issues"
42
42
  },
43
43
  "dependencies": {
44
- "@conform-to/dom": "1.9.0"
44
+ "@conform-to/dom": "1.10.0"
45
45
  },
46
46
  "devDependencies": {
47
47
  "@babel/core": "^7.17.8",
@@ -50,7 +50,6 @@
50
50
  "@babel/preset-typescript": "^7.20.2",
51
51
  "@rollup/plugin-babel": "^5.3.1",
52
52
  "@rollup/plugin-node-resolve": "^13.3.0",
53
- "@standard-schema/spec": "^1.0.0",
54
53
  "@types/react": "^18.2.43",
55
54
  "@types/react-dom": "^18.3.5",
56
55
  "react": "^18.2.0",