Skip to content
Merged
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
10 changes: 10 additions & 0 deletions .changeset/cyan-papayas-search.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
'@tanstack/preact-form': minor
'@tanstack/svelte-form': minor
'@tanstack/react-form': minor
'@tanstack/solid-form': minor
'@tanstack/form-core': minor
'@tanstack/vue-form': minor
---

Feature: Specify default options for `createFormHook`
140 changes: 90 additions & 50 deletions packages/form-core/src/FieldApi/FieldApi.lib.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
import { runFieldListenerPipeline } from '../listeners.lib'
import { devtools } from '../devtoolsBridge.lib'
import { reconcileValidatorInstances } from '../ValidatorInstance.lib'
import { resolveDefaultOptions } from '../defaultOptions.lib'
import {
attachWatchingListenerField,
attachWatchingValidatorField,
Expand Down Expand Up @@ -130,20 +131,32 @@ export function transformFieldOptionsFieldNames<
get name() {
return transformFieldName(options.name)
},
get validators() {
return transformFieldOptionItemsWithWatchedFields(
options.validators,
transformFieldName,
)
},
get listeners() {
return transformFieldOptionItemsWithWatchedFields(
options.listeners,
transformFieldName,
)
},
} as Partial<TFieldOptions>

if (Object.hasOwn(options, 'validators')) {
Object.defineProperty(overrides, 'validators', {
enumerable: true,
get() {
return transformFieldOptionItemsWithWatchedFields(
options.validators,
transformFieldName,
)
},
})
}

if (Object.hasOwn(options, 'listeners')) {
Object.defineProperty(overrides, 'listeners', {
enumerable: true,
get() {
return transformFieldOptionItemsWithWatchedFields(
options.listeners,
transformFieldName,
)
},
})
}

return mergeOptions(fieldOptions, overrides)
}

Expand Down Expand Up @@ -193,46 +206,46 @@ export function getOrCreateFieldApi(
segments: NameSegments,
form: AnyInternalFormApi,
options?: Omit<AnyFieldApiOptions, 'name'>,
scope: FieldOptionsScope = 'field',
): AnyInternalFieldApi {
const segment = segments.shift()
if (segment === undefined) {
// If trieNode is the root, we need to return a field node, not the root
if (node._isRoot) {
throw new Error('Root node cannot be a field API')
}
// Say we internally make a field for data storage:
// form._getOrCreateFieldApi({ name: 'foo' })

// later in the render cycle, a user renders a component that actually does
// form._getOrCreateFieldApi({ name: 'foo', validators: [...] })

// This would be too late! Even worse, we're going to send an error that validators
// changed length when the user did nothing wrong
// TODO
if (options) {
node._update(options)
// Internal trie nodes defer their options until they are first requested as
// a configured field. Adapter updates handle subsequent option changes.
if (scope !== 'internal' && !node._fieldOptionsInitialized) {
node._update(options ?? {}, scope)
}
return node
}

let childNode = node._getChild(segment)
if (childNode) {
return getOrCreateFieldApi(childNode, segments, form, options)
return getOrCreateFieldApi(childNode, segments, form, options, scope)
}

childNode = new InternalFieldApi({
segment,
parent: node,
form: form,
// We're creating fields on our way to the leaf, so don't
// pass options like listeners etc.
...(segments.length === 0 ? options : {}),
})
childNode = new InternalFieldApi(
{
segment,
parent: node,
form: form,
},
'internal',
)

node._setChild(childNode)
if (segments.length === 0) {
const field = getOrCreateFieldApi(childNode, segments, form, options, scope)
devtools().fieldAdded?.(childNode)
return field
}

devtools().fieldAdded?.(childNode)

return getOrCreateFieldApi(childNode, segments, form, options)
return getOrCreateFieldApi(childNode, segments, form, options, scope)
}

/**
Expand Down Expand Up @@ -271,6 +284,8 @@ export interface InternalFieldApiParams extends Omit<
validators?: FieldValidators<any, any, any>
}

export type FieldOptionsScope = 'internal' | 'field'

interface ListenToFieldsMeta {
field: AnyInternalFieldApi
name: string
Expand Down Expand Up @@ -324,6 +339,8 @@ export class InternalFieldApi<
_watchingValidatorFields: FieldWatchingValidatorFields | null
/** Lazily allocated runtime state for debounced field listeners. */
_pipelineCache: PipelineCache | null = null
/** Whether this trie node has received usage-site field options. */
_fieldOptionsInitialized: boolean
_isKilled = false

_segmentValue: NameSegment
Expand Down Expand Up @@ -468,15 +485,23 @@ export class InternalFieldApi<
return this._setDefaultValueCache(value, defaultValue, isDefaultValue)
}

constructor({
segment,
parent,
validators,
form,
listeners,
errorVisibility,
errorBoundary,
}: InternalFieldApiParams) {
constructor(
options: InternalFieldApiParams,
scope: FieldOptionsScope = 'field',
) {
this._fieldOptionsInitialized = scope !== 'internal'
const defaultOptions =
scope === 'field' ? options.form._defaultOptions?.field : undefined
const {
segment,
parent,
validators,
form,
listeners,
errorVisibility,
errorBoundary,
} = resolveDefaultOptions(options, defaultOptions)

this._segmentValue = segment
this._parent = parent
this.form = form
Expand Down Expand Up @@ -523,16 +548,25 @@ export class InternalFieldApi<
reconciledValidators.attach.forEach(attachWatchingValidatorField)
}

_update(options: Omit<AnyFieldApiOptions, 'name' | 'form'>) {
_update(
options: Omit<AnyFieldApiOptions, 'name' | 'form'>,
scope: FieldOptionsScope = 'field',
) {
if (this._isKilled) return

this._errorVisibility = options.errorVisibility
this._errorBoundary = options.errorBoundary ?? false
const isInitializing =
scope !== 'internal' && !this._fieldOptionsInitialized
const defaultOptions =
scope === 'field' ? this.form._defaultOptions?.field : undefined
const resolvedOptions = resolveDefaultOptions(options, defaultOptions)

this._errorVisibility = resolvedOptions.errorVisibility
this._errorBoundary = resolvedOptions.errorBoundary ?? false

const reconciledListeners = reconcileWatchedListenerFields({
field: this,
prevListenToFields: this._listenToFields,
nextListeners: options.listeners,
nextListeners: resolvedOptions.listeners,
form: this.form,
})

Expand All @@ -549,13 +583,13 @@ export class InternalFieldApi<
? [...reconciledListeners.attach, ...reconciledListeners.detach]
: null

if (options.validators) {
if (resolvedOptions.validators) {
const previousValidators = this._validatorInstances?.map(
(instance) => instance.definition,
)
const nextValidators =
options.validators.length > 0
? (options.validators as Array<AnyFieldValidator>)
resolvedOptions.validators.length > 0
? (resolvedOptions.validators as Array<AnyFieldValidator>)
: null
this._validatorInstances = reconcileValidatorInstances<
AnyFieldValidator,
Expand All @@ -564,7 +598,9 @@ export class InternalFieldApi<
AnyInternalFieldApi
>({
definitions: nextValidators,
previousDefinitions: previousValidators ?? null,
previousDefinitions: isInitializing
? undefined
: (previousValidators ?? null),
instances: this._validatorInstances,
owner: this,
scope: 'field',
Expand Down Expand Up @@ -603,6 +639,10 @@ export class InternalFieldApi<
if (dependencyChanges && dependencyChanges.length > 0) {
notifyDependencyChanges?.(dependencyChanges)
}

if (scope !== 'internal') {
this._fieldOptionsInitialized = true
}
}

/**
Expand Down
4 changes: 2 additions & 2 deletions packages/form-core/src/FieldApi/linked-fields.lib.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ function reconcileWatchedFields<TItem extends { watchFields?: Array<string> }>({
if (names.length === 0) return

nextListenToFields[watcherIndex] = names.map((name) => {
const sourceField = form._getOrCreateFieldApi({ name })
const sourceField = form._getOrCreateFieldApi({ name }, 'internal')
const key = toWatcherKey(watcherIndex, name)
const prevMeta = prevByKey.get(key)

Expand Down Expand Up @@ -167,7 +167,7 @@ export function reconcileWatchedValidatorFields({
const names = [...new Set(validatorInstance.definition.watchFields ?? [])]

for (const name of names) {
const sourceField = form._getOrCreateFieldApi({ name })
const sourceField = form._getOrCreateFieldApi({ name }, 'internal')
next.set(name, sourceField)

const previousField = previous?.get(name)
Expand Down
Loading
Loading