Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

@tanstack/form-core

Package Overview
Dependencies
Maintainers
2
Versions
106
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@tanstack/form-core - npm Package Compare versions

Comparing version 0.18.1 to 0.19.0

13

dist/esm/FieldApi.d.ts
import { Store } from '@tanstack/store';
import type { FormApi } from './FormApi.js';
import type { FieldInfo, FormApi } from './FormApi.js';
import type { ValidationCause, ValidationError, ValidationErrorMap, Validator } from './types.js';

@@ -22,5 +22,7 @@ import type { Updater } from './utils.js';

onChangeAsyncDebounceMs?: number;
onChangeListenTo?: DeepKeys<TParentData>[];
onBlur?: FieldValidateOrFn<TParentData, TName, TFieldValidator, TFormValidator, TData>;
onBlurAsync?: FieldAsyncValidateOrFn<TParentData, TName, TFieldValidator, TFormValidator, TData>;
onBlurAsyncDebounceMs?: number;
onBlurListenTo?: DeepKeys<TParentData>[];
onSubmit?: FieldValidateOrFn<TParentData, TName, TFieldValidator, TFormValidator, TData>;

@@ -83,3 +85,3 @@ onSubmitAsync?: FieldAsyncValidateOrFn<TParentData, TName, TFieldValidator, TFormValidator, TData>;

setMeta: (updater: Updater<FieldMeta>) => void;
getInfo: () => import("./FormApi").FieldInfo<TParentData, TFormValidator>;
getInfo: () => FieldInfo<TParentData, TFormValidator>;
pushValue: (value: TData extends any[] ? TData[number] : never) => void;

@@ -89,10 +91,11 @@ insertValue: (index: number, value: TData extends any[] ? TData[number] : never) => void;

swapValues: (aIndex: number, bIndex: number) => void;
getLinkedFields: (cause: ValidationCause) => FieldApi<any, any, any, any, any>[];
moveValue: (aIndex: number, bIndex: number) => void;
validateSync: (value: TData | undefined, cause: ValidationCause) => {
validateSync: (cause: ValidationCause) => {
hasErrored: boolean;
};
validateAsync: (value: TData | undefined, cause: ValidationCause) => Promise<ValidationError[]>;
validate: (cause: ValidationCause, value?: TData) => ValidationError[] | Promise<ValidationError[]>;
validateAsync: (cause: ValidationCause) => Promise<ValidationError[]>;
validate: (cause: ValidationCause) => ValidationError[] | Promise<ValidationError[]>;
handleChange: (updater: Updater<TData>) => void;
handleBlur: () => void;
}

@@ -68,3 +68,3 @@ import { Store } from "@tanstack/store";

this.form.setFieldValue(this.name, updater, options);
this.validate("change", this.state.value);
this.validate("change");
};

@@ -88,14 +88,39 @@ this._getMeta = () => this.form.getFieldMeta(this.name);

this.swapValues = (aIndex, bIndex) => this.form.swapFieldValues(this.name, aIndex, bIndex);
this.getLinkedFields = (cause) => {
const fields = Object.values(this.form.fieldInfo);
const linkedFields = [];
for (const field of fields) {
if (!field.instance)
continue;
const { onChangeListenTo, onBlurListenTo } = field.instance.options.validators || {};
if (cause === "change" && (onChangeListenTo == null ? void 0 : onChangeListenTo.includes(this.name))) {
linkedFields.push(field.instance);
}
if (cause === "blur" && (onBlurListenTo == null ? void 0 : onBlurListenTo.includes(this.name))) {
linkedFields.push(field.instance);
}
}
return linkedFields;
};
this.moveValue = (aIndex, bIndex) => this.form.moveFieldValues(this.name, aIndex, bIndex);
this.validateSync = (value = this.state.value, cause) => {
this.validateSync = (cause) => {
const validates = getSyncValidatorArray(cause, this.options);
const linkedFields = this.getLinkedFields(cause);
const linkedFieldValidates = linkedFields.reduce(
(acc, field) => {
const fieldValidates = getSyncValidatorArray(cause, field.options);
fieldValidates.forEach((validate) => {
validate.field = field;
});
return acc.concat(fieldValidates);
},
[]
);
let hasErrored = false;
this.form.store.batch(() => {
for (const validateObj of validates) {
if (!validateObj.validate)
continue;
const validateFieldFn = (field, validateObj) => {
const error = normalizeError(
this.runValidator({
field.runValidator({
validate: validateObj.validate,
value: { value, fieldApi: this },
value: { value: field.getValue(), fieldApi: field },
type: "validate"

@@ -105,4 +130,4 @@ })

const errorMapKey = getErrorMapKey(validateObj.cause);
if (this.state.meta.errorMap[errorMapKey] !== error) {
this.setMeta((prev) => ({
if (field.state.meta.errorMap[errorMapKey] !== error) {
field.setMeta((prev) => ({
...prev,

@@ -118,3 +143,13 @@ errorMap: {

}
};
for (const validateObj of validates) {
if (!validateObj.validate)
continue;
validateFieldFn(this, validateObj);
}
for (const fieldValitateObj of linkedFieldValidates) {
if (!fieldValitateObj.validate)
continue;
validateFieldFn(fieldValitateObj.field, fieldValitateObj);
}
});

@@ -133,13 +168,26 @@ const submitErrKey = getErrorMapKey("submit");

};
this.validateAsync = async (value = this.state.value, cause) => {
this.validateAsync = async (cause) => {
const validates = getAsyncValidatorArray(cause, this.options);
const linkedFields = this.getLinkedFields(cause);
const linkedFieldValidates = linkedFields.reduce(
(acc, field) => {
const fieldValidates = getAsyncValidatorArray(cause, field.options);
fieldValidates.forEach((validate) => {
validate.field = field;
});
return acc.concat(fieldValidates);
},
[]
);
if (!this.state.meta.isValidating) {
this.setMeta((prev) => ({ ...prev, isValidating: true }));
}
const promises = [];
for (const validateObj of validates) {
if (!validateObj.validate)
continue;
for (const linkedField of linkedFields) {
linkedField.setMeta((prev) => ({ ...prev, isValidating: true }));
}
const validatesPromises = [];
const linkedPromises = [];
const validateFieldAsyncFn = (field, validateObj, promises) => {
const key = getErrorMapKey(validateObj.cause);
const fieldValidatorMeta = this.getInfo().validationMetaMap[key];
const fieldValidatorMeta = field.getInfo().validationMetaMap[key];
fieldValidatorMeta == null ? void 0 : fieldValidatorMeta.lastAbortController.abort();

@@ -163,4 +211,4 @@ const controller = new AbortController();

value: {
value,
fieldApi: this,
value: field.getValue(),
fieldApi: field,
signal: controller.signal

@@ -180,3 +228,3 @@ },

const error = normalizeError(rawError);
this.setMeta((prev) => {
field.setMeta((prev) => {
return {

@@ -194,11 +242,29 @@ ...prev,

);
};
for (const validateObj of validates) {
if (!validateObj.validate)
continue;
validateFieldAsyncFn(this, validateObj, validatesPromises);
}
for (const fieldValitateObj of linkedFieldValidates) {
if (!fieldValitateObj.validate)
continue;
validateFieldAsyncFn(
fieldValitateObj.field,
fieldValitateObj,
linkedPromises
);
}
let results = [];
if (promises.length) {
results = await Promise.all(promises);
if (validatesPromises.length || linkedPromises.length) {
results = await Promise.all(validatesPromises);
await Promise.all(linkedPromises);
}
this.setMeta((prev) => ({ ...prev, isValidating: false }));
for (const linkedField of linkedFields) {
linkedField.setMeta((prev) => ({ ...prev, isValidating: false }));
}
return results.filter(Boolean);
};
this.validate = (cause, value) => {
this.validate = (cause) => {
if (!this.state.meta.isTouched)

@@ -210,7 +276,7 @@ return [];

}
const { hasErrored } = this.validateSync(value, cause);
const { hasErrored } = this.validateSync(cause);
if (hasErrored && !this.options.asyncAlways) {
return this.state.meta.errors;
}
return this.validateAsync(value, cause);
return this.validateAsync(cause);
};

@@ -217,0 +283,0 @@ this.handleChange = (updater) => {

@@ -25,3 +25,3 @@ import type { ValidationCause } from './types.js';

}
interface AsyncValidator<T> {
export interface AsyncValidator<T> {
cause: ValidationCause;

@@ -35,3 +35,3 @@ validate: T;

}
interface SyncValidator<T> {
export interface SyncValidator<T> {
cause: ValidationCause;

@@ -38,0 +38,0 @@ validate: T;

{
"name": "@tanstack/form-core",
"version": "0.18.1",
"version": "0.19.0",
"description": "Powerful, type-safe, framework agnostic forms.",

@@ -5,0 +5,0 @@ "author": "tannerlinsley",

import { Store } from '@tanstack/store'
import { getAsyncValidatorArray, getSyncValidatorArray } from './utils'
import type { FormApi } from './FormApi'
import type { FieldInfo, FormApi } from './FormApi'
import type {

@@ -10,3 +10,3 @@ ValidationCause,

} from './types'
import type { Updater } from './utils'
import type { AsyncValidator, SyncValidator, Updater } from './utils'
import type { DeepKeys, DeepValue, NoInfer } from './util-types'

@@ -154,2 +154,3 @@

onChangeAsyncDebounceMs?: number
onChangeListenTo?: DeepKeys<TParentData>[]
onBlur?: FieldValidateOrFn<

@@ -170,2 +171,3 @@ TParentData,

onBlurAsyncDebounceMs?: number
onBlurListenTo?: DeepKeys<TParentData>[]
onSubmit?: FieldValidateOrFn<

@@ -453,3 +455,3 @@ TParentData,

this.form.setFieldValue(this.name, updater as never, options)
this.validate('change', this.state.value)
this.validate('change')
}

@@ -489,8 +491,45 @@

getLinkedFields = (cause: ValidationCause) => {
const fields = Object.values(this.form.fieldInfo) as FieldInfo<
any,
TFormValidator
>[]
const linkedFields: FieldApi<any, any, any, any>[] = []
for (const field of fields) {
if (!field.instance) continue
const { onChangeListenTo, onBlurListenTo } =
field.instance.options.validators || {}
if (
cause === 'change' &&
onChangeListenTo?.includes(this.name as string)
) {
linkedFields.push(field.instance)
}
if (cause === 'blur' && onBlurListenTo?.includes(this.name as string)) {
linkedFields.push(field.instance)
}
}
return linkedFields
}
moveValue = (aIndex: number, bIndex: number) =>
this.form.moveFieldValues(this.name, aIndex, bIndex)
validateSync = (value = this.state.value, cause: ValidationCause) => {
validateSync = (cause: ValidationCause) => {
const validates = getSyncValidatorArray(cause, this.options)
const linkedFields = this.getLinkedFields(cause)
const linkedFieldValidates = linkedFields.reduce(
(acc, field) => {
const fieldValidates = getSyncValidatorArray(cause, field.options)
fieldValidates.forEach((validate) => {
;(validate as any).field = field
})
return acc.concat(fieldValidates as never)
},
[] as Array<SyncValidator<any> & { field: FieldApi<any, any, any, any> }>,
)
// Needs type cast as eslint errantly believes this is always falsy

@@ -500,8 +539,10 @@ let hasErrored = false as boolean

this.form.store.batch(() => {
for (const validateObj of validates) {
if (!validateObj.validate) continue
const validateFieldFn = (
field: FieldApi<any, any, any, any>,
validateObj: SyncValidator<any>,
) => {
const error = normalizeError(
this.runValidator({
field.runValidator({
validate: validateObj.validate,
value: { value, fieldApi: this },
value: { value: field.getValue(), fieldApi: field },
type: 'validate',

@@ -511,4 +552,4 @@ }),

const errorMapKey = getErrorMapKey(validateObj.cause)
if (this.state.meta.errorMap[errorMapKey] !== error) {
this.setMeta((prev) => ({
if (field.state.meta.errorMap[errorMapKey] !== error) {
field.setMeta((prev) => ({
...prev,

@@ -525,2 +566,11 @@ errorMap: {

}
for (const validateObj of validates) {
if (!validateObj.validate) continue
validateFieldFn(this, validateObj)
}
for (const fieldValitateObj of linkedFieldValidates) {
if (!fieldValitateObj.validate) continue
validateFieldFn(fieldValitateObj.field, fieldValitateObj)
}
})

@@ -550,5 +600,19 @@

validateAsync = async (value = this.state.value, cause: ValidationCause) => {
validateAsync = async (cause: ValidationCause) => {
const validates = getAsyncValidatorArray(cause, this.options)
const linkedFields = this.getLinkedFields(cause)
const linkedFieldValidates = linkedFields.reduce(
(acc, field) => {
const fieldValidates = getAsyncValidatorArray(cause, field.options)
fieldValidates.forEach((validate) => {
;(validate as any).field = field
})
return acc.concat(fieldValidates as never)
},
[] as Array<
AsyncValidator<any> & { field: FieldApi<any, any, any, any> }
>,
)
if (!this.state.meta.isValidating) {

@@ -558,2 +622,6 @@ this.setMeta((prev) => ({ ...prev, isValidating: true }))

for (const linkedField of linkedFields) {
linkedField.setMeta((prev) => ({ ...prev, isValidating: true }))
}
/**

@@ -563,8 +631,12 @@ * We have to use a for loop and generate our promises this way, otherwise it won't be sync

*/
const promises: Promise<ValidationError | undefined>[] = []
const validatesPromises: Promise<ValidationError | undefined>[] = []
const linkedPromises: Promise<ValidationError | undefined>[] = []
for (const validateObj of validates) {
if (!validateObj.validate) continue
const validateFieldAsyncFn = (
field: FieldApi<any, any, any, any>,
validateObj: AsyncValidator<any>,
promises: Promise<ValidationError | undefined>[],
) => {
const key = getErrorMapKey(validateObj.cause)
const fieldValidatorMeta = this.getInfo().validationMetaMap[key]
const fieldValidatorMeta = field.getInfo().validationMetaMap[key]

@@ -590,4 +662,4 @@ fieldValidatorMeta?.lastAbortController.abort()

value: {
value,
fieldApi: this,
value: field.getValue(),
fieldApi: field,
signal: controller.signal,

@@ -607,3 +679,3 @@ },

const error = normalizeError(rawError)
this.setMeta((prev) => {
field.setMeta((prev) => {
return {

@@ -624,5 +696,20 @@ ...prev,

// TODO: Dedupe this logic to reduce bundle size
for (const validateObj of validates) {
if (!validateObj.validate) continue
validateFieldAsyncFn(this, validateObj, validatesPromises)
}
for (const fieldValitateObj of linkedFieldValidates) {
if (!fieldValitateObj.validate) continue
validateFieldAsyncFn(
fieldValitateObj.field,
fieldValitateObj,
linkedPromises,
)
}
let results: ValidationError[] = []
if (promises.length) {
results = await Promise.all(promises)
if (validatesPromises.length || linkedPromises.length) {
results = await Promise.all(validatesPromises)
await Promise.all(linkedPromises)
}

@@ -632,2 +719,6 @@

for (const linkedField of linkedFields) {
linkedField.setMeta((prev) => ({ ...prev, isValidating: false }))
}
return results.filter(Boolean)

@@ -638,3 +729,2 @@ }

cause: ValidationCause,
value?: TData,
): ValidationError[] | Promise<ValidationError[]> => {

@@ -649,3 +739,3 @@ // If the field is pristine and validatePristine is false, do not validate

// Attempt to sync validate first
const { hasErrored } = this.validateSync(value, cause)
const { hasErrored } = this.validateSync(cause)

@@ -656,3 +746,3 @@ if (hasErrored && !this.options.asyncAlways) {

// No error? Attempt async validation
return this.validateAsync(value, cause)
return this.validateAsync(cause)
}

@@ -659,0 +749,0 @@

@@ -724,2 +724,160 @@ import { describe, expect, it, vi } from 'vitest'

})
it('should run onChange on a linked field', () => {
const form = new FormApi({
defaultValues: {
password: '',
confirm_password: '',
},
})
const passField = new FieldApi({
form,
name: 'password',
})
const passconfirmField = new FieldApi({
form,
name: 'confirm_password',
validators: {
onChangeListenTo: ['password'],
onChange: ({ value, fieldApi }) => {
if (value !== fieldApi.form.getFieldValue('password')) {
return 'Passwords do not match'
}
return undefined
},
},
})
passField.mount()
passconfirmField.mount()
passField.setValue('one', { touch: true })
expect(passconfirmField.state.meta.errors).toStrictEqual([
'Passwords do not match',
])
passconfirmField.setValue('one', { touch: true })
expect(passconfirmField.state.meta.errors).toStrictEqual([])
passField.setValue('two', { touch: true })
expect(passconfirmField.state.meta.errors).toStrictEqual([
'Passwords do not match',
])
})
it('should run onBlur on a linked field', () => {
const form = new FormApi({
defaultValues: {
password: '',
confirm_password: '',
},
})
const passField = new FieldApi({
form,
name: 'password',
})
const passconfirmField = new FieldApi({
form,
name: 'confirm_password',
validators: {
onBlurListenTo: ['password'],
onBlur: ({ value, fieldApi }) => {
if (value !== fieldApi.form.getFieldValue('password')) {
return 'Passwords do not match'
}
return undefined
},
},
})
passField.mount()
passconfirmField.mount()
passField.setValue('one', { touch: true })
expect(passconfirmField.state.meta.errors).toStrictEqual([])
passField.handleBlur()
expect(passconfirmField.state.meta.errors).toStrictEqual([
'Passwords do not match',
])
passconfirmField.setValue('one', { touch: true })
expect(passconfirmField.state.meta.errors).toStrictEqual([
'Passwords do not match',
])
passField.handleBlur()
expect(passconfirmField.state.meta.errors).toStrictEqual([])
passField.setValue('two', { touch: true })
passField.handleBlur()
expect(passconfirmField.state.meta.errors).toStrictEqual([
'Passwords do not match',
])
})
it('should run onChangeAsync on a linked field', async () => {
vi.useRealTimers()
let resolve!: () => void
let promise = new Promise((r) => {
resolve = r as never
})
const fn = vi.fn()
const form = new FormApi({
defaultValues: {
password: '',
confirm_password: '',
},
})
const passField = new FieldApi({
form,
name: 'password',
})
const passconfirmField = new FieldApi({
form,
name: 'confirm_password',
validators: {
onChangeListenTo: ['password'],
onChangeAsync: async ({ value, fieldApi }) => {
await promise
fn()
if (value !== fieldApi.form.getFieldValue('password')) {
return 'Passwords do not match'
}
return undefined
},
},
})
passField.mount()
passconfirmField.mount()
passField.setValue('one', { touch: true })
resolve()
// Allow for a micro-tick to allow the promise to resolve
await sleep(1)
expect(passconfirmField.state.meta.errors).toStrictEqual([
'Passwords do not match',
])
promise = new Promise((r) => {
resolve = r as never
})
passconfirmField.setValue('one', { touch: true })
resolve()
// Allow for a micro-tick to allow the promise to resolve
await sleep(1)
expect(passconfirmField.state.meta.errors).toStrictEqual([])
promise = new Promise((r) => {
resolve = r as never
})
passField.setValue('two', { touch: true })
resolve()
// Allow for a micro-tick to allow the promise to resolve
await sleep(1)
expect(passconfirmField.state.meta.errors).toStrictEqual([
'Passwords do not match',
])
})
})

@@ -160,3 +160,3 @@ import type { ValidationCause } from './types'

interface AsyncValidator<T> {
export interface AsyncValidator<T> {
cause: ValidationCause

@@ -230,3 +230,3 @@ validate: T

interface SyncValidator<T> {
export interface SyncValidator<T> {
cause: ValidationCause

@@ -233,0 +233,0 @@ validate: T

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

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc