Skip to content

Commit

Permalink
fix: Remove form-core type overrides from adapters (#794)
Browse files Browse the repository at this point in the history
* chore: remove core overrides in React adapter

* chore: remove module declarating types from solid form

* chore: remove module declarating types from vue form

* chore: fix prettier, knip
  • Loading branch information
crutchcorn authored Jun 28, 2024
1 parent f53062f commit 145e0a9
Show file tree
Hide file tree
Showing 7 changed files with 178 additions and 190 deletions.
19 changes: 7 additions & 12 deletions packages/react-form/src/createServerValidate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,11 @@ type OnServerValidateOrFn<
? FFN | OnServerValidateFn<TFormData>
: OnServerValidateFn<TFormData>

declare module '@tanstack/form-core' {
// eslint-disable-next-line no-shadow
interface FormOptions<
TFormData,
TFormValidator extends
| Validator<TFormData, unknown>
| undefined = undefined,
> {
onServerValidate?: OnServerValidateOrFn<TFormData, TFormValidator>
}
interface ServerFormOptions<
TFormData,
TFormValidator extends Validator<TFormData, unknown> | undefined = undefined,
> extends FormOptions<TFormData, TFormValidator> {
onServerValidate?: OnServerValidateOrFn<TFormData, TFormValidator>
}

type ValidateFormData<
Expand All @@ -41,13 +36,13 @@ export const createServerValidate = <
TFormData,
TFormValidator extends Validator<TFormData, unknown> | undefined = undefined,
>(
defaultOpts?: FormOptions<TFormData, TFormValidator>,
defaultOpts: ServerFormOptions<TFormData, TFormValidator>,
) =>
(async (
formData: FormData,
info?: Parameters<typeof decode>[1],
): Promise<Partial<FormApi<TFormData, TFormValidator>['state']>> => {
const { validatorAdapter, onServerValidate } = defaultOpts || {}
const { validatorAdapter, onServerValidate } = defaultOpts

const runValidator = (propsValue: { value: TFormData }) => {
if (validatorAdapter && typeof onServerValidate !== 'function') {
Expand Down
38 changes: 15 additions & 23 deletions packages/react-form/src/useField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,27 +5,16 @@ import { useIsomorphicLayoutEffect } from './useIsomorphicLayoutEffect'
import type { NodeType, UseFieldOptions } from './types'
import type { DeepKeys, DeepValue, Validator } from '@tanstack/form-core'

declare module '@tanstack/form-core' {
interface ReactFieldApi<
TParentData,
TFormValidator extends
| Validator<TParentData, unknown>
| undefined = undefined,
> {
/**
* When using `@tanstack/react-form`, the core field API is extended at type level with additional methods for React-specific functionality:
* A pre-bound and type-safe sub-field component using this field as a root.
*/
// eslint-disable-next-line no-shadow
interface FieldApi<
TParentData,
TName extends DeepKeys<TParentData>,
TFieldValidator extends
| Validator<DeepValue<TParentData, TName>, unknown>
| undefined = undefined,
TFormValidator extends
| Validator<TParentData, unknown>
| undefined = undefined,
TData extends DeepValue<TParentData, TName> = DeepValue<TParentData, TName>,
> {
/**
* A pre-bound and type-safe sub-field component using this field as a root.
*/
Field: FieldComponent<TParentData, TFormValidator>
}
Field: FieldComponent<TParentData, TFormValidator>
}

/**
Expand Down Expand Up @@ -75,17 +64,20 @@ export function useField<
TFormValidator,
TData
>,
): FieldApi<TParentData, TName, TFieldValidator, TFormValidator, TData> {
) {
const [fieldApi] = useState(() => {
const api = new FieldApi({
...opts,
form: opts.form,
name: opts.name,
})

api.Field = Field as never
const extendedApi: typeof api & ReactFieldApi<TParentData, TFormValidator> =
api as never

extendedApi.Field = Field as never

return api
return extendedApi
})

useIsomorphicLayoutEffect(fieldApi.mount, [fieldApi])
Expand All @@ -107,7 +99,7 @@ export function useField<
: undefined,
)

return fieldApi as never
return fieldApi
}

/**
Expand Down
73 changes: 37 additions & 36 deletions packages/react-form/src/useForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,31 +7,32 @@ import type { NoInfer } from '@tanstack/react-store'
import type { FormOptions, FormState, Validator } from '@tanstack/form-core'
import type { NodeType } from './types'

declare module '@tanstack/form-core' {
/**
* Fields that are added onto the `FormAPI` from `@tanstack/form-core` and returned from `useForm`
*/
interface ReactFormApi<
TFormData,
TFormValidator extends Validator<TFormData, unknown> | undefined = undefined,
> {
/**
* When using `@tanstack/react-form`, the core form API is extended at type level with additional methods for React-specific functionality:
* A React component to render form fields. With this, you can render and manage individual form fields.
*/
// eslint-disable-next-line no-shadow
interface FormApi<TFormData, TFormValidator> {
/**
* A React component to render form fields. With this, you can render and manage individual form fields.
*/
Field: FieldComponent<TFormData, TFormValidator>
/**
* A custom React hook that provides functionalities related to individual form fields. It gives you access to field values, errors, and allows you to set or update field values.
*/
useField: UseField<TFormData, TFormValidator>
/**
* A `useStore` hook that connects to the internal store of the form. It can be used to access the form's current state or any other related state information. You can optionally pass in a selector function to cherry-pick specific parts of the state
*/
useStore: <TSelected = NoInfer<FormState<TFormData>>>(
selector?: (state: NoInfer<FormState<TFormData>>) => TSelected,
) => TSelected
Field: FieldComponent<TFormData, TFormValidator>
/**
* A custom React hook that provides functionalities related to individual form fields. It gives you access to field values, errors, and allows you to set or update field values.
*/
useField: UseField<TFormData, TFormValidator>
/**
* A `useStore` hook that connects to the internal store of the form. It can be used to access the form's current state or any other related state information. You can optionally pass in a selector function to cherry-pick specific parts of the state
*/
useStore: <TSelected = NoInfer<FormState<TFormData>>>(
selector?: (state: NoInfer<FormState<TFormData>>) => TSelected,
) => TSelected
/**
* A `Subscribe` function that allows you to listen and react to changes in the form's state. It's especially useful when you need to execute side effects or render specific components in response to state updates.
*/
Subscribe: <TSelected = NoInfer<FormState<TFormData>>>(props: {
/**
* A `Subscribe` function that allows you to listen and react to changes in the form's state. It's especially useful when you need to execute side effects or render specific components in response to state updates.
*/
Subscribe: <TSelected = NoInfer<FormState<TFormData>>>(props: {
/**
TypeScript versions <=5.0.4 have a bug that prevents
the type of the `TSelected` generic from being inferred
from the return type of this method.
Expand All @@ -42,43 +43,43 @@ declare module '@tanstack/form-core' {
@see {@link https://github.com/TanStack/form/pull/606/files#r1506715714 | This discussion on GitHub for the details}
@see {@link https://github.com/microsoft/TypeScript/issues/52786 | The bug report in `microsoft/TypeScript`}
*/
selector?: (state: NoInfer<FormState<TFormData>>) => TSelected
children: ((state: NoInfer<TSelected>) => NodeType) | NodeType
}) => NodeType
}
selector?: (state: NoInfer<FormState<TFormData>>) => TSelected
children: ((state: NoInfer<TSelected>) => NodeType) | NodeType
}) => NodeType
}

/**
* A custom React Hook that returns an instance of the `FormApi` class.
* A custom React Hook that returns an extended instance of the `FormApi` class.
*
* This API encapsulates all the necessary functionalities related to the form. It allows you to manage form state, handle submissions, and interact with form fields
*/
export function useForm<
TFormData,
TFormValidator extends Validator<TFormData, unknown> | undefined = undefined,
>(
opts?: FormOptions<TFormData, TFormValidator>,
): FormApi<TFormData, TFormValidator> {
>(opts?: FormOptions<TFormData, TFormValidator>) {
const [formApi] = useState(() => {
const api = new FormApi<TFormData, TFormValidator>(opts)
api.Field = function APIField(props) {

const extendedApi: typeof api & ReactFormApi<TFormData, TFormValidator> =
api as never
extendedApi.Field = function APIField(props) {
return (<Field {...props} form={api} />) as never
}
// eslint-disable-next-line react-hooks/rules-of-hooks
api.useField = (props) => useField({ ...props, form: api })
api.useStore = (selector) => {
extendedApi.useField = (props) => useField({ ...props, form: api })
extendedApi.useStore = (selector) => {
// eslint-disable-next-line react-hooks/rules-of-hooks
return useStore(api.store as any, selector as any) as any
}
api.Subscribe = (props) => {
extendedApi.Subscribe = (props) => {
return functionalUpdate(
props.children,
// eslint-disable-next-line react-hooks/rules-of-hooks
useStore(api.store as any, props.selector as any),
) as any
}

return api
return extendedApi
})

useIsomorphicLayoutEffect(formApi.mount, [])
Expand All @@ -93,5 +94,5 @@ export function useForm<
formApi.update(opts)
})

return formApi as any
return formApi
}
69 changes: 44 additions & 25 deletions packages/solid-form/src/createField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,21 +17,13 @@ import type {
import type { JSXElement } from 'solid-js'
import type { CreateFieldOptions } from './types'

declare module '@tanstack/form-core' {
// eslint-disable-next-line no-shadow
interface FieldApi<
TParentData,
TName extends DeepKeys<TParentData>,
TFieldValidator extends
| Validator<DeepValue<TParentData, TName>, unknown>
| undefined = undefined,
TFormValidator extends
| Validator<TParentData, unknown>
| undefined = undefined,
TData extends DeepValue<TParentData, TName> = DeepValue<TParentData, TName>,
> {
Field: FieldComponent<TParentData, TFormValidator>
}
interface SolidFieldApi<
TParentData,
TFormValidator extends
| Validator<TParentData, unknown>
| undefined = undefined,
> {
Field: FieldComponent<TParentData, TFormValidator>
}

export type CreateField<
Expand All @@ -56,12 +48,35 @@ export type CreateField<
>,
'form'
>,
) => () => FieldApi<TParentData, TName, TFieldValidator, TFormValidator, TData>
) => () => FieldApi<
TParentData,
TName,
TFieldValidator,
TFormValidator,
TData
> &
SolidFieldApi<TParentData, TFormValidator>

// ugly way to trick solid into triggering updates for changes on the fieldApi
function makeFieldReactive<FieldApiT extends FieldApi<any, any, any, any>>(
fieldApi: FieldApiT,
): () => FieldApiT {
function makeFieldReactive<
TParentData,
TName extends DeepKeys<TParentData>,
TFieldValidator extends
| Validator<DeepValue<TParentData, TName>, unknown>
| undefined = undefined,
TFormValidator extends
| Validator<TParentData, unknown>
| undefined = undefined,
TData extends DeepValue<TParentData, TName> = DeepValue<TParentData, TName>,
FieldApiT extends FieldApi<
TParentData,
TName,
TFieldValidator,
TFormValidator,
TData
> = FieldApi<TParentData, TName, TFieldValidator, TFormValidator, TData> &
SolidFieldApi<TParentData, TFormValidator>,
>(fieldApi: FieldApiT): () => FieldApiT {
const [flag, setFlag] = createSignal(false)
const fieldApiMemo = createMemo(() => [flag(), fieldApi] as const)
const unsubscribeStore = fieldApi.store.subscribe(() => setFlag((f) => !f))
Expand All @@ -87,24 +102,28 @@ export function createField<
TFormValidator,
TData
>,
): () => FieldApi<TParentData, TName, TFieldValidator, TFormValidator, TData> {
) {
const options = opts()

const fieldApi = new FieldApi(options)
fieldApi.Field = Field as never
const api = new FieldApi(options)

const extendedApi: typeof api & SolidFieldApi<TParentData, TFormValidator> =
api as never

extendedApi.Field = Field as never

/**
* fieldApi.update should not have any side effects. Think of it like a `useRef`
* that we need to keep updated every render with the most up-to-date information.
*
* createComputed to make sure this effect runs before render effects
*/
createComputed(() => fieldApi.update(opts()))
createComputed(() => api.update(opts()))

// Instantiates field meta and removes it when unrendered
onMount(() => onCleanup(fieldApi.mount()))
onMount(() => onCleanup(api.mount()))

return makeFieldReactive(fieldApi) as never
return makeFieldReactive(extendedApi as never)
}

type FieldComponentProps<
Expand Down
Loading

0 comments on commit 145e0a9

Please sign in to comment.