Skip to content

Commit

Permalink
fix(useSelectFieldProps): make field initializable via options (#91)
Browse files Browse the repository at this point in the history
  • Loading branch information
MiroslavPetrik authored Jan 18, 2024
1 parent a5d2eb0 commit 126b454
Show file tree
Hide file tree
Showing 7 changed files with 104 additions and 40 deletions.
25 changes: 25 additions & 0 deletions src/components/select/Select.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,31 @@ export const OptionalString = formStory({
},
});

export const InitializedString = formStory({
parameters: {
docs: {
description: {
story: "Field is initialized via the `initialValue` prop.",
},
},
},
args: {
fields: {
country: stringField(),
},
children: ({ fields }) => (
<SelectField
field={fields.country}
initialValue={countryOptions[2]?.key}
label="Country of Origin"
options={countryOptions}
getValue={({ key }) => key}
getLabel={({ name }) => name}
/>
),
},
});

const ratingOptions = [5, 4, 3, 2, 1];

export const RequiredNumber = formStory({
Expand Down
49 changes: 35 additions & 14 deletions src/components/select/Select.test.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import { act, render, renderHook, screen } from "@testing-library/react";
import { userEvent } from "@testing-library/user-event";
import { formAtom, useFormActions, useFormSubmit } from "form-atoms";
import {
formAtom,
useFieldValue,
useFormActions,
useFormSubmit,
} from "form-atoms";
import { describe, expect, it, vi } from "vitest";
import { z } from "zod";

Expand Down Expand Up @@ -132,7 +137,7 @@ describe("<Select />", () => {
expect(onSubmit).not.toHaveBeenCalled();
});

it("initializes properly when field has custom shape", async () => {
describe("with options of custom shape", () => {
const options = [
{ username: "foo", id: "1" },
{ username: "boo", id: "2" },
Expand All @@ -153,22 +158,38 @@ describe("<Select />", () => {
getValue: (user: User) => user,
};

const form = formAtom({ field: props.field });
const { result } = renderHook(() => useFormActions(form));
render(<Select {...props} />);
it("initializes properly when field has custom shape", async () => {
const form = formAtom({ field: props.field });
const { result } = renderHook(() => useFormActions(form));
render(<Select {...props} />);

await act(() =>
userEvent.selectOptions(screen.getByRole("combobox"), [
screen.getByText("boo 2"),
]),
);
await act(() =>
userEvent.selectOptions(screen.getByRole("combobox"), [
screen.getByText("boo 2"),
]),
);

const onSubmit = vi.fn();
await act(async () => {
result.current.submit(onSubmit)();
const onSubmit = vi.fn();
await act(async () => {
result.current.submit(onSubmit)();
});

expect(onSubmit).toHaveBeenCalledWith({ field: options[1] });
});

expect(onSubmit).toHaveBeenCalledWith({ field: options[1] });
it.skip("initializes via initialValue prop", async () => {
const field = zodField({
value: undefined,
schema,
});

render(<Select {...props} initialValue={options[1]} />);

const { result } = renderHook(() => useFieldValue(field));

// TODO: value stays undefined
expect(result.current).toBe(options[1]);
});
});

describe("with optional field", () => {
Expand Down
26 changes: 15 additions & 11 deletions src/components/select/Select.tsx
Original file line number Diff line number Diff line change
@@ -1,30 +1,34 @@
import {
SelectField,
UseSelectFieldProps,
UseSelectOptionsProps,
SelectFieldProps,
UseOptionsProps,
useSelectFieldProps,
useSelectOptions,
} from "../../hooks";
import { PlaceholderOption } from "../placeholder-option";

export type SelectProps<
export type SelectProps<Option, Field extends SelectField> = SelectFieldProps<
Option,
Field extends SelectField,
> = UseSelectFieldProps<Option, Field> &
UseSelectOptionsProps<Option> & { placeholder?: string };
Field
> &
UseOptionsProps<Option> & { placeholder?: string };

export const Select = <Option, Field extends SelectField>({
field,
getValue,
getLabel,
options,
placeholder = "Please select an option",
initialValue,
}: SelectProps<Option, Field>) => {
const props = useSelectFieldProps({
field,
options,
getValue,
});
const props = useSelectFieldProps(
{
field,
options,
getValue,
},
{ initialValue },
);

const { selectOptions } = useSelectOptions({
field,
Expand Down
6 changes: 1 addition & 5 deletions src/components/select/SelectField.mock.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,11 @@
import { ReactNode } from "react";

import { FieldErrors, FieldLabel, Select, SelectProps } from "..";
import type { SelectField as _SelectField } from "../../hooks";

export const SelectField = <Option, Field extends _SelectField>({
field,
label,
...props
}: {
label: ReactNode;
} & SelectProps<Option, Field>) => (
}: SelectProps<Option, Field>) => (
<div style={{ margin: "20px 0" }}>
<FieldLabel field={field} label={label} />
<Select field={field} {...props} />
Expand Down
14 changes: 14 additions & 0 deletions src/hooks/use-select-field-props/useSelectFieldProps.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,20 @@ describe("useSelectFieldProps()", () => {
getValue: (val: string) => val,
};

it("initializes field via options", () => {
const field = stringField();
const index = 2;

const { result } = renderHook(() =>
useSelectFieldProps(
{ field, ...props },
{ initialValue: props.options[index] },
),
);

expect(result.current.value).toBe(index);
});

describe("initial value", () => {
it("is -1 when field is empty", () => {
const field = stringField();
Expand Down
20 changes: 13 additions & 7 deletions src/hooks/use-select-field-props/useSelectFieldProps.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,20 @@
import { UseFieldOptions } from "form-atoms";
import { useAtomValue } from "jotai";
import { ChangeEvent, useCallback, useMemo } from "react";

import { UseOptionsProps, useFieldProps } from "..";
import { ZodField, ZodFieldValue } from "../../fields";
import { FieldProps, type UseOptionsProps, useFieldProps } from "..";
import type { ZodField, ZodFieldValue } from "../../fields";

/**
* This restricts ZodField to have optional schema defaulting to 'undefined of required schema'.
*/
export type SelectField = ZodField<any>;

export type SelectFieldProps<
Option,
Field extends SelectField,
> = UseSelectFieldProps<Option, Field> & FieldProps<Field>;

export type UseSelectFieldProps<Option, Field extends SelectField> = {
field: Field;
getValue: (option: Option) => NonNullable<ZodFieldValue<Field>>;
Expand All @@ -19,11 +25,10 @@ export type UseSelectFieldProps<Option, Field extends SelectField> = {
*/
export const EMPTY_SELECT_VALUE = -1;

export const useSelectFieldProps = <Option, Field extends SelectField>({
field,
options,
getValue,
}: UseSelectFieldProps<Option, Field>) => {
export const useSelectFieldProps = <Option, Field extends SelectField>(
{ field, options, getValue }: UseSelectFieldProps<Option, Field>,
fieldOptions?: UseFieldOptions<ZodFieldValue<Field>>,
) => {
const atom = useAtomValue(field);
const fieldValue = useAtomValue(atom.value);
// TODO: getValue should be useMemo dependency, currently we asume that it is stable
Expand All @@ -44,6 +49,7 @@ export const useSelectFieldProps = <Option, Field extends SelectField>({
const props = useFieldProps<Field, HTMLSelectElement | HTMLInputElement>(
field,
getEventValue,
fieldOptions,
);

return { ...props, value };
Expand Down
4 changes: 1 addition & 3 deletions src/hooks/use-select-options/useSelectOptions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,7 @@ import { useMemo } from "react";

import { UseOptionsProps, useOptions } from "../use-options";

export type UseSelectOptionsProps<Option> = UseOptionsProps<Option>;

export function useSelectOptions<Option>(props: UseSelectOptionsProps<Option>) {
export function useSelectOptions<Option>(props: UseOptionsProps<Option>) {
const { renderOptions } = useOptions(props);

return useMemo(
Expand Down

0 comments on commit 126b454

Please sign in to comment.