Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: Remove form-core type overrides from adapters #794

Merged
merged 4 commits into from
Jun 28, 2024
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
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 @@
? 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 @@
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

Check warning on line 45 in packages/react-form/src/createServerValidate.ts

View check run for this annotation

Codecov / codecov/patch

packages/react-form/src/createServerValidate.ts#L45

Added line #L45 was not covered by tests

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
Loading