diff --git a/apps/docs/content/components/input/built-in-validation.raw.jsx b/apps/docs/content/components/input/built-in-validation.raw.jsx new file mode 100644 index 0000000000..70ca6412fa --- /dev/null +++ b/apps/docs/content/components/input/built-in-validation.raw.jsx @@ -0,0 +1,40 @@ +import {Button, Form, Input} from "@nextui-org/react"; + +export default function App() { + const [submitted, setSubmitted] = React.useState(null); + + const onSubmit = (e) => { + e.preventDefault(); + const data = Object.fromEntries(new FormData(e.currentTarget)); + + setSubmitted(data); + }; + + return ( +
+ { + if (validationDetails.typeMismatch) { + return "Please enter a valid email address"; + } + + return validationErrors; + }} + label="Email" + labelPlacement="outside" + name="email" + placeholder="Enter your email" + type="email" + /> + + {submitted && ( +
+ You submitted: {JSON.stringify(submitted)} +
+ )} +
+ ); +} diff --git a/apps/docs/content/components/input/built-in-validation.ts b/apps/docs/content/components/input/built-in-validation.ts new file mode 100644 index 0000000000..407bbc6a41 --- /dev/null +++ b/apps/docs/content/components/input/built-in-validation.ts @@ -0,0 +1,9 @@ +import App from "./built-in-validation.raw.jsx?raw"; + +const react = { + "/App.jsx": App, +}; + +export default { + ...react, +}; diff --git a/apps/docs/content/components/input/custom-validation.raw.jsx b/apps/docs/content/components/input/custom-validation.raw.jsx new file mode 100644 index 0000000000..78525f9f58 --- /dev/null +++ b/apps/docs/content/components/input/custom-validation.raw.jsx @@ -0,0 +1,40 @@ +import {Button, Form, Input} from "@nextui-org/react"; + +export default function App() { + const [submitted, setSubmitted] = React.useState(null); + + const onSubmit = (e) => { + e.preventDefault(); + const data = Object.fromEntries(new FormData(e.currentTarget)); + + setSubmitted(data); + }; + + return ( +
+ { + if (value.length < 3) { + return "Username must be at least 3 characters long"; + } + + return value === "admin" ? "Nice try!" : null; + }} + /> + + {submitted && ( +
+ You submitted: {JSON.stringify(submitted)} +
+ )} +
+ ); +} diff --git a/apps/docs/content/components/input/custom-validation.ts b/apps/docs/content/components/input/custom-validation.ts new file mode 100644 index 0000000000..b0bf5b8588 --- /dev/null +++ b/apps/docs/content/components/input/custom-validation.ts @@ -0,0 +1,9 @@ +import App from "./custom-validation.raw.jsx?raw"; + +const react = { + "/App.jsx": App, +}; + +export default { + ...react, +}; diff --git a/apps/docs/content/components/input/index.ts b/apps/docs/content/components/input/index.ts index 73664fc946..93c65cf335 100644 --- a/apps/docs/content/components/input/index.ts +++ b/apps/docs/content/components/input/index.ts @@ -14,6 +14,10 @@ import startEndContent from "./start-end-content"; import errorMessage from "./error-message"; import regexValidation from "./regex-validation"; import controlled from "./controlled"; +import builtInValidation from "./built-in-validation"; +import customValidation from "./custom-validation"; +import realTimeValidation from "./real-time-validation"; +import serverValidation from "./server-validation"; import customStyles from "./custom-styles"; import customImpl from "./custom-impl"; @@ -34,6 +38,10 @@ export const inputContent = { errorMessage, regexValidation, controlled, + builtInValidation, + customValidation, + realTimeValidation, + serverValidation, customStyles, customImpl, }; diff --git a/apps/docs/content/components/input/real-time-validation.raw.jsx b/apps/docs/content/components/input/real-time-validation.raw.jsx new file mode 100644 index 0000000000..cfb5d915c5 --- /dev/null +++ b/apps/docs/content/components/input/real-time-validation.raw.jsx @@ -0,0 +1,53 @@ +import {Button, Form, Input} from "@nextui-org/react"; + +export default function App() { + const [submitted, setSubmitted] = React.useState(null); + const [password, setPassword] = React.useState(""); + const errors = []; + + const onSubmit = (e) => { + e.preventDefault(); + const data = Object.fromEntries(new FormData(e.currentTarget)); + + setSubmitted(data); + }; + + if (password.length < 4) { + errors.push("Password must be 4 characters or more."); + } + if ((password.match(/[A-Z]/g) || []).length < 1) { + errors.push("Password must include at least 1 upper case letter"); + } + if ((password.match(/[^a-z0-9]/gi) || []).length < 1) { + errors.push("Password must include at least 1 symbol."); + } + + return ( +
+ ( + + )} + isInvalid={errors.length > 0} + label="Password" + labelPlacement="outside" + name="password" + placeholder="Enter your password" + value={password} + onValueChange={setPassword} + /> + + {submitted && ( +
+ You submitted: {JSON.stringify(submitted)} +
+ )} +
+ ); +} diff --git a/apps/docs/content/components/input/real-time-validation.ts b/apps/docs/content/components/input/real-time-validation.ts new file mode 100644 index 0000000000..6f8034a877 --- /dev/null +++ b/apps/docs/content/components/input/real-time-validation.ts @@ -0,0 +1,9 @@ +import App from "./real-time-validation.raw.jsx?raw"; + +const react = { + "/App.jsx": App, +}; + +export default { + ...react, +}; diff --git a/apps/docs/content/components/input/server-validation.raw.jsx b/apps/docs/content/components/input/server-validation.raw.jsx new file mode 100644 index 0000000000..3169541794 --- /dev/null +++ b/apps/docs/content/components/input/server-validation.raw.jsx @@ -0,0 +1,49 @@ +import {Button, Form, Input} from "@nextui-org/react"; + +export default function App() { + const [isLoading, setIsLoading] = React.useState(false); + const [errors, setErrors] = React.useState({}); + + const onSubmit = async (e) => { + e.preventDefault(); + setIsLoading(true); + + const data = Object.fromEntries(new FormData(e.currentTarget)); + const result = await callServer(data); + + setErrors(result.errors); + setIsLoading(false); + }; + + return ( +
+ + +
+ ); +} + +// Fake server used in this example. +async function callServer(_) { + await new Promise((resolve) => setTimeout(resolve, 500)); + + return { + errors: { + username: "Sorry, this username is taken.", + }, + }; +} diff --git a/apps/docs/content/components/input/server-validation.ts b/apps/docs/content/components/input/server-validation.ts new file mode 100644 index 0000000000..84a2823b6a --- /dev/null +++ b/apps/docs/content/components/input/server-validation.ts @@ -0,0 +1,9 @@ +import App from "./server-validation.raw.jsx?raw"; + +const react = { + "/App.jsx": App, +}; + +export default { + ...react, +}; diff --git a/apps/docs/content/docs/components/input.mdx b/apps/docs/content/docs/components/input.mdx index a2451d3010..bd2997ce14 100644 --- a/apps/docs/content/docs/components/input.mdx +++ b/apps/docs/content/docs/components/input.mdx @@ -125,6 +125,43 @@ You can use the `value` and `onValueChange` properties to control the input valu > **Note**: NextUI `Input` also supports native events like `onChange`, useful for form libraries > such as [Formik](https://formik.org/) and [React Hook Form](https://react-hook-form.com/). +### With Form + +`Input` can be used with a `Form` component to leverage form state management. By default, `Form` components use `validationBehavior="aria"`, which will not block form submission if any inputs are invalid. For more on form and validation behaviors, see the [Forms](/docs/guide/forms) guide. + +#### Built-in Validation + +`Input` supports the following [native HTML constraints](https://developer.mozilla.org/docs/Web/HTML/Constraint_validation): + +- `isRequired` indicates that a field must have a value before the form can be submitted. +- `minLength` and `maxLength` specify the minimum and length of text input. +- `pattern` provides a custom regular expression that a text input must conform to. +- `type="email"` and `type="url"` provide built-in validation for email addresses and URLs. + +When using native validation, error messages can be customized by passing a function to `errorMessage` and checking the [ValidityState](https://developer.mozilla.org/docs/Web/API/ValidityState) of `validationDetails`. + + + +#### Custom Validation + +In addition to built-in constraints, you can provide a function to the `validate` property for custom validation. + + + +#### 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. + + + +#### 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. + + + ## Slots - **base**: Input wrapper, it handles alignment, placement, and general appearance. @@ -274,6 +311,30 @@ In case you need to customize the input even further, you can use the `useInput` description: "Whether to use native HTML form validation or ARIA validation. When wrapped in a Form component, the default is `aria`. Otherwise, the default is `native`.", default: "native" }, + { + attribute: "minLength", + type: "number", + description: "The minimum length of the text input.", + default: "-" + }, + { + attribute: "maxLength", + type: "number", + description: "The maximum length of the text input.", + default: "-" + }, + { + attribute: "pattern", + type: "string", + description: "A regular expression that the input value is checked against.", + default: "-" + }, + { + attribute: "type", + type: "text | email | url | password | tel | search", + description: "The type of the input.", + default: "text" + }, { attribute: "startContent", type: "ReactNode", diff --git a/apps/docs/content/docs/guide/forms.mdx b/apps/docs/content/docs/guide/forms.mdx index f9f4af75c0..92888b5c34 100644 --- a/apps/docs/content/docs/guide/forms.mdx +++ b/apps/docs/content/docs/guide/forms.mdx @@ -36,7 +36,7 @@ Most fields should have a visible label. In rare exceptions, the `aria-label` or ## 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. +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. Frameworks like [Next.js](https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions-and-mutations#forms), [Remix](https://remix.run/docs/en/main/guides/forms), and [React Router](https://reactrouter.com/en/main/route/form-submission) provide their own ways to handle form submission. @@ -196,7 +196,7 @@ NextUI supports native HTML constraint validation and allows for custom validati #### 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. +These constraints are checked by the browser when the user commits changes (e.g., on blur) or submits the form. You can display validation errors with custom styles instead of the browser's default UI. ```tsx {10} @@ -235,7 +235,7 @@ Supported constraints include: - `minValue` and `maxValue` specify the minimum and maximum value in a date picker or number field. - `minLength` and `maxLength` specify the minimum and length of text input. - `pattern` provides a custom regular expression that a text input must conform to. -- `type="email"` and `type="url"` provide builtin validation for email addresses and URLs. +- `type="email"` and `type="url"` provide built-in validation for email addresses and URLs. See each component's documentation for more details on the supported validation props.