Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 92 additions & 0 deletions docs/framework/react/guides/linked-fields.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,3 +75,95 @@ function App() {
```

This similarly works with `onBlurListenTo` property, which will re-run the validation when the field is blurred.

## Dynamic Validation with onDynamicListenTo

For more advanced use cases where you need dynamic validation that responds to field changes but follows React Hook Form-like behavior, you can use `onDynamicListenTo` with `onDynamic` validators.

The `onDynamicListenTo` property works similarly to `onChangeListenTo` and `onBlurListenTo`, but it's specifically designed for dynamic validation scenarios where you want more control over when validation occurs.

```tsx
function App() {
const form = useForm({
defaultValues: {
password: '',
confirm_password: '',
},
validationLogic: revalidateLogic(),
})

return (
<div>
<form.Field name="password">
{(field) => (
<label>
<div>Password</div>
<input
value={field.state.value}
onChange={(e) => field.handleChange(e.target.value)}
/>
</label>
)}
</form.Field>
<form.Field
name="confirm_password"
validators={{
onDynamicListenTo: ['password'],
onDynamic: ({ value, fieldApi }) => {
if (value !== fieldApi.form.getFieldValue('password')) {
return 'Passwords do not match'
}
return undefined
},
}}
>
{(field) => (
<div>
<label>
<div>Confirm Password</div>
<input
value={field.state.value}
onChange={(e) => field.handleChange(e.target.value)}
/>
</label>
{field.state.meta.errors.map((err) => (
<div key={err}>{err}</div>
))}
</div>
)}
</form.Field>
</div>
)
}
```

### Key Differences

- **onChangeListenTo**: Runs validation immediately when the listened field changes
- **onBlurListenTo**: Runs validation when the listened field is blurred
- **onDynamicListenTo**: Runs validation based on the form's validation logic (typically used with `revalidateLogic` for React Hook Form-like behavior)

### Multiple Field Listening

You can listen to multiple fields by providing an array

```tsx
<form.Field
name="dependent_field"
validators={{
onDynamicListenTo: ['field1', 'field2', 'field3'],
onDynamic: ({ value, fieldApi }) => {
// Validation logic that depends on multiple fields
const field1Value = fieldApi.form.getFieldValue('field1')
const field2Value = fieldApi.form.getFieldValue('field2')
const field3Value = fieldApi.form.getFieldValue('field3')

if (field1Value && field2Value && field3Value && !value) {
return 'This field is required when all other fields have values'
}
return undefined
},
}}
>
</form.Field>
```
15 changes: 14 additions & 1 deletion packages/form-core/src/FieldApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -368,6 +368,10 @@ export interface FieldValidators<
onDynamic?: TOnDynamic
onDynamicAsync?: TOnDynamicAsync
onDynamicAsyncDebounceMs?: number
/**
* An optional list of field names that should trigger this field's `onDynamic` and `onDynamicAsync` events when its value changes
*/
onDynamicListenTo?: DeepKeys<TParentData>[]
}

export interface FieldListeners<
Expand Down Expand Up @@ -1355,6 +1359,12 @@ export class FieldApi<
this.triggerOnChangeListener()

this.validate('change')

// Trigger dynamic validation on linked fields
const linkedFields = this.getLinkedFields('dynamic')
for (const field of linkedFields) {
field.validate('dynamic')
}
}

getMeta = () => this.store.state.meta
Expand Down Expand Up @@ -1478,14 +1488,17 @@ export class FieldApi<
const linkedFields: AnyFieldApi[] = []
for (const field of fields) {
if (!field.instance) continue
const { onChangeListenTo, onBlurListenTo } =
const { onChangeListenTo, onBlurListenTo, onDynamicListenTo } =
field.instance.options.validators || {}
if (cause === 'change' && onChangeListenTo?.includes(this.name)) {
linkedFields.push(field.instance)
}
if (cause === 'blur' && onBlurListenTo?.includes(this.name as string)) {
linkedFields.push(field.instance)
}
if (cause === 'dynamic' && onDynamicListenTo?.includes(this.name as string)) {
linkedFields.push(field.instance)
}
}

return linkedFields
Expand Down
14 changes: 13 additions & 1 deletion packages/form-core/src/ValidationLogic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export interface ValidationLogicProps {
| undefined
| null
event: {
type: 'blur' | 'change' | 'submit' | 'mount' | 'server'
type: 'blur' | 'change' | 'submit' | 'mount' | 'server' | 'dynamic'
fieldName?: string
async: boolean
}
Expand Down Expand Up @@ -147,6 +147,11 @@ export const defaultValidationLogic: ValidationLogicFn = (props) => {
cause: 'submit',
} as const

const onDynamicValidator = {
fn: isAsync ? props.validators.onDynamicAsync : props.validators.onDynamic,
cause: 'dynamic',
} as const

// Allows us to clear onServer errors
const onServerValidator = isAsync
? undefined
Expand Down Expand Up @@ -193,6 +198,13 @@ export const defaultValidationLogic: ValidationLogicFn = (props) => {
form: props.form,
})
}
case 'dynamic': {
// Run dynamic validation
return props.runValidation({
validators: [onDynamicValidator],
form: props.form,
})
}
default: {
throw new Error(`Unknown validation event type: ${props.event.type}`)
}
Expand Down
Loading