Skip to content

Commit

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

* Revert "Revert "✨  Add support for generic components using `FieldPathWithValue` (#6562)" (#6717)"

This reverts commit 08883bb.

* keep FieldError simple

* fix with error object with only FieldError type

* 7.18.0-next.0

* include test case for controller with type check
  • Loading branch information
bluebill1049 committed Oct 22, 2021
1 parent e373b4b commit f42add5
Show file tree
Hide file tree
Showing 5 changed files with 187 additions and 25 deletions.
137 changes: 130 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 Down Expand Up @@ -509,6 +514,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 +532,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 Expand Up @@ -590,4 +597,120 @@ describe('useController', () => {
screen.getByText('expected value');
});
});

describe('When expected type is provided', () => {
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?.message) {
return null;
}

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

const GenericController = <FormValues extends FieldValues>({
name,
control,
}: {
name: FieldPathWithValue<FormValues, ExpectedType>;
control: Control<FormValues>;
}) => {
return (
<Controller
render={({ field }) => <input {...field} />}
control={control}
name={name}
/>
);
};

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

return (
<div>
<Generic name="key.0" control={control} />
<GenericController name="key.1" control={control} />
</div>
);
};

render(<Component />);
});

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');
});
});
});
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 };
41 changes: 31 additions & 10 deletions src/types/controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@ import { RegisterOptions } from './validator';
import {
Control,
FieldError,
FieldPath,
FieldPathValue,
FieldPathWithValue,
FieldValues,
IsAny,
Noop,
RefCallBack,
UnpackNestedValue,
Expand All @@ -22,49 +23,69 @@ export type ControllerFieldState = {

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;
};

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>;
field: ControllerRenderProps<TFieldValues, TResult, TName>;
fieldState: ControllerFieldState;
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 @@ -182,3 +182,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 f42add5

Please sign in to comment.