Skip to content

Commit

Permalink
[DataGrid] Allow to customize label and value for singleSelect (#7684)
Browse files Browse the repository at this point in the history
  • Loading branch information
m4theushw authored Feb 16, 2023
1 parent 948707f commit c72cf1a
Show file tree
Hide file tree
Showing 9 changed files with 167 additions and 91 deletions.
24 changes: 23 additions & 1 deletion docs/data/data-grid/column-definition/column-definition.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down
2 changes: 2 additions & 0 deletions docs/pages/x/api/data-grid/grid-single-select-col-def.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ import { GridSingleSelectColDef } from '@mui/x-data-grid';
| <span class="prop-name optional">filterOperators<sup><abbr title="optional">?</abbr></sup></span> | <span class="prop-type">GridFilterOperator&lt;R, V, F&gt;[]</span> | | Allows setting the filter operators for this column. |
| <span class="prop-name optional">flex<sup><abbr title="optional">?</abbr></sup></span> | <span class="prop-type">number</span> | | If set, it indicates that a column has fluid width. Range [0, ∞). |
| <span class="prop-name optional">getApplyQuickFilterFn<sup><abbr title="optional">?</abbr></sup></span> | <span class="prop-type">(value: any, colDef: GridStateColDef, apiRef: React.MutableRefObject&lt;GridApiCommunity&gt;) =&gt; null \| ((params: GridCellParams&lt;R, V, F&gt;) =&gt; boolean)</span> | | The callback that generates a filtering function for a given quick filter value.<br />This function can return `null` to skip filtering for this value and column. |
| <span class="prop-name optional">getOptionLabel<sup><abbr title="optional">?</abbr></sup></span> | <span class="prop-type">(value: ValueOptions) =&gt; string</span> | | Used to determine the label displayed for a given value option. |
| <span class="prop-name optional">getOptionValue<sup><abbr title="optional">?</abbr></sup></span> | <span class="prop-type">(value: ValueOptions) =&gt; any</span> | | Used to determine the value used for a value option. |
| <span class="prop-name optional">groupable<sup><abbr title="optional">?</abbr></sup></span> | <span class="prop-type">boolean</span> | <span class="prop-default">true</span> | If `true`, the rows can be grouped based on this column values (pro-plan only).<br />Only available in DataGridPremium. |
| <span class="prop-name optional">groupingValueGetter<sup><abbr title="optional">?</abbr></sup> [<span class="plan-premium" title="Premium plan"></span>](/x/introduction/licensing/#premium-plan)</span> | <span class="prop-type">(params: GridGroupingValueGetterParams&lt;R, V&gt;) =&gt; GridKeyValue \| null \| undefined</span> | | Function that transforms a complex cell value into a key that be used for grouping the rows. |
| <span class="prop-name optional">headerAlign<sup><abbr title="optional">?</abbr></sup></span> | <span class="prop-type">GridAlignment</span> | | Header cell element alignment. |
Expand Down
24 changes: 16 additions & 8 deletions packages/grid/x-data-grid/src/colDef/gridSingleSelectColDef.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<Record<string, any>> => {
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<GridSingleSelectColDef, 'field'> = {
...GRID_STRING_COL_DEF,
type: 'singleSelect',
getOptionLabel: defaultGetOptionLabel,
getOptionValue: defaultGetOptionValue,
valueFormatter(params) {
const { id, field, value, api } = params;
const colDef = params.api.getColumn(field);
Expand All @@ -38,11 +46,11 @@ export const GRID_SINGLE_SELECT_COL_DEF: Omit<GridSingleSelectColDef, 'field'> =
}

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(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,17 @@ 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';
import { useGridApiContext } from '../../hooks/utils/useGridApiContext';

export interface GridEditSingleSelectCellProps
extends GridRenderEditCellParams,
Omit<SelectProps, 'id' | 'tabIndex' | 'value'> {
Omit<SelectProps, 'id' | 'tabIndex' | 'value'>,
Pick<GridSingleSelectColDef, 'getOptionLabel' | 'getOptionValue'> {
/**
* Callback called when the value is changed by the user.
* @param {SelectChangeEvent<any>} event The event source of the callback.
Expand All @@ -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 {
Expand Down Expand Up @@ -62,7 +56,8 @@ function GridEditSingleSelectCell(props: GridEditSingleSelectCellProps) {
error,
onValueChange,
initialOpen = rootProps.editMode === GridEditModes.Cell,
getOptionLabel = getLabelFromValueOption,
getOptionLabel: getOptionLabelProp,
getOptionValue: getOptionValueProp,
...other
} = props;

Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -127,6 +133,10 @@ function GridEditSingleSelectCell(props: GridEditSingleSelectCellProps) {

const OptionComponent = isSelectNative ? 'option' : MenuItem;

if (!valueOptions || !colDef) {
return null;
}

return (
<rootProps.components.BaseSelect
ref={ref}
Expand All @@ -145,7 +155,7 @@ function GridEditSingleSelectCell(props: GridEditSingleSelectCellProps) {
{...rootProps.componentsProps?.baseSelect}
>
{valueOptions.map((valueOption) => {
const value = typeof valueOption === 'object' ? valueOption.value : valueOption;
const value = getOptionValue(valueOption);

return (
<OptionComponent key={value} value={value}>
Expand Down Expand Up @@ -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.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -26,24 +22,11 @@ export interface GridFilterInputMultipleSingleSelectProps
| 'color'
| 'getOptionLabel'
>,
Pick<GridSingleSelectColDef, 'getOptionLabel' | 'getOptionValue'>,
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<any>();

function GridFilterInputMultipleSingleSelect(props: GridFilterInputMultipleSingleSelectProps) {
Expand All @@ -58,7 +41,8 @@ function GridFilterInputMultipleSingleSelect(props: GridFilterInputMultipleSingl
helperText,
size,
variant = 'standard',
getOptionLabel = getLabelFromValueOption,
getOptionLabel: getOptionLabelProp,
getOptionValue: getOptionValueProp,
...other
} = props;
const TextFieldProps = {
Expand All @@ -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 [];
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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<AutocompleteProps<ValueOptions, true, false, true>['onChange']>
>(
(event, value) => {
applyValue({ ...item, value: [...value.map(getValueFromOption)] });
applyValue({ ...item, value: value.map(getOptionValue) });
},
[applyValue, item],
[applyValue, item, getOptionValue],
);

return (
Expand Down Expand Up @@ -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]),
Expand Down
Loading

0 comments on commit c72cf1a

Please sign in to comment.