Skip to content

Commit

Permalink
feat(form): added a new useTextField hook to validate the TextField…
Browse files Browse the repository at this point in the history
… and TextArea values
  • Loading branch information
mlaursen committed Nov 22, 2020
1 parent 4dfd50a commit 578257c
Show file tree
Hide file tree
Showing 5 changed files with 598 additions and 0 deletions.
25 changes: 25 additions & 0 deletions packages/form/src/text-field/getErrorIcon.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { ReactNode } from "react";

/**
* A function that can be used to dynamically get an error icon based on the
* current visible error.
*
* @param errorMessage The current error message or an empty string
* @param error Boolean if the `TextField` or `TextArea` are considered to be in
* an errored state
* @param errorIcon The current `errorIcon` that was provided to the
* `useTextField` hook.
* @return An icon to render or falsey to render nothing.
*/
export type GetErrorIcon = (
errorMessage: string,
error: boolean,
errorIcon: ReactNode
) => ReactNode;

/**
* The default implementation for showing an error icon in `TextField` and
* `TextArea` components that will only display when the error flag is enabled.
*/
export const defaultGetErrorIcon: GetErrorIcon = (_message, error, errorIcon) =>
error && errorIcon;
122 changes: 122 additions & 0 deletions packages/form/src/text-field/getErrorMessage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import { InputHTMLAttributes } from "react";

export type TextConstraints = Pick<
InputHTMLAttributes<HTMLInputElement>,
"pattern" | "required" | "minLength" | "maxLength"
>;

/**
* Since the default validation messages can be verbose, this type is used to
* configure when/how to display the native browser messages when the validation
* state changes during the `change` event phase. The validation message will
* always be shown on blur.
*
* When this is:
*
* - `true` -> always show the browser message when it exists
* - `false` -> never show the browser message
* - `"recommended"` -> only shows the browser message if it is not one of the
* `RECOMMENDED_STATE_KEYS` validation errors
* - `keyof ValidityState` -> only shows the browser message if it is not the
* specific validation error
* - `(keyof ValidityState)[]` -> only shows the browser message if it is not
* the specific validation errors
*/
export type ChangeValidationBehavior =
| boolean
| "recommended"
| keyof ValidityState
| readonly (keyof ValidityState)[];

export interface ErrorMessageOptions extends TextConstraints {
/**
* The current input or textarea's validity state.
*/
validity: ValidityState;

/**
* The browser defined validation message based on the validity state. This
* will be the empty string when there are no errors.
*/
validationMessage: string;

/**
* The current `TextField` or `TextArea` value.
*/
value: string;

/**
* Boolean if this is triggered from a blur event instead of a change event.
*/
isBlurEvent: boolean;

/**
* The change event validation behavior that is specified in the hook.
*/
validateOnChange: ChangeValidationBehavior;
}

/**
* A function to get a custom error message for specific errors. This is really
* useful when using the `pattern` attribute to give additional information or
* changing the native "language translated" error message.
*
* @param options An object containing metadata that can be used to create an
* error message for your `TextField` or `TextArea`.
* @return An error message to display or an empty string.
*/
export type GetErrorMessage = (options: ErrorMessageOptions) => string;

/** @internal */
const RECOMMENDED_STATE_KEYS: readonly (keyof ValidityState)[] = [
"valueMissing",
"tooShort",
"tooLong",
"badInput",
];

/**
* The default implementation for getting an error message for the `TextField`
* or `TextArea` components that:
*
* - prevents the browser `minLength` and `tooLong` error text from appearing
* during change events since the message is extremely verbose
* - prevents the `valueMissing` and `badInput` error text from appearing during
* change events since it's better to wait for the blur event.
*
* The above behavior is also configured by the {@link ChangeValidationBehavior}.
*/
export const defaultGetErrorMessage: GetErrorMessage = ({
isBlurEvent,
validity,
validationMessage,
validateOnChange,
}) => {
if (isBlurEvent || !validationMessage) {
return validationMessage;
}

if (!validateOnChange) {
return "";
}

let keys = RECOMMENDED_STATE_KEYS;
if (
typeof validateOnChange === "string" &&
validateOnChange !== "recommended"
) {
keys = [validateOnChange];
} else if (Array.isArray(validateOnChange)) {
keys = validateOnChange;
}

if (
Object.entries(validity).some(
([key, value]) => value && !keys.includes(key as keyof ValidityState)
)
) {
return validationMessage;
}

return "";
};
6 changes: 6 additions & 0 deletions packages/form/src/text-field/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,10 @@ export * from "./TextFieldContainer";
export * from "./TextFieldAddon";
export * from "./Password";
export * from "./TextArea";

export * from "./FormMessage";

export * from "./isErrored";
export * from "./getErrorIcon";
export * from "./getErrorMessage";
export * from "./useTextField";
34 changes: 34 additions & 0 deletions packages/form/src/text-field/isErrored.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { ErrorMessageOptions } from "./getErrorMessage";

export interface IsErroredOptions extends ErrorMessageOptions {
/**
* The current error message or an empty string.
*/
errorMessage: string;
}

/**
* A function that is used to determine if a `TextField` or `TextArea` is in an
* errored state.
*
* @param options All the current options that can be used to determine the
* error state.
* @return True if the component is considered to be in an errored state.
*/
export type IsErrored = (options: IsErroredOptions) => boolean;

/**
* The default implementation for checking if a `TextField` or `TextArea` is
* errored by returning `true` if the `errorMessage` string is truthy or the
* value is not within the `minLength` and `maxLength` constraints when they
* exist.
*/
export const defaultIsErrored: IsErrored = ({
value,
errorMessage,
minLength,
maxLength,
}) =>
!!errorMessage ||
(typeof minLength === "number" && value.length < minLength) ||
(typeof maxLength === "number" && value.length > maxLength);
Loading

0 comments on commit 578257c

Please sign in to comment.