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 121 #122

Merged
merged 9 commits into from
Mar 12, 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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ import { z } from "zod";

const personForm = formAtom({
name: textField(),
age: numberField({ schema: z.number().min(18) }); // override default schema
age: numberField({ schema: (s) => s.min(18) }); // extend the default schema
character: stringField().optional(); // make field optional
});

Expand Down
8 changes: 2 additions & 6 deletions src/components/checkbox-group/CheckboxGroup.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,18 +24,14 @@ const checkboxGroupStory = <Option, Field extends ZodArrayField>(
Omit<Partial<CheckboxGroupProps<Option, Field>>, "field">;
} & Omit<StoryObj<typeof meta>, "args">,
) => ({
...storyObj,
decorators: [
(Story: () => JSX.Element) => (
<StoryForm fields={{ field: storyObj.args.field }}>
{() => (
<p>
<Story />
</p>
)}
{() => <Story />}
</StoryForm>
),
],
...storyObj,
});

export const Required = checkboxGroupStory({
Expand Down
4 changes: 2 additions & 2 deletions src/components/checkbox-group/CheckboxGroupField.mock.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,14 @@ export const CheckboxGroupField = <Option, Field extends ZodArrayField>({
}: CheckboxGroupFieldProps<Option, Field>) => (
<>
<FieldLabel field={field} label={label} />
<p>
<section>
<CheckboxGroup
field={field}
getLabel={getLabel}
getValue={getValue}
options={options}
/>
</p>
</section>
<PicoFieldErrors field={field} />
</>
);
4 changes: 2 additions & 2 deletions src/components/multi-select/MultiSelect.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,11 @@ const MultiSelectField = <Option, Field extends ZodArrayField>({
}: {
label: ReactNode;
} & MultiSelectProps<Option, Field>) => (
<div style={{ margin: "20px 0" }}>
<>
<FieldLabel field={field} label={label} />
<MultiSelect field={field} {...props} />
<FieldErrors field={field} />
</div>
</>
);

export const RequiredArrayString = formStory({
Expand Down
8 changes: 2 additions & 6 deletions src/components/radio-group/RadioGroup.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,18 +27,14 @@ const radioGroupStory = <Option, Field extends SelectField>(
Omit<Partial<RadioGroupProps<Option, Field>>, "field">;
} & Omit<StoryObj<typeof meta>, "args">,
) => ({
...storyObj,
decorators: [
(Story: () => JSX.Element) => (
<StoryForm fields={{ field: storyObj.args.field }}>
{() => (
<p>
<Story />
</p>
)}
{() => <Story />}
</StoryForm>
),
],
...storyObj,
});

const bashAnswers = [
Expand Down
8 changes: 2 additions & 6 deletions src/components/select/Select.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,18 +27,14 @@ const selectStory = <Option, Field extends TSelectField>(
Omit<Partial<SelectProps<Option, Field>>, "field">;
} & Omit<StoryObj<typeof meta>, "args">,
) => ({
...storyObj,
decorators: [
(Story: () => JSX.Element) => (
<StoryForm fields={{ field: storyObj.args.field }}>
{() => (
<p>
<Story />
</p>
)}
{() => <Story />}
</StoryForm>
),
],
...storyObj,
});

export const RequiredString = selectStory({
Expand Down
27 changes: 25 additions & 2 deletions src/fields/array-field/arrayField.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import { renderHook } from "@testing-library/react";
import { formAtom, useFormValues } from "form-atoms";
import { act, renderHook } from "@testing-library/react";
import {
formAtom,
useFieldActions,
useFieldErrors,
useFormValues,
} from "form-atoms";
import { describe, expect, it } from "vitest";
import { z } from "zod";

Expand All @@ -20,4 +25,22 @@ describe("arrayField()", () => {
explicitUndefined: [],
});
});

describe("schema", () => {
it("extends the internal schema", async () => {
const field = arrayField({
elementSchema: z.number(),
value: [1, 2, 3],
schema: (s) => s.max(2),
});

const { result: actions } = renderHook(() => useFieldActions(field));
const { result: errors } = renderHook(() => useFieldErrors(field));

await act(async () => actions.current.validate());
expect(errors.current).toEqual([
"Array must contain at most 2 element(s)",
]);
});
});
});
38 changes: 21 additions & 17 deletions src/fields/array-field/arrayField.ts
Original file line number Diff line number Diff line change
@@ -1,35 +1,39 @@
import { ArrayCardinality, ZodAny, ZodArray, ZodSchema, z } from "zod";
import type { ArrayCardinality, ZodAny, ZodArray, ZodSchema } from "zod";
import { z } from "zod";

import {
ZodField,
ZodFieldConfig,
ZodParams,
defaultParams,
zodField,
} from "../zod-field";
import { prepareSchema } from "../../utils";
import { FieldConfig } from "../field";
import { type ZodField, defaultParams, zodField } from "../zod-field";

export type ZodArrayField<Element extends ZodSchema = ZodAny> = ZodField<
ZodArray<Element, ArrayCardinality>,
ZodArray<Element, ArrayCardinality>
>;

export type ArrayFieldParams<ElementSchema extends z.Schema> = Partial<
ZodFieldConfig<
ZodArray<ElementSchema, "atleastone">,
ZodArray<ElementSchema, "many">
>
> &
ZodParams;
export type ArrayFieldParams<ElementSchema extends z.Schema> = FieldConfig<
ZodArray<ElementSchema, "atleastone">,
ZodArray<ElementSchema, "many">
>;

export const arrayField = <ElementSchema extends z.Schema>({
required_error = defaultParams.required_error,
value = [],
elementSchema,
schema,
optionalSchema,
...config
}: { elementSchema: ElementSchema } & ArrayFieldParams<ElementSchema>) =>
zodField({
value,
schema: z.array(elementSchema).nonempty(required_error),
optionalSchema: z.array(elementSchema),
...prepareSchema({
initial: {
schema: z.array(elementSchema).nonempty(required_error),
optionalSchema: z.array(elementSchema),
},
user: {
schema,
optionalSchema,
},
}),
...config,
});
7 changes: 4 additions & 3 deletions src/fields/boolean-field/booleanField.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { ExtractAtomValue } from "jotai";
import { ZodBoolean, z } from "zod";

import { ZodFieldConfig, zodField } from "..";
import { ZodParams, defaultParams } from "../zod-field/zodParams";
import { zodField } from "..";
import { FieldConfig } from "../field";
import { defaultParams } from "../zod-field/zodParams";

export type BooleanField = ReturnType<typeof booleanField>;

Expand All @@ -13,7 +14,7 @@ export type BooleanFieldValue = ExtractAtomValue<
export const booleanField = ({
required_error = defaultParams.required_error,
...config
}: Partial<ZodFieldConfig<ZodBoolean>> & ZodParams = {}) =>
}: Omit<FieldConfig<ZodBoolean>, "schema" | "optionalSchema"> = {}) =>
zodField({
value: undefined,
schema: z.boolean({ required_error }),
Expand Down
5 changes: 3 additions & 2 deletions src/fields/checkbox-field/CheckboxInput.mock.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { FieldErrors, FieldLabel } from "../../components";
import { FieldLabel } from "../../components";
import {
type CheckboxFieldProps,
useCheckboxFieldProps,
useRequiredProps,
} from "../../hooks";
import { PicoFieldErrors } from "../../scenarios/PicoFieldErrors";

export const CheckboxInput = ({
field,
Expand All @@ -19,7 +20,7 @@ export const CheckboxInput = ({
<input type="checkbox" {...props} {...requiredProps} />
<FieldLabel field={field} label={label} />
<div>
<FieldErrors field={field} />
<PicoFieldErrors field={field} />
</div>
</div>
);
Expand Down
10 changes: 5 additions & 5 deletions src/fields/checkbox-field/checkboxField.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { ExtractAtomValue } from "jotai";
import { ZodBoolean, ZodLiteral, z } from "zod";

import { ZodFieldConfig, zodField } from "..";
import { ZodParams, defaultParams } from "../zod-field/zodParams";
import { zodField } from "..";
import { FieldConfig } from "../field";
import { defaultParams } from "../zod-field/zodParams";

export type CheckboxField = ReturnType<typeof checkboxField>;

Expand All @@ -16,11 +17,10 @@ export const checkboxField = ({
...config
}: Partial<
Omit<
ZodFieldConfig<ZodLiteral<true>, ZodBoolean>,
FieldConfig<ZodLiteral<true>, ZodBoolean>,
"schema" | "optionalSchema" | "validate"
>
> &
ZodParams = {}) =>
> = {}) =>
zodField({
value,
/**
Expand Down
5 changes: 3 additions & 2 deletions src/fields/date-field/DateInput.mock.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { FieldErrors, FieldLabel } from "../../components";
import { FieldLabel } from "../../components";
import { DateFieldProps, useDateFieldProps } from "../../hooks";
import { PicoFieldErrors } from "../../scenarios/PicoFieldErrors";

export const getDateString = (date: Date = new Date()) =>
date.toISOString().slice(0, 10);
Expand All @@ -15,7 +16,7 @@ export const DateInput = ({ field, label, initialValue }: DateFieldProps) => {
{...props}
value={`${value ? getDateString(value) : ""}`}
/>
<FieldErrors field={field} />
<PicoFieldErrors field={field} />
</div>
);
};
17 changes: 17 additions & 0 deletions src/fields/date-field/dateField.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,23 @@ export const InitializedInput = formStory({
},
});

export const ExtendSchema = formStory({
args: {
fields: {
deadline: dateField({
name: "deadline",
schema: (s) => s.min(new Date(), { message: "Must be in the future" }),
}),
},
children: ({ fields }) => (
<DateInput
field={fields.deadline}
label="Dead line (can't be in the past)"
/>
),
},
});

const nowPlusDays = (days = 0) => {
const date = new Date();
date.setDate(date.getDate() + days);
Expand Down
25 changes: 25 additions & 0 deletions src/fields/date-field/dateField.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { act, renderHook } from "@testing-library/react";
import { useFieldActions, useFieldErrors } from "form-atoms";
import { describe, expect, it } from "vitest";

import { dateField } from "./dateField";

describe("dateField()", () => {
describe("schema", () => {
it("extends the internal schema", async () => {
const field = dateField({
value: new Date("2000-01-01"),
schema: (s) =>
s.max(new Date("1999-12-31"), {
message: "Date can't be in the 21st century",
}),
});

const { result: actions } = renderHook(() => useFieldActions(field));
const { result: errors } = renderHook(() => useFieldErrors(field));

await act(async () => actions.current.validate());
expect(errors.current).toEqual(["Date can't be in the 21st century"]);
});
});
});
19 changes: 14 additions & 5 deletions src/fields/date-field/dateField.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { ExtractAtomValue } from "jotai";
import { type ExtractAtomValue } from "jotai";
import { ZodDate, z } from "zod";

import { ZodFieldConfig, zodField } from "..";
import { ZodParams, defaultParams } from "../zod-field/zodParams";
import { zodField } from "..";
import { prepareSchema } from "../../utils";
import { type FieldConfig } from "../field";
import { defaultParams } from "../zod-field/zodParams";

export type DateField = ReturnType<typeof dateField>;

Expand All @@ -12,10 +14,17 @@ export type DateFieldValue = ExtractAtomValue<

export const dateField = ({
required_error = defaultParams.required_error,
schema,
optionalSchema,
...config
}: Partial<ZodFieldConfig<ZodDate>> & ZodParams = {}) =>
}: FieldConfig<ZodDate> = {}) =>
zodField({
value: undefined,
schema: z.date({ required_error }),
...prepareSchema({
initial: {
schema: z.date({ required_error }),
},
user: { schema, optionalSchema },
}),
...config,
});
10 changes: 5 additions & 5 deletions src/fields/digit-field/digitField.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { ExtractAtomValue } from "jotai";
import { z } from "zod";

import { ZodFieldConfig, zodField } from "..";
import { ZodParams } from "../zod-field/zodParams";
import { zodField } from "..";
import { FieldConfig } from "../field";

export type DigitField = ReturnType<typeof digitField>;

Expand All @@ -25,9 +25,9 @@

type ZodDigitSchema = typeof zodDigitSchema;

export const digitField = ({
...config
}: Partial<ZodFieldConfig<ZodDigitSchema>> & ZodParams = {}) =>
export const digitField = (
config: Omit<FieldConfig<ZodDigitSchema>, "schema" | "optionalSchema"> = {},
) =>

Check warning on line 30 in src/fields/digit-field/digitField.ts

View check run for this annotation

Codecov / codecov/patch

src/fields/digit-field/digitField.ts#L29-L30

Added lines #L29 - L30 were not covered by tests
zodField({
value: undefined,
schema: zodDigitSchema,
Expand Down
Loading
Loading