Skip to content

Commit

Permalink
Revert "Revert "✨ Add support for generic components using `FieldPath…
Browse files Browse the repository at this point in the history
…WithValue` (#6562)" (#6717)"

This reverts commit 08883bb.
  • Loading branch information
bluebill1049 committed Oct 8, 2021
1 parent c190993 commit 65e1836
Show file tree
Hide file tree
Showing 5 changed files with 183 additions and 29 deletions.
113 changes: 106 additions & 7 deletions src/__tests__/useController.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,12 @@ import {
} from '@testing-library/react';

import { Controller } from '../controller';
import { Control } from '../types';
import {
Control,
FieldPathWithValue,
FieldValues,
NestedValue,
} from '../types';
import { useController } from '../useController';
import { useForm } from '../useForm';
import { FormProvider, useFormContext } from '../useFormContext';
Expand All @@ -33,6 +38,98 @@ describe('useController', () => {
render(<Component />);
});

it('should render generic component correctly', () => {
type ExpectedType = { test: string };

const Generic = <FormValues extends FieldValues>({
name,
control,
}: {
name: FieldPathWithValue<FormValues, ExpectedType>;
control: Control<FormValues>;
}) => {
const {
field: { value, ...fieldProps },
fieldState: { error },
} = useController<FormValues, ExpectedType>({
name,
control,
defaultValue: { test: 'value' },
});

if (error?.test?.message) {
return null;
}

return <input type="text" value={value.test} {...fieldProps} />;
};

const Component = () => {
const { control } = useForm<{
test: string;
key: ExpectedType[];
}>({
defaultValues: { test: 'test', key: [{ test: 'input value' }] },
});

return <Generic name="key.0" control={control} />;
};

render(<Component />);

const input = screen.queryByRole('textbox') as HTMLInputElement | null;
expect(input).toBeInTheDocument();
expect(input?.value).toEqual('input value');
});

it('should be able to access values and error in generic components using NestedValue', () => {
type ExpectedType = NestedValue<{ test: string }>;

const Generic = <FormValues extends FieldValues>({
name,
control,
}: {
name: FieldPathWithValue<FormValues, ExpectedType>;
control: Control<FormValues>;
}) => {
const {
field: { value, ...fieldProps },
fieldState: { error },
} = useController<FormValues, ExpectedType>({
name,
control,
defaultValue: { test: 'value' },
});

if (error?.message) {
return <>There was an error</>;
}

return <input type="text" value={value.test} {...fieldProps} />;
};

const Component = () => {
const { control } = useForm<{
test: string;
key: ExpectedType[];
}>({
defaultValues: {
test: 'test',
key: [{ test: 'input value' }],
},
});

return <Generic name="key.0" control={control} />;
};

render(<Component />);

const input = screen.queryByRole('textbox') as HTMLInputElement | null;

expect(input).toBeInTheDocument();
expect(input?.value).toEqual('input value');
});

it('should only subscribe to formState at each useController level', async () => {
const renderCounter = [0, 0];
type FormValues = {
Expand Down Expand Up @@ -509,6 +606,12 @@ describe('useController', () => {
});

it('should return defaultValues when component is not yet mounted', async () => {
type FormValues = {
test: {
deep: { test: string; test1: string }[];
};
};

const defaultValues = {
test: {
deep: [
Expand All @@ -521,15 +624,11 @@ describe('useController', () => {
};

const App = () => {
const { control, getValues } = useForm<{
test: {
deep: { test: string; test1: string }[];
};
}>({
const { control, getValues } = useForm<FormValues>({
defaultValues,
});

const { field } = useController({
const { field } = useController<FormValues, string>({
control,
name: 'test.deep.0.test',
});
Expand Down
12 changes: 8 additions & 4 deletions src/controller.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
import { ControllerProps, FieldPath, FieldValues } from './types';
import { ControllerProps, FieldPathWithValue, FieldValues } from './types';
import { useController } from './useController';

const Controller = <
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
TResult = any,
TName extends FieldPathWithValue<TFieldValues, TResult> = FieldPathWithValue<
TFieldValues,
TResult
>,
>(
props: ControllerProps<TFieldValues, TName>,
) => props.render(useController<TFieldValues, TName>(props));
props: ControllerProps<TFieldValues, TResult, TName>,
) => props.render(useController<TFieldValues, TResult, TName>(props));

export { Controller };
65 changes: 51 additions & 14 deletions src/types/controller.ts
Original file line number Diff line number Diff line change
@@ -1,70 +1,107 @@
import * as React from 'react';

import { FieldErrors } from '..';

import { RegisterOptions } from './validator';
import {
Control,
FieldError,
FieldPath,
FieldPathValue,
FieldPathWithValue,
FieldValues,
IsAny,
NestedValue,
Noop,
Primitive,
RefCallBack,
UnpackNestedValue,
UseFormStateReturn,
} from './';

export type ControllerFieldState = {
export type ControllerFieldState<
TFieldValues extends FieldValues = FieldValues,
TResult = any,
TName extends FieldPathWithValue<TFieldValues, TResult> = FieldPathWithValue<
TFieldValues,
TResult
>,
FieldValuesAtPath = IsAny<TResult> extends true
? FieldPathValue<TFieldValues, TName>
: TResult,
> = {
invalid: boolean;
isTouched: boolean;
isDirty: boolean;
error?: FieldError;
error?: FieldValuesAtPath extends NestedValue | Primitive
? FieldError
: FieldErrors<FieldValuesAtPath>;
};

export type ControllerRenderProps<
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
TResult = any,
TName extends FieldPathWithValue<TFieldValues, TResult> = FieldPathWithValue<
TFieldValues,
TResult
>,
> = {
onChange: (...event: any[]) => void;
onBlur: Noop;
value: UnpackNestedValue<FieldPathValue<TFieldValues, TName>>;
value: IsAny<TResult> extends true
? UnpackNestedValue<FieldPathValue<TFieldValues, TName>>
: UnpackNestedValue<TResult>;
name: TName;
ref: RefCallBack;
};

export type UseControllerProps<
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
TResult = any,
TName extends FieldPathWithValue<TFieldValues, TResult> = FieldPathWithValue<
TFieldValues,
TResult
>,
> = {
name: TName;
rules?: Omit<
RegisterOptions<TFieldValues, TName>,
'valueAsNumber' | 'valueAsDate' | 'setValueAs' | 'disabled'
>;
shouldUnregister?: boolean;
defaultValue?: UnpackNestedValue<FieldPathValue<TFieldValues, TName>>;
defaultValue?: IsAny<TResult> extends true
? UnpackNestedValue<FieldPathValue<TFieldValues, TName>>
: UnpackNestedValue<TResult>;
control?: Control<TFieldValues>;
};

export type UseControllerReturn<
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
TResult = any,
TName extends FieldPathWithValue<TFieldValues, TResult> = FieldPathWithValue<
TFieldValues,
TResult
>,
> = {
field: ControllerRenderProps<TFieldValues, TName>;
field: ControllerRenderProps<TFieldValues, TResult, TName>;
formState: UseFormStateReturn<TFieldValues>;
fieldState: ControllerFieldState;
fieldState: ControllerFieldState<TFieldValues, TResult, TName>;
};

export type ControllerProps<
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
TResult = any,
TName extends FieldPathWithValue<TFieldValues, TResult> = FieldPathWithValue<
TFieldValues,
TResult
>,
> = {
render: ({
field,
fieldState,
formState,
}: {
field: ControllerRenderProps<TFieldValues, TName>;
fieldState: ControllerFieldState;
field: ControllerRenderProps<TFieldValues, TResult, TName>;
fieldState: ControllerFieldState<TFieldValues, TResult, TName>;
formState: UseFormStateReturn<TFieldValues>;
}) => React.ReactElement;
} & UseControllerProps<TFieldValues, TName>;
} & UseControllerProps<TFieldValues, TResult, TName>;
10 changes: 10 additions & 0 deletions src/types/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -172,3 +172,13 @@ export type UnionLike<T> = [T] extends [Date | FileList | File | NestedValue]
Exclude<UnionKeys<T>, keyof T> | OptionalKeys<T>
>
: T;

export type FieldPathWithValue<
TFieldValues extends FieldValues,
TResult = unknown,
FieldPaths extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
> = {
[key in FieldPaths]: FieldPathValue<TFieldValues, key> extends TResult
? key
: never;
}[FieldPaths];
12 changes: 8 additions & 4 deletions src/useController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import get from './utils/get';
import { EVENTS } from './constants';
import {
Field,
FieldPath,
FieldPathWithValue,
FieldValues,
InternalFieldName,
UseControllerProps,
Expand All @@ -18,10 +18,14 @@ import { useSubscribe } from './useSubscribe';

export function useController<
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
TResult = any,
TName extends FieldPathWithValue<TFieldValues, TResult> = FieldPathWithValue<
TFieldValues,
TResult
>,
>(
props: UseControllerProps<TFieldValues, TName>,
): UseControllerReturn<TFieldValues, TName> {
props: UseControllerProps<TFieldValues, TResult, TName>,
): UseControllerReturn<TFieldValues, TResult, TName> {
const methods = useFormContext<TFieldValues>();
const { name, control = methods.control, shouldUnregister } = props;
const [value, setInputStateValue] = React.useState(
Expand Down

0 comments on commit 65e1836

Please sign in to comment.