oxform-core 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.
@@ -0,0 +1,7 @@
1
+ export { FieldApi } from '#field-api';
2
+ export { FormApi } from '#form-api';
3
+
4
+ export type * from '#field-api';
5
+ export type * from '#form-api.types';
6
+ export type * from '#more-types';
7
+ export type { EventLike } from '#types';
@@ -0,0 +1 @@
1
+ export type { StandardSchema } from '#types';
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "oxform-core",
3
+ "version": "0.1.0",
4
+ "description": "Oxform is a lean and flexible framework-agnostic form library.",
5
+ "author": "Frantss <frantss.bongiovanni@gmail.com>",
6
+ "license": "MIT",
7
+ "sideEffects": false,
8
+ "type": "module",
9
+ "main": "./dist/index.js",
10
+ "imports": {
11
+ "#*": [
12
+ "./src/*.ts",
13
+ "./src/**/*.ts"
14
+ ]
15
+ },
16
+ "exports": {
17
+ ".": "./dist/index.js",
18
+ "./package.json": "./package.json",
19
+ "./schema": "./dist/schema.js"
20
+ },
21
+ "module": "./dist/index.js",
22
+ "types": "./dist/index.d.ts",
23
+ "scripts": {
24
+ "build": "tsdown --config-loader unrun",
25
+ "test": "vitest",
26
+ "lint:types": "tsc --noEmit"
27
+ },
28
+ "dependencies": {
29
+ "@standard-schema/spec": "catalog:oxform",
30
+ "@tanstack/store": "^0.8.0",
31
+ "remeda": "^2.32.0",
32
+ "type-fest": "^5.3.1"
33
+ },
34
+ "devDependencies": {
35
+ "tsdown": "catalog:oxform",
36
+ "typescript": "catalog:oxform",
37
+ "vitest": "catalog:oxform",
38
+ "zod": "^4.2.1"
39
+ }
40
+ }
package/readme.md ADDED
@@ -0,0 +1,13 @@
1
+ # Oxform Core
2
+
3
+ This package contains the core logic for Oxform.
4
+ You probably don't want to use this package directly. Instead check out the React integration (`oxform-react`).
5
+
6
+ ## Installation
7
+
8
+ ```bash
9
+ npm install oxform-core
10
+ yarn add oxform-core
11
+ pnpm add oxform-core
12
+ bun add oxform-core
13
+ ```
@@ -0,0 +1,15 @@
1
+ import type { PersistedFieldMeta, PersistedFormStatus } from '#form-api.types';
2
+
3
+ export const defaultMeta = {
4
+ blurred: false,
5
+ touched: false,
6
+ dirty: false,
7
+ } satisfies PersistedFieldMeta;
8
+
9
+ export const defaultStatus = {
10
+ submits: 0,
11
+ submitting: false,
12
+ validating: false,
13
+ successful: false,
14
+ dirty: false,
15
+ } satisfies PersistedFormStatus;
@@ -0,0 +1,139 @@
1
+ import type { FormApi } from '#form-api';
2
+ import type {
3
+ FieldChangeOptions,
4
+ FieldMeta,
5
+ FormIssue,
6
+ FormResetFieldOptions,
7
+ FormSetErrorsOptions,
8
+ ValidateOptions,
9
+ } from '#form-api.types';
10
+ import type { DeepKeys, DeepValue } from '#more-types';
11
+ import type { EventLike, StandardSchema } from '#types';
12
+ import { get } from '#utils/get';
13
+ import type { Updater } from '#utils/update';
14
+ import { Derived } from '@tanstack/store';
15
+ import { stringToPath } from 'remeda';
16
+
17
+ export type FieldOptions<
18
+ Schema extends StandardSchema,
19
+ Name extends DeepKeys<StandardSchema.InferInput<Schema>> = DeepKeys<StandardSchema.InferInput<Schema>>,
20
+ > = {
21
+ form: FormApi<Schema>;
22
+ name: Name;
23
+ };
24
+
25
+ export type FieldStore<Value> = {
26
+ value: Value;
27
+ defaultValue: Value;
28
+ meta: FieldMeta;
29
+ errors: FormIssue[];
30
+ };
31
+
32
+ export type FieldProps<Value> = {
33
+ defaultValue: Value;
34
+ value: Value;
35
+ ref: (element: HTMLElement | null) => void;
36
+ onChange: (event: EventLike) => void;
37
+ onBlur: (event: EventLike) => void;
38
+ onFocus: (event: EventLike) => void;
39
+ };
40
+
41
+ export class FieldApi<
42
+ Schema extends StandardSchema,
43
+ Name extends DeepKeys<StandardSchema.InferInput<Schema>>,
44
+ in out Value extends DeepValue<StandardSchema.InferInput<Schema>, Name>,
45
+ > {
46
+ public options: FieldOptions<Schema, Name>;
47
+ public form: FormApi<Schema>;
48
+ public store: Derived<FieldStore<Value>>;
49
+
50
+ constructor(options: FieldOptions<Schema, Name>) {
51
+ this.options = options;
52
+ this.form = options.form;
53
+
54
+ this.store = new Derived<FieldStore<Value>>({
55
+ deps: [this.form.store],
56
+ fn: () => {
57
+ const value = this.form.field.get(this.options.name as never) as Value;
58
+ const defaultValue = get(this.form.options.defaultValues as never, stringToPath(this.options.name)) as Value;
59
+ const meta = this.form.field.meta(this.options.name as never);
60
+ const errors = this.form.field.errors(this.options.name as never);
61
+
62
+ return {
63
+ value,
64
+ defaultValue,
65
+ meta,
66
+ errors,
67
+ };
68
+ },
69
+ });
70
+ }
71
+
72
+ public '~mount' = () => {
73
+ const unsubscribe = this.store.mount();
74
+
75
+ return unsubscribe;
76
+ };
77
+
78
+ public '~update' = (options: FieldOptions<Schema, Name>) => {
79
+ // ref: https://github.com/TanStack/form/blob/main/packages/form-core/src/FieldApi.ts#L1300
80
+ this.options = options;
81
+ };
82
+
83
+ public get value() {
84
+ return this.store.state.value;
85
+ }
86
+
87
+ public get defaultValue() {
88
+ return this.store.state.defaultValue;
89
+ }
90
+
91
+ public get meta() {
92
+ return this.store.state.meta;
93
+ }
94
+
95
+ public get errors() {
96
+ return this.store.state.errors;
97
+ }
98
+
99
+ public focus = () => this.form.field.focus(this.options.name);
100
+
101
+ public blur = () => this.form.field.blur(this.options.name);
102
+
103
+ /**
104
+ * Changes the value of this field with optional control over side effects.
105
+ * @param value - The new value to set for the field
106
+ * @param options - Optional configuration for controlling validation, dirty state, and touched state
107
+ */
108
+ public change = (value: Updater<Value>, options?: FieldChangeOptions) => {
109
+ return this.form.field.change(this.options.name, value as never, options);
110
+ };
111
+
112
+ public register = () => this.form.field.register(this.options.name);
113
+
114
+ /**
115
+ * Validates this specific field using the specified validation type.
116
+ * @param options - Optional validation options specifying the validation type ('change' | 'submit' | 'blur' | 'focus')
117
+ * @returns Promise resolving to an array of validation issues for this field
118
+ */
119
+ public validate = (options?: ValidateOptions) => {
120
+ return this.form.validate(this.options.name, options);
121
+ };
122
+
123
+ /**
124
+ * Resets this field to its default value and optionally resets metadata and errors.
125
+ * @param options - Reset options for controlling what gets reset and what gets kept
126
+ */
127
+ public reset = (options?: FormResetFieldOptions<Value>) => {
128
+ return this.form.field.reset(this.options.name, options);
129
+ };
130
+
131
+ /**
132
+ * Sets validation errors for this specific field.
133
+ * @param errors - Array of validation errors to set
134
+ * @param mode - How to handle existing errors: 'replace' (default), 'append', or 'keep'
135
+ */
136
+ public setErrors = (errors: FormIssue[], options?: FormSetErrorsOptions) => {
137
+ return this.form.field.setErrors(this.options.name, errors, options);
138
+ };
139
+ }
@@ -0,0 +1,84 @@
1
+ import { defaultStatus } from '#field-api.constants';
2
+ import type { FormOptions, FormResetOptions, FormSubmitErrorHandler, FormSubmitSuccessHandler } from '#form-api.types';
3
+ import { FormArrayFieldApi } from '#form-array-field-api';
4
+ import { FormContextApi } from '#form-context-api';
5
+ import { FormFieldApi } from '#form-field-api';
6
+ import type { StandardSchema } from '#types';
7
+
8
+ export class FormApi<Schema extends StandardSchema> {
9
+ private context: FormContextApi<Schema>;
10
+ public field: FormFieldApi<Schema>;
11
+ public array: FormArrayFieldApi<Schema>;
12
+
13
+ constructor(options: FormOptions<Schema>) {
14
+ this.context = new FormContextApi(options);
15
+ this.field = new FormFieldApi(this.context);
16
+ this.array = new FormArrayFieldApi({ field: this.field, context: this.context });
17
+ }
18
+
19
+ public '~mount' = () => {
20
+ const unsubscribe = this.context.store.mount();
21
+
22
+ return unsubscribe;
23
+ };
24
+
25
+ public '~update' = (options: FormOptions<NoInfer<Schema>>) => {
26
+ this.context.options = options;
27
+ };
28
+
29
+ public get store() {
30
+ return this.context.store;
31
+ }
32
+
33
+ public get status() {
34
+ return this.context.store.state.status;
35
+ }
36
+
37
+ public get values() {
38
+ return this.context.store.state.values;
39
+ }
40
+
41
+ public get options() {
42
+ return this.context.options;
43
+ }
44
+
45
+ public get validate() {
46
+ return this.context.validate;
47
+ }
48
+
49
+ public submit =
50
+ (onSuccess: FormSubmitSuccessHandler<NoInfer<Schema>>, onError?: FormSubmitErrorHandler<NoInfer<Schema>>) =>
51
+ async () => {
52
+ this.context.setStatus({ submitting: true, dirty: true });
53
+
54
+ const issues = await this.context.validate(undefined, { type: 'submit' });
55
+ const valid = issues.length === 0;
56
+
57
+ if (valid) {
58
+ await onSuccess(this.context.store.state.values as never, this as never);
59
+ } else {
60
+ await onError?.(issues, this as never);
61
+ }
62
+
63
+ this.context.setStatus({
64
+ submits: this.context.persisted.state.status.submits + 1,
65
+ submitting: false,
66
+ successful: valid,
67
+ });
68
+ };
69
+
70
+ public reset = (options?: FormResetOptions<StandardSchema.InferInput<NoInfer<Schema>>>) => {
71
+ this.context.persisted.setState(current => {
72
+ return {
73
+ values: options?.values ?? this.context.options.defaultValues,
74
+ fields: options?.keep?.fields ? current.fields : {},
75
+ refs: options?.keep?.refs ? current.refs : {},
76
+ errors: options?.keep?.errors ? current.errors : {},
77
+ status: {
78
+ ...defaultStatus,
79
+ ...options?.status,
80
+ },
81
+ };
82
+ });
83
+ };
84
+ }
@@ -0,0 +1,148 @@
1
+ import type { FormApi } from '#form-api';
2
+ import type { DeepKeys } from '#more-types';
3
+ import type { PartialDeep, Simplify, StandardSchema } from '#types';
4
+
5
+ export type PersistedFormStatus = {
6
+ submits: number;
7
+ submitting: boolean;
8
+ validating: boolean;
9
+ successful: boolean;
10
+ dirty: boolean;
11
+ };
12
+
13
+ export type FormStatus = Simplify<
14
+ PersistedFormStatus & {
15
+ submitted: boolean;
16
+ valid: boolean;
17
+ }
18
+ >;
19
+
20
+ export type PersistedFieldMeta = {
21
+ blurred: boolean;
22
+ touched: boolean;
23
+ dirty: boolean;
24
+ };
25
+
26
+ export type FieldMeta = Simplify<
27
+ PersistedFieldMeta & {
28
+ touched: boolean;
29
+ default: boolean;
30
+ valid: boolean;
31
+ pristine: boolean;
32
+ }
33
+ >;
34
+
35
+ export type FormBaseStore<Schema extends StandardSchema> = {
36
+ values: StandardSchema.InferInput<Schema>;
37
+ fields: Record<string, PersistedFieldMeta>;
38
+ refs: Record<string, HTMLElement | null>;
39
+ status: PersistedFormStatus;
40
+ errors: Record<string, FormIssue[]>;
41
+ };
42
+
43
+ export type FormStore<Schema extends StandardSchema> = {
44
+ values: StandardSchema.InferInput<Schema>;
45
+ fields: Record<string, FieldMeta>;
46
+ refs: Record<string, HTMLElement | null>;
47
+ status: FormStatus;
48
+ errors: Record<string, FormIssue[]>;
49
+ };
50
+
51
+ export type FieldControl<Value> = {
52
+ focus: () => void;
53
+ blur: () => void;
54
+ change: (value: Value) => void;
55
+ register: (element: HTMLElement | null) => void;
56
+ };
57
+
58
+ type FormValidatorSchema<Schema extends StandardSchema> = NoInfer<
59
+ StandardSchema<PartialDeep<StandardSchema.InferInput<Schema>>>
60
+ >;
61
+ type FormValidatorFunction<Schema extends StandardSchema> = (store: FormStore<Schema>) => FormValidatorSchema<Schema>;
62
+ type FormValidator<Schema extends StandardSchema> = FormValidatorSchema<Schema> | FormValidatorFunction<Schema>;
63
+
64
+ export type FormOptions<Schema extends StandardSchema> = {
65
+ schema: Schema;
66
+ values?: StandardSchema.InferInput<Schema>;
67
+ defaultValues: StandardSchema.InferInput<Schema>;
68
+ validate?: {
69
+ change?: FormValidator<Schema>;
70
+ submit?: FormValidator<Schema>;
71
+ blur?: FormValidator<Schema>;
72
+ focus?: FormValidator<Schema>;
73
+ };
74
+ related?: Record<DeepKeys<StandardSchema.InferInput<Schema>>, DeepKeys<StandardSchema.InferInput<Schema>>[]>;
75
+ };
76
+
77
+ export type FormIssue = StandardSchema.Issue;
78
+
79
+ export type ValidationType = 'change' | 'submit' | 'blur' | 'focus';
80
+
81
+ export type ValidateOptions = {
82
+ type?: ValidationType;
83
+ };
84
+
85
+ export type FieldChangeOptions = {
86
+ should?: {
87
+ /** Whether to validate the field after changing its value. Defaults to true. */
88
+ validate?: boolean;
89
+ /** Whether to mark the field as dirty after changing its value. Defaults to true. */
90
+ dirty?: boolean;
91
+ /** Whether to mark the field as touched after changing its value. Defaults to true. */
92
+ touch?: boolean;
93
+ };
94
+ };
95
+
96
+ export type FieldResetMeta = {
97
+ blurred?: boolean;
98
+ touched?: boolean;
99
+ dirty?: boolean;
100
+ };
101
+
102
+ export type FieldResetKeepOptions = {
103
+ errors?: boolean;
104
+ refs?: boolean;
105
+ meta?: boolean;
106
+ };
107
+
108
+ export type FormResetFieldOptions<Value> = {
109
+ value?: Value;
110
+ meta?: FieldResetMeta;
111
+ keep?: FieldResetKeepOptions;
112
+ };
113
+
114
+ export type FormResetKeepOptions = {
115
+ /** Keep current field errors */
116
+ errors?: boolean;
117
+ /** Keep current references to html input elements */
118
+ refs?: boolean;
119
+ /** Keep current field metadata */
120
+ fields?: boolean;
121
+ };
122
+
123
+ export type FormResetOptions<Values> = {
124
+ values?: Values;
125
+ status?: Partial<PersistedFormStatus>;
126
+ keep?: FormResetKeepOptions;
127
+ };
128
+
129
+ export type FieldSetErrorsMode = 'replace' | 'append' | 'keep';
130
+
131
+ export type FormSetErrorsOptions = {
132
+ mode?: FieldSetErrorsMode;
133
+ };
134
+
135
+ export type FormSubmitSuccessHandler<Schema extends StandardSchema> = (
136
+ data: StandardSchema.InferOutput<Schema>,
137
+ form: FormApi<Schema>,
138
+ ) => void | Promise<void>;
139
+
140
+ export type FormSubmitErrorHandler<Schema extends StandardSchema> = (
141
+ issues: FormIssue[],
142
+ form: FormApi<Schema>,
143
+ ) => void | Promise<void>;
144
+
145
+ export type FormSubmitHandlers<Schema extends StandardSchema> = {
146
+ onSuccess: FormSubmitSuccessHandler<Schema>;
147
+ onError?: FormSubmitErrorHandler<Schema>;
148
+ };
@@ -0,0 +1,233 @@
1
+ import { defaultMeta } from '#field-api.constants';
2
+ import type { FieldChangeOptions } from '#form-api.types';
3
+ import type { FormContextApi } from '#form-context-api';
4
+ import type { FormFieldApi } from '#form-field-api';
5
+ import type { DeepKeysOfType, DeepValue, UnwrapOneLevelOfArray } from '#more-types';
6
+ import type { StandardSchema } from '#types';
7
+ import { get } from '#utils/get';
8
+ import { update, type Updater } from '#utils/update';
9
+ import { stringToPath } from 'remeda';
10
+
11
+ export class FormArrayFieldApi<
12
+ Schema extends StandardSchema<any>,
13
+ Values extends StandardSchema.InferInput<Schema> = StandardSchema.InferInput<Schema>,
14
+ ArrayField extends DeepKeysOfType<Values, any[] | null | undefined> = DeepKeysOfType<
15
+ Values,
16
+ any[] | null | undefined
17
+ >,
18
+ > {
19
+ private context: FormContextApi<Schema>;
20
+ private field: FormFieldApi<Schema>;
21
+
22
+ constructor({ field, context }: { field: FormFieldApi<Schema>; context: FormContextApi<Schema> }) {
23
+ this.context = context;
24
+ this.field = field;
25
+ }
26
+
27
+ public insert = <Name extends ArrayField>(
28
+ name: Name,
29
+ index: number,
30
+ value: Updater<UnwrapOneLevelOfArray<DeepValue<Values, Name>>>,
31
+ options?: FieldChangeOptions,
32
+ ) => {
33
+ if (index < 0) index = 0;
34
+
35
+ this.field.change(
36
+ name,
37
+ current => {
38
+ const array = (current as any[]) ?? [];
39
+
40
+ return [
41
+ ...array.slice(0, index),
42
+ ...(Array.from({ length: index - array.length }, () => undefined) as any[]),
43
+ update(value, current as never),
44
+ ...array.slice(index),
45
+ ] as never;
46
+ },
47
+ options,
48
+ );
49
+
50
+ this.context.persisted.setState(current => {
51
+ const fields = { ...current.fields };
52
+ const value = get(current.values as never, stringToPath(name)) as any[] | undefined;
53
+ const length = value?.length;
54
+
55
+ if (length === undefined) return current;
56
+
57
+ for (let i = index; i < length; i++) {
58
+ const moving = current.fields[`${name}.${i}`];
59
+ fields[`${name}.${i + 1}`] = moving ?? defaultMeta;
60
+ }
61
+
62
+ fields[`${name}.${index}`] = defaultMeta;
63
+
64
+ return {
65
+ ...current,
66
+ fields,
67
+ };
68
+ });
69
+ };
70
+
71
+ public append = <Name extends ArrayField>(
72
+ name: Name,
73
+ value: UnwrapOneLevelOfArray<DeepValue<Values, Name>>,
74
+ options?: FieldChangeOptions,
75
+ ) => {
76
+ const current = this.field.get(name) as any[];
77
+ return this.insert(name, current?.length ?? 0, value, options);
78
+ };
79
+
80
+ public prepend = <Name extends ArrayField>(
81
+ name: Name,
82
+ value: UnwrapOneLevelOfArray<DeepValue<Values, Name>>,
83
+ options?: FieldChangeOptions,
84
+ ) => {
85
+ return this.insert(name, 0, value, options);
86
+ };
87
+
88
+ public swap = <Name extends ArrayField>(name: Name, from: number, to: number, options?: FieldChangeOptions) => {
89
+ const start = from >= 0 ? from : 0;
90
+ const end = to >= 0 ? to : 0;
91
+
92
+ this.field.change(
93
+ name,
94
+ current => {
95
+ const array = (current as any[]) ?? [];
96
+
97
+ if (start === end) return array as never; // no-op
98
+
99
+ const a = array[start];
100
+ const b = array[end];
101
+
102
+ return [...array.slice(0, start), b, ...array.slice(start + 1, end), a, ...array.slice(end + 1)] as never;
103
+ },
104
+ options,
105
+ );
106
+
107
+ this.context.persisted.setState(current => {
108
+ const fields = { ...current.fields };
109
+
110
+ fields[`${name}.${start}`] = current.fields[`${name}.${end}`] ?? defaultMeta;
111
+ fields[`${name}.${end}`] = current.fields[`${name}.${start}`] ?? defaultMeta;
112
+
113
+ return {
114
+ ...current,
115
+ fields,
116
+ };
117
+ });
118
+ };
119
+
120
+ public move<Name extends ArrayField>(name: Name, _from: number, _to: number, options?: FieldChangeOptions) {
121
+ const from = Math.max(_from, 0);
122
+ const to = Math.max(_to, 0);
123
+ const backwards = from > to;
124
+
125
+ this.field.change(
126
+ name,
127
+ current => {
128
+ const array: any[] = current ? [...(current as any[])] : [];
129
+
130
+ if (from === to) return array as never;
131
+
132
+ const moved = array[from];
133
+ return array.toSpliced(backwards ? to : to + 1, 0, moved).toSpliced(backwards ? from + 1 : from, 1) as never;
134
+ },
135
+ options,
136
+ );
137
+
138
+ this.context.persisted.setState(current => {
139
+ const fields = { ...current.fields };
140
+ const value = get(current.values as never, stringToPath(name)) as any[] | undefined;
141
+ const length = value?.length;
142
+
143
+ const start = Math.min(from, to);
144
+ const end = Math.max(from, to);
145
+
146
+ if (length === undefined) return current;
147
+
148
+ if (!backwards) {
149
+ fields[`${name}.${to}`] = current.fields[`${name}.${from}`] ?? defaultMeta;
150
+ }
151
+
152
+ for (let i = backwards ? start + 1 : start; i < end; i++) {
153
+ const shift = backwards ? -1 : 1;
154
+ const moving = current.fields[`${name}.${i + shift}`];
155
+ fields[`${name}.${i}`] = moving ?? defaultMeta;
156
+ }
157
+
158
+ return {
159
+ ...current,
160
+ fields,
161
+ };
162
+ });
163
+ }
164
+
165
+ public update = <Name extends ArrayField>(
166
+ name: Name,
167
+ index: number,
168
+ value: Updater<UnwrapOneLevelOfArray<DeepValue<Values, Name>>>,
169
+ options?: FieldChangeOptions,
170
+ ) => {
171
+ this.field.change(
172
+ name,
173
+ current => {
174
+ const array = (current as any[]) ?? [];
175
+ const position = Math.max(Math.min(index, array.length - 1), 0);
176
+
177
+ return [...array.slice(0, position), update(value, current as never), ...array.slice(position + 1)] as never;
178
+ },
179
+ options,
180
+ );
181
+
182
+ this.context.resetFieldMeta(`${name}.${index}`);
183
+ this.context.setFieldMeta(`${name}.${index}`, {
184
+ dirty: options?.should?.dirty !== false,
185
+ touched: options?.should?.touch !== false,
186
+ });
187
+ };
188
+
189
+ public remove<Name extends ArrayField>(name: Name, index: number, options?: FieldChangeOptions) {
190
+ let position = index;
191
+
192
+ this.field.change(
193
+ name,
194
+ current => {
195
+ const array = (current as any[]) ?? [];
196
+ position = Math.max(Math.min(index, array.length - 1), 0);
197
+
198
+ return [...array.slice(0, position), ...array.slice(position + 1)] as never;
199
+ },
200
+ { ...options, should: { ...options?.should, validate: false } },
201
+ );
202
+
203
+ this.context.persisted.setState(current => {
204
+ const fields = { ...current.fields };
205
+ const value = get(current.values as never, stringToPath(name)) as any[] | undefined;
206
+ const length = value?.length ?? 0;
207
+
208
+ for (let i = position; i < length; i++) {
209
+ const moving = current.fields[`${name}.${i + 1}`];
210
+ fields[`${name}.${i}`] = moving ?? defaultMeta;
211
+ }
212
+
213
+ delete fields[`${name}.${length}`];
214
+
215
+ return {
216
+ ...current,
217
+ fields,
218
+ };
219
+ });
220
+
221
+ const shouldValidate = options?.should?.validate !== false;
222
+ if (shouldValidate) void this.context.validate(name, { type: 'change' });
223
+ }
224
+
225
+ public replace<Name extends ArrayField>(
226
+ name: Name,
227
+ value: Updater<DeepValue<Values, Name>>,
228
+ options?: FieldChangeOptions,
229
+ ) {
230
+ this.context.resetFieldMeta(name);
231
+ this.field.change(name, value as never, options);
232
+ }
233
+ }