diff --git a/docs/data/data-grid/column-definition/column-definition.md b/docs/data/data-grid/column-definition/column-definition.md index 4ec1481a9022e..1e39f4f2c9866 100644 --- a/docs/data/data-grid/column-definition/column-definition.md +++ b/docs/data/data-grid/column-definition/column-definition.md @@ -227,7 +227,29 @@ However, some types require additional properties to be set to make them work co ``` :::warning - When using objects values for `valueOptions` you need to provide `value` and `label` fields for each option: `{ value: string, label: string }` + When using objects values for `valueOptions` you need to provide the `value` and `label` attributes for each option. + However, you can customize which attribute is used as value and label by using `getOptionValue` and `getOptionLabel`, respectively. + + ```tsx + // Without getOptionValue and getOptionLabel + { + valueOptions: [ + { value: 'BR', label: 'Brazil' } + { value: 'FR', label: 'France' } + ] + } + + // With getOptionValue and getOptionLabel + { + getOptionValue: (value: any) => value.code, + getOptionLabel: (value: any) => value.name, + valueOptions: [ + { code: 'BR', name: 'Brazil' } + { code: 'FR', name: 'France' } + ] + } + ``` + ::: - If the column type is `'actions'`, you need to provide a `getActions` function that returns an array of actions available for each row (React elements). diff --git a/docs/pages/x/api/data-grid/grid-single-select-col-def.md b/docs/pages/x/api/data-grid/grid-single-select-col-def.md index 368098516b948..44bd9c064f7f7 100644 --- a/docs/pages/x/api/data-grid/grid-single-select-col-def.md +++ b/docs/pages/x/api/data-grid/grid-single-select-col-def.md @@ -31,6 +31,8 @@ import { GridSingleSelectColDef } from '@mui/x-data-grid'; | filterOperators? | GridFilterOperator<R, V, F>[] | | Allows setting the filter operators for this column. | | flex? | number | | If set, it indicates that a column has fluid width. Range [0, ∞). | | getApplyQuickFilterFn? | (value: any, colDef: GridStateColDef, apiRef: React.MutableRefObject<GridApiCommunity>) => null \| ((params: GridCellParams<R, V, F>) => boolean) | | The callback that generates a filtering function for a given quick filter value.
This function can return `null` to skip filtering for this value and column. | +| getOptionLabel? | (value: ValueOptions) => string | | Used to determine the label displayed for a given value option. | +| getOptionValue? | (value: ValueOptions) => any | | Used to determine the value used for a value option. | | groupable? | boolean | true | If `true`, the rows can be grouped based on this column values (pro-plan only).
Only available in DataGridPremium. | | groupingValueGetter? [](/x/introduction/licensing/#premium-plan) | (params: GridGroupingValueGetterParams<R, V>) => GridKeyValue \| null \| undefined | | Function that transforms a complex cell value into a key that be used for grouping the rows. | | headerAlign? | GridAlignment | | Header cell element alignment. | diff --git a/packages/grid/x-data-grid/src/colDef/gridSingleSelectColDef.tsx b/packages/grid/x-data-grid/src/colDef/gridSingleSelectColDef.tsx index 7fdb4a8defb6c..225659e9a32ca 100644 --- a/packages/grid/x-data-grid/src/colDef/gridSingleSelectColDef.tsx +++ b/packages/grid/x-data-grid/src/colDef/gridSingleSelectColDef.tsx @@ -2,18 +2,26 @@ import { GRID_STRING_COL_DEF } from './gridStringColDef'; import { GridSingleSelectColDef, ValueOptions } from '../models/colDef/gridColDef'; import { renderEditSingleSelectCell } from '../components/cell/GridEditSingleSelectCell'; import { getGridSingleSelectOperators } from './gridSingleSelectOperators'; -import { - getLabelFromValueOption, - isSingleSelectColDef, -} from '../components/panel/filterPanel/filterPanelUtils'; +import { isSingleSelectColDef } from '../components/panel/filterPanel/filterPanelUtils'; +import { isObject } from '../utils/utils'; -const isArrayOfObjects = (options: any): options is Array<{ value: any; label: string }> => { +const isArrayOfObjects = (options: any): options is Array> => { return typeof options[0] === 'object'; }; +const defaultGetOptionValue = (value: ValueOptions) => { + return isObject(value) ? value.value : value; +}; + +const defaultGetOptionLabel = (value: ValueOptions) => { + return isObject(value) ? value.label : String(value); +}; + export const GRID_SINGLE_SELECT_COL_DEF: Omit = { ...GRID_STRING_COL_DEF, type: 'singleSelect', + getOptionLabel: defaultGetOptionLabel, + getOptionValue: defaultGetOptionValue, valueFormatter(params) { const { id, field, value, api } = params; const colDef = params.api.getColumn(field); @@ -38,11 +46,11 @@ export const GRID_SINGLE_SELECT_COL_DEF: Omit = } if (!isArrayOfObjects(valueOptions)) { - return getLabelFromValueOption(value); + return colDef.getOptionLabel!(value); } - const valueOption = valueOptions.find((option) => option.value === value); - return valueOption ? getLabelFromValueOption(valueOption) : ''; + const valueOption = valueOptions.find((option) => colDef.getOptionValue!(option) === value); + return valueOption ? colDef.getOptionLabel!(valueOption) : ''; }, renderEditCell: renderEditSingleSelectCell, filterOperators: getGridSingleSelectOperators(), diff --git a/packages/grid/x-data-grid/src/components/cell/GridEditSingleSelectCell.tsx b/packages/grid/x-data-grid/src/components/cell/GridEditSingleSelectCell.tsx index 93d9076fdfabe..21816d12ecf93 100644 --- a/packages/grid/x-data-grid/src/components/cell/GridEditSingleSelectCell.tsx +++ b/packages/grid/x-data-grid/src/components/cell/GridEditSingleSelectCell.tsx @@ -7,9 +7,8 @@ import { GridRenderEditCellParams } from '../../models/params/gridCellParams'; import { isEscapeKey } from '../../utils/keyboardUtils'; import { useGridRootProps } from '../../hooks/utils/useGridRootProps'; import { GridEditModes } from '../../models/gridEditRowModel'; -import { ValueOptions } from '../../models/colDef/gridColDef'; +import { GridSingleSelectColDef, ValueOptions } from '../../models/colDef/gridColDef'; import { - getLabelFromValueOption, getValueFromValueOptions, isSingleSelectColDef, } from '../panel/filterPanel/filterPanelUtils'; @@ -17,7 +16,8 @@ import { useGridApiContext } from '../../hooks/utils/useGridApiContext'; export interface GridEditSingleSelectCellProps extends GridRenderEditCellParams, - Omit { + Omit, + Pick { /** * Callback called when the value is changed by the user. * @param {SelectChangeEvent} event The event source of the callback. @@ -29,12 +29,6 @@ export interface GridEditSingleSelectCellProps * If true, the select opens by default. */ initialOpen?: boolean; - /** - * Used to determine the text displayed for a given value option. - * @param {ValueOptions} value The current value option. - * @returns {string} The text to be displayed. - */ - getOptionLabel?: (value: ValueOptions) => string; } function isKeyboardEvent(event: any): event is React.KeyboardEvent { @@ -62,7 +56,8 @@ function GridEditSingleSelectCell(props: GridEditSingleSelectCellProps) { error, onValueChange, initialOpen = rootProps.editMode === GridEditModes.Cell, - getOptionLabel = getLabelFromValueOption, + getOptionLabel: getOptionLabelProp, + getOptionValue: getOptionValueProp, ...other } = props; @@ -95,11 +90,22 @@ function GridEditSingleSelectCell(props: GridEditSingleSelectCellProps) { return null; } + const getOptionValue = getOptionValueProp || colDef.getOptionValue!; + const getOptionLabel = getOptionLabelProp || colDef.getOptionLabel!; + const handleChange: SelectProps['onChange'] = async (event) => { + if (!isSingleSelectColDef(colDef) || !valueOptions) { + return; + } + setOpen(false); const target = event.target as HTMLInputElement; // NativeSelect casts the value to a string. - const formattedTargetValue = getValueFromValueOptions(target.value, valueOptions); + const formattedTargetValue = getValueFromValueOptions( + target.value, + valueOptions, + getOptionValue, + ); if (onValueChange) { await onValueChange(event, formattedTargetValue); @@ -127,6 +133,10 @@ function GridEditSingleSelectCell(props: GridEditSingleSelectCellProps) { const OptionComponent = isSelectNative ? 'option' : MenuItem; + if (!valueOptions || !colDef) { + return null; + } + return ( {valueOptions.map((valueOption) => { - const value = typeof valueOption === 'object' ? valueOption.value : valueOption; + const value = getOptionValue(valueOption); return ( @@ -184,11 +194,17 @@ GridEditSingleSelectCell.propTypes = { */ formattedValue: PropTypes.any, /** - * Used to determine the text displayed for a given value option. + * Used to determine the label displayed for a given value option. * @param {ValueOptions} value The current value option. * @returns {string} The text to be displayed. */ getOptionLabel: PropTypes.func, + /** + * Used to determine the value used for a value option. + * @param {ValueOptions} value The current value option. + * @returns {string} The value to be used. + */ + getOptionValue: PropTypes.func, /** * If true, the cell is the active element. */ diff --git a/packages/grid/x-data-grid/src/components/panel/filterPanel/GridFilterInputMultipleSingleSelect.tsx b/packages/grid/x-data-grid/src/components/panel/filterPanel/GridFilterInputMultipleSingleSelect.tsx index 31d967c10139c..786502607f19d 100644 --- a/packages/grid/x-data-grid/src/components/panel/filterPanel/GridFilterInputMultipleSingleSelect.tsx +++ b/packages/grid/x-data-grid/src/components/panel/filterPanel/GridFilterInputMultipleSingleSelect.tsx @@ -3,11 +3,7 @@ import PropTypes from 'prop-types'; import Autocomplete, { AutocompleteProps, createFilterOptions } from '@mui/material/Autocomplete'; import Chip from '@mui/material/Chip'; import { unstable_useId as useId } from '@mui/utils'; -import { - getLabelFromValueOption, - getValueFromOption, - isSingleSelectColDef, -} from './filterPanelUtils'; +import { isSingleSelectColDef } from './filterPanelUtils'; import { useGridRootProps } from '../../../hooks/utils/useGridRootProps'; import { GridFilterInputValueProps } from './GridFilterInputValueProps'; import type { GridSingleSelectColDef, ValueOptions } from '../../../models/colDef/gridColDef'; @@ -26,24 +22,11 @@ export interface GridFilterInputMultipleSingleSelectProps | 'color' | 'getOptionLabel' >, + Pick, GridFilterInputValueProps { type?: 'singleSelect'; - /** - * Used to determine the text displayed for a given value option. - * @param {ValueOptions} value The current value option. - * @returns {string} The text to be displayed. - */ - getOptionLabel?: (value: ValueOptions) => string; } -const isOptionEqualToValue: AutocompleteProps< - ValueOptions, - true, - false, - true ->['isOptionEqualToValue'] = (option, value) => - getValueFromOption(option) === getValueFromOption(value); - const filter = createFilterOptions(); function GridFilterInputMultipleSingleSelect(props: GridFilterInputMultipleSingleSelectProps) { @@ -58,7 +41,8 @@ function GridFilterInputMultipleSingleSelect(props: GridFilterInputMultipleSingl helperText, size, variant = 'standard', - getOptionLabel = getLabelFromValueOption, + getOptionLabel: getOptionLabelProp, + getOptionValue: getOptionValueProp, ...other } = props; const TextFieldProps = { @@ -80,6 +64,14 @@ function GridFilterInputMultipleSingleSelect(props: GridFilterInputMultipleSingl } } + const getOptionValue = getOptionValueProp || resolvedColumn?.getOptionValue!; + const getOptionLabel = getOptionLabelProp || resolvedColumn?.getOptionLabel!; + + const isOptionEqualToValue = React.useCallback( + (option: ValueOptions, value: ValueOptions) => getOptionValue(option) === getOptionValue(value), + [getOptionValue], + ); + const resolvedValueOptions = React.useMemo(() => { if (!resolvedColumn?.valueOptions) { return []; @@ -93,8 +85,8 @@ function GridFilterInputMultipleSingleSelect(props: GridFilterInputMultipleSingl }, [resolvedColumn]); const resolvedFormattedValueOptions = React.useMemo(() => { - return resolvedValueOptions?.map(getValueFromOption); - }, [resolvedValueOptions]); + return resolvedValueOptions?.map(getOptionValue); + }, [resolvedValueOptions, getOptionValue]); // The value is computed from the item.value and used directly // If it was done by a useEffect/useState, the Autocomplete could receive incoherent value and options @@ -104,14 +96,10 @@ function GridFilterInputMultipleSingleSelect(props: GridFilterInputMultipleSingl } if (resolvedValueOptions !== undefined) { const itemValueIndexes = item.value.map((element) => { - // get the index matching between values and valueOptions - const formattedElement = getValueFromOption(element); - const index = - resolvedFormattedValueOptions?.findIndex( - (formatedOption) => formatedOption === formattedElement, - ) || 0; - - return index; + // Gets the index matching between values and valueOptions + return resolvedFormattedValueOptions?.findIndex( + (formatedOption) => formatedOption === element, + ); }); return itemValueIndexes @@ -124,17 +112,17 @@ function GridFilterInputMultipleSingleSelect(props: GridFilterInputMultipleSingl React.useEffect(() => { if (!Array.isArray(item.value) || filteredValues.length !== item.value.length) { // Updates the state if the filter value has been cleaned by the component - applyValue({ ...item, value: filteredValues.map(getValueFromOption) }); + applyValue({ ...item, value: filteredValues.map(getOptionValue) }); } - }, [item, filteredValues, applyValue]); + }, [item, filteredValues, applyValue, getOptionValue]); const handleChange = React.useCallback< NonNullable['onChange']> >( (event, value) => { - applyValue({ ...item, value: [...value.map(getValueFromOption)] }); + applyValue({ ...item, value: value.map(getOptionValue) }); }, - [applyValue, item], + [applyValue, item, getOptionValue], ); return ( @@ -191,11 +179,17 @@ GridFilterInputMultipleSingleSelect.propTypes = { PropTypes.object, ]), /** - * Used to determine the text displayed for a given value option. + * Used to determine the label displayed for a given value option. * @param {ValueOptions} value The current value option. * @returns {string} The text to be displayed. */ getOptionLabel: PropTypes.func, + /** + * Used to determine the value used for a value option. + * @param {ValueOptions} value The current value option. + * @returns {string} The value to be used. + */ + getOptionValue: PropTypes.func, item: PropTypes.shape({ field: PropTypes.string.isRequired, id: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), diff --git a/packages/grid/x-data-grid/src/components/panel/filterPanel/GridFilterInputSingleSelect.tsx b/packages/grid/x-data-grid/src/components/panel/filterPanel/GridFilterInputSingleSelect.tsx index ee393a96297fa..c62a5beec5c67 100644 --- a/packages/grid/x-data-grid/src/components/panel/filterPanel/GridFilterInputSingleSelect.tsx +++ b/packages/grid/x-data-grid/src/components/panel/filterPanel/GridFilterInputSingleSelect.tsx @@ -4,18 +4,15 @@ import { TextFieldProps } from '@mui/material/TextField'; import { unstable_useId as useId } from '@mui/utils'; import MenuItem from '@mui/material/MenuItem'; import { GridFilterInputValueProps } from './GridFilterInputValueProps'; -import { GridSingleSelectColDef, ValueOptions } from '../../../models/colDef/gridColDef'; +import { GridSingleSelectColDef } from '../../../models/colDef/gridColDef'; import { useGridRootProps } from '../../../hooks/utils/useGridRootProps'; -import { - getLabelFromValueOption, - getValueFromValueOptions, - isSingleSelectColDef, -} from './filterPanelUtils'; +import { getValueFromValueOptions, isSingleSelectColDef } from './filterPanelUtils'; const renderSingleSelectOptions = ( { valueOptions, field }: GridSingleSelectColDef, OptionComponent: React.ElementType, - getOptionLabel: (value: ValueOptions) => React.ReactNode, + getOptionLabel: NonNullable, + getOptionValue: NonNullable, ) => { const iterableColumnValues = typeof valueOptions === 'function' @@ -23,9 +20,7 @@ const renderSingleSelectOptions = ( : ['', ...(valueOptions || [])]; return iterableColumnValues.map((option) => { - const isOptionTypeObject = typeof option === 'object'; - - const value = isOptionTypeObject ? option.value : option; + const value = getOptionValue(option); const label = getOptionLabel(option); return ( @@ -37,14 +32,9 @@ const renderSingleSelectOptions = ( }; export type GridFilterInputSingleSelectProps = GridFilterInputValueProps & - TextFieldProps & { + TextFieldProps & + Pick & { type?: 'singleSelect'; - /** - * Used to determine the text displayed for a given value option. - * @param {ValueOptions} value The current value option. - * @returns {string} The text to be displayed. - */ - getOptionLabel?: (value: ValueOptions) => string; }; function GridFilterInputSingleSelect(props: GridFilterInputSingleSelectProps) { @@ -54,7 +44,8 @@ function GridFilterInputSingleSelect(props: GridFilterInputSingleSelectProps) { type, apiRef, focusElementRef, - getOptionLabel = getLabelFromValueOption, + getOptionLabel: getOptionLabelProp, + getOptionValue: getOptionValueProp, ...others } = props; const [filterValueState, setFilterValueState] = React.useState(item.value ?? ''); @@ -72,6 +63,9 @@ function GridFilterInputSingleSelect(props: GridFilterInputSingleSelectProps) { } } + const getOptionValue = getOptionValueProp || resolvedColumn?.getOptionValue!; + const getOptionLabel = getOptionLabelProp || resolvedColumn?.getOptionLabel!; + const currentValueOptions = React.useMemo(() => { if (!resolvedColumn) { return undefined; @@ -86,12 +80,12 @@ function GridFilterInputSingleSelect(props: GridFilterInputSingleSelectProps) { let value = event.target.value; // NativeSelect casts the value to a string. - value = getValueFromValueOptions(value, currentValueOptions); + value = getValueFromValueOptions(value, currentValueOptions, getOptionValue); setFilterValueState(String(value)); applyValue({ ...item, value }); }, - [applyValue, item, currentValueOptions], + [currentValueOptions, getOptionValue, applyValue, item], ); React.useEffect(() => { @@ -99,7 +93,7 @@ function GridFilterInputSingleSelect(props: GridFilterInputSingleSelectProps) { if (currentValueOptions !== undefined) { // sanitize if valueOptions are provided - itemValue = getValueFromValueOptions(item.value, currentValueOptions); + itemValue = getValueFromValueOptions(item.value, currentValueOptions, getOptionValue); if (itemValue !== item.value) { applyValue({ ...item, value: itemValue }); return; @@ -111,7 +105,11 @@ function GridFilterInputSingleSelect(props: GridFilterInputSingleSelectProps) { itemValue = itemValue ?? ''; setFilterValueState(String(itemValue)); - }, [item, currentValueOptions, applyValue]); + }, [item, currentValueOptions, applyValue, getOptionValue]); + + if (!isSingleSelectColDef(resolvedColumn)) { + return null; + } if (!isSingleSelectColDef(resolvedColumn)) { return null; @@ -142,6 +140,7 @@ function GridFilterInputSingleSelect(props: GridFilterInputSingleSelectProps) { resolvedColumn, isSelectNative ? 'option' : MenuItem, getOptionLabel, + getOptionValue, )} ); @@ -161,11 +160,17 @@ GridFilterInputSingleSelect.propTypes = { PropTypes.object, ]), /** - * Used to determine the text displayed for a given value option. + * Used to determine the label displayed for a given value option. * @param {ValueOptions} value The current value option. * @returns {string} The text to be displayed. */ getOptionLabel: PropTypes.func, + /** + * Used to determine the value used for a value option. + * @param {ValueOptions} value The current value option. + * @returns {string} The value to be used. + */ + getOptionValue: PropTypes.func, item: PropTypes.shape({ field: PropTypes.string.isRequired, id: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), diff --git a/packages/grid/x-data-grid/src/components/panel/filterPanel/filterPanelUtils.ts b/packages/grid/x-data-grid/src/components/panel/filterPanel/filterPanelUtils.ts index 29909210a80ad..8a7958af0c8b6 100644 --- a/packages/grid/x-data-grid/src/components/panel/filterPanel/filterPanelUtils.ts +++ b/packages/grid/x-data-grid/src/components/panel/filterPanel/filterPanelUtils.ts @@ -8,22 +8,19 @@ export function isSingleSelectColDef(colDef: GridColDef | null): colDef is GridS return colDef?.type === 'singleSelect'; } -export function getValueFromOption(option: any | undefined) { - if (typeof option === 'object' && option !== null) { - return option.value; - } - return option; -} - -export function getValueFromValueOptions(value: string, valueOptions?: any[]) { +export function getValueFromValueOptions( + value: string, + valueOptions: any[] | undefined, + getOptionValue: NonNullable, +) { if (valueOptions === undefined) { return undefined; } const result = valueOptions.find((option) => { - const optionValue = getValueFromOption(option); + const optionValue = getOptionValue(option); return String(optionValue) === String(value); }); - return getValueFromOption(result); + return getOptionValue(result); } export const getLabelFromValueOption = (valueOption: ValueOptions) => { diff --git a/packages/grid/x-data-grid/src/models/colDef/gridColDef.ts b/packages/grid/x-data-grid/src/models/colDef/gridColDef.ts index 457a618406029..cdea807119b3a 100644 --- a/packages/grid/x-data-grid/src/models/colDef/gridColDef.ts +++ b/packages/grid/x-data-grid/src/models/colDef/gridColDef.ts @@ -25,7 +25,7 @@ import { GridApiCommunity } from '../api/gridApiCommunity'; */ export type GridAlignment = 'left' | 'right' | 'center'; -export type ValueOptions = string | number | { value: any; label: string }; +export type ValueOptions = string | number | { value: any; label: string } | Record; /** * Value that can be used as a key for grouping rows @@ -270,6 +270,18 @@ export interface GridSingleSelectColDef | ((params: GridValueOptionsParams) => Array); + /** + * Used to determine the label displayed for a given value option. + * @param {ValueOptions} value The current value option. + * @returns {string} The text to be displayed. + */ + getOptionLabel?: (value: ValueOptions) => string; + /** + * Used to determine the value used for a value option. + * @param {ValueOptions} value The current value option. + * @returns {string} The value to be used. + */ + getOptionValue?: (value: ValueOptions) => any; } /** diff --git a/packages/grid/x-data-grid/src/tests/DataGrid.spec.tsx b/packages/grid/x-data-grid/src/tests/DataGrid.spec.tsx index 19b4692de014e..8594b30c33d5a 100644 --- a/packages/grid/x-data-grid/src/tests/DataGrid.spec.tsx +++ b/packages/grid/x-data-grid/src/tests/DataGrid.spec.tsx @@ -165,6 +165,26 @@ function SingleSelectColDef() { type: 'singleSelect', valueOptions: ['United Kingdom', 'Spain', 'Brazil'], }, + { + field: 'country', + type: 'singleSelect', + valueOptions: [ + { value: 'UK', label: 'United Kingdom' }, + { value: 'ES', label: 'Spain' }, + { value: 'BR', label: 'Brazil' }, + ], + }, + { + field: 'country', + type: 'singleSelect', + getOptionValue: (value: any) => value.code, + getOptionLabel: (value: any) => value.label, + valueOptions: [ + { code: 'UK', name: 'United Kingdom' }, + { code: 'ES', name: 'Spain' }, + { code: 'BR', name: 'Brazil' }, + ], + }, ]} />