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

✨ Add support for generic components using FieldPathWithValue #6562

Merged
merged 21 commits into from
Oct 2, 2021
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
3674c13
✨ Add support for generic components using FieldPathWithValue
julienfouilhe Sep 15, 2021
2058b5b
✅ Fix tests
julienfouilhe Sep 16, 2021
0ec5243
✨ Fix inference
julienfouilhe Sep 16, 2021
cae4e5a
Revert "✅ Fix tests"
julienfouilhe Sep 16, 2021
1fdf829
🎨 Improve readibility of `FieldPathWithValue`
julienfouilhe Sep 16, 2021
c71740c
Merge branch 'master' into generic-components
bluebill1049 Sep 16, 2021
e2710df
✨ Use TResult for `ControllerFieldState` too
julienfouilhe Sep 16, 2021
c5508fe
✨ Put `UnpackNestedValue` back
julienfouilhe Sep 16, 2021
f3cf1ee
Merge branch 'master' into generic-components
bluebill1049 Sep 18, 2021
35c50f5
Merge branch 'master' into generic-components
bluebill1049 Sep 18, 2021
d98f28a
Merge branch 'master' into generic-components
bluebill1049 Sep 19, 2021
67d1361
Merge branch 'master' into generic-components
bluebill1049 Sep 20, 2021
3995e2f
✅ Add test for generic components with NestedValue
julienfouilhe Sep 22, 2021
d959d59
Merge branch 'master' into generic-components
bluebill1049 Sep 22, 2021
4c4b35d
🐛 Fix issues with new fielderrors
julienfouilhe Sep 22, 2021
9de10fa
Merge remote-tracking branch 'origin/master' into generic-components
julienfouilhe Sep 28, 2021
cbc07d9
✅ Add expectations
julienfouilhe Sep 28, 2021
e7396f1
Merge branch 'master' into generic-components
bluebill1049 Sep 28, 2021
1339ab4
Merge branch 'master' into generic-components
bluebill1049 Sep 30, 2021
4e7c435
Merge branch 'master' into generic-components
bluebill1049 Sep 30, 2021
40d5fd3
Merge branch 'master' into generic-components
bluebill1049 Oct 1, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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 />);
julienfouilhe marked this conversation as resolved.
Show resolved Hide resolved

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 }>;
julienfouilhe marked this conversation as resolved.
Show resolved Hide resolved

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 };
77 changes: 56 additions & 21 deletions src/types/controller.ts
Original file line number Diff line number Diff line change
@@ -1,80 +1,115 @@
import * as React from 'react';

import { FieldErrors } from '..';

import { RegisterOptions } from './validator';
import {
Control,
DeepMapImpl,
DeepPartialImpl,
FieldError,
FieldPath,
FieldPathValue,
FieldPathWithValue,
FieldValues,
IsAny,
NestedValue,
Primitive,
RefCallBack,
UnionLike,
UnpackNestedValue,
julienfouilhe marked this conversation as resolved.
Show resolved Hide resolved
UseFormStateReturn,
} from './';

type ControllerFieldError<T> = DeepMapImpl<
DeepPartialImpl<UnionLike<T>>,
FieldError
>;
type ControllerFieldError<
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't pay close enough attention here, @kotarella1110 this was completely changed after the merge.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

which potentially lead to this: #6679

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@bluebill1049 I don't understand the linked issue is talking about versions before this was merged right?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Screen Shot 2021-10-05 at 6 41 06 pm

i am just digging around at the moment, see if i can find a fix for it.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DeepMapImpl<DeepPartialImpl<UnionLike<TFieldValues>>, FieldError>

vs

DeepMap<DeepPartial<UnionLike<TFieldValues>>, FieldError>

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

still try to get my head around it.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok confirmed the latest definitely related for causing the issue, it could be related to the user's typescript version as well, as i couldn't reproduce the issue at the lib level but codesandbox with CRA, after omit TResult and the issue went away.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TFieldValues extends FieldValues = FieldValues,
TResult = any,
TName extends FieldPathWithValue<TFieldValues, TResult> = FieldPathWithValue<
TFieldValues,
TResult
>,
FieldValuesAtPath = IsAny<TResult> extends true
? FieldPathValue<TFieldValues, TName>
: TResult,
> = FieldValuesAtPath extends NestedValue | Primitive
? FieldError
: FieldErrors<FieldValuesAtPath>;

export type ControllerFieldState<
TFieldValues extends FieldValues = FieldValues,
TFieldName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
TResult = any,
TName extends FieldPathWithValue<TFieldValues, TResult> = FieldPathWithValue<
TFieldValues,
TResult
>,
> = {
invalid: boolean;
isTouched: boolean;
isDirty: boolean;
error?: ControllerFieldError<FieldPathValue<TFieldValues, TFieldName>>;
error?: ControllerFieldError<TFieldValues, TResult, TName>;
};

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: () => void;
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<TFieldValues, TName>;
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<TFieldValues, TName>;
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 @@ -160,3 +160,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,
julienfouilhe marked this conversation as resolved.
Show resolved Hide resolved
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