From ad79545d957e827869ac1326c546affdd3124dbc Mon Sep 17 00:00:00 2001 From: Ryo Matsukawa <76232929+ryo-manba@users.noreply.github.com> Date: Thu, 28 Nov 2024 04:57:10 +0900 Subject: [PATCH] docs: add forms guide (#3822) --- apps/docs/components/docs/toc.tsx | 2 +- apps/docs/config/routes.json | 6 + apps/docs/content/docs/guide/form.mdx | 416 +++++++++++++++++++++++ apps/docs/content/docs/guide/routing.mdx | 66 ++-- 4 files changed, 456 insertions(+), 34 deletions(-) create mode 100644 apps/docs/content/docs/guide/form.mdx diff --git a/apps/docs/components/docs/toc.tsx b/apps/docs/components/docs/toc.tsx index 00b4544f12..7f4962ad4f 100644 --- a/apps/docs/components/docs/toc.tsx +++ b/apps/docs/components/docs/toc.tsx @@ -19,7 +19,7 @@ const paddingLeftByLevel: Record = { 1: "pl-0", 2: "pl-0", 3: "pl-3", - 4: "pl-3", + 4: "pl-6", }; export const DocsToc: FC = ({headings}) => { diff --git a/apps/docs/config/routes.json b/apps/docs/config/routes.json index 1d5500f469..efc13d67db 100644 --- a/apps/docs/config/routes.json +++ b/apps/docs/config/routes.json @@ -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", diff --git a/apps/docs/content/docs/guide/form.mdx b/apps/docs/content/docs/guide/form.mdx new file mode 100644 index 0000000000..1729953a71 --- /dev/null +++ b/apps/docs/content/docs/guide/form.mdx @@ -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. + + + +## 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"; + + +``` + +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) => { + // 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 ( +
+ + +
+ ); +} +``` + +### 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 ( +
+ + +
+ ); +} +``` + +## 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"; + +
+ + +
+``` + +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"; + +
+ { + 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"; + } + }} + /> + +
+``` + +> **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"; + +
+ { + if (value < 0 || value > 100) { + return "Value must be between 0 and 100"; + } + }} + /> + +
+``` + +### 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 ( + 0} + errorMessage={() => ( +
    + {errors.map((error, i) => ( +
  • {error}
  • + ))} +
+ )} + /> + ); +} +``` + +### 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) => { + e.preventDefault(); + + const data = Object.fromEntries(new FormData(e.currentTarget)); + const result = await callServer(data); + + setErrors(result.errors); + }; + + return ( +
+ + + +
+ ); +} + +// 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 ( +
+ + +
+ ); +} +``` + +```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) => { + e.preventDefault(); + + const data = Object.fromEntries(new FormData(e.currentTarget)); + const result = await callServer(data); + + setErrors(result.errors); + }; + + return ( +
+ + + +
+ ); +} + +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 ( +
+ ( + + )} + rules={{required: "Name is required."}} + /> + + + ); +} +``` diff --git a/apps/docs/content/docs/guide/routing.mdx b/apps/docs/content/docs/guide/routing.mdx index 92044633c8..07f1a26453 100644 --- a/apps/docs/content/docs/guide/routing.mdx +++ b/apps/docs/content/docs/guide/routing.mdx @@ -45,7 +45,7 @@ function App() { -## 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 ( - + {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 `` element should also be defined inside `` 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( @@ -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 `` +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 `` 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 ( - 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