mirror of
https://github.com/LukeHagar/form.git
synced 2025-12-06 04:19:43 +00:00
fix: router + store
This commit is contained in:
@@ -1,6 +1,11 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import ReactDOM from "react-dom/client";
|
import ReactDOM from "react-dom/client";
|
||||||
import { FieldApi, createFormFactory } from "@tanstack/react-form";
|
import {
|
||||||
|
FieldApi,
|
||||||
|
FormApi,
|
||||||
|
createFormFactory,
|
||||||
|
useField,
|
||||||
|
} from "@tanstack/react-form";
|
||||||
|
|
||||||
type Person = {
|
type Person = {
|
||||||
firstName: string;
|
firstName: string;
|
||||||
@@ -35,7 +40,7 @@ function FieldInfo({ field }: { field: FieldApi<any, any> }) {
|
|||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
const form = formFactory.useForm({
|
const form = formFactory.useForm({
|
||||||
onSubmit: async (values) => {
|
onSubmit: async (values, formApi) => {
|
||||||
// Do something with form data
|
// Do something with form data
|
||||||
console.log(values);
|
console.log(values);
|
||||||
},
|
},
|
||||||
@@ -54,11 +59,14 @@ export default function App() {
|
|||||||
{/* A type-safe and pre-bound field component*/}
|
{/* A type-safe and pre-bound field component*/}
|
||||||
<form.Field
|
<form.Field
|
||||||
name="firstName"
|
name="firstName"
|
||||||
validateOn="change"
|
onChange={(value) =>
|
||||||
validate={(value) => !value && "A first name is required"}
|
!value
|
||||||
validateAsyncOn="change"
|
? "A first name is required"
|
||||||
validateAsyncDebounceMs={500}
|
: value.length < 3
|
||||||
validateAsync={async (value) => {
|
? "First name must be at least 3 characters"
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
onChangeAsync={async (value) => {
|
||||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||||
return (
|
return (
|
||||||
value.includes("error") && 'No "error" allowed in first name'
|
value.includes("error") && 'No "error" allowed in first name'
|
||||||
@@ -68,7 +76,6 @@ export default function App() {
|
|||||||
// Avoid hasty abstractions. Render props are great!
|
// Avoid hasty abstractions. Render props are great!
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<input placeholder="uncontrolled" />
|
|
||||||
<input {...field.getInputProps()} />
|
<input {...field.getInputProps()} />
|
||||||
<FieldInfo field={field} />
|
<FieldInfo field={field} />
|
||||||
</>
|
</>
|
||||||
@@ -76,7 +83,7 @@ export default function App() {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
{/* <div>
|
||||||
<form.Field
|
<form.Field
|
||||||
name="lastName"
|
name="lastName"
|
||||||
children={(field) => (
|
children={(field) => (
|
||||||
@@ -172,7 +179,7 @@ export default function App() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div> */}
|
||||||
<form.Subscribe
|
<form.Subscribe
|
||||||
selector={(state) => [state.canSubmit, state.isSubmitting]}
|
selector={(state) => [state.canSubmit, state.isSubmitting]}
|
||||||
children={([canSubmit, isSubmitting]) => (
|
children={([canSubmit, isSubmitting]) => (
|
||||||
@@ -188,4 +195,5 @@ export default function App() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const rootElement = document.getElementById("root")!;
|
const rootElement = document.getElementById("root")!;
|
||||||
|
|
||||||
ReactDOM.createRoot(rootElement).render(<App />);
|
ReactDOM.createRoot(rootElement).render(<App />);
|
||||||
|
|||||||
@@ -116,6 +116,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@tanstack/store": "^0.0.1-beta.84",
|
||||||
"fs-extra": "^11.1.1",
|
"fs-extra": "^11.1.1",
|
||||||
"rollup-plugin-dts": "^5.3.0"
|
"rollup-plugin-dts": "^5.3.0"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,6 @@
|
|||||||
"build:types": "tsc --build"
|
"build:types": "tsc --build"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tanstack/store": "0.0.1-beta.84"
|
"@tanstack/store": "0.0.1-beta.88"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,22 +5,30 @@ import { Store } from '@tanstack/store'
|
|||||||
|
|
||||||
export type ValidationCause = 'change' | 'blur' | 'submit'
|
export type ValidationCause = 'change' | 'blur' | 'submit'
|
||||||
|
|
||||||
|
type ValidateFn<TData, TFormData> = (
|
||||||
|
value: TData,
|
||||||
|
fieldApi: FieldApi<TData, TFormData>,
|
||||||
|
) => ValidationError
|
||||||
|
|
||||||
|
type ValidateAsyncFn<TData, TFormData> = (
|
||||||
|
value: TData,
|
||||||
|
fieldApi: FieldApi<TData, TFormData>,
|
||||||
|
) => ValidationError | Promise<ValidationError>
|
||||||
|
|
||||||
export interface FieldOptions<TData, TFormData> {
|
export interface FieldOptions<TData, TFormData> {
|
||||||
name: unknown extends TFormData ? string : DeepKeys<TFormData>
|
name: unknown extends TFormData ? string : DeepKeys<TFormData>
|
||||||
index?: TData extends any[] ? number : never
|
index?: TData extends any[] ? number : never
|
||||||
defaultValue?: TData
|
defaultValue?: TData
|
||||||
validate?: (
|
asyncDebounceMs?: number
|
||||||
value: TData,
|
asyncAlways?: boolean
|
||||||
fieldApi: FieldApi<TData, TFormData>,
|
onMount?: (formApi: FieldApi<TData, TFormData>) => void
|
||||||
) => ValidationError
|
onChange?: ValidateFn<TData, TFormData>
|
||||||
validateAsync?: (
|
onChangeAsync?: ValidateAsyncFn<TData, TFormData>
|
||||||
value: TData,
|
onChangeAsyncDebounceMs?: number
|
||||||
fieldApi: FieldApi<TData, TFormData>,
|
onBlur?: ValidateFn<TData, TFormData>
|
||||||
) => ValidationError | Promise<ValidationError>
|
onBlurAsync?: ValidateAsyncFn<TData, TFormData>
|
||||||
validatePristine?: boolean // Default: false
|
onBlurAsyncDebounceMs?: number
|
||||||
validateOn?: ValidationCause // Default: 'change'
|
onSubmitAsync?: ValidateAsyncFn<TData, TFormData>
|
||||||
validateAsyncOn?: ValidationCause // Default: 'blur'
|
|
||||||
validateAsyncDebounceMs?: number
|
|
||||||
defaultMeta?: Partial<FieldMeta>
|
defaultMeta?: Partial<FieldMeta>
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -73,13 +81,7 @@ export class FieldApi<TData, TFormData> {
|
|||||||
name!: DeepKeys<TFormData>
|
name!: DeepKeys<TFormData>
|
||||||
store!: Store<FieldState<TData>>
|
store!: Store<FieldState<TData>>
|
||||||
state!: FieldState<TData>
|
state!: FieldState<TData>
|
||||||
options: RequiredByKey<
|
options: FieldOptions<TData, TFormData> = {} as any
|
||||||
FieldOptions<TData, TFormData>,
|
|
||||||
| 'validatePristine'
|
|
||||||
| 'validateOn'
|
|
||||||
| 'validateAsyncOn'
|
|
||||||
| 'validateAsyncDebounceMs'
|
|
||||||
> = {} as any
|
|
||||||
|
|
||||||
constructor(opts: FieldApiOptions<TData, TFormData>) {
|
constructor(opts: FieldApiOptions<TData, TFormData>) {
|
||||||
this.form = opts.form
|
this.form = opts.form
|
||||||
@@ -108,12 +110,14 @@ export class FieldApi<TData, TFormData> {
|
|||||||
? next.meta.error
|
? next.meta.error
|
||||||
: undefined
|
: undefined
|
||||||
|
|
||||||
// Do not validate pristine fields
|
const prev = this.state
|
||||||
const prevState = this.state
|
|
||||||
this.state = next
|
this.state = next
|
||||||
if (next.value !== prevState.value) {
|
|
||||||
|
if (next.value !== prev.value) {
|
||||||
this.validate('change', next.value)
|
this.validate('change', next.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return this.state
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -126,10 +130,23 @@ export class FieldApi<TData, TFormData> {
|
|||||||
const info = this.getInfo()
|
const info = this.getInfo()
|
||||||
info.instances[this.uid] = this
|
info.instances[this.uid] = this
|
||||||
|
|
||||||
const unsubscribe = this.form.store.subscribe(() => {
|
const unsubscribe = this.form.store.subscribe((next) => {
|
||||||
this.#updateStore()
|
this.store.batch(() => {
|
||||||
|
const nextValue = this.getValue()
|
||||||
|
const nextMeta = this.getMeta()
|
||||||
|
|
||||||
|
if (nextValue !== this.state.value) {
|
||||||
|
this.store.setState((prev) => ({ ...prev, value: nextValue }))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nextMeta !== this.state.meta) {
|
||||||
|
this.store.setState((prev) => ({ ...prev, meta: nextMeta }))
|
||||||
|
}
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
this.options.onMount?.(this)
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
unsubscribe()
|
unsubscribe()
|
||||||
delete info.instances[this.uid]
|
delete info.instances[this.uid]
|
||||||
@@ -139,28 +156,11 @@ export class FieldApi<TData, TFormData> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#updateStore = () => {
|
|
||||||
this.store.batch(() => {
|
|
||||||
const nextValue = this.getValue()
|
|
||||||
const nextMeta = this.getMeta()
|
|
||||||
|
|
||||||
if (nextValue !== this.state.value) {
|
|
||||||
this.store.setState((prev) => ({ ...prev, value: nextValue }))
|
|
||||||
}
|
|
||||||
|
|
||||||
if (nextMeta !== this.state.meta) {
|
|
||||||
this.store.setState((prev) => ({ ...prev, meta: nextMeta }))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
update = (opts: FieldApiOptions<TData, TFormData>) => {
|
update = (opts: FieldApiOptions<TData, TFormData>) => {
|
||||||
this.options = {
|
this.options = {
|
||||||
validatePristine: this.form.options.defaultValidatePristine ?? false,
|
asyncDebounceMs: this.form.options.asyncDebounceMs ?? 0,
|
||||||
validateOn: this.form.options.defaultValidateOn ?? 'change',
|
onChangeAsyncDebounceMs: this.form.options.onChangeAsyncDebounceMs ?? 0,
|
||||||
validateAsyncOn: this.form.options.defaultValidateAsyncOn ?? 'blur',
|
onBlurAsyncDebounceMs: this.form.options.onBlurAsyncDebounceMs ?? 0,
|
||||||
validateAsyncDebounceMs:
|
|
||||||
this.form.options.defaultValidateAsyncDebounceMs ?? 0,
|
|
||||||
...opts,
|
...opts,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -207,12 +207,12 @@ export class FieldApi<TData, TFormData> {
|
|||||||
form: this.form,
|
form: this.form,
|
||||||
})
|
})
|
||||||
|
|
||||||
validateSync = async (value = this.state.value) => {
|
validateSync = async (value = this.state.value, cause: ValidationCause) => {
|
||||||
const { validate } = this.options
|
const { onChange, onBlur } = this.options
|
||||||
|
const validate =
|
||||||
|
cause === 'submit' ? undefined : cause === 'change' ? onChange : onBlur
|
||||||
|
|
||||||
if (!validate) {
|
if (!validate) return
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use the validationCount for all field instances to
|
// Use the validationCount for all field instances to
|
||||||
// track freshness of the validation
|
// track freshness of the validation
|
||||||
@@ -249,12 +249,33 @@ export class FieldApi<TData, TFormData> {
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
validateAsync = async (value = this.state.value) => {
|
validateAsync = async (value = this.state.value, cause: ValidationCause) => {
|
||||||
const { validateAsync, validateAsyncDebounceMs } = this.options
|
const {
|
||||||
|
onChangeAsync,
|
||||||
|
onBlurAsync,
|
||||||
|
onSubmitAsync,
|
||||||
|
asyncDebounceMs,
|
||||||
|
onBlurAsyncDebounceMs,
|
||||||
|
onChangeAsyncDebounceMs,
|
||||||
|
} = this.options
|
||||||
|
|
||||||
if (!validateAsync) {
|
const validate =
|
||||||
return
|
cause === 'change'
|
||||||
}
|
? onChangeAsync
|
||||||
|
: cause === 'submit'
|
||||||
|
? onSubmitAsync
|
||||||
|
: onBlurAsync
|
||||||
|
|
||||||
|
if (!validate) return
|
||||||
|
|
||||||
|
const debounceMs =
|
||||||
|
cause === 'submit'
|
||||||
|
? 0
|
||||||
|
: (cause === 'change'
|
||||||
|
? onChangeAsyncDebounceMs
|
||||||
|
: onBlurAsyncDebounceMs) ??
|
||||||
|
asyncDebounceMs ??
|
||||||
|
500
|
||||||
|
|
||||||
if (this.state.meta.isValidating !== true)
|
if (this.state.meta.isValidating !== true)
|
||||||
this.setMeta((prev) => ({ ...prev, isValidating: true }))
|
this.setMeta((prev) => ({ ...prev, isValidating: true }))
|
||||||
@@ -273,14 +294,14 @@ export class FieldApi<TData, TFormData> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (validateAsyncDebounceMs > 0) {
|
if (debounceMs > 0) {
|
||||||
await new Promise((r) => setTimeout(r, validateAsyncDebounceMs))
|
await new Promise((r) => setTimeout(r, debounceMs))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only kick off validation if this validation is the latest attempt
|
// Only kick off validation if this validation is the latest attempt
|
||||||
if (checkLatest()) {
|
if (checkLatest()) {
|
||||||
try {
|
try {
|
||||||
const rawError = await validateAsync(value, this)
|
const rawError = await validate(value, this)
|
||||||
|
|
||||||
if (checkLatest()) {
|
if (checkLatest()) {
|
||||||
const error = normalizeError(rawError)
|
const error = normalizeError(rawError)
|
||||||
@@ -308,44 +329,25 @@ export class FieldApi<TData, TFormData> {
|
|||||||
return this.getInfo().validationPromise
|
return this.getInfo().validationPromise
|
||||||
}
|
}
|
||||||
|
|
||||||
shouldValidate = (isAsync: boolean, cause?: ValidationCause) => {
|
validate = (
|
||||||
const { validateOn, validateAsyncOn } = this.options
|
cause: ValidationCause,
|
||||||
const level = getValidationCauseLevel(cause)
|
|
||||||
|
|
||||||
// Must meet *at least* the validation level to validate,
|
|
||||||
// e.g. if validateOn is 'change' and validateCause is 'blur',
|
|
||||||
// the field will still validate
|
|
||||||
return Object.keys(validateCauseLevels).some((d) =>
|
|
||||||
isAsync
|
|
||||||
? validateAsyncOn
|
|
||||||
: validateOn === d && level >= validateCauseLevels[d],
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
validate = async (
|
|
||||||
cause?: ValidationCause,
|
|
||||||
value?: TData,
|
value?: TData,
|
||||||
): Promise<ValidationError> => {
|
): ValidationError | Promise<ValidationError> => {
|
||||||
// If the field is pristine and validatePristine is false, do not validate
|
// If the field is pristine and validatePristine is false, do not validate
|
||||||
if (!this.options.validatePristine && !this.state.meta.isTouched) return
|
if (!this.state.meta.isTouched) return
|
||||||
|
|
||||||
// Attempt to sync validate first
|
// Attempt to sync validate first
|
||||||
if (this.shouldValidate(false, cause)) {
|
this.validateSync(value, cause)
|
||||||
this.validateSync(value)
|
|
||||||
}
|
|
||||||
|
|
||||||
// If there is an error, return it, do not attempt async validation
|
// If there is an error, return it, do not attempt async validation
|
||||||
if (this.state.meta.error) {
|
if (this.state.meta.error) {
|
||||||
return this.state.meta.error
|
if (!this.options.asyncAlways) {
|
||||||
|
return this.state.meta.error
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// No error? Attempt async validation
|
// No error? Attempt async validation
|
||||||
if (this.shouldValidate(true, cause)) {
|
return this.validateAsync(value, cause)
|
||||||
return this.validateAsync(value)
|
|
||||||
}
|
|
||||||
|
|
||||||
// If there is no sync error or async validation attempt, there is no error
|
|
||||||
return undefined
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getChangeProps = <T extends UserChangeProps<any>>(
|
getChangeProps = <T extends UserChangeProps<any>>(
|
||||||
@@ -359,9 +361,12 @@ export class FieldApi<TData, TFormData> {
|
|||||||
props.onChange?.(value)
|
props.onChange?.(value)
|
||||||
},
|
},
|
||||||
onBlur: (e) => {
|
onBlur: (e) => {
|
||||||
|
const prevTouched = this.state.meta.isTouched
|
||||||
this.setMeta((prev) => ({ ...prev, isTouched: true }))
|
this.setMeta((prev) => ({ ...prev, isTouched: true }))
|
||||||
|
if (!prevTouched) {
|
||||||
|
this.validate('change')
|
||||||
|
}
|
||||||
this.validate('blur')
|
this.validate('blur')
|
||||||
props.onBlur?.(e)
|
|
||||||
},
|
},
|
||||||
} as ChangeProps<TData> & Omit<T, keyof ChangeProps<TData>>
|
} as ChangeProps<TData> & Omit<T, keyof ChangeProps<TData>>
|
||||||
}
|
}
|
||||||
@@ -381,16 +386,6 @@ export class FieldApi<TData, TFormData> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const validateCauseLevels = {
|
|
||||||
change: 0,
|
|
||||||
blur: 1,
|
|
||||||
submit: 2,
|
|
||||||
}
|
|
||||||
|
|
||||||
function getValidationCauseLevel(cause?: ValidationCause) {
|
|
||||||
return !cause ? 3 : validateCauseLevels[cause]
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizeError(rawError?: ValidationError) {
|
function normalizeError(rawError?: ValidationError) {
|
||||||
if (rawError) {
|
if (rawError) {
|
||||||
if (typeof rawError !== 'string') {
|
if (typeof rawError !== 'string') {
|
||||||
|
|||||||
@@ -17,13 +17,27 @@ export type FormSubmitEvent = Register extends {
|
|||||||
export type FormOptions<TData> = {
|
export type FormOptions<TData> = {
|
||||||
defaultValues?: TData
|
defaultValues?: TData
|
||||||
defaultState?: Partial<FormState<TData>>
|
defaultState?: Partial<FormState<TData>>
|
||||||
onSubmit?: (values: TData, formApi: FormApi<TData>) => void
|
asyncDebounceMs?: number
|
||||||
onInvalidSubmit?: (values: TData, formApi: FormApi<TData>) => void
|
onMount?: (values: TData, formApi: FormApi<TData>) => ValidationError
|
||||||
validate?: (values: TData, formApi: FormApi<TData>) => Promise<any>
|
onMountAsync?: (
|
||||||
defaultValidatePristine?: boolean
|
values: TData,
|
||||||
defaultValidateOn?: ValidationCause
|
formApi: FormApi<TData>,
|
||||||
defaultValidateAsyncOn?: ValidationCause
|
) => ValidationError | Promise<ValidationError>
|
||||||
defaultValidateAsyncDebounceMs?: number
|
onMountAsyncDebounceMs?: number
|
||||||
|
onChange?: (values: TData, formApi: FormApi<TData>) => ValidationError
|
||||||
|
onChangeAsync?: (
|
||||||
|
values: TData,
|
||||||
|
formApi: FormApi<TData>,
|
||||||
|
) => ValidationError | Promise<ValidationError>
|
||||||
|
onChangeAsyncDebounceMs?: number
|
||||||
|
onBlur?: (values: TData, formApi: FormApi<TData>) => ValidationError
|
||||||
|
onBlurAsync?: (
|
||||||
|
values: TData,
|
||||||
|
formApi: FormApi<TData>,
|
||||||
|
) => ValidationError | Promise<ValidationError>
|
||||||
|
onBlurAsyncDebounceMs?: number
|
||||||
|
onSubmit?: (values: TData, formApi: FormApi<TData>) => any | Promise<any>
|
||||||
|
onSubmitInvalid?: (values: TData, formApi: FormApi<TData>) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export type FieldInfo<TFormData> = {
|
export type FieldInfo<TFormData> = {
|
||||||
@@ -99,7 +113,7 @@ export class FormApi<TFormData> {
|
|||||||
getDefaultFormState({
|
getDefaultFormState({
|
||||||
...opts?.defaultState,
|
...opts?.defaultState,
|
||||||
values: opts?.defaultValues ?? opts?.defaultState?.values,
|
values: opts?.defaultValues ?? opts?.defaultState?.values,
|
||||||
isFormValid: !opts?.validate,
|
isFormValid: true,
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
onUpdate: (next) => {
|
onUpdate: (next) => {
|
||||||
@@ -175,7 +189,7 @@ export class FormApi<TFormData> {
|
|||||||
reset = () =>
|
reset = () =>
|
||||||
this.store.setState(() => getDefaultFormState(this.options.defaultValues!))
|
this.store.setState(() => getDefaultFormState(this.options.defaultValues!))
|
||||||
|
|
||||||
validateAllFields = async () => {
|
validateAllFields = async (cause: ValidationCause) => {
|
||||||
const fieldValidationPromises: Promise<ValidationError>[] = [] as any
|
const fieldValidationPromises: Promise<ValidationError>[] = [] as any
|
||||||
|
|
||||||
this.store.batch(() => {
|
this.store.batch(() => {
|
||||||
@@ -187,9 +201,9 @@ export class FormApi<TFormData> {
|
|||||||
// Mark them as touched
|
// Mark them as touched
|
||||||
instance.setMeta((prev) => ({ ...prev, isTouched: true }))
|
instance.setMeta((prev) => ({ ...prev, isTouched: true }))
|
||||||
// Validate the field
|
// Validate the field
|
||||||
if (instance.options.validate) {
|
fieldValidationPromises.push(
|
||||||
fieldValidationPromises.push(instance.validate())
|
Promise.resolve().then(() => instance.validate(cause)),
|
||||||
}
|
)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
@@ -199,63 +213,7 @@ export class FormApi<TFormData> {
|
|||||||
return Promise.all(fieldValidationPromises)
|
return Promise.all(fieldValidationPromises)
|
||||||
}
|
}
|
||||||
|
|
||||||
validateForm = async () => {
|
validateForm = async () => {}
|
||||||
const { validate } = this.options
|
|
||||||
|
|
||||||
if (!validate) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use the formValidationCount for all field instances to
|
|
||||||
// track freshness of the validation
|
|
||||||
this.store.setState((prev) => ({
|
|
||||||
...prev,
|
|
||||||
isValidating: true,
|
|
||||||
formValidationCount: prev.formValidationCount + 1,
|
|
||||||
}))
|
|
||||||
|
|
||||||
const formValidationCount = this.state.formValidationCount
|
|
||||||
|
|
||||||
const checkLatest = () =>
|
|
||||||
formValidationCount === this.state.formValidationCount
|
|
||||||
|
|
||||||
if (!this.validationMeta.validationPromise) {
|
|
||||||
this.validationMeta.validationPromise = new Promise((resolve, reject) => {
|
|
||||||
this.validationMeta.validationResolve = resolve
|
|
||||||
this.validationMeta.validationReject = reject
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const doValidation = async () => {
|
|
||||||
try {
|
|
||||||
const error = await validate(this.state.values, this)
|
|
||||||
|
|
||||||
if (checkLatest()) {
|
|
||||||
this.store.setState((prev) => ({
|
|
||||||
...prev,
|
|
||||||
isValidating: false,
|
|
||||||
error: error
|
|
||||||
? typeof error === 'string'
|
|
||||||
? error
|
|
||||||
: 'Invalid Form Values'
|
|
||||||
: null,
|
|
||||||
}))
|
|
||||||
|
|
||||||
this.validationMeta.validationResolve?.(error)
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
if (checkLatest()) {
|
|
||||||
this.validationMeta.validationReject?.(err)
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
delete this.validationMeta.validationPromise
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
doValidation()
|
|
||||||
|
|
||||||
return this.validationMeta.validationPromise
|
|
||||||
}
|
|
||||||
|
|
||||||
handleSubmit = async (e: FormSubmitEvent) => {
|
handleSubmit = async (e: FormSubmitEvent) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
@@ -284,12 +242,12 @@ export class FormApi<TFormData> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Validate all fields
|
// Validate all fields
|
||||||
await this.validateAllFields()
|
await this.validateAllFields('submit')
|
||||||
|
|
||||||
// Fields are invalid, do not submit
|
// Fields are invalid, do not submit
|
||||||
if (!this.state.isFieldsValid) {
|
if (!this.state.isFieldsValid) {
|
||||||
done()
|
done()
|
||||||
this.options.onInvalidSubmit?.(this.state.values, this)
|
this.options.onSubmitInvalid?.(this.state.values, this)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -298,7 +256,7 @@ export class FormApi<TFormData> {
|
|||||||
|
|
||||||
if (!this.state.isValid) {
|
if (!this.state.isValid) {
|
||||||
done()
|
done()
|
||||||
this.options.onInvalidSubmit?.(this.state.values, this)
|
this.options.onSubmitInvalid?.(this.state.values, this)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -352,22 +310,22 @@ export class FormApi<TFormData> {
|
|||||||
updater: Updater<DeepValue<TFormData, TField>>,
|
updater: Updater<DeepValue<TFormData, TField>>,
|
||||||
opts?: { touch?: boolean },
|
opts?: { touch?: boolean },
|
||||||
) => {
|
) => {
|
||||||
const touch = opts?.touch ?? true
|
const touch = opts?.touch
|
||||||
|
|
||||||
this.store.batch(() => {
|
this.store.batch(() => {
|
||||||
this.store.setState((prev) => {
|
|
||||||
return {
|
|
||||||
...prev,
|
|
||||||
values: setBy(prev.values, field, updater),
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
if (touch) {
|
if (touch) {
|
||||||
this.setFieldMeta(field, (prev) => ({
|
this.setFieldMeta(field, (prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
isTouched: true,
|
isTouched: true,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.store.setState((prev) => {
|
||||||
|
return {
|
||||||
|
...prev,
|
||||||
|
values: setBy(prev.values, field, updater),
|
||||||
|
}
|
||||||
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -392,10 +350,6 @@ export class FormApi<TFormData> {
|
|||||||
this.setFieldValue(
|
this.setFieldValue(
|
||||||
field,
|
field,
|
||||||
(prev) => {
|
(prev) => {
|
||||||
// invariant( // TODO: bring in invariant
|
|
||||||
// Array.isArray(prev),
|
|
||||||
// `Cannot insert a field value into a non-array field. Check that this field's existing value is an array: ${field}.`
|
|
||||||
// )
|
|
||||||
return (prev as DeepValue<TFormData, TField>[]).map((d, i) =>
|
return (prev as DeepValue<TFormData, TField>[]).map((d, i) =>
|
||||||
i === index ? value : d,
|
i === index ? value : d,
|
||||||
) as any
|
) as any
|
||||||
@@ -412,10 +366,6 @@ export class FormApi<TFormData> {
|
|||||||
this.setFieldValue(
|
this.setFieldValue(
|
||||||
field,
|
field,
|
||||||
(prev) => {
|
(prev) => {
|
||||||
// invariant( // TODO: bring in invariant
|
|
||||||
// Array.isArray(prev),
|
|
||||||
// `Cannot insert a field value into a non-array field. Check that this field's existing value is an array: ${field}.`
|
|
||||||
// )
|
|
||||||
return (prev as DeepValue<TFormData, TField>[]).filter(
|
return (prev as DeepValue<TFormData, TField>[]).filter(
|
||||||
(_d, i) => i !== index,
|
(_d, i) => i !== index,
|
||||||
) as any
|
) as any
|
||||||
|
|||||||
@@ -37,7 +37,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tanstack/form-core": "workspace:*",
|
"@tanstack/form-core": "workspace:*",
|
||||||
"@tanstack/react-store": "0.0.1-beta.84"
|
"@tanstack/react-store": "0.0.1-beta.85"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"react": "^17.0.0 || ^18.0.0",
|
"react": "^17.0.0 || ^18.0.0",
|
||||||
|
|||||||
3676
pnpm-lock.yaml
generated
3676
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user