From 266c3274d03e4fd52c692eeef1ee9248dcf66189 Mon Sep 17 00:00:00 2001 From: Hulpoi George-Valentin <7074307+GeorgeHulpoi@users.noreply.github.com> Date: Wed, 8 Nov 2023 14:40:31 -0500 Subject: [PATCH] feat: Custom Error, Label, and before/after field components (#3747) Co-authored-by: Dan Ribbens --- docs/admin/components.mdx | 106 ++++++++++++++++++ .../forms/field-types/Checkbox/Input.tsx | 16 ++- .../forms/field-types/Checkbox/index.tsx | 19 +++- .../forms/field-types/Code/index.tsx | 12 +- .../forms/field-types/DateTime/Input.tsx | 20 +++- .../forms/field-types/DateTime/index.tsx | 13 ++- .../forms/field-types/Email/index.tsx | 36 +++--- .../forms/field-types/JSON/index.tsx | 22 +++- .../forms/field-types/Number/index.tsx | 57 ++++++---- .../forms/field-types/Point/index.tsx | 73 +++++++----- .../forms/field-types/RadioGroup/Input.tsx | 15 ++- .../forms/field-types/RadioGroup/index.tsx | 3 + .../forms/field-types/Relationship/index.tsx | 12 +- .../forms/field-types/Select/Input.tsx | 15 ++- .../forms/field-types/Select/index.tsx | 3 + .../forms/field-types/Text/Input.tsx | 47 +++++--- .../forms/field-types/Text/index.tsx | 16 ++- .../forms/field-types/Textarea/Input.tsx | 21 +++- .../forms/field-types/Textarea/index.tsx | 5 + .../forms/field-types/Upload/Input.tsx | 15 ++- .../forms/field-types/Upload/index.tsx | 12 +- packages/payload/src/fields/config/schema.ts | 80 +++++++++++++ packages/payload/src/fields/config/types.ts | 66 +++++++++++ test/fields/collections/Text/AfterInput.tsx | 9 ++ test/fields/collections/Text/BeforeInput.tsx | 9 ++ test/fields/collections/Text/CustomError.tsx | 15 +++ test/fields/collections/Text/CustomLabel.tsx | 13 +++ test/fields/collections/Text/index.ts | 78 ++++++++----- test/fields/collections/Text/shared.ts | 6 + test/fields/e2e.spec.ts | 70 +++++++++--- test/fields/int.spec.ts | 2 +- test/fields/seed.ts | 10 +- 32 files changed, 729 insertions(+), 167 deletions(-) create mode 100644 test/fields/collections/Text/AfterInput.tsx create mode 100644 test/fields/collections/Text/BeforeInput.tsx create mode 100644 test/fields/collections/Text/CustomError.tsx create mode 100644 test/fields/collections/Text/CustomLabel.tsx create mode 100644 test/fields/collections/Text/shared.ts diff --git a/docs/admin/components.mdx b/docs/admin/components.mdx index 7ebd82f3c7..a1f44664cb 100644 --- a/docs/admin/components.mdx +++ b/docs/admin/components.mdx @@ -432,6 +432,15 @@ All Payload fields support the ability to swap in your own React components. So, | **`Cell`** | Used in the `List` view's table to represent a table-based preview of the data stored in the field. [More](#cell-component) | | **`Field`** | Swap out the field itself within all `Edit` views. [More](#field-component) | +As an alternative to replacing the entire Field component, you may want to keep the majority of the default Field component and only swap components within. This allows you to replace the **`Label`** or **`Error`** within a field component or add additional components inside the field with **`BeforeInput`** or **`AfterInput`**. **`BeforeInput`** and **`AfterInput`** are allowed in any fields that don't contain other fields, except [UI](/docs/fields/ui) and [Rich Text](/docs/fields/rich-text). + +| Component | Description | +| ----------------- | --------------------------------------------------------------------------------------------------------------- | +| **`Label`** | Override the default Label in the Field Component. [More](#label-component) | +| **`Error`** | Override the default Label in the Field Component. [More](#error-component) | +| **`BeforeInput`** | An array of elements that will be added before `input`/`textarea` elements. [More](#afterinput-and-beforeinput) | +| **`AfterInput`** | An array of elements that will be added after `input`/`textarea` elements. [More](#afterinput-and-beforeinput) | + ## Cell Component These are the props that will be passed to your custom Cell to use in your own components. @@ -487,6 +496,103 @@ const CustomTextField: React.FC = ({ path }) => { components, including the useField hook, [click here](/docs/admin/hooks). +## Label Component + +These are the props that will be passed to your custom Label. + +| Property | Description | +| ---------------- | ---------------------------------------------------------------- | +| **`htmlFor`** | Property used to set `for` attribute for label. | +| **`label`** | Label value provided in field, it can be used with i18n. | +| **`required`** | A boolean value that represents if the field is required or not. | + +#### Example + +```tsx +import React from 'react' +import { useTranslation } from 'react-i18next' + +import { getTranslation } from 'payload/utilities/getTranslation' + +type Props = { + htmlFor?: string + label?: Record | false | string + required?: boolean +} + +const CustomLabel: React.FC = (props) => { + const { htmlFor, label, required = false } = props + + const { i18n } = useTranslation() + + if (label) { + return ( + {getTranslation(label, i18n)} + {required && *} + ); + } + + return null +} +``` + +## Error Component + +These are the props that will be passed to your custom Error. + +| Property | Description | +| ---------------- | ------------------------------------------------------------- | +| **`message`** | The error message. | +| **`showError`** | A boolean value that represents if the error should be shown. | + +#### Example + +```tsx +import React from 'react' + +type Props = { + message: string + showError?: boolean +} + +const CustomError: React.FC = (props) => { + const { message, showError } = props + + if (showError) { + return

{message}

+ } else return null; +} +``` + +## AfterInput and BeforeInput + +With these properties you can add multiple components before and after the input element. For example, you can add an absolutely positioned button to clear the current field value. + +#### Example + +```tsx +import React from 'react' +import './style.scss' + +const ClearButton: React.FC = () => { + return +} + +const fieldField: Field = { + name: 'title', + type: 'text', + admin: { + components: { + AfterInput: [ + + ] + } + } +} + +export default titleField; +``` + ## Custom providers As your admin customizations gets more complex you may want to share state between fields or other components. You can add custom providers to do add your own context to any Payload app for use in other custom components within the admin panel. Within your config add `admin.components.providers`, these can be used to share context or provide other custom functionality. Read the [React context](https://reactjs.org/docs/context.html) docs to learn more. diff --git a/packages/payload/src/admin/components/forms/field-types/Checkbox/Input.tsx b/packages/payload/src/admin/components/forms/field-types/Checkbox/Input.tsx index 38a9f35695..d4cc638d44 100644 --- a/packages/payload/src/admin/components/forms/field-types/Checkbox/Input.tsx +++ b/packages/payload/src/admin/components/forms/field-types/Checkbox/Input.tsx @@ -1,13 +1,18 @@ import React from 'react' +import type { Props as LabelProps } from '../../Label/types' + import Check from '../../../icons/Check' import Line from '../../../icons/Line' -import Label from '../../Label' +import DefaultLabel from '../../Label' import './index.scss' const baseClass = 'checkbox-input' type CheckboxInputProps = { + AfterInput?: React.ReactElement[] + BeforeInput?: React.ReactElement[] + Label?: React.ComponentType 'aria-label'?: string checked?: boolean className?: string @@ -25,6 +30,9 @@ export const CheckboxInput: React.FC = (props) => { const { id, name, + AfterInput, + BeforeInput, + Label, 'aria-label': ariaLabel, checked, className, @@ -36,6 +44,8 @@ export const CheckboxInput: React.FC = (props) => { required, } = props + const LabelComp = Label || DefaultLabel + return (
= (props) => { .join(' ')} >
+ {BeforeInput} = (props) => { ref={inputRef} type="checkbox" /> + {AfterInput} {!partialChecked && } {partialChecked && }
- {label &&
) } diff --git a/packages/payload/src/admin/components/forms/field-types/Checkbox/index.tsx b/packages/payload/src/admin/components/forms/field-types/Checkbox/index.tsx index 670475ecfc..9b3507d2fe 100644 --- a/packages/payload/src/admin/components/forms/field-types/Checkbox/index.tsx +++ b/packages/payload/src/admin/components/forms/field-types/Checkbox/index.tsx @@ -5,7 +5,7 @@ import type { Props } from './types' import { checkbox } from '../../../../../fields/validations' import { getTranslation } from '../../../../../utilities/getTranslation' -import Error from '../../Error' +import DefaultError from '../../Error' import FieldDescription from '../../FieldDescription' import useField from '../../useField' import withCondition from '../../withCondition' @@ -18,7 +18,15 @@ const baseClass = 'checkbox' const Checkbox: React.FC = (props) => { const { name, - admin: { className, condition, description, readOnly, style, width } = {}, + admin: { + className, + condition, + description, + readOnly, + style, + width, + components: { Error, Label, BeforeInput, AfterInput } = {}, + } = {}, disableFormData, label, onChange, @@ -27,6 +35,8 @@ const Checkbox: React.FC = (props) => { validate = checkbox, } = props + const ErrorComp = Error || DefaultError + const { i18n } = useTranslation() const path = pathFromProps || name @@ -72,7 +82,7 @@ const Checkbox: React.FC = (props) => { }} >
- +
= (props) => { name={path} onToggle={onToggle} readOnly={readOnly} + Label={Label} + BeforeInput={BeforeInput} + AfterInput={AfterInput} required={required} /> diff --git a/packages/payload/src/admin/components/forms/field-types/Code/index.tsx b/packages/payload/src/admin/components/forms/field-types/Code/index.tsx index e1d484eb52..9dbef2c80f 100644 --- a/packages/payload/src/admin/components/forms/field-types/Code/index.tsx +++ b/packages/payload/src/admin/components/forms/field-types/Code/index.tsx @@ -4,9 +4,9 @@ import type { Props } from './types' import { code } from '../../../../../fields/validations' import { CodeEditor } from '../../../elements/CodeEditor' -import Error from '../../Error' +import DefaultError from '../../Error' import FieldDescription from '../../FieldDescription' -import Label from '../../Label' +import DefaultLabel from '../../Label' import useField from '../../useField' import withCondition from '../../withCondition' import './index.scss' @@ -31,6 +31,7 @@ const Code: React.FC = (props) => { readOnly, style, width, + components: { Error, Label } = {}, } = {}, label, path: pathFromProps, @@ -38,6 +39,9 @@ const Code: React.FC = (props) => { validate = code, } = props + const ErrorComp = Error || DefaultError + const LabelComp = Label || DefaultLabel + const path = pathFromProps || name const memoizedValidate = useCallback( @@ -69,8 +73,8 @@ const Code: React.FC = (props) => { width, }} > - -