useTextField
function useTextField<E extends HTMLInputElement | HTMLTextAreaElement>(
options: TextFieldHookOptions<E>
): ValidatedTextFieldImplementation<E>;
The useTextField
hook can be used for extremely simple validation using the
browser's constraint validation.
If values need to be validate with complex logic or against other fields, use
react-hook-form or another library instead.
Example Usage
The main purpose of this hook is to help validate the value using constraint
validation. Use the required
, pattern
, minLength
, and maxLength
options
to start getting real time validation once the field is blurred or the form is
submitted.
"use client";
import { Box } from "@react-md/core/box/Box";
import { box } from "@react-md/core/box/styles";
import { Button } from "@react-md/core/button/Button";
import { Form } from "@react-md/core/form/Form";
import { TextField } from "@react-md/core/form/TextField";
import { useTextField } from "@react-md/core/form/useTextField";
import { type ReactElement } from "react";
export default function AddingConstraintsExample(): ReactElement {
const { fieldProps, reset } = useTextField({
name: "example",
pattern: "^[A-Za-z,! ]+$",
required: true,
minLength: 4,
maxLength: 20,
// a default value could be provided
// defaultValue: "Hello, world!",
});
return (
<Form
className={box({ stacked: true, fullWidth: true })}
onReset={() => {
reset();
}}
>
<TextField {...fieldProps} label="Example" />
<Box justify="end" fullWidth disablePadding>
<Button type="reset" theme="warning" themeType="outline">
Reset
</Button>
<Button type="submit" theme="primary" themeType="contained">
Submit
</Button>
</Box>
</Form>
);
}
Custom Validation
Custom error messaging can be provided using the getErrorMessage
option. It
should return a string
"use client";
/* eslint-disable @typescript-eslint/no-unused-vars */
import { TextField } from "@react-md/core/form/TextField";
import { useTextField } from "@react-md/core/form/useTextField";
import { defaultGetErrorMessage } from "@react-md/core/form/validation";
import { type ReactElement } from "react";
export default function CustomValidationExample(): ReactElement {
const { fieldProps } = useTextField({
name: "example",
pattern: "^[A-Za-z,! ]+$",
required: true,
minLength: 4,
maxLength: 20,
getErrorMessage(options) {
const {
value,
pattern,
required,
minLength,
maxLength,
validity,
validationMessage,
isNumber,
isBlurEvent,
validationType,
} = options;
if (validity.tooLong) {
return `No more than ${maxLength} characters.`;
}
if (validity.tooShort) {
return `No more than ${minLength} characters.`;
}
if (validity.valueMissing) {
return "This value is required!";
}
if (value === "bad value") {
return "Value cannot be bad value";
}
return defaultGetErrorMessage(options);
},
});
return <TextField {...fieldProps} label="Example" />;
}
Additional Return Values
The useTextField
hook returns some other values and functions to customize or
control the text field behavior.
function Example() {
const { value, error, errorMessage, reset, setState, fieldRef, fieldProps } =
useTextField({
name: "example",
});
return <TextField label="Field" {...fieldProps} />;
}
Inline Counter
An inline counter can be displayed be enabling the counter
option along with
the maxLength
option. If the user should be able to insert text beyond the
max length limit, enable the disableMaxLength
option as well to prevent
passing the maxLength
option to the <input>
>
"use client";
import { TextField } from "@react-md/core/form/TextField";
import { useTextField } from "@react-md/core/form/useTextField";
import { type ReactElement } from "react";
export default function InlineCounterExample(): ReactElement {
const { fieldProps } = useTextField({
name: "example",
counter: true,
required: true,
maxLength: 20,
// this allows the user to type beyond the max length limit and display
// an error message. omit or set to `false` to enforce the max length instead
disableMaxLength: true,
});
return <TextField {...fieldProps} label="Example" />;
}
Validation Type
This validation timing can be customized by providing the validationType
option which can be one of:
"recommended"
(default) - validate when the"badInput"
,"tooLong"
, or"valueMissing"
error occurs as the user types. otherwise display any errors once the input is blurred"blur"
- only validate on blur"change"
- validate on each keystroke- One or many of the ValidityState input options
- i.e.
"valueMissing"
or["badInput", "customError", "tooLong"]
- i.e.
"use client";
import { Box } from "@react-md/core/box/Box";
import { box } from "@react-md/core/box/styles";
import { Button } from "@react-md/core/button/Button";
import { Form } from "@react-md/core/form/Form";
import { TextField } from "@react-md/core/form/TextField";
import { useTextField } from "@react-md/core/form/useTextField";
import { type ReactElement } from "react";
export default function ValidationTypeExample(): ReactElement {
const { fieldProps, reset } = useTextField({
name: "example",
pattern: "^[A-Za-z,! ]+$",
counter: true,
required: true,
maxLength: 20,
validationType: "change",
disableMaxLength: true,
});
return (
<Form
className={box({ stacked: true, fullWidth: true })}
onReset={() => {
reset();
}}
>
<TextField {...fieldProps} label="Example" />
<Box justify="end" fullWidth disablePadding>
<Button type="reset" theme="warning" themeType="outline">
Reset
</Button>
<Button type="submit" theme="primary" themeType="contained">
Submit
</Button>
</Box>
</Form>
);
}
Form-level Error Messaging
It is sometimes useful to display error messages in a different area isntead
of underneath the TextField
. Enable the disableMessage
option and manually
render the errorMessage
instead.
See the W3C User Notifications for more information on creating accessible form-level alerts.
"use client";
import { Box } from "@react-md/core/box/Box";
import { Button } from "@react-md/core/button/Button";
import { cssUtils } from "@react-md/core/cssUtils";
import { Form } from "@react-md/core/form/Form";
import { TextField } from "@react-md/core/form/TextField";
import { useTextField } from "@react-md/core/form/useTextField";
import { Typography } from "@react-md/core/typography/Typography";
import { type ReactElement, useId } from "react";
export default function FormLevelErrorMessagesExample(): ReactElement {
const errorId = useId();
const { fieldProps, reset, error, errorMessage } = useTextField({
name: "example",
pattern: "^[A-Za-z,! ]+$",
counter: true,
required: true,
maxLength: 20,
disableMessage: true,
helpText: "Accepts letters, spaces, commas, or explamation marks.",
// make it so only submit events trigger the error message and then
// remove the error message after blurring
validationType: "blur",
onBlur(event) {
if (!error) {
event.stopPropagation();
}
},
});
return (
<Form
onReset={() => {
reset();
}}
style={{ width: "100%" }}
>
{error && (
<div role="alert">
<Typography
type="headline-4"
className={cssUtils({ textColor: "error" })}
margin="none"
>
There are errors in this form
</Typography>
<Typography as="ul" style={{ marginBottom: "4em" }}>
<li>
<a id={errorId} href={`#${fieldProps.id}`}>
The Example field: {errorMessage}
</a>
</li>
</Typography>
</div>
)}
<Box stacked fullWidth disablePadding>
<TextField
{...fieldProps}
label="Example"
aria-describedby={(error && errorId) || undefined}
/>
<Box justify="end" fullWidth disablePadding>
<Button type="reset" theme="warning" themeType="outline">
Reset
</Button>
<Button type="submit" theme="primary" themeType="contained">
Submit
</Button>
</Box>
</Box>
</Form>
);
}
Parameters
options
- An object with the following definition:
export type TextFieldValidationOptions = Pick<
InputHTMLAttributes<HTMLInputElement>,
"minLength" | "maxLength" | "required" | "pattern"
>;
export type TextFieldChangeHandlers<
E extends HTMLInputElement | HTMLTextAreaElement,
> = Pick<HTMLAttributes<E>, "onBlur" | "onChange" | "onInvalid">;
export interface TextFieldHookOptions<
E extends HTMLInputElement | HTMLTextAreaElement,
> extends TextFieldValidationOptions,
TextFieldChangeHandlers<E> {
/**
* An optional id to use for the `TextField` or `TextArea` that is also used
* to create an id for the inline help/error messages.
*
* @defaultValue `"text-field-" + useId()`
*/
id?: string;
/**
* An optional ref that should be merged with the ref returned by this hook.
* This should really only be used if you are making a custom component using
* this hook and forwarding refs. If you need a ref to access the `<input>` or
* `<textarea>` DOM node, you can use the `fieldRef` returned by this hook
* instead.
*
* @example Accessing TextField DOM Node
* ```tsx
* import { TextField, useTextField } from "@react-md/core";
* import { useEffect } from "react";
* import type { ReactElement } from "react";
*
* function Example(): ReactElement {
* const { fieldRef, fieldProps } = useTextField({ name: "example" });
*
* useEffect(() => {
* fieldRef.current;
* // ^ HTMLInputElement | null
* }, [fieldRef]);
*
* return <TextField {...fieldProps} label="Example" />;
* }
* ```
*/
ref?: Ref<E>;
/**
* A unique name to attach to the `TextField`, `TextArea` or `Password`
* component.
*/
name: string;
/**
* Boolean if the `FormMessage` should also display a counter for the
* remaining letters allowed based on the `maxLength`.
*
* This will still be considered false if the `maxLength` value is not
* provided.
*
* @defaultValue `false`
*/
counter?: boolean;
/**
* This is used internally for the `useNumberField` hook and probably
* shouldn't be used otherwise. This is just passed into the
* {@link getErrorMessage} options and is used for additional validation.
*
* @defaultValue `false`
*/
isNumber?: boolean;
/**
* The default value to use for the `TextField` or `TextArea` one initial
* render. If you want to manually change the value to something else after
* the initial render, either change the `key` for the component containing
* this hook, or use the `setState` function returned from this hook.
*
* @defaultValue `""`
*/
defaultValue?: UseStateInitializer<string>;
/**
* An optional help text to display in the `FormMessage` component when there
* is not an error.
*/
helpText?: ReactNode;
/**
* A function used to determine if the `TextField` or `TextArea` is an in
* errored state.
*
* @see {@link defaultIsErrored}
* @defaultValue `defaultIsErrored`
*/
isErrored?: IsErrored;
/**
* An optional error icon used in the {@link getErrorIcon} option.
*
* @defaultValue `getIcon("error")`
*/
errorIcon?: ReactNode;
/**
* A function used to get the error icon to display at the right of the
* `TextField` or `TextArea`. The default behavior will only show an icon when
* the `error` state is `true` and an `errorIcon` option has been provided.
*
* @see {@link defaultGetErrorIcon}
* @defaultValue `defaultGetErrorIcon`
*/
getErrorIcon?: GetErrorIcon;
/**
* A function to get and display an error message based on the `TextField` or
* `TextArea` validity.
*
* @see {@link defaultGetErrorMessage}
* @defaultValue `defaultGetErrorMessage`
*/
getErrorMessage?: GetErrorMessage;
/**
* An optional function that will be called whenever the `error` state is
* changed. This can be used for more complex forms to `disable` the Submit
* button or anything else if any field has an error.
*
* @defaultValue `() => {}`
*/
onErrorChange?: ErrorChangeHandler<E>;
/**
* Boolean if the `TextField` or `TextArea` will **not** be rendered along
* with a `FormMessage` component. This will prevent the `aria-describedby`
* prop from being returned when set to `true`.
*
* @defaultValue `false`
*/
disableMessage?: boolean;
/**
* Boolean if the `maxLength` prop should not be passed to the `TextField`
* component since it will prevent any additional characters from being
* entered in the text field which might feel like weird behavior to some
* users. This should really only be used when the `counter` option is also
* enabled and rendering along with a `FormMessage` component.
*
* @defaultValue `false`
*/
disableMaxLength?: boolean;
/**
* @defaultValue `"recommended"`
*/
validationType?: TextFieldValidationType;
}
Returns
The return type has the following shape, but changes depending on the provided options.
Just know that it is always save to pass the fieldProps
to the TextField
component.
export interface TextFieldHookState {
/**
* The current value for the `TextField` or `TextArea`.
*/
value: string;
/**
* This will be `true` when the `TextField`/`TextArea` has an error.
*/
error: boolean;
/**
* The error message returned by {@link GetErrorMessage}/the browser's
* validation message. This is normally an empty string when the {@link error}
* state is `false`.
*/
errorMessage: string;
}
export interface TextFieldImplementation<
E extends HTMLInputElement | HTMLTextAreaElement,
> extends TextFieldHookState {
fieldRef: RefObject<E>;
reset: () => void;
setState: UseStateSetter<Readonly<TextFieldHookState>>;
fieldProps: ProvidedTextFieldProps<E>;
}
export interface ProvidedTextFieldProps<
E extends HTMLInputElement | HTMLTextAreaElement,
> extends TextFieldValidationOptions,
TextFieldChangeHandlers<E>,
Required<Pick<TextFieldProps, "id" | "name" | "value" | "error">>,
Pick<TextFieldProps, "aria-describedby" | "rightAddon"> {
/**
* A ref that must be passed to the `TextField`/`TextArea` so that the custom
* validity behavior can work.
*/
ref: RefCallback<E>;
}