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 schema implementation #19

Merged
merged 1 commit into from
Apr 4, 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
5 changes: 5 additions & 0 deletions .changeset/real-spoons-rescue.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"signal-form": minor
---

Add schema implementation
2 changes: 2 additions & 0 deletions .storybook/preview.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import "./styles.css";

export const parameters = {
actions: { argTypesRegex: "^on[A-Z].*" },
controls: {
Expand Down
37 changes: 37 additions & 0 deletions .storybook/styles.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
body {
font-family: sans-serif;
}

.field-errors {
margin: 0.3rem 0;
padding: 0;
}

.field-errors > li {
list-style: none;
color: red;
padding: 0;
margin: 0;
}

label {
font-weight: bold;
}

input:not([type]),
input[type="text"],
input[type="number"] {
border: 1px solid #ccc;
padding: 0.5rem 1rem;
background: white;
border-radius: 0.3rem;
display: block;
}

textarea {
border: 1px solid #ccc;
padding: 0.5rem 1rem;
background: white;
border-radius: 0.3rem;
display: block;
}
32 changes: 21 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,16 +43,17 @@ As you can see, signal-form ships with some basic components for form elements.

## Adding a schema

Forms can be automatically validated by defining a schema using yup. Unlike with remix-validated-form, the schema is optional.
Forms can be automatically validated by defining a schema using [yup][]. Unlike
with remix-validated-form, the schema is optional.

```tsx
import { SignalForm, Input, CheckBoxInput, schema } from "signal-form";

const Schema = schema.object().shape({
firstName: schema.string().required(),
lastName: schema.string().required(),
email: schema.string(),
lastName: schema.boolean(),
firstName: schema.textField().required(),
lastName: schema.textField().required(),
email: schema.textField(),
isAdmin: schema.checkBox(),
});

export function UserForm(): JSX.Element {
Expand All @@ -75,7 +76,13 @@ export function UserForm(): JSX.Element {
}
```

Note that we're importing `schema` from `signal-form`, rather than using `yup` directly. This is a thin wrapper around `yup` that sets up some transforms which make the schema work better with form data.
Note that we're importing `schema` from `signal-form`. The `schema` object
contains both all schema methods from [yup][], such as `object` and `string`,
but also has additional methods such as `textField` and `checkBox` which have
additional transforms to make working with the schemas more convenient.

Currently only [yup][] is supported as a schema validation library, alternative such
as [zod][] are not supported.

## Nested objects

Expand Down Expand Up @@ -370,11 +377,11 @@ function FullName(): JSX.Element {
export function UserForm(): JSX.Element {
return (
<SignalForm>
<FieldsFor name="author">
<TextField name="firstName" label="First name"/>
<TextField name="lastName" label="Last name"/>
<FullName/>
</FieldsFor>
<FieldsFor name="author">
<TextField name="firstName" label="First name"/>
<TextField name="lastName" label="Last name"/>
<FullName/>
</FieldsFor>
</SignalForm>
);
}
Expand All @@ -401,3 +408,6 @@ export function PostForm(): JSX.Element {
);
}
```

[yup]: https://github.com/jquense/yup
[zod]: https://zod.dev
21 changes: 21 additions & 0 deletions lib/controls/field-errors.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { useField } from "~/use-field";

export type FieldErrorsProps = {
name: string;
};

export function FieldErrors({ name }: FieldErrorsProps) {
let field = useField(name);

if (!field.touched.value || field.errors.value.length === 0) {
return null;
} else {
return (
<ul className="field-errors">
{field.errors.value.map((error, index) => (
<li key={index}>{error.message}</li>
))}
</ul>
);
}
}
2 changes: 2 additions & 0 deletions lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,5 @@ export type * from "./use-fields-array";

export * from "./use-is-submitting";
export type * from "./use-is-submitting";

export { schema } from "./schema";
15 changes: 15 additions & 0 deletions lib/schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import * as yup from "yup";

export const schema = {
...yup,
textField: () => yup.string().transform((v) => v || undefined),
numberField: () => yup.number().transform((v) => (v ? Number(v) : undefined)),
checkBox: () =>
yup
.boolean()
.transform(
(v) => !![v].flat().find((i) => [true, "on", "true"].includes(i))
),
select: (options: string[]) => yup.string().oneOf(options),
radioButton: (options: string[]) => yup.string().oneOf(options),
};
14 changes: 13 additions & 1 deletion lib/utils/parse-form-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,19 @@ export function parseFormData(formData: FormData): ParsedObject {
keys.forEach((k, index) => {
// Check if it's the last key
if (index === keys.length - 1) {
current[k] = value;
// Keys may occur multiple times in the form data, this could be the
// case for multi-selects for example. In this case we need to convert
// the value to an array.
if (current.hasOwnProperty(k)) {
// if it's already an array, append the value, otherwise convert it
if (Array.isArray(current[k])) {
current[k] = [...current[k], value];
} else {
current[k] = [current[k], value];
}
} else {
current[k] = value;
}
} else {
// Initialize the next level in the structure if it doesn't exist
if (!current[k]) {
Expand Down
Loading
Loading