@tanstack/form-core
Advanced tools
Comparing version 0.3.7 to 0.4.0
export { FieldApi, FieldApiOptions, FieldMeta, FieldOptions, FieldState, ResolveName, ValidationCause } from './index.js'; | ||
import './utils.js'; | ||
import '@tanstack/store'; | ||
import './types.js'; |
@@ -1,11 +0,5 @@ | ||
import { | ||
__privateAdd, | ||
__privateGet | ||
} from "./chunk-4QZDOMDG.js"; | ||
// src/FieldApi.ts | ||
import { Store } from "@tanstack/store"; | ||
var uid = 0; | ||
var _leaseValidateAsync; | ||
var _FieldApi = class _FieldApi { | ||
var FieldApi = class _FieldApi { | ||
constructor(opts) { | ||
@@ -87,3 +81,21 @@ this.options = {}; | ||
this.getInfo().validationCount = validationCount; | ||
const error = normalizeError(validate(value, this)); | ||
const doValidate = () => { | ||
if (this.options.validator && typeof validate !== "function") { | ||
return this.options.validator().validate( | ||
value, | ||
validate | ||
); | ||
} | ||
if (this.form.options.validator && typeof validate !== "function") { | ||
return this.form.options.validator().validate( | ||
value, | ||
validate | ||
); | ||
} | ||
return validate( | ||
value, | ||
this | ||
); | ||
}; | ||
const error = normalizeError(doValidate()); | ||
const errorMapKey = getErrorMapKey(cause); | ||
@@ -103,9 +115,9 @@ if (this.state.meta.errorMap[errorMapKey] !== error) { | ||
}; | ||
__privateAdd(this, _leaseValidateAsync, () => { | ||
this.__leaseValidateAsync = () => { | ||
const count = (this.getInfo().validationAsyncCount || 0) + 1; | ||
this.getInfo().validationAsyncCount = count; | ||
return count; | ||
}); | ||
}; | ||
this.cancelValidateAsync = () => { | ||
__privateGet(this, _leaseValidateAsync).call(this); | ||
this.__leaseValidateAsync(); | ||
this.setMeta((prev) => ({ | ||
@@ -130,5 +142,6 @@ ...prev, | ||
const debounceMs = cause === "submit" ? 0 : (cause === "change" ? onChangeAsyncDebounceMs : onBlurAsyncDebounceMs) ?? asyncDebounceMs ?? 0; | ||
if (this.state.meta.isValidating !== true) | ||
if (this.state.meta.isValidating !== true) { | ||
this.setMeta((prev) => ({ ...prev, isValidating: true })); | ||
const validationAsyncCount = __privateGet(this, _leaseValidateAsync).call(this); | ||
} | ||
const validationAsyncCount = this.__leaseValidateAsync(); | ||
const checkLatest = () => validationAsyncCount === this.getInfo().validationAsyncCount; | ||
@@ -144,6 +157,21 @@ if (!this.getInfo().validationPromise) { | ||
} | ||
const doValidate = () => { | ||
if (this.options.validator && typeof validate !== "function") { | ||
return this.options.validator().validateAsync( | ||
value, | ||
validate | ||
); | ||
} | ||
if (this.form.options.validator && typeof validate !== "function") { | ||
return this.form.options.validator().validateAsync(value, validate); | ||
} | ||
return validate( | ||
value, | ||
this | ||
); | ||
}; | ||
if (checkLatest()) { | ||
const prevErrors = this.getMeta().errors; | ||
try { | ||
const rawError = await validate(value, this); | ||
const rawError = await doValidate(); | ||
if (checkLatest()) { | ||
@@ -178,5 +206,7 @@ const error = normalizeError(rawError); | ||
return []; | ||
const errorMapKey = getErrorMapKey(cause); | ||
const prevError = this.getMeta().errorMap[errorMapKey]; | ||
this.validateSync(value, cause); | ||
const errorMapKey = getErrorMapKey(cause); | ||
if (this.getMeta().errorMap[errorMapKey]) { | ||
const newError = this.getMeta().errorMap[errorMapKey]; | ||
if (prevError !== newError) { | ||
if (!this.options.asyncAlways) { | ||
@@ -235,4 +265,2 @@ return this.state.meta.errors; | ||
}; | ||
_leaseValidateAsync = new WeakMap(); | ||
var FieldApi = _FieldApi; | ||
function normalizeError(rawError) { | ||
@@ -239,0 +267,0 @@ if (rawError) { |
import '@tanstack/store'; | ||
import './utils.js'; | ||
export { FieldInfo, FormApi, FormOptions, FormState, ValidationError, ValidationErrorMap, ValidationErrorMapKeys, ValidationMeta } from './index.js'; | ||
export { FieldInfo, FormApi, FormOptions, FormState, ValidationErrorMap, ValidationErrorMapKeys, ValidationMeta } from './index.js'; | ||
import './types.js'; |
@@ -1,3 +0,1 @@ | ||
import "./chunk-4QZDOMDG.js"; | ||
// src/FormApi.ts | ||
@@ -65,14 +63,12 @@ import { Store } from "@tanstack/store"; | ||
this.store.batch(() => { | ||
void Object.values(this.fieldInfo).forEach( | ||
(field) => { | ||
Object.values(field.instances).forEach((instance) => { | ||
if (!instance.state.meta.isTouched) { | ||
instance.setMeta((prev) => ({ ...prev, isTouched: true })); | ||
fieldValidationPromises.push( | ||
Promise.resolve().then(() => instance.validate(cause)) | ||
); | ||
} | ||
}); | ||
} | ||
); | ||
void Object.values(this.fieldInfo).forEach((field) => { | ||
Object.values(field.instances).forEach((instance) => { | ||
if (!instance.state.meta.isTouched) { | ||
instance.setMeta((prev) => ({ ...prev, isTouched: true })); | ||
fieldValidationPromises.push( | ||
Promise.resolve().then(() => instance.validate(cause)) | ||
); | ||
} | ||
}); | ||
}); | ||
}); | ||
@@ -79,0 +75,0 @@ return Promise.all(fieldValidationPromises); |
import { Store } from '@tanstack/store'; | ||
import { DeepKeys, DeepValue, Updater } from './utils.js'; | ||
export { Narrow, Pretty, RequiredByKey, UpdaterFn, functionalUpdate, getBy, isNonEmptyArray, setBy } from './utils.js'; | ||
import { ValidationError, Validator } from './types.js'; | ||
type FormOptions<TData> = { | ||
type ValidateFn$1<TData, ValidatorType> = (values: TData, formApi: FormApi<TData, ValidatorType>) => ValidationError; | ||
type ValidateOrFn$1<TData, ValidatorType> = ValidatorType extends Validator<TData> ? Parameters<ReturnType<ValidatorType>['validate']>[1] : ValidateFn$1<TData, ValidatorType>; | ||
type ValidateAsyncFn$1<TData, ValidatorType> = (value: TData, fieldApi: FormApi<TData, ValidatorType>) => ValidationError | Promise<ValidationError>; | ||
type FormOptions<TData, ValidatorType> = { | ||
defaultValues?: TData; | ||
defaultState?: Partial<FormState<TData>>; | ||
asyncDebounceMs?: number; | ||
onMount?: (values: TData, formApi: FormApi<TData>) => ValidationError; | ||
onMountAsync?: (values: TData, formApi: FormApi<TData>) => ValidationError | Promise<ValidationError>; | ||
validator?: ValidatorType; | ||
onMount?: ValidateOrFn$1<TData, ValidatorType>; | ||
onMountAsync?: ValidateAsyncFn$1<TData, ValidatorType>; | ||
onMountAsyncDebounceMs?: number; | ||
onChange?: (values: TData, formApi: FormApi<TData>) => ValidationError; | ||
onChangeAsync?: (values: TData, formApi: FormApi<TData>) => ValidationError | Promise<ValidationError>; | ||
onChange?: ValidateOrFn$1<TData, ValidatorType>; | ||
onChangeAsync?: ValidateAsyncFn$1<TData, ValidatorType>; | ||
onChangeAsyncDebounceMs?: number; | ||
onBlur?: (values: TData, formApi: FormApi<TData>) => ValidationError; | ||
onBlurAsync?: (values: TData, formApi: FormApi<TData>) => ValidationError | Promise<ValidationError>; | ||
onBlur?: ValidateOrFn$1<TData, ValidatorType>; | ||
onBlurAsync?: ValidateAsyncFn$1<TData, ValidatorType>; | ||
onBlurAsyncDebounceMs?: number; | ||
onSubmit?: (values: TData, formApi: FormApi<TData>) => any | Promise<any>; | ||
onSubmitInvalid?: (values: TData, formApi: FormApi<TData>) => void; | ||
onSubmit?: (values: TData, formApi: FormApi<TData, ValidatorType>) => any | Promise<any>; | ||
onSubmitInvalid?: (values: TData, formApi: FormApi<TData, ValidatorType>) => void; | ||
}; | ||
type FieldInfo<TFormData> = { | ||
instances: Record<string, FieldApi<TFormData, any, any>>; | ||
type FieldInfo<TFormData, ValidatorType> = { | ||
instances: Record<string, FieldApi<TFormData, any, unknown, ValidatorType>>; | ||
} & ValidationMeta; | ||
@@ -31,3 +36,2 @@ type ValidationMeta = { | ||
}; | ||
type ValidationError = undefined | false | null | string; | ||
type ValidationErrorMapKeys = `on${Capitalize<ValidationCause>}`; | ||
@@ -54,11 +58,11 @@ type ValidationErrorMap = { | ||
}; | ||
declare class FormApi<TFormData> { | ||
options: FormOptions<TFormData>; | ||
declare class FormApi<TFormData, ValidatorType> { | ||
options: FormOptions<TFormData, ValidatorType>; | ||
store: Store<FormState<TFormData>>; | ||
state: FormState<TFormData>; | ||
fieldInfo: Record<DeepKeys<TFormData>, FieldInfo<TFormData>>; | ||
fieldInfo: Record<DeepKeys<TFormData>, FieldInfo<TFormData, ValidatorType>>; | ||
fieldName?: string; | ||
validationMeta: ValidationMeta; | ||
constructor(opts?: FormOptions<TFormData>); | ||
update: (options?: FormOptions<TFormData>) => void; | ||
constructor(opts?: FormOptions<TFormData, ValidatorType>); | ||
update: (options?: FormOptions<TFormData, ValidatorType>) => void; | ||
reset: () => void; | ||
@@ -69,3 +73,3 @@ validateAllFields: (cause: ValidationCause) => Promise<ValidationError[][]>; | ||
getFieldMeta: <TField extends DeepKeys<TFormData>>(field: TField) => FieldMeta | undefined; | ||
getFieldInfo: <TField extends DeepKeys<TFormData>>(field: TField) => FieldInfo<TFormData>; | ||
getFieldInfo: <TField extends DeepKeys<TFormData>>(field: TField) => FieldInfo<TFormData, ValidatorType>; | ||
setFieldMeta: <TField extends DeepKeys<TFormData>>(field: TField, updater: Updater<FieldMeta>) => void; | ||
@@ -75,6 +79,6 @@ setFieldValue: <TField extends DeepKeys<TFormData>>(field: TField, updater: Updater<DeepValue<TFormData, TField>>, opts?: { | ||
}) => void; | ||
pushFieldValue: <TField extends DeepKeys<TFormData>>(field: TField, value: DeepValue<TFormData, TField>[number], opts?: { | ||
pushFieldValue: <TField extends DeepKeys<TFormData>>(field: TField, value: DeepValue<TFormData, TField> extends any[] ? DeepValue<TFormData, TField>[number] : never, opts?: { | ||
touch?: boolean; | ||
}) => void; | ||
insertFieldValue: <TField extends DeepKeys<TFormData>>(field: TField, index: number, value: DeepValue<TFormData, TField>[number], opts?: { | ||
insertFieldValue: <TField extends DeepKeys<TFormData>>(field: TField, index: number, value: DeepValue<TFormData, TField> extends any[] ? DeepValue<TFormData, TField>[number] : never, opts?: { | ||
touch?: boolean; | ||
@@ -89,15 +93,8 @@ }) => void; | ||
type ValidationCause = 'change' | 'blur' | 'submit' | 'mount'; | ||
type ValidateFn<TParentData, TName extends DeepKeys<TParentData>, TData> = (value: TData, fieldApi: FieldApi<TParentData, TName>) => ValidationError; | ||
type ValidateAsyncFn<TParentData, TName extends DeepKeys<TParentData>, TData> = (value: TData, fieldApi: FieldApi<TParentData, TName>) => ValidationError | Promise<ValidationError>; | ||
interface FieldOptions<TParentData, | ||
/** | ||
* This allows us to restrict the name to only be a valid field name while | ||
* also assigning it to a generic | ||
*/ | ||
TName extends DeepKeys<TParentData>, | ||
/** | ||
* If TData is unknown, we can use the TName generic to determine the type | ||
*/ | ||
TData = DeepValue<TParentData, TName>> { | ||
name: DeepKeys<TParentData>; | ||
type ValidateFn<TParentData, TName extends DeepKeys<TParentData>, ValidatorType, TData extends DeepValue<TParentData, TName> = DeepValue<TParentData, TName>> = (value: TData, fieldApi: FieldApi<TParentData, TName, ValidatorType, TData>) => ValidationError; | ||
type ValidateOrFn<TParentData, TName extends DeepKeys<TParentData>, ValidatorType, FormValidator, TData extends DeepValue<TParentData, TName> = DeepValue<TParentData, TName>> = ValidatorType extends Validator<TData> ? Parameters<ReturnType<ValidatorType>['validate']>[1] | ValidateFn<TParentData, TName, ValidatorType, TData> : FormValidator extends Validator<TData> ? Parameters<ReturnType<FormValidator>['validate']>[1] | ValidateFn<TParentData, TName, ValidatorType, TData> : ValidateFn<TParentData, TName, ValidatorType, TData>; | ||
type ValidateAsyncFn<TParentData, TName extends DeepKeys<TParentData>, ValidatorType, TData extends DeepValue<TParentData, TName> = DeepValue<TParentData, TName>> = (value: TData, fieldApi: FieldApi<TParentData, TName, ValidatorType, TData>) => ValidationError | Promise<ValidationError>; | ||
type AsyncValidateOrFn<TParentData, TName extends DeepKeys<TParentData>, ValidatorType, FormValidator, TData extends DeepValue<TParentData, TName> = DeepValue<TParentData, TName>> = ValidatorType extends Validator<TData> ? Parameters<ReturnType<ValidatorType>['validate']>[1] | ValidateAsyncFn<TParentData, TName, ValidatorType, TData> : FormValidator extends Validator<TData> ? Parameters<ReturnType<FormValidator>['validate']>[1] | ValidateAsyncFn<TParentData, TName, ValidatorType, TData> : ValidateAsyncFn<TParentData, TName, ValidatorType, TData>; | ||
interface FieldOptions<TParentData, TName extends DeepKeys<TParentData>, ValidatorType, FormValidator, TData extends DeepValue<TParentData, TName> = DeepValue<TParentData, TName>> { | ||
name: TName; | ||
index?: TData extends any[] ? number : never; | ||
@@ -107,14 +104,15 @@ defaultValue?: TData; | ||
asyncAlways?: boolean; | ||
onMount?: (formApi: FieldApi<TParentData, TName>) => void; | ||
onChange?: ValidateFn<TParentData, TName, TData>; | ||
onChangeAsync?: ValidateAsyncFn<TParentData, TName, TData>; | ||
validator?: ValidatorType; | ||
onMount?: (formApi: FieldApi<TParentData, TName, ValidatorType, TData>) => void; | ||
onChange?: ValidateOrFn<TParentData, TName, ValidatorType, FormValidator, TData>; | ||
onChangeAsync?: AsyncValidateOrFn<TParentData, TName, ValidatorType, FormValidator, TData>; | ||
onChangeAsyncDebounceMs?: number; | ||
onBlur?: ValidateFn<TParentData, TName, TData>; | ||
onBlurAsync?: ValidateAsyncFn<TParentData, TName, TData>; | ||
onBlur?: ValidateOrFn<TParentData, TName, ValidatorType, FormValidator, TData>; | ||
onBlurAsync?: AsyncValidateOrFn<TParentData, TName, ValidatorType, FormValidator, TData>; | ||
onBlurAsyncDebounceMs?: number; | ||
onSubmitAsync?: ValidateAsyncFn<TParentData, TName, TData>; | ||
onSubmitAsync?: AsyncValidateOrFn<TParentData, TName, ValidatorType, FormValidator, TData>; | ||
defaultMeta?: Partial<FieldMeta>; | ||
} | ||
interface FieldApiOptions<TParentData, TName extends DeepKeys<TParentData>, TData = DeepValue<TParentData, TName>> extends FieldOptions<TParentData, TName, TData> { | ||
form: FormApi<TParentData>; | ||
interface FieldApiOptions<TParentData, TName extends DeepKeys<TParentData>, ValidatorType, FormValidator, TData extends DeepValue<TParentData, TName> = DeepValue<TParentData, TName>> extends FieldOptions<TParentData, TName, ValidatorType, FormValidator, TData> { | ||
form: FormApi<TParentData, FormValidator>; | ||
} | ||
@@ -133,16 +131,13 @@ type FieldMeta = { | ||
type ResolveName<TParentData> = unknown extends TParentData ? string : DeepKeys<TParentData>; | ||
declare class FieldApi<TParentData, TName extends DeepKeys<TParentData>, TData = DeepValue<TParentData, TName>> { | ||
#private; | ||
declare class FieldApi<TParentData, TName extends DeepKeys<TParentData>, ValidatorType, FormValidator, TData extends DeepValue<TParentData, TName> = DeepValue<TParentData, TName>> { | ||
uid: number; | ||
form: FieldApiOptions<TParentData, TName, TData>['form']; | ||
form: FieldApiOptions<TParentData, TName, ValidatorType, TData>['form']; | ||
name: DeepKeys<TParentData>; | ||
options: FieldApiOptions<TParentData, TName>; | ||
options: FieldApiOptions<TParentData, TName, ValidatorType, TData>; | ||
store: Store<FieldState<TData>>; | ||
state: FieldState<TData>; | ||
prevState: FieldState<TData>; | ||
constructor(opts: FieldApiOptions<TParentData, TName, TData> & { | ||
form: FormApi<TParentData>; | ||
}); | ||
constructor(opts: FieldApiOptions<TParentData, TName, ValidatorType, FormValidator, TData>); | ||
mount: () => () => void; | ||
update: (opts: FieldApiOptions<TParentData, TName, TData>) => void; | ||
update: (opts: FieldApiOptions<TParentData, TName, ValidatorType, TData>) => void; | ||
getValue: () => TData; | ||
@@ -156,3 +151,3 @@ setValue: (updater: Updater<TData>, options?: { | ||
setMeta: (updater: Updater<FieldMeta>) => void; | ||
getInfo: () => FieldInfo<TParentData>; | ||
getInfo: () => FieldInfo<TParentData, TData>; | ||
pushValue: (value: TData extends any[] ? TData[number] : never) => void; | ||
@@ -162,4 +157,5 @@ insertValue: (index: number, value: TData extends any[] ? TData[number] : never) => void; | ||
swapValues: (aIndex: number, bIndex: number) => void; | ||
getSubField: <TSubName extends DeepKeys<TData>, TSubData = DeepValue<TData, TSubName>>(name: TSubName) => FieldApi<TData, TSubName, TSubData>; | ||
getSubField: <TSubName extends DeepKeys<TData>, TSubData extends DeepValue<TData, TSubName> = DeepValue<TData, TSubName>>(name: TSubName) => FieldApi<TData, TSubName, ValidatorType, TSubData, DeepValue<TData, TSubName>>; | ||
validateSync: (value: TData | undefined, cause: ValidationCause) => void; | ||
__leaseValidateAsync: () => number; | ||
cancelValidateAsync: () => void; | ||
@@ -172,2 +168,2 @@ validateAsync: (value: TData | undefined, cause: ValidationCause) => Promise<ValidationError[]>; | ||
export { DeepKeys, DeepValue, FieldApi, FieldApiOptions, FieldInfo, FieldMeta, FieldOptions, FieldState, FormApi, FormOptions, FormState, ResolveName, Updater, ValidationCause, ValidationError, ValidationErrorMap, ValidationErrorMapKeys, ValidationMeta }; | ||
export { DeepKeys, DeepValue, FieldApi, FieldApiOptions, FieldInfo, FieldMeta, FieldOptions, FieldState, FormApi, FormOptions, FormState, ResolveName, Updater, ValidationCause, ValidationError, ValidationErrorMap, ValidationErrorMapKeys, ValidationMeta, Validator }; |
@@ -5,2 +5,3 @@ // src/index.ts | ||
export * from "./utils.js"; | ||
export * from "./types.js"; | ||
//# sourceMappingURL=index.js.map |
@@ -1,3 +0,1 @@ | ||
import "./chunk-4QZDOMDG.js"; | ||
// src/utils.ts | ||
@@ -4,0 +2,0 @@ function functionalUpdate(updater, input) { |
export { FieldApi, FieldApiOptions, FieldMeta, FieldOptions, FieldState, ResolveName, ValidationCause } from './index.js'; | ||
import './utils.js'; | ||
import '@tanstack/store'; | ||
import './types.js'; |
@@ -79,3 +79,21 @@ // src/FieldApi.ts | ||
this.getInfo().validationCount = validationCount; | ||
const error = normalizeError(validate(value, this)); | ||
const doValidate = () => { | ||
if (this.options.validator && typeof validate !== "function") { | ||
return this.options.validator().validate( | ||
value, | ||
validate | ||
); | ||
} | ||
if (this.form.options.validator && typeof validate !== "function") { | ||
return this.form.options.validator().validate( | ||
value, | ||
validate | ||
); | ||
} | ||
return validate( | ||
value, | ||
this | ||
); | ||
}; | ||
const error = normalizeError(doValidate()); | ||
const errorMapKey = getErrorMapKey(cause); | ||
@@ -95,3 +113,3 @@ if (this.state.meta.errorMap[errorMapKey] !== error) { | ||
}; | ||
this.#leaseValidateAsync = () => { | ||
this.__leaseValidateAsync = () => { | ||
const count = (this.getInfo().validationAsyncCount || 0) + 1; | ||
@@ -102,3 +120,3 @@ this.getInfo().validationAsyncCount = count; | ||
this.cancelValidateAsync = () => { | ||
this.#leaseValidateAsync(); | ||
this.__leaseValidateAsync(); | ||
this.setMeta((prev) => ({ | ||
@@ -122,5 +140,6 @@ ...prev, | ||
const debounceMs = cause === "submit" ? 0 : (cause === "change" ? onChangeAsyncDebounceMs : onBlurAsyncDebounceMs) ?? asyncDebounceMs ?? 0; | ||
if (this.state.meta.isValidating !== true) | ||
if (this.state.meta.isValidating !== true) { | ||
this.setMeta((prev) => ({ ...prev, isValidating: true })); | ||
const validationAsyncCount = this.#leaseValidateAsync(); | ||
} | ||
const validationAsyncCount = this.__leaseValidateAsync(); | ||
const checkLatest = () => validationAsyncCount === this.getInfo().validationAsyncCount; | ||
@@ -136,6 +155,21 @@ if (!this.getInfo().validationPromise) { | ||
} | ||
const doValidate = () => { | ||
if (this.options.validator && typeof validate !== "function") { | ||
return this.options.validator().validateAsync( | ||
value, | ||
validate | ||
); | ||
} | ||
if (this.form.options.validator && typeof validate !== "function") { | ||
return this.form.options.validator().validateAsync(value, validate); | ||
} | ||
return validate( | ||
value, | ||
this | ||
); | ||
}; | ||
if (checkLatest()) { | ||
const prevErrors = this.getMeta().errors; | ||
try { | ||
const rawError = await validate(value, this); | ||
const rawError = await doValidate(); | ||
if (checkLatest()) { | ||
@@ -170,5 +204,7 @@ const error = normalizeError(rawError); | ||
return []; | ||
const errorMapKey = getErrorMapKey(cause); | ||
const prevError = this.getMeta().errorMap[errorMapKey]; | ||
this.validateSync(value, cause); | ||
const errorMapKey = getErrorMapKey(cause); | ||
if (this.getMeta().errorMap[errorMapKey]) { | ||
const newError = this.getMeta().errorMap[errorMapKey]; | ||
if (prevError !== newError) { | ||
if (!this.options.asyncAlways) { | ||
@@ -226,3 +262,2 @@ return this.state.meta.errors; | ||
} | ||
#leaseValidateAsync; | ||
}; | ||
@@ -229,0 +264,0 @@ function normalizeError(rawError) { |
import '@tanstack/store'; | ||
import './utils.js'; | ||
export { FieldInfo, FormApi, FormOptions, FormState, ValidationError, ValidationErrorMap, ValidationErrorMapKeys, ValidationMeta } from './index.js'; | ||
export { FieldInfo, FormApi, FormOptions, FormState, ValidationErrorMap, ValidationErrorMapKeys, ValidationMeta } from './index.js'; | ||
import './types.js'; |
@@ -60,14 +60,12 @@ // src/FormApi.ts | ||
this.store.batch(() => { | ||
void Object.values(this.fieldInfo).forEach( | ||
(field) => { | ||
Object.values(field.instances).forEach((instance) => { | ||
if (!instance.state.meta.isTouched) { | ||
instance.setMeta((prev) => ({ ...prev, isTouched: true })); | ||
fieldValidationPromises.push( | ||
Promise.resolve().then(() => instance.validate(cause)) | ||
); | ||
} | ||
}); | ||
} | ||
); | ||
void Object.values(this.fieldInfo).forEach((field) => { | ||
Object.values(field.instances).forEach((instance) => { | ||
if (!instance.state.meta.isTouched) { | ||
instance.setMeta((prev) => ({ ...prev, isTouched: true })); | ||
fieldValidationPromises.push( | ||
Promise.resolve().then(() => instance.validate(cause)) | ||
); | ||
} | ||
}); | ||
}); | ||
}); | ||
@@ -74,0 +72,0 @@ return Promise.all(fieldValidationPromises); |
import { Store } from '@tanstack/store'; | ||
import { DeepKeys, DeepValue, Updater } from './utils.js'; | ||
export { Narrow, Pretty, RequiredByKey, UpdaterFn, functionalUpdate, getBy, isNonEmptyArray, setBy } from './utils.js'; | ||
import { ValidationError, Validator } from './types.js'; | ||
type FormOptions<TData> = { | ||
type ValidateFn$1<TData, ValidatorType> = (values: TData, formApi: FormApi<TData, ValidatorType>) => ValidationError; | ||
type ValidateOrFn$1<TData, ValidatorType> = ValidatorType extends Validator<TData> ? Parameters<ReturnType<ValidatorType>['validate']>[1] : ValidateFn$1<TData, ValidatorType>; | ||
type ValidateAsyncFn$1<TData, ValidatorType> = (value: TData, fieldApi: FormApi<TData, ValidatorType>) => ValidationError | Promise<ValidationError>; | ||
type FormOptions<TData, ValidatorType> = { | ||
defaultValues?: TData; | ||
defaultState?: Partial<FormState<TData>>; | ||
asyncDebounceMs?: number; | ||
onMount?: (values: TData, formApi: FormApi<TData>) => ValidationError; | ||
onMountAsync?: (values: TData, formApi: FormApi<TData>) => ValidationError | Promise<ValidationError>; | ||
validator?: ValidatorType; | ||
onMount?: ValidateOrFn$1<TData, ValidatorType>; | ||
onMountAsync?: ValidateAsyncFn$1<TData, ValidatorType>; | ||
onMountAsyncDebounceMs?: number; | ||
onChange?: (values: TData, formApi: FormApi<TData>) => ValidationError; | ||
onChangeAsync?: (values: TData, formApi: FormApi<TData>) => ValidationError | Promise<ValidationError>; | ||
onChange?: ValidateOrFn$1<TData, ValidatorType>; | ||
onChangeAsync?: ValidateAsyncFn$1<TData, ValidatorType>; | ||
onChangeAsyncDebounceMs?: number; | ||
onBlur?: (values: TData, formApi: FormApi<TData>) => ValidationError; | ||
onBlurAsync?: (values: TData, formApi: FormApi<TData>) => ValidationError | Promise<ValidationError>; | ||
onBlur?: ValidateOrFn$1<TData, ValidatorType>; | ||
onBlurAsync?: ValidateAsyncFn$1<TData, ValidatorType>; | ||
onBlurAsyncDebounceMs?: number; | ||
onSubmit?: (values: TData, formApi: FormApi<TData>) => any | Promise<any>; | ||
onSubmitInvalid?: (values: TData, formApi: FormApi<TData>) => void; | ||
onSubmit?: (values: TData, formApi: FormApi<TData, ValidatorType>) => any | Promise<any>; | ||
onSubmitInvalid?: (values: TData, formApi: FormApi<TData, ValidatorType>) => void; | ||
}; | ||
type FieldInfo<TFormData> = { | ||
instances: Record<string, FieldApi<TFormData, any, any>>; | ||
type FieldInfo<TFormData, ValidatorType> = { | ||
instances: Record<string, FieldApi<TFormData, any, unknown, ValidatorType>>; | ||
} & ValidationMeta; | ||
@@ -31,3 +36,2 @@ type ValidationMeta = { | ||
}; | ||
type ValidationError = undefined | false | null | string; | ||
type ValidationErrorMapKeys = `on${Capitalize<ValidationCause>}`; | ||
@@ -54,11 +58,11 @@ type ValidationErrorMap = { | ||
}; | ||
declare class FormApi<TFormData> { | ||
options: FormOptions<TFormData>; | ||
declare class FormApi<TFormData, ValidatorType> { | ||
options: FormOptions<TFormData, ValidatorType>; | ||
store: Store<FormState<TFormData>>; | ||
state: FormState<TFormData>; | ||
fieldInfo: Record<DeepKeys<TFormData>, FieldInfo<TFormData>>; | ||
fieldInfo: Record<DeepKeys<TFormData>, FieldInfo<TFormData, ValidatorType>>; | ||
fieldName?: string; | ||
validationMeta: ValidationMeta; | ||
constructor(opts?: FormOptions<TFormData>); | ||
update: (options?: FormOptions<TFormData>) => void; | ||
constructor(opts?: FormOptions<TFormData, ValidatorType>); | ||
update: (options?: FormOptions<TFormData, ValidatorType>) => void; | ||
reset: () => void; | ||
@@ -69,3 +73,3 @@ validateAllFields: (cause: ValidationCause) => Promise<ValidationError[][]>; | ||
getFieldMeta: <TField extends DeepKeys<TFormData>>(field: TField) => FieldMeta | undefined; | ||
getFieldInfo: <TField extends DeepKeys<TFormData>>(field: TField) => FieldInfo<TFormData>; | ||
getFieldInfo: <TField extends DeepKeys<TFormData>>(field: TField) => FieldInfo<TFormData, ValidatorType>; | ||
setFieldMeta: <TField extends DeepKeys<TFormData>>(field: TField, updater: Updater<FieldMeta>) => void; | ||
@@ -75,6 +79,6 @@ setFieldValue: <TField extends DeepKeys<TFormData>>(field: TField, updater: Updater<DeepValue<TFormData, TField>>, opts?: { | ||
}) => void; | ||
pushFieldValue: <TField extends DeepKeys<TFormData>>(field: TField, value: DeepValue<TFormData, TField>[number], opts?: { | ||
pushFieldValue: <TField extends DeepKeys<TFormData>>(field: TField, value: DeepValue<TFormData, TField> extends any[] ? DeepValue<TFormData, TField>[number] : never, opts?: { | ||
touch?: boolean; | ||
}) => void; | ||
insertFieldValue: <TField extends DeepKeys<TFormData>>(field: TField, index: number, value: DeepValue<TFormData, TField>[number], opts?: { | ||
insertFieldValue: <TField extends DeepKeys<TFormData>>(field: TField, index: number, value: DeepValue<TFormData, TField> extends any[] ? DeepValue<TFormData, TField>[number] : never, opts?: { | ||
touch?: boolean; | ||
@@ -89,15 +93,8 @@ }) => void; | ||
type ValidationCause = 'change' | 'blur' | 'submit' | 'mount'; | ||
type ValidateFn<TParentData, TName extends DeepKeys<TParentData>, TData> = (value: TData, fieldApi: FieldApi<TParentData, TName>) => ValidationError; | ||
type ValidateAsyncFn<TParentData, TName extends DeepKeys<TParentData>, TData> = (value: TData, fieldApi: FieldApi<TParentData, TName>) => ValidationError | Promise<ValidationError>; | ||
interface FieldOptions<TParentData, | ||
/** | ||
* This allows us to restrict the name to only be a valid field name while | ||
* also assigning it to a generic | ||
*/ | ||
TName extends DeepKeys<TParentData>, | ||
/** | ||
* If TData is unknown, we can use the TName generic to determine the type | ||
*/ | ||
TData = DeepValue<TParentData, TName>> { | ||
name: DeepKeys<TParentData>; | ||
type ValidateFn<TParentData, TName extends DeepKeys<TParentData>, ValidatorType, TData extends DeepValue<TParentData, TName> = DeepValue<TParentData, TName>> = (value: TData, fieldApi: FieldApi<TParentData, TName, ValidatorType, TData>) => ValidationError; | ||
type ValidateOrFn<TParentData, TName extends DeepKeys<TParentData>, ValidatorType, FormValidator, TData extends DeepValue<TParentData, TName> = DeepValue<TParentData, TName>> = ValidatorType extends Validator<TData> ? Parameters<ReturnType<ValidatorType>['validate']>[1] | ValidateFn<TParentData, TName, ValidatorType, TData> : FormValidator extends Validator<TData> ? Parameters<ReturnType<FormValidator>['validate']>[1] | ValidateFn<TParentData, TName, ValidatorType, TData> : ValidateFn<TParentData, TName, ValidatorType, TData>; | ||
type ValidateAsyncFn<TParentData, TName extends DeepKeys<TParentData>, ValidatorType, TData extends DeepValue<TParentData, TName> = DeepValue<TParentData, TName>> = (value: TData, fieldApi: FieldApi<TParentData, TName, ValidatorType, TData>) => ValidationError | Promise<ValidationError>; | ||
type AsyncValidateOrFn<TParentData, TName extends DeepKeys<TParentData>, ValidatorType, FormValidator, TData extends DeepValue<TParentData, TName> = DeepValue<TParentData, TName>> = ValidatorType extends Validator<TData> ? Parameters<ReturnType<ValidatorType>['validate']>[1] | ValidateAsyncFn<TParentData, TName, ValidatorType, TData> : FormValidator extends Validator<TData> ? Parameters<ReturnType<FormValidator>['validate']>[1] | ValidateAsyncFn<TParentData, TName, ValidatorType, TData> : ValidateAsyncFn<TParentData, TName, ValidatorType, TData>; | ||
interface FieldOptions<TParentData, TName extends DeepKeys<TParentData>, ValidatorType, FormValidator, TData extends DeepValue<TParentData, TName> = DeepValue<TParentData, TName>> { | ||
name: TName; | ||
index?: TData extends any[] ? number : never; | ||
@@ -107,14 +104,15 @@ defaultValue?: TData; | ||
asyncAlways?: boolean; | ||
onMount?: (formApi: FieldApi<TParentData, TName>) => void; | ||
onChange?: ValidateFn<TParentData, TName, TData>; | ||
onChangeAsync?: ValidateAsyncFn<TParentData, TName, TData>; | ||
validator?: ValidatorType; | ||
onMount?: (formApi: FieldApi<TParentData, TName, ValidatorType, TData>) => void; | ||
onChange?: ValidateOrFn<TParentData, TName, ValidatorType, FormValidator, TData>; | ||
onChangeAsync?: AsyncValidateOrFn<TParentData, TName, ValidatorType, FormValidator, TData>; | ||
onChangeAsyncDebounceMs?: number; | ||
onBlur?: ValidateFn<TParentData, TName, TData>; | ||
onBlurAsync?: ValidateAsyncFn<TParentData, TName, TData>; | ||
onBlur?: ValidateOrFn<TParentData, TName, ValidatorType, FormValidator, TData>; | ||
onBlurAsync?: AsyncValidateOrFn<TParentData, TName, ValidatorType, FormValidator, TData>; | ||
onBlurAsyncDebounceMs?: number; | ||
onSubmitAsync?: ValidateAsyncFn<TParentData, TName, TData>; | ||
onSubmitAsync?: AsyncValidateOrFn<TParentData, TName, ValidatorType, FormValidator, TData>; | ||
defaultMeta?: Partial<FieldMeta>; | ||
} | ||
interface FieldApiOptions<TParentData, TName extends DeepKeys<TParentData>, TData = DeepValue<TParentData, TName>> extends FieldOptions<TParentData, TName, TData> { | ||
form: FormApi<TParentData>; | ||
interface FieldApiOptions<TParentData, TName extends DeepKeys<TParentData>, ValidatorType, FormValidator, TData extends DeepValue<TParentData, TName> = DeepValue<TParentData, TName>> extends FieldOptions<TParentData, TName, ValidatorType, FormValidator, TData> { | ||
form: FormApi<TParentData, FormValidator>; | ||
} | ||
@@ -133,16 +131,13 @@ type FieldMeta = { | ||
type ResolveName<TParentData> = unknown extends TParentData ? string : DeepKeys<TParentData>; | ||
declare class FieldApi<TParentData, TName extends DeepKeys<TParentData>, TData = DeepValue<TParentData, TName>> { | ||
#private; | ||
declare class FieldApi<TParentData, TName extends DeepKeys<TParentData>, ValidatorType, FormValidator, TData extends DeepValue<TParentData, TName> = DeepValue<TParentData, TName>> { | ||
uid: number; | ||
form: FieldApiOptions<TParentData, TName, TData>['form']; | ||
form: FieldApiOptions<TParentData, TName, ValidatorType, TData>['form']; | ||
name: DeepKeys<TParentData>; | ||
options: FieldApiOptions<TParentData, TName>; | ||
options: FieldApiOptions<TParentData, TName, ValidatorType, TData>; | ||
store: Store<FieldState<TData>>; | ||
state: FieldState<TData>; | ||
prevState: FieldState<TData>; | ||
constructor(opts: FieldApiOptions<TParentData, TName, TData> & { | ||
form: FormApi<TParentData>; | ||
}); | ||
constructor(opts: FieldApiOptions<TParentData, TName, ValidatorType, FormValidator, TData>); | ||
mount: () => () => void; | ||
update: (opts: FieldApiOptions<TParentData, TName, TData>) => void; | ||
update: (opts: FieldApiOptions<TParentData, TName, ValidatorType, TData>) => void; | ||
getValue: () => TData; | ||
@@ -156,3 +151,3 @@ setValue: (updater: Updater<TData>, options?: { | ||
setMeta: (updater: Updater<FieldMeta>) => void; | ||
getInfo: () => FieldInfo<TParentData>; | ||
getInfo: () => FieldInfo<TParentData, TData>; | ||
pushValue: (value: TData extends any[] ? TData[number] : never) => void; | ||
@@ -162,4 +157,5 @@ insertValue: (index: number, value: TData extends any[] ? TData[number] : never) => void; | ||
swapValues: (aIndex: number, bIndex: number) => void; | ||
getSubField: <TSubName extends DeepKeys<TData>, TSubData = DeepValue<TData, TSubName>>(name: TSubName) => FieldApi<TData, TSubName, TSubData>; | ||
getSubField: <TSubName extends DeepKeys<TData>, TSubData extends DeepValue<TData, TSubName> = DeepValue<TData, TSubName>>(name: TSubName) => FieldApi<TData, TSubName, ValidatorType, TSubData, DeepValue<TData, TSubName>>; | ||
validateSync: (value: TData | undefined, cause: ValidationCause) => void; | ||
__leaseValidateAsync: () => number; | ||
cancelValidateAsync: () => void; | ||
@@ -172,2 +168,2 @@ validateAsync: (value: TData | undefined, cause: ValidationCause) => Promise<ValidationError[]>; | ||
export { DeepKeys, DeepValue, FieldApi, FieldApiOptions, FieldInfo, FieldMeta, FieldOptions, FieldState, FormApi, FormOptions, FormState, ResolveName, Updater, ValidationCause, ValidationError, ValidationErrorMap, ValidationErrorMapKeys, ValidationMeta }; | ||
export { DeepKeys, DeepValue, FieldApi, FieldApiOptions, FieldInfo, FieldMeta, FieldOptions, FieldState, FormApi, FormOptions, FormState, ResolveName, Updater, ValidationCause, ValidationError, ValidationErrorMap, ValidationErrorMapKeys, ValidationMeta, Validator }; |
@@ -5,2 +5,3 @@ // src/index.ts | ||
export * from "./utils.js"; | ||
export * from "./types.js"; | ||
//# sourceMappingURL=index.js.map |
{ | ||
"name": "@tanstack/form-core", | ||
"version": "0.3.7", | ||
"version": "0.4.0", | ||
"description": "Powerful, type-safe, framework agnostic forms.", | ||
@@ -5,0 +5,0 @@ "author": "tannerlinsley", |
import { type DeepKeys, type DeepValue, type Updater } from './utils' | ||
import type { FormApi, ValidationError, ValidationErrorMap } from './FormApi' | ||
import type { FormApi, ValidationErrorMap } from './FormApi' | ||
import { Store } from '@tanstack/store' | ||
import type { Validator, ValidationError } from './types' | ||
export type ValidationCause = 'change' | 'blur' | 'submit' | 'mount' | ||
type ValidateFn<TParentData, TName extends DeepKeys<TParentData>, TData> = ( | ||
type ValidateFn< | ||
TParentData, | ||
TName extends DeepKeys<TParentData>, | ||
ValidatorType, | ||
TData extends DeepValue<TParentData, TName> = DeepValue<TParentData, TName>, | ||
> = ( | ||
value: TData, | ||
fieldApi: FieldApi<TParentData, TName>, | ||
fieldApi: FieldApi<TParentData, TName, ValidatorType, TData>, | ||
) => ValidationError | ||
type ValidateOrFn< | ||
TParentData, | ||
TName extends DeepKeys<TParentData>, | ||
ValidatorType, | ||
FormValidator, | ||
TData extends DeepValue<TParentData, TName> = DeepValue<TParentData, TName>, | ||
> = ValidatorType extends Validator<TData> | ||
? | ||
| Parameters<ReturnType<ValidatorType>['validate']>[1] | ||
| ValidateFn<TParentData, TName, ValidatorType, TData> | ||
: FormValidator extends Validator<TData> | ||
? | ||
| Parameters<ReturnType<FormValidator>['validate']>[1] | ||
| ValidateFn<TParentData, TName, ValidatorType, TData> | ||
: ValidateFn<TParentData, TName, ValidatorType, TData> | ||
type ValidateAsyncFn< | ||
TParentData, | ||
TName extends DeepKeys<TParentData>, | ||
TData, | ||
ValidatorType, | ||
TData extends DeepValue<TParentData, TName> = DeepValue<TParentData, TName>, | ||
> = ( | ||
value: TData, | ||
fieldApi: FieldApi<TParentData, TName>, | ||
fieldApi: FieldApi<TParentData, TName, ValidatorType, TData>, | ||
) => ValidationError | Promise<ValidationError> | ||
type AsyncValidateOrFn< | ||
TParentData, | ||
TName extends DeepKeys<TParentData>, | ||
ValidatorType, | ||
FormValidator, | ||
TData extends DeepValue<TParentData, TName> = DeepValue<TParentData, TName>, | ||
> = ValidatorType extends Validator<TData> | ||
? | ||
| Parameters<ReturnType<ValidatorType>['validate']>[1] | ||
| ValidateAsyncFn<TParentData, TName, ValidatorType, TData> | ||
: FormValidator extends Validator<TData> | ||
? | ||
| Parameters<ReturnType<FormValidator>['validate']>[1] | ||
| ValidateAsyncFn<TParentData, TName, ValidatorType, TData> | ||
: ValidateAsyncFn<TParentData, TName, ValidatorType, TData> | ||
export interface FieldOptions< | ||
TParentData, | ||
/** | ||
* This allows us to restrict the name to only be a valid field name while | ||
* also assigning it to a generic | ||
*/ | ||
TName extends DeepKeys<TParentData>, | ||
/** | ||
* If TData is unknown, we can use the TName generic to determine the type | ||
*/ | ||
TData = DeepValue<TParentData, TName>, | ||
ValidatorType, | ||
FormValidator, | ||
TData extends DeepValue<TParentData, TName> = DeepValue<TParentData, TName>, | ||
> { | ||
name: DeepKeys<TParentData> | ||
name: TName | ||
index?: TData extends any[] ? number : never | ||
@@ -38,10 +72,37 @@ defaultValue?: TData | ||
asyncAlways?: boolean | ||
onMount?: (formApi: FieldApi<TParentData, TName>) => void | ||
onChange?: ValidateFn<TParentData, TName, TData> | ||
onChangeAsync?: ValidateAsyncFn<TParentData, TName, TData> | ||
validator?: ValidatorType | ||
onMount?: ( | ||
formApi: FieldApi<TParentData, TName, ValidatorType, TData>, | ||
) => void | ||
onChange?: ValidateOrFn< | ||
TParentData, | ||
TName, | ||
ValidatorType, | ||
FormValidator, | ||
TData | ||
> | ||
onChangeAsync?: AsyncValidateOrFn< | ||
TParentData, | ||
TName, | ||
ValidatorType, | ||
FormValidator, | ||
TData | ||
> | ||
onChangeAsyncDebounceMs?: number | ||
onBlur?: ValidateFn<TParentData, TName, TData> | ||
onBlurAsync?: ValidateAsyncFn<TParentData, TName, TData> | ||
onBlur?: ValidateOrFn<TParentData, TName, ValidatorType, FormValidator, TData> | ||
onBlurAsync?: AsyncValidateOrFn< | ||
TParentData, | ||
TName, | ||
ValidatorType, | ||
FormValidator, | ||
TData | ||
> | ||
onBlurAsyncDebounceMs?: number | ||
onSubmitAsync?: ValidateAsyncFn<TParentData, TName, TData> | ||
onSubmitAsync?: AsyncValidateOrFn< | ||
TParentData, | ||
TName, | ||
ValidatorType, | ||
FormValidator, | ||
TData | ||
> | ||
defaultMeta?: Partial<FieldMeta> | ||
@@ -53,5 +114,13 @@ } | ||
TName extends DeepKeys<TParentData>, | ||
TData = DeepValue<TParentData, TName>, | ||
> extends FieldOptions<TParentData, TName, TData> { | ||
form: FormApi<TParentData> | ||
ValidatorType, | ||
FormValidator, | ||
TData extends DeepValue<TParentData, TName> = DeepValue<TParentData, TName>, | ||
> extends FieldOptions< | ||
TParentData, | ||
TName, | ||
ValidatorType, | ||
FormValidator, | ||
TData | ||
> { | ||
form: FormApi<TParentData, FormValidator> | ||
} | ||
@@ -81,8 +150,10 @@ | ||
TName extends DeepKeys<TParentData>, | ||
TData = DeepValue<TParentData, TName>, | ||
ValidatorType, | ||
FormValidator, | ||
TData extends DeepValue<TParentData, TName> = DeepValue<TParentData, TName>, | ||
> { | ||
uid: number | ||
form: FieldApiOptions<TParentData, TName, TData>['form'] | ||
form: FieldApiOptions<TParentData, TName, ValidatorType, TData>['form'] | ||
name!: DeepKeys<TParentData> | ||
options: FieldApiOptions<TParentData, TName> = {} as any | ||
options: FieldApiOptions<TParentData, TName, ValidatorType, TData> = {} as any | ||
store!: Store<FieldState<TData>> | ||
@@ -93,7 +164,11 @@ state!: FieldState<TData> | ||
constructor( | ||
opts: FieldApiOptions<TParentData, TName, TData> & { | ||
form: FormApi<TParentData> | ||
}, | ||
opts: FieldApiOptions< | ||
TParentData, | ||
TName, | ||
ValidatorType, | ||
FormValidator, | ||
TData | ||
>, | ||
) { | ||
this.form = opts.form | ||
this.form = opts.form as never | ||
this.uid = uid++ | ||
@@ -106,3 +181,3 @@ // Support field prefixing from FieldScope | ||
this.name = opts.name as any | ||
this.name = opts.name as never | ||
@@ -175,3 +250,3 @@ if (opts.defaultValue !== undefined) { | ||
if (!Object.keys(info.instances).length) { | ||
delete this.form.fieldInfo[this.name] | ||
delete this.form.fieldInfo[this.name as never] | ||
} | ||
@@ -181,3 +256,5 @@ } | ||
update = (opts: FieldApiOptions<TParentData, TName, TData>) => { | ||
update = ( | ||
opts: FieldApiOptions<TParentData, TName, ValidatorType, TData>, | ||
) => { | ||
// Default Value | ||
@@ -248,6 +325,6 @@ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition | ||
TSubName extends DeepKeys<TData>, | ||
TSubData = DeepValue<TData, TSubName>, | ||
TSubData extends DeepValue<TData, TSubName> = DeepValue<TData, TSubName>, | ||
>( | ||
name: TSubName, | ||
): FieldApi<TData, TSubName, TSubData> => | ||
): FieldApi<TData, TSubName, ValidatorType, TSubData> => | ||
new FieldApi({ | ||
@@ -262,2 +339,3 @@ name: `${this.name}.${name}` as never, | ||
cause === 'submit' ? undefined : cause === 'change' ? onChange : onBlur | ||
if (!validate) return | ||
@@ -269,3 +347,25 @@ | ||
this.getInfo().validationCount = validationCount | ||
const error = normalizeError(validate(value as never, this as never)) | ||
const doValidate = () => { | ||
if (this.options.validator && typeof validate !== 'function') { | ||
return (this.options.validator as Validator<TData>)().validate( | ||
value, | ||
validate, | ||
) | ||
} | ||
if (this.form.options.validator && typeof validate !== 'function') { | ||
return (this.form.options.validator as Validator<TData>)().validate( | ||
value, | ||
validate, | ||
) | ||
} | ||
return (validate as ValidateFn<TParentData, TName, ValidatorType, TData>)( | ||
value, | ||
this as never, | ||
) | ||
} | ||
const error = normalizeError(doValidate()) | ||
const errorMapKey = getErrorMapKey(cause) | ||
@@ -288,3 +388,3 @@ if (this.state.meta.errorMap[errorMapKey] !== error) { | ||
#leaseValidateAsync = () => { | ||
__leaseValidateAsync = () => { | ||
const count = (this.getInfo().validationAsyncCount || 0) + 1 | ||
@@ -297,3 +397,3 @@ this.getInfo().validationAsyncCount = count | ||
// Lease a new validation count to ignore any pending validations | ||
this.#leaseValidateAsync() | ||
this.__leaseValidateAsync() | ||
// Cancel any pending validation state | ||
@@ -332,8 +432,9 @@ this.setMeta((prev) => ({ | ||
if (this.state.meta.isValidating !== true) | ||
if (this.state.meta.isValidating !== true) { | ||
this.setMeta((prev) => ({ ...prev, isValidating: true })) | ||
} | ||
// Use the validationCount for all field instances to | ||
// track freshness of the validation | ||
const validationAsyncCount = this.#leaseValidateAsync() | ||
const validationAsyncCount = this.__leaseValidateAsync() | ||
@@ -354,2 +455,22 @@ const checkLatest = () => | ||
const doValidate = () => { | ||
if (this.options.validator && typeof validate !== 'function') { | ||
return (this.options.validator as Validator<TData>)().validateAsync( | ||
value, | ||
validate, | ||
) | ||
} | ||
if (this.form.options.validator && typeof validate !== 'function') { | ||
return ( | ||
this.form.options.validator as Validator<TData> | ||
)().validateAsync(value, validate) | ||
} | ||
return (validate as ValidateFn<TParentData, TName, ValidatorType, TData>)( | ||
value, | ||
this as never, | ||
) | ||
} | ||
// Only kick off validation if this validation is the latest attempt | ||
@@ -359,3 +480,3 @@ if (checkLatest()) { | ||
try { | ||
const rawError = await validate(value as never, this as never) | ||
const rawError = await doValidate() | ||
if (checkLatest()) { | ||
@@ -396,8 +517,14 @@ const error = normalizeError(rawError) | ||
if (!this.state.meta.isTouched) return [] | ||
// Store the previous error for the errorMapKey (eg. onChange, onBlur, onSubmit) | ||
const errorMapKey = getErrorMapKey(cause) | ||
const prevError = this.getMeta().errorMap[errorMapKey] | ||
// Attempt to sync validate first | ||
this.validateSync(value, cause) | ||
const errorMapKey = getErrorMapKey(cause) | ||
// If there is an error mapped to the errorMapKey (eg. onChange, onBlur, onSubmit), return the errors array, do not attempt async validation | ||
if (this.getMeta().errorMap[errorMapKey]) { | ||
// If there is a new error mapped to the errorMapKey (eg. onChange, onBlur, onSubmit), return the errors array, do not attempt async validation | ||
const newError = this.getMeta().errorMap[errorMapKey] | ||
if (prevError !== newError) { | ||
if (!this.options.asyncAlways) { | ||
@@ -404,0 +531,0 @@ return this.state.meta.errors |
import { Store } from '@tanstack/store' | ||
// | ||
import type { DeepKeys, DeepValue, Updater } from './utils' | ||
import { functionalUpdate, getBy, isNonEmptyArray, setBy } from './utils' | ||
import type { FieldApi, FieldMeta, ValidationCause } from './FieldApi' | ||
import type { ValidationError, Validator } from './types' | ||
export type FormOptions<TData> = { | ||
type ValidateFn<TData, ValidatorType> = ( | ||
values: TData, | ||
formApi: FormApi<TData, ValidatorType>, | ||
) => ValidationError | ||
type ValidateOrFn<TData, ValidatorType> = ValidatorType extends Validator<TData> | ||
? Parameters<ReturnType<ValidatorType>['validate']>[1] | ||
: ValidateFn<TData, ValidatorType> | ||
type ValidateAsyncFn<TData, ValidatorType> = ( | ||
value: TData, | ||
fieldApi: FormApi<TData, ValidatorType>, | ||
) => ValidationError | Promise<ValidationError> | ||
export type FormOptions<TData, ValidatorType> = { | ||
defaultValues?: TData | ||
defaultState?: Partial<FormState<TData>> | ||
asyncDebounceMs?: number | ||
onMount?: (values: TData, formApi: FormApi<TData>) => ValidationError | ||
onMountAsync?: ( | ||
values: TData, | ||
formApi: FormApi<TData>, | ||
) => ValidationError | Promise<ValidationError> | ||
validator?: ValidatorType | ||
onMount?: ValidateOrFn<TData, ValidatorType> | ||
onMountAsync?: ValidateAsyncFn<TData, ValidatorType> | ||
onMountAsyncDebounceMs?: number | ||
onChange?: (values: TData, formApi: FormApi<TData>) => ValidationError | ||
onChangeAsync?: ( | ||
values: TData, | ||
formApi: FormApi<TData>, | ||
) => ValidationError | Promise<ValidationError> | ||
onChange?: ValidateOrFn<TData, ValidatorType> | ||
onChangeAsync?: ValidateAsyncFn<TData, ValidatorType> | ||
onChangeAsyncDebounceMs?: number | ||
onBlur?: (values: TData, formApi: FormApi<TData>) => ValidationError | ||
onBlurAsync?: ( | ||
onBlur?: ValidateOrFn<TData, ValidatorType> | ||
onBlurAsync?: ValidateAsyncFn<TData, ValidatorType> | ||
onBlurAsyncDebounceMs?: number | ||
onSubmit?: ( | ||
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 | ||
formApi: FormApi<TData, ValidatorType>, | ||
) => any | Promise<any> | ||
onSubmitInvalid?: ( | ||
values: TData, | ||
formApi: FormApi<TData, ValidatorType>, | ||
) => void | ||
} | ||
export type FieldInfo<TFormData> = { | ||
instances: Record<string, FieldApi<TFormData, any, any>> | ||
export type FieldInfo<TFormData, ValidatorType> = { | ||
instances: Record<string, FieldApi<TFormData, any, unknown, ValidatorType>> | ||
} & ValidationMeta | ||
@@ -45,4 +57,2 @@ | ||
export type ValidationError = undefined | false | null | string | ||
export type ValidationErrorMapKeys = `on${Capitalize<ValidationCause>}` | ||
@@ -96,5 +106,5 @@ | ||
export class FormApi<TFormData> { | ||
export class FormApi<TFormData, ValidatorType> { | ||
// // This carries the context for nested fields | ||
options: FormOptions<TFormData> = {} | ||
options: FormOptions<TFormData, ValidatorType> = {} | ||
store!: Store<FormState<TFormData>> | ||
@@ -104,7 +114,8 @@ // Do not use __state directly, as it is not reactive. | ||
state!: FormState<TFormData> | ||
fieldInfo: Record<DeepKeys<TFormData>, FieldInfo<TFormData>> = {} as any | ||
fieldInfo: Record<DeepKeys<TFormData>, FieldInfo<TFormData, ValidatorType>> = | ||
{} as any | ||
fieldName?: string | ||
validationMeta: ValidationMeta = {} | ||
constructor(opts?: FormOptions<TFormData>) { | ||
constructor(opts?: FormOptions<TFormData, ValidatorType>) { | ||
this.store = new Store<FormState<TFormData>>( | ||
@@ -163,3 +174,3 @@ getDefaultFormState({ | ||
update = (options?: FormOptions<TFormData>) => { | ||
update = (options?: FormOptions<TFormData, ValidatorType>) => { | ||
if (!options) return | ||
@@ -209,17 +220,17 @@ | ||
this.store.batch(() => { | ||
void (Object.values(this.fieldInfo) as FieldInfo<any>[]).forEach( | ||
(field) => { | ||
Object.values(field.instances).forEach((instance) => { | ||
// If any fields are not touched | ||
if (!instance.state.meta.isTouched) { | ||
// Mark them as touched | ||
instance.setMeta((prev) => ({ ...prev, isTouched: true })) | ||
// Validate the field | ||
fieldValidationPromises.push( | ||
Promise.resolve().then(() => instance.validate(cause)), | ||
) | ||
} | ||
}) | ||
}, | ||
) | ||
void ( | ||
Object.values(this.fieldInfo) as FieldInfo<any, ValidatorType>[] | ||
).forEach((field) => { | ||
Object.values(field.instances).forEach((instance) => { | ||
// If any fields are not touched | ||
if (!instance.state.meta.isTouched) { | ||
// Mark them as touched | ||
instance.setMeta((prev) => ({ ...prev, isTouched: true })) | ||
// Validate the field | ||
fieldValidationPromises.push( | ||
Promise.resolve().then(() => instance.validate(cause)), | ||
) | ||
} | ||
}) | ||
}) | ||
}) | ||
@@ -298,3 +309,3 @@ | ||
field: TField, | ||
): FieldInfo<TFormData> => { | ||
): FieldInfo<TFormData, ValidatorType> => { | ||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition | ||
@@ -347,3 +358,5 @@ return (this.fieldInfo[field] ||= { | ||
field: TField, | ||
value: DeepValue<TFormData, TField>[number], | ||
value: DeepValue<TFormData, TField> extends any[] | ||
? DeepValue<TFormData, TField>[number] | ||
: never, | ||
opts?: { touch?: boolean }, | ||
@@ -361,3 +374,5 @@ ) => { | ||
index: number, | ||
value: DeepValue<TFormData, TField>[number], | ||
value: DeepValue<TFormData, TField> extends any[] | ||
? DeepValue<TFormData, TField>[number] | ||
: never, | ||
opts?: { touch?: boolean }, | ||
@@ -364,0 +379,0 @@ ) => { |
export * from './FormApi' | ||
export * from './FieldApi' | ||
export * from './utils' | ||
export * from './types' |
@@ -554,3 +554,3 @@ import { expect } from 'vitest' | ||
} | ||
const form = new FormApi<Form>() | ||
const form = new FormApi<Form, unknown>() | ||
@@ -557,0 +557,0 @@ const field = new FieldApi({ |
@@ -5,20 +5,17 @@ import { assertType } from 'vitest' | ||
it('should type a subfield properly', () => { | ||
it('should type value properly', () => { | ||
const form = new FormApi({ | ||
defaultValues: { | ||
names: { | ||
first: 'one', | ||
second: 'two', | ||
}, | ||
} as const, | ||
}) | ||
name: 'test', | ||
}, | ||
} as const) | ||
const field = new FieldApi({ | ||
form, | ||
name: 'names', | ||
name: 'name', | ||
}) | ||
const subfield = field.getSubField('first') | ||
assertType<'one'>(subfield.getValue()) | ||
assertType<'test'>(field.state.value) | ||
assertType<'name'>(field.options.name) | ||
assertType<'test'>(field.getValue()) | ||
}) | ||
@@ -43,1 +40,19 @@ | ||
}) | ||
it('should type onChangeAsync properly', () => { | ||
const form = new FormApi({ | ||
defaultValues: { | ||
name: 'test', | ||
}, | ||
} as const) | ||
const field = new FieldApi({ | ||
form, | ||
name: 'name', | ||
onChangeAsync: async (value) => { | ||
assertType<'test'>(value) | ||
return undefined | ||
}, | ||
}) | ||
}) |
@@ -122,3 +122,3 @@ import { expect } from 'vitest' | ||
form.pushFieldValue('name', 'other') | ||
form.setFieldValue('name', 'other') | ||
form.state.submissionAttempts = 300 | ||
@@ -125,0 +125,0 @@ |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
381234
71
4752