From 4db8de357faf93449122d9feed1652f627c7938d Mon Sep 17 00:00:00 2001 From: ghadabezine <114864513+ghadabezine@users.noreply.github.com> Date: Mon, 12 May 2025 23:54:53 +0100 Subject: [PATCH 1/2] feat: add emptyText prop to SelectArrayInput component --- .../src/input/SelectArrayInput.tsx | 37 ++++++++++++------- 1 file changed, 23 insertions(+), 14 deletions(-) diff --git a/packages/ra-ui-materialui/src/input/SelectArrayInput.tsx b/packages/ra-ui-materialui/src/input/SelectArrayInput.tsx index cdcf06a7f0b..a3bc3203fb7 100644 --- a/packages/ra-ui-materialui/src/input/SelectArrayInput.tsx +++ b/packages/ra-ui-materialui/src/input/SelectArrayInput.tsx @@ -125,6 +125,7 @@ export const SelectArrayInput = (inProps: SelectArrayInputProps) => { variant, disabled, readOnly, + emptyText, ...rest } = props; @@ -332,21 +333,28 @@ export const SelectArrayInput = (inProps: SelectArrayInputProps) => { renderValue={(selected: any[]) => (
{(Array.isArray(selected) ? selected : []) - .map(item => - (allChoices || []).find( - // eslint-disable-next-line eqeqeq - choice => getChoiceValue(choice) == item + .length === 0 && emptyText ? ( + {emptyText} + ) : ( + (Array.isArray(selected) ? selected : []) + .map(item => + (allChoices || []).find( + choice => + getChoiceValue(choice) == item + ) ) - ) - .filter(item => !!item) - .map(item => ( - - ))} + .filter(item => !!item) + .map(item => ( + + )) + )}
)} disabled={disabled || readOnly} @@ -382,6 +390,7 @@ export type SelectArrayInputProps = ChoicesProps & options?: SelectProps; disableValue?: string; source?: string; + emptyText?: string | React.ReactElement; onChange?: (event: ChangeEvent | RaRecord) => void; }; From 950d37f2c0478267ee2be1eee7e16becc329b76a Mon Sep 17 00:00:00 2001 From: ghadabezine <114864513+ghadabezine@users.noreply.github.com> Date: Mon, 12 May 2025 23:57:55 +0100 Subject: [PATCH 2/2] docs: add documentation for custom field components with defaultProps --- docs/CustomFields.md | 182 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 182 insertions(+) create mode 100644 docs/CustomFields.md diff --git a/docs/CustomFields.md b/docs/CustomFields.md new file mode 100644 index 00000000000..5fdb562edce --- /dev/null +++ b/docs/CustomFields.md @@ -0,0 +1,182 @@ +# Custom Fields + +This document explains how to create custom field components in react-admin that integrate seamlessly with the rest of the framework. + +## Basic Structure + +A custom field component should: + +1. Extend the `FieldProps` interface +2. Use the `useFieldValue` hook to get the field value +3. Handle empty values appropriately +4. Support translation +5. Support the common field props (label, source, etc.) + +Here's a basic example: + +```tsx +import * as React from 'react'; +import { Typography } from '@mui/material'; +import { useFieldValue, useTranslate, FieldProps } from 'react-admin'; +import { sanitizeFieldRestProps } from './sanitizeFieldRestProps'; + +export const CustomField = = Record>( + props: CustomFieldProps +) => { + const { className, emptyText, ...rest } = props; + const value = useFieldValue(props); + const translate = useTranslate(); + + if (value == null) { + return emptyText ? ( + + {translate(emptyText, { _: emptyText })} + + ) : null; + } + + return ( + + {value} + + ); +}; + +export interface CustomFieldProps< + RecordType extends Record = Record +> extends FieldProps { + // Add any custom props here +} +``` + +## Using defaultProps for Labels + +When creating a custom field component, it's recommended to use `defaultProps` to set default values for the `label` prop. This ensures consistent behavior across your application and makes the component more reusable. + +Here's how to implement it: + +```tsx +import * as React from 'react'; +import { Typography } from '@mui/material'; +import { useFieldValue, useTranslate, FieldProps } from 'react-admin'; +import { sanitizeFieldRestProps } from './sanitizeFieldRestProps'; + +export const CustomField = = Record>( + props: CustomFieldProps +) => { + // ... component implementation +}; + +CustomField.defaultProps = { + label: 'Custom Field', // Default label + addLabel: true, // Show label by default +}; + +export interface CustomFieldProps< + RecordType extends Record = Record +> extends FieldProps { + // Add any custom props here +} +``` + +The benefits of using `defaultProps` for labels include: + +1. **Consistency**: All instances of your field will have a default label if none is provided +2. **Type Safety**: TypeScript will know about the default values +3. **Documentation**: It makes it clear what the default behavior is +4. **Maintainability**: You can change the default value in one place + +## Best Practices + +1. **Always extend FieldProps**: This ensures your component supports all standard field props like `source`, `label`, `emptyText`, etc. + +2. **Use defaultProps for common values**: Set sensible defaults for `label` and other commonly used props. + +3. **Handle empty values**: Always check for null/undefined values and use the `emptyText` prop appropriately. + +4. **Support translation**: Use the `useTranslate` hook for all user-facing strings, including the default label. + +5. **Use sanitizeFieldRestProps**: This utility function removes field-specific props before passing them to the underlying component. + +6. **Type your props properly**: Use generics to ensure type safety with your record type. + +## Example with All Best Practices + +```tsx +import * as React from 'react'; +import { Typography } from '@mui/material'; +import { useFieldValue, useTranslate, FieldProps } from 'react-admin'; +import { sanitizeFieldRestProps } from './sanitizeFieldRestProps'; + +export const CustomField = = Record>( + props: CustomFieldProps +) => { + const { className, emptyText, ...rest } = props; + const value = useFieldValue(props); + const translate = useTranslate(); + + if (value == null) { + return emptyText ? ( + + {translate(emptyText, { _: emptyText })} + + ) : null; + } + + return ( + + {translate(value.toString())} + + ); +}; + +CustomField.defaultProps = { + label: 'custom.field', // Translation key for default label + addLabel: true, + emptyText: 'custom.field.empty', // Translation key for empty state +}; + +export interface CustomFieldProps< + RecordType extends Record = Record +> extends FieldProps { + // Add any custom props here +} +``` + +## Usage + +```tsx +// Basic usage with default label + + +// Override default label + + +// Disable label + + +// Custom empty text + +``` + +Remember to add translations for your default labels and empty text messages in your translation files. \ No newline at end of file