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",