docs: add forms guide (#3822)

This commit is contained in:
Ryo Matsukawa 2024-11-28 04:57:10 +09:00 committed by GitHub
parent d51d3546cd
commit ad79545d95
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 456 additions and 34 deletions

View File

@ -19,7 +19,7 @@ const paddingLeftByLevel: Record<number, string> = {
1: "pl-0",
2: "pl-0",
3: "pl-3",
4: "pl-3",
4: "pl-6",
};
export const DocsToc: FC<DocsTocProps> = ({headings}) => {

View File

@ -37,6 +37,12 @@
"keywords": "client side routing, routing, browser routing, nextui, next.js router, react router, remix router",
"path": "/docs/guide/routing.mdx"
},
{
"key": "form",
"title": "Forms",
"keywords": "forms, form validation, nextui",
"path": "/docs/guide/form.mdx"
},
{
"key": "upgrade-to-v2",
"title": "Upgrade to v2",

View File

@ -0,0 +1,416 @@
---
title: Forms
description: Learn how to handle forms in NextUI.
---
# Forms
NextUI form components are designed to be flexible and function as HTML form elements. They support form data submission, custom validation, real-time validation, and offer an accessible UI.
<CarbonAd/>
## Labels and help text
Accessible forms require clear and descriptive labels. NextUI components allow you to add labels to each field through the label prop.
You can also display help text such as descriptions or error messages.
```tsx
import {Input} from "@nextui-org/react";
<Input
type="password"
label="Password"
description="Password must be at least 8 characters."
/>
```
Labels should usually be visually displayed, but in rare cases, you need to provide an aria-label or aria-labelledby attribute to identify the element for screen readers.
## Submitting data
How you submit form data depends on your framework, application, and server.
By default, HTML forms are submitted via a full-page refresh in the browser.
You can call `preventDefault` in the `onSubmit` event to handle form data submission via an API.
### Uncontrolled forms
A simple way to get form data is to use `FormData` API. You can send this data to a backend API or convert it into a JavaScript object using [`Object.fromEntries`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/fromEntries).
Each field should have a `name` prop to identify it, and the values will be serialized as strings by the browser.
```tsx
import {Button, Form, Input} from "@nextui-org/react";
function Example() {
const [submitted, setSubmitted] = React.useState(null);
const onSubmit = (e: React.FormEvent<HTMLFormElement>) => {
// Prevent default browser page refresh.
e.preventDefault();
// Get form data as an object.
const data = Object.fromEntries(new FormData(e.currentTarget));
// Submit data to your backend API. (currently logged to the console)
console.log("Form submitted:", data);
};
return (
<Form onSubmit={onSubmit}>
<Input label="Name" name="name" />
<Button type="submit">Submit</Button>
</Form>
);
}
```
### Controlled forms
NextUI form components are uncontrolled by default, but if you need to manage state in real-time, you can use the `useState` hook to make the component controlled.
```tsx
import {Button, Form, Input} from "@nextui-org/react";
function Example() {
const [name, setName] = React.useState("");
const onSubmit = (e) => {
e.preventDefault();
// Submit data to your backend API.
alert(name);
};
return (
<Form onSubmit={onSubmit}>
<Input value={name} onValueChange={setName} />
<Button type="submit">Submit</Button>
</Form>
);
}
```
## Validation
Form validation is crucial for ensuring that users enter the correct data.
NextUI supports native HTML constraint validation and allows for custom validation and real-time validation.
### Built-in validation
NextUI form components support [native HTML validation](https://developer.mozilla.org/docs/Web/HTML/Constraint_validation) attributes like `isRequired` and `minLength`.
These constraints are checked by the browser when the user commits changes (e.g., onBlur) or submits the form.
You can display validation errors with custom styles instead of the browser's default UI.
```tsx
import {Button, Form, Input} from "@nextui-org/react";
<Form validationBehavior="native">
<Input name="email" type="email" isRequired />
<Button type="submit">Submit</Button>
</Form>
```
To enable native validation, set `validationBehavior="native"`.
By default, `validationBehavior="aria"` is set, which only marks the field as required or invalid for assistive technologies, without preventing form submission.
You can change the form defaults for your entire app using [NextUI Provider](/docs/api-references/nextui-provider).
### Customizing error messages
By default, error messages are provided by the browser.
You can customize these messages by providing a function to the `errorMessage` prop.
```tsx {9-19}
import {Button, Form, Input} from "@nextui-org/react";
<Form validationBehavior="native">
<Input
label="Number"
isRequired
min={0}
max={100}
errorMessage={(validationResult) => {
if (validationResult.validationDetails.rangeOverflow) {
return "Value is too high";
}
if (validationResult.validationDetails.rangeUnderflow) {
return "Value is too low";
}
if (validationResult.validationDetails.valueMissing) {
return "Value is required";
}
}}
/>
<Button type="submit">Submit</Button>
</Form>
```
> **Note**: The default error messages are localized by the browser based on the browser/operating system language settings. The [locale setting in NextUI Provider](/docs/api-references/nextui-provider#props) does not affect validation errors.
### Custom validation
In addition to built-in constraints, you can provide a function to the `validate` prop to support custom validation.
```tsx {7-11}
import {Button, Form, Input} from "@nextui-org/react";
<Form>
<Input
label="Number"
type="number"
validate={(value) => {
if (value < 0 || value > 100) {
return "Value must be between 0 and 100";
}
}}
/>
<Button type="submit">Submit</Button>
</Form>
```
### Realtime validation
If you want to display validation errors while the user is typing, you can control the field value and use the `isInvalid` prop along with the `errorMessage` prop.
```tsx {22-31}
import {Input} from "@nextui-org/react";
export function Example() {
const [password, setPassword] = React.useState("");
const errors: string[] = [];
if (password.length < 8) {
errors.push("Password must be 8 characters or more.");
}
if ((password.match(/[A-Z]/g) ?? []).length < 2) {
errors.push("Password must include at least 2 upper case letters");
}
if ((password.match(/[^a-z]/gi) ?? []).length < 2) {
errors.push("Password must include at least 2 symbols.");
}
return (
<Input
label="Name"
value={password}
onValueChange={setPassword}
isInvalid={errors.length > 0}
errorMessage={() => (
<ul>
{errors.map((error, i) => (
<li key={i}>{error}</li>
))}
</ul>
)}
/>
);
}
```
### Server validation
Client-side validation provides immediate feedback, but you should also validate data on the server to ensure accuracy and security.
NextUI allows you to display server-side validation errors by using the `validationErrors` prop in the `Form` component.
This prop should be an object where each key is the field `name` and the value is the error message.
```tsx {15}
import {Button, Form, Input} from "@nextui-org/react";
function Example() {
const [errors, setErrors] = React.useState({});
const onSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const data = Object.fromEntries(new FormData(e.currentTarget));
const result = await callServer(data);
setErrors(result.errors);
};
return (
<Form validationErrors={errors} onSubmit={onSubmit}>
<Input label="Username" name="username" />
<Input label="Password" name="password" type="password" />
<Button type="submit">Submit</Button>
</Form>
);
}
// Fake server used in this example.
function callServer(data) {
return {
errors: {
username: "Sorry, this username is taken.",
},
};
}
```
#### Schema validation
NextUI supports errors from schema validation libraries like Zod.
You can use Zod's `flatten` method to get error messages for each field and return them as part of the server response.
```tsx
// In your server.
import {z} from "zod";
const schema = z.object({
name: z.string().min(1),
age: z.coerce.number().positive()
});
function handleRequest(formData: FormData) {
const result = schema.safeParse(
Object.fromEntries(formData)
);
if (!result.success) {
return {
errors: result.error.flatten().fieldErrors
};
}
return {
errors: {}
};
}
```
#### React Server Actions
[Server Actions](https://react.dev/reference/rsc/server-actions) that allows seamless form submission to the server and retrieval of results.
The [`useActionState`](https://react.dev/reference/react/useActionState) hook can be used to get the result of server actions (such as errors) after submitting a form.
```tsx {14}
// app/add-form.tsx
"use client";
import {useActionState} from "react";
import {Button, Input, Label} from "@nextui-org/react";
import {createTodo} from "@/app/actions";
export function AddForm() {
const [{ errors }, formAction] = useActionState(createTodo, {
errors: {}
});
return (
<Form action={formAction} validationErrors={errors}>
<Input name="todo" label="Task" />
<Button type="submit">Add</Button>
</Form>
);
}
```
```ts
// app/actions.ts
"use server";
export async function createTodo(
prevState: any,
formData: FormData
) {
try {
// Create the todo.
} catch (err) {
return {
errors: {
todo: "Invalid todo."
}
};
}
}
```
#### Remix
Remix actions handle form submissions on the server.
You can use the [`useSubmit`](https://remix.run/docs/en/main/hooks/use-submit) hook to submit form data to the server and the [`useActionData`](https://remix.run/docs/en/main/hooks/use-action-data) hook to retrieve validation errors from the server.
```tsx {18}
// app/routes/signup.tsx
import type {ActionFunctionArgs} from "@remix-run/node";
import {useActionData, useSubmit} from "@remix-run/react";
import {Button, Form, Input} from "@nextui-org/react";
export default function SignupForm() {
const [errors, setErrors] = React.useState({});
const onSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const data = Object.fromEntries(new FormData(e.currentTarget));
const result = await callServer(data);
setErrors(result.errors);
};
return (
<Form validationErrors={errors} onSubmit={onSubmit}>
<Input label="Username" name="username" />
<Input label="Password" name="password" type="password" />
<Button type="submit">Submit</Button>
</Form>
);
}
export async function action(
{ request }: ActionFunctionArgs
) {
try {
// Validate data and perform action...
} catch (err) {
return {
errors: {
username: "Sorry, this username is taken."
}
};
}
}
```
## Form libraries
In most cases, the built-in validation features of NextUI are sufficient. However, if you're building more complex forms or integrating NextUI components into an existing form, you can use a form library like React Hook Form or Formik.
### React Hook Form
You can integrate NextUI components using [`Controller`](https://react-hook-form.com/docs/usecontroller/controller).
`Controller` allows you to manage field values and validation errors, and reflect the validation result in NextUI components.
```tsx
import {Controller, useForm} from "react-hook-form";
import {Button, Input, Label} from "@nextui-org/react";
function App() {
const { handleSubmit, control } = useForm({
defaultValues: {
name: ""
}
});
const onSubmit = (data) => {
// Call your API here.
};
return (
<Form onSubmit={handleSubmit(onSubmit)}>
<Controller
control={control}
name="name"
render={({field: {name, value, onChange, onBlur, ref}, fieldState: {invalid, error}}) => (
<Input
ref={ref}
isRequired
errorMessage={error?.message}
isInvalid={invalid}
label="Name"
name={name}
value={value}
onBlur={onBlur}
onChange={onChange}
/>
)}
rules={{required: "Name is required."}}
/>
<Button type="submit">Submit</Button>
</Form>
);
}
```

View File

@ -45,7 +45,7 @@ function App() {
<Spacer y={2} />
## Next.js
## Next.js
### App Router
@ -58,10 +58,10 @@ Go to your `app/providers.tsx` or `app/providers.jsx` (create it if it doesn't e
```tsx {8}
// app/providers.tsx
'use client'
"use client"
import {NextUIProvider} from '@nextui-org/react';
import {useRouter} from 'next/navigation'
import {NextUIProvider} from "@nextui-org/react";
import {useRouter} from "next/navigation"
export function Providers({children}: { children: React.ReactNode }) {
const router = useRouter();
@ -84,7 +84,7 @@ import {Providers} from "./providers";
export default function RootLayout({children}: { children: React.ReactNode }) {
return (
<html lang="en" className='dark'>
<html lang="en" className="dark">
<body>
<Providers>
{children}
@ -99,11 +99,11 @@ export default function RootLayout({children}: { children: React.ReactNode }) {
#### Add useHref (Optional)
If you are using the Next.js [basePath](https://nextjs.org/docs/app/api-reference/next-config-js/basePath) setting, you'll need to configure an environment variable to access it.
If you are using the Next.js [basePath](https://nextjs.org/docs/app/api-reference/next-config-js/basePath) setting, you'll need to configure an environment variable to access it.
```js
// next.config.js
const basePath = '...';
const basePath = "...";
const nextConfig = {
basePath,
env: {
@ -115,10 +115,10 @@ Then, provide a custom `useHref` function to prepend it to the href for all link
```tsx {9,12}
// app/providers.tsx
'use client'
"use client"
import {NextUIProvider} from '@nextui-org/react';
import {useRouter} from 'next/navigation'
import {NextUIProvider} from "@nextui-org/react";
import {useRouter} from "next/navigation"
export function Providers({children}: { children: React.ReactNode }) {
const router = useRouter();
@ -136,14 +136,14 @@ export function Providers({children}: { children: React.ReactNode }) {
### Pages Router
Go to pages`/_app.js` or `pages/_app.tsx` (create it if it doesn't exist) and add the`useRouter` hook
Go to pages`/_app.js` or `pages/_app.tsx` (create it if it doesn't exist) and add the`useRouter` hook
from `next/router`, it returns a router object that can be used to perform navigation.
```tsx {7,10}
// pages/_app.tsx
import type { AppProps } from 'next/app';
import {NextUIProvider} from '@nextui-org/react';
import {useRouter} from 'next/router';
import type { AppProps } from "next/app";
import {NextUIProvider} from "@nextui-org/react";
import {useRouter} from "next/router";
function MyApp({ Component, pageProps }: AppProps) {
const router = useRouter();
@ -162,9 +162,9 @@ When using the [basePath](https://nextjs.org/docs/app/api-reference/next-config-
```tsx {8,11}
// pages/_app.tsx
import type { AppProps } from 'next/app';
import {NextUIProvider} from '@nextui-org/react';
import {useRouter} from 'next/router';
import type { AppProps } from "next/app";
import {NextUIProvider} from "@nextui-org/react";
import {useRouter} from "next/router";
function MyApp({ Component, pageProps }: AppProps) {
const router = useRouter();
@ -182,17 +182,17 @@ export default MyApp;
## React Router
The `useNavigate` hook from `react-router-dom` returns a `navigate` function that can be used to perform navigation.
The `useNavigate` hook from `react-router-dom` returns a `navigate` function that can be used to perform navigation.
The `useHref` hook can also be provided if you're using React Router's `basename` option. Ensure that the component that calls useNavigate and renders Provider is inside the router component (e.g. `BrowserRouter`) so that it has access to React Router's internal context. The React Router `<Routes>` element should also be defined inside `<NextUIProvider>` so that links inside the rendered routes have access to the router.
Go to the `App` file commonly called `App.jsx` or `App.tsx`, add the `useNavigate` hook and pass the
Go to the `App` file commonly called `App.jsx` or `App.tsx`, add the `useNavigate` hook and pass the
`navigate` function to the `NextUIProvider`:
```jsx {6,9}
// App.tsx or App.jsx
import {BrowserRouter, useNavigate, useHref} from 'react-router-dom';
import {NextUIProvider} from '@nextui-org/react';
import {BrowserRouter, useNavigate, useHref} from "react-router-dom";
import {NextUIProvider} from "@nextui-org/react";
function App() {
const navigate = useNavigate();
@ -210,7 +210,7 @@ function App() {
// main.tsx or main.jsx
ReactDOM.createRoot(document.getElementById('root')).render(
ReactDOM.createRoot(document.getElementById("root")).render(
<React.StrictMode>
<BrowserRouter>
<App />
@ -219,22 +219,22 @@ ReactDOM.createRoot(document.getElementById('root')).render(
)
```
Ensure that the component that calls `useNavigate` and renders `NextUIProvider` is inside the router
component (e.g. `BrowserRouter`) so that it has access to React Router's internal context. The React Router `<Routes>`
Ensure that the component that calls `useNavigate` and renders `NextUIProvider` is inside the router
component (e.g. `BrowserRouter`) so that it has access to React Router's internal context. The React Router `<Routes>`
element should also be defined inside `NextUIProvider` so that links inside the rendered routes have access
to the router.
## Remix
Remix uses React Router under the hood, so the same `useNavigate` and `useHref` hook described above also works in Remix
apps. `NextUIProvider` should be rendered at the `root` of each page that includes NextUI components, or in
`app/root.tsx` to add it to all pages. See the [Remix docs](https://remix.run/docs/en/main/file-conventions/root)
Remix uses React Router under the hood, so the same `useNavigate` and `useHref` hook described above also works in Remix
apps. `NextUIProvider` should be rendered at the `root` of each page that includes NextUI components, or in
`app/root.tsx` to add it to all pages. See the [Remix docs](https://remix.run/docs/en/main/file-conventions/root)
for more details.
```jsx {14}
// app/root.tsx
import {useNavigate, useHref, Outlet} from '@remix-run/react';
import {NextUIProvider} from '@nextui-org/react';
import {useNavigate, useHref, Outlet} from "@remix-run/react";
import {NextUIProvider} from "@nextui-org/react";
export default function App() {
const navigate = useNavigate();
@ -257,17 +257,17 @@ export default function App() {
## TanStack
To use [TanStack Router](https://tanstack.com/router/latest) with NextUI, render NextUI's RouterProvider inside your root route. Use `router.navigate` in the `navigate` prop, and `router.buildLocation` in the `useHref` prop.
To use [TanStack Router](https://tanstack.com/router/latest) with NextUI, render NextUI's RouterProvider inside your root route. Use `router.navigate` in the `navigate` prop, and `router.buildLocation` in the `useHref` prop.
```tsx {9,10}
// app/root.tsx
import {NextUIProvider} from '@nextui-org/react';
import {NextUIProvider} from "@nextui-org/react";
function RootRoute() {
let router = useRouter();
return (
<NextUIProvider
<NextUIProvider
navigate={(to, options) => router.navigate({ to, ...options })}
useHref={(to) => router.buildLocation({ to }).href}
>
@ -279,7 +279,7 @@ function RootRoute() {
## Usage examples
Now that you have set up the `NextUIProvider` in your app, you can use the `href` prop in the `Tabs`,
Now that you have set up the `NextUIProvider` in your app, you can use the `href` prop in the `Tabs`,
`Listbox` and `Dropdown` items to navigate between pages.
The [Link](/docs/components/link) component will also use the `navigate` function from the