Skip to content

Commit

Permalink
Merge pull request #25 from airjp73/get-input-props
Browse files Browse the repository at this point in the history
feat: encapsulate validation behavior in `getInputProps`
  • Loading branch information
airjp73 authored Jan 16, 2022
2 parents ae9b273 + 0a3e04d commit 7aa55bd
Show file tree
Hide file tree
Showing 29 changed files with 724 additions and 94 deletions.
14 changes: 5 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,18 +64,14 @@ type MyInputProps = {
};

export const MyInput = ({ name, label }: InputProps) => {
const { validate, clearError, defaultValue, error } = useField(name);
const { error, getInputProps } = useField(name);
return (
<div>
<label htmlFor={name}>{label}</label>
<input
id={name}
name={name}
onBlur={validate}
onChange={clearError}
defaultValue={defaultValue}
/>
{error && <span className="my-error-class">{error}</span>}
<input {...getInputProps({ id: name })} />
{error && (
<span className="my-error-class">{error}</span>
)}
</div>
);
};
Expand Down
31 changes: 13 additions & 18 deletions apps/docs/app/components/FormInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,7 @@ export const FormInput: FC<
onChange,
...rest
}) => {
const { error, clearError, validate, defaultValue } =
useField(name);
const { error, getInputProps } = useField(name);

return (
<div className={className}>
Expand All @@ -40,22 +39,18 @@ export const FormInput: FC<
</div>
<div className="mt-1 relative flex rounded-md shadow-sm">
<input
type="text"
name={name}
id={name}
className={classNames(
"border focus:ring-teal-500 focus:border-teal-500 focus:z-10 block w-full sm:text-sm text-black pr-10",
"rounded-md p-2",
error &&
"border-red-800 bg-red-50 text-red-800 placeholder-red-300 focus:outline-none focus:ring-red-500 focus:border-red-500"
)}
onChange={(event) => {
if (error) clearError();
onChange?.(event);
}}
onBlur={validate}
defaultValue={defaultValue}
{...rest}
{...getInputProps({
onChange,
id: name,
type: "text",
className: classNames(
"border focus:ring-teal-500 focus:border-teal-500 focus:z-10 block w-full sm:text-sm text-black pr-10",
"rounded-md p-2",
error &&
"border-red-800 bg-red-50 text-red-800 placeholder-red-300 focus:outline-none focus:ring-red-500 focus:border-red-500"
),
...rest,
})}
/>
{error && (
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
Expand Down
16 changes: 14 additions & 2 deletions apps/docs/app/components/Layout.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import { MenuAlt2Icon } from "@heroicons/react/outline";
import React, { FC, Fragment, useState } from "react";
import { useMatches } from "remix";
import React, {
FC,
Fragment,
useEffect,
useState,
} from "react";
import { useLocation, useMatches } from "remix";
import { Sidebar } from "../components/Sidebar";
import { Footer } from "./Footer";

Expand Down Expand Up @@ -61,6 +66,13 @@ const navSections: Section[] = [
];

export const Layout: FC = ({ children }) => {
const location = useLocation();
const focusedProp = location.hash.replace("#", "");
useEffect(() => {
const el = document.getElementById(focusedProp);
if (el) el.scrollIntoView();
}, [focusedProp]);

const [sidebarOpen, setSidebarOpen] = useState(false);

const matches = useMatches();
Expand Down
17 changes: 16 additions & 1 deletion apps/docs/app/components/PropHeader.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { LinkIcon } from "@heroicons/react/outline";
import classNames from "classnames";
import { useLocation } from "remix";

type PropHeaderProps = {
variant?: "h1" | "h3";
Expand All @@ -23,14 +25,27 @@ export const PropHeader = ({
optional,
variant: Variant = "h3",
}: PropHeaderProps) => {
const location = useLocation();
const focusedProp = location.hash.replace("#", "");
return (
<Variant>
<div className={variantHeaderColors[Variant]}>
{prop}
<a
id={prop}
href={`#${prop}`}
className={classNames(
"-ml-6 flex items-center group",
focusedProp !== prop && "no-underline"
)}
>
<LinkIcon className="h-4 w-4 mr-2 invisible group-hover:visible" />
{prop}
</a>
</div>
<div
className={classNames(
"text-zinc-500",
"whitespace-pre-wrap",
variantTypeSizes[Variant]
)}
>
Expand Down
15 changes: 6 additions & 9 deletions apps/docs/app/routes/integrate-your-components.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ so these features only work if you use it in a `ValidatedForm`.
However, it is still safe to use your input outside of a `ValidatedForm`,
it just won't be able to take advantage of the features.

If this example doesn't validate at the right times for your use-case
or you don't have a plain input,
check out the [useField](/reference/use-field) documentation to see what other helpers and options are available.

```tsx
import { useField } from "remix-validated-form";

Expand All @@ -30,18 +34,11 @@ type MyInputProps = {
};

export const MyInput = ({ name, label }: InputProps) => {
const { validate, clearError, defaultValue, error } =
useField(name);
const { error, getInputProps } = useField(name);
return (
<div>
<label htmlFor={name}>{label}</label>
<input
id={name}
name={name}
onBlur={validate}
onChange={clearError}
defaultValue={defaultValue}
/>
<input {...getInputProps({ id: name })} />
{error && (
<span className="my-error-class">{error}</span>
)}
Expand Down
73 changes: 73 additions & 0 deletions apps/docs/app/routes/reference/use-field.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ meta:
title: useField (Remix Validated Form)
---

import outdent from "outdent";
import { PropHeader } from "~/components/PropHeader";

<PropHeader
Expand All @@ -24,6 +25,14 @@ and the helpers will be no-ops.

The validation error message if there is one.

<PropHeader
prop="getInputProps"
type={`<T extends {}>(props?: T) => T`}
/>

A prop-getter used to get the props for the input.
This will automatically set up the validation behavior based on the [`validationBehavior`](#validationBehavior) option.

<PropHeader prop="defaultValue" type="any" />

The default value of the field, if there is one.
Expand All @@ -36,8 +45,72 @@ Clears the error message.

Validates the form field and populates the `error` prop if there is an error.

<PropHeader prop="touched" type="boolean" />

Whether or not the field has been touched.
If you're using `getInputProps`, a field will become touched on blur.

<PropHeader
prop="setTouched"
type="(touched: boolean) => void"
/>

Sets the `touched` state of the field to whatever you pass in.

## UseFieldOptions

<PropHeader
prop="validationBehavior"
type={outdent`
{
initial?: "onChange" | "onBlur" | "onSubmit";
whenTouched?: "onChange" | "onBlur" | "onSubmit";
whenSubmitted?: "onChange" | "onBlur" | "onSubmit";
}`}
/>

Allows you to configure when the field should validate.
The keys (`initial`, `whenTouched`, `whenSubmitted`) are states the field/form could be in,
and the values (`onChange`, `onBlur`, `onSubmit`) are when you want to validate while in that state.

**Note:** This behavior only applies when you are using `getInputProps`.
If you're managing the validation manually (using `validate`, `clearError`, `setTouched`, etc),
this will have no effect.

#### Field/form states

- `initial`
- The field has not been touched and the form has not been submitted.
- `whenTouched`
- The user touched the field. A field becomes touched on blur, or when you call `setTouched(true)`.
- `whenSubmitted`
- The user attempted to submit the form.

#### When to validate

- `onChange`
- Validates every time the `onChange` event of the input fires.
- `onBlur`
- Validates every time the `onBlur` event of the input fires.
- Clears the error message when the user starts typing in that field again.
- `onSubmit`
- The field does not validate itself at all and only validates when the form is submitted.

No matter which option you choose, the form will always be validated again on submit.

#### Default configuration

By default the field will validate on blur,
then validate on every change after the field has been touched our submitted.

```
{
initial: "onBlur",
whenTouched: "onChange",
whenSubmitted: "onChange",
}
```

<PropHeader
prop="handleReceiveFocus"
type="() => void"
Expand Down
25 changes: 24 additions & 1 deletion apps/docs/app/routes/reference/use-form-context.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,13 @@ If no `action` prop is passed, this will be `undefined`.

<PropHeader prop="isSubmitting" type="boolean" />

Whether or not the form is submitting.
Whether or not the form is currently submitting.

<PropHeader prop="hasBeenSubmitted" type="boolean" />

Whether or not a submit has been attempted.
This will be `true` after the first submit attempt, even if the form is invalid.
This resets when the form resets.

<PropHeader
prop="defaultValues"
Expand All @@ -63,3 +69,20 @@ Whether or not the form is submitting.
The default values of the `ValidatedForm` component.
In cases where the user has javascript disabled,
this can sometimes be something other than what is passed into the `ValidatedForm` directly.

<PropHeader
prop="touchedFields"
type="Record<string, boolean>"
/>

Contains all the touched fields.
The keys of the object are the field names and values are whether or not the field has been touched.
If a field has not been touched at all, the value will be `undefined`.
If you set touched to `false` manually, then the value will be `false`.

<PropHeader
prop="setFieldTouched"
type="(fieldName: string, touched: boolean) => void"
/>

Manually set the `touched` state of the specified field.
1 change: 1 addition & 0 deletions apps/docs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
"framer-motion": "^5.5.5",
"highlight.js": "^11.3.1",
"lodash": "^4.17.21",
"outdent": "^0.8.0",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"rehype-highlight": "^5.0.1",
Expand Down
12 changes: 5 additions & 7 deletions apps/sample-app/app/components/FormInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,19 +19,17 @@ export const FormInput = ({
isRequired,
...rest
}: FormInputProps & InputProps) => {
const { validate, clearError, defaultValue, error } = useField(name);
const { getInputProps, error } = useField(name);

return (
<>
<FormControl isInvalid={!!error} isRequired={isRequired}>
<FormLabel htmlFor={name}>{label}</FormLabel>
<Input
id={name}
name={name}
onBlur={validate}
onChange={clearError}
defaultValue={defaultValue}
{...rest}
{...getInputProps({
id: name,
...rest,
})}
/>
{error && <FormErrorMessage>{error}</FormErrorMessage>}
</FormControl>
Expand Down
12 changes: 5 additions & 7 deletions apps/sample-app/app/components/FormSelect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,17 +19,15 @@ export const FormSelect = ({
isRequired,
...rest
}: FormSelectProps & SelectProps) => {
const { validate, clearError, defaultValue, error } = useField(name);
const { getInputProps, error } = useField(name);
return (
<FormControl isInvalid={!!error} isRequired={isRequired}>
<FormLabel htmlFor={name}>{label}</FormLabel>
<Select
id={name}
name={name}
onBlur={validate}
onChange={clearError}
defaultValue={defaultValue}
{...rest}
{...getInputProps({
id: name,
...rest,
})}
/>
{error && <FormErrorMessage>{error}</FormErrorMessage>}
</FormControl>
Expand Down
17 changes: 3 additions & 14 deletions apps/test-app/app/components/Input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,26 +4,15 @@ import { useField } from "remix-validated-form";
type InputProps = {
name: string;
label: string;
validateOnBlur?: boolean;
};

export const Input = forwardRef(
(
{ name, label, validateOnBlur }: InputProps,
ref: React.ForwardedRef<HTMLInputElement>
) => {
const { validate, clearError, defaultValue, error } = useField(name);
({ name, label }: InputProps, ref: React.ForwardedRef<HTMLInputElement>) => {
const { getInputProps, error } = useField(name);
return (
<div>
<label htmlFor={name}>{label}</label>
<input
id={name}
name={name}
onBlur={validateOnBlur ? validate : undefined}
onChange={clearError}
defaultValue={defaultValue}
ref={ref}
/>
<input {...getInputProps({ ref, id: name })} />
{error && <span style={{ color: "red" }}>{error}</span>}
</div>
);
Expand Down
Loading

1 comment on commit 7aa55bd

@vercel
Copy link

@vercel vercel bot commented on 7aa55bd Jan 16, 2022

Choose a reason for hiding this comment

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

Please sign in to comment.