Skip to content

Commit

Permalink
Merge pull request #7215 from marmelab/avoid-cloning-optionText
Browse files Browse the repository at this point in the history
Avoid cloning element for optionText
  • Loading branch information
fzaninotto authored Feb 10, 2022
2 parents 83ec0da + 1ac9619 commit 66197e9
Show file tree
Hide file tree
Showing 18 changed files with 136 additions and 84 deletions.
21 changes: 21 additions & 0 deletions UPGRADE.md
Original file line number Diff line number Diff line change
Expand Up @@ -1413,6 +1413,27 @@ const PostShow = () => (
};
```

## `<AutocompleteInput>`, `<AutocompleteArrayInput>`, `<SelectInput>`, `<SelectArrayInput>`, `<CheckboxGroupInput>` and `<RadioButtonGroupInput>` No Longer Inject Props To React Elements Passed as `optionText`

To access the record, you can use the `useRecordContext` hook.

```diff
const choices = [
{ id: 123, first_name: 'Leo', last_name: 'Tolstoi' },
{ id: 456, first_name: 'Jane', last_name: 'Austen' },
];

-const FullNameField = ({ record }) => {
+const FullNameField = () => {
+ const record = useRecordContext();
return <span>{record.first_name} {record.last_name}</span>;
}

const AuthorsInput = () => {
<RadioButtonGroupInput source="authors" choices={choices} optionText={<FullNameField />}/>
}
```

## No More Props Injection In `<Title>`

`<Title>` no longer clones the `title` prop and injects it to the `record`. Call the `useRecordContext` hook to get the current record.
Expand Down
8 changes: 5 additions & 3 deletions examples/simple/src/comments/CommentEdit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {
useCreateSuggestionContext,
useCreate,
useCreatePath,
useRecordContext,
} from 'react-admin';

const LinkToRelatedPost = ({ record }: { record?: RaRecord }) => {
Expand All @@ -46,11 +47,12 @@ const LinkToRelatedPost = ({ record }: { record?: RaRecord }) => {
);
};

const OptionRenderer = ({ record, ...rest }: { record?: RaRecord }) => {
const OptionRenderer = (props: any) => {
const record = useRecordContext();
return record.id === '@@ra-create' ? (
<div {...rest}>{record.name}</div>
<div {...props}>{record.name}</div>
) : (
<div {...rest}>
<div {...props}>
{record?.title} - {record?.id}
</div>
);
Expand Down
8 changes: 5 additions & 3 deletions packages/ra-core/src/form/useChoices.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { render, screen } from '@testing-library/react';

import { useChoices } from './useChoices';
import { TestTranslationProvider } from '../i18n';
import { useRecordContext } from '../controller';

describe('useChoices hook', () => {
const defaultProps = {
Expand Down Expand Up @@ -56,9 +57,10 @@ describe('useChoices hook', () => {
});

it('should use optionText with an element value as text identifier', () => {
const Foobar = ({ record }: { record?: any }) => (
<span>{record.foobar}</span>
);
const Foobar = () => {
const record = useRecordContext();
return <span>{record.foobar}</span>;
};
render(
<Component
{...defaultProps}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { ReactElement, isValidElement, cloneElement, useCallback } from 'react';
import * as React from 'react';
import { ReactElement, isValidElement, useCallback } from 'react';
import get from 'lodash/get';

import { useTranslate } from '../i18n';
import { RaRecord } from '../types';
import { RecordContextProvider } from '../controller';

export type OptionTextElement = ReactElement<{
record: RaRecord;
Expand Down Expand Up @@ -48,9 +50,11 @@ export const useChoices = ({
const getChoiceText = useCallback(
choice => {
if (isValidElement<{ record: any }>(optionText)) {
return cloneElement<{ record: any }>(optionText, {
record: choice,
});
return (
<RecordContextProvider value={choice}>
{optionText}
</RecordContextProvider>
);
}
const choiceName =
typeof optionText === 'function'
Expand Down
12 changes: 6 additions & 6 deletions packages/ra-ui-materialui/src/field/SelectField.spec.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import * as React from 'react';
import { FC } from 'react';
import expect from 'expect';
import { render, screen } from '@testing-library/react';
import {
RaRecord,
TestTranslationProvider,
RecordContextProvider,
TestTranslationProvider,
useRecordContext,
} from 'ra-core';

import { SelectField } from './SelectField';
Expand Down Expand Up @@ -123,9 +122,10 @@ describe('<SelectField />', () => {
});

it('should use optionText with an element value as text identifier', () => {
const Foobar: FC<{ record?: RaRecord }> = ({ record }) => (
<span>{record.foobar}</span>
);
const Foobar = () => {
const record = useRecordContext();
return <span>{record.foobar}</span>;
};
render(
<SelectField
{...defaultProps}
Expand Down
4 changes: 2 additions & 2 deletions packages/ra-ui-materialui/src/field/SelectField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,8 @@ import { PublicFieldProps, InjectedFieldProps, fieldPropTypes } from './types';
* const optionRenderer = choice => `${choice.first_name} ${choice.last_name}`;
* <SelectField source="author_id" choices={choices} optionText={optionRenderer} />
*
* `optionText` also accepts a React Element, that will be cloned and receive
* the related choice as the `record` prop. You can use Field components there.
* `optionText` also accepts a React Element, that can access
* the related choice through the `useRecordContext` hook. You can use Field components there.
* @example
* const choices = [
* { id: 123, first_name: 'Leo', last_name: 'Tolstoi' },
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import * as React from 'react';
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { testDataProvider, TestTranslationProvider } from 'ra-core';
import {
testDataProvider,
TestTranslationProvider,
useRecordContext,
} from 'ra-core';

import { AdminContext } from '../AdminContext';
import { SimpleForm } from '../form';
Expand Down Expand Up @@ -478,9 +482,10 @@ describe('<AutocompleteArrayInput />', () => {
});

it('should allow customized rendering of suggesting item', () => {
const SuggestionItem = ({ record }: { record?: any }) => (
<div aria-label={record.name} />
);
const SuggestionItem = props => {
const record = useRecordContext();
return <div {...props} aria-label={record && record.name} />;
};

render(
<AdminContext dataProvider={testDataProvider()}>
Expand Down
16 changes: 10 additions & 6 deletions packages/ra-ui-materialui/src/input/AutocompleteArrayInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,19 +35,23 @@ import { AutocompleteInput, AutocompleteInputProps } from './AutocompleteInput';
* const optionRenderer = choice => `${choice.first_name} ${choice.last_name}`;
* <AutocompleteArrayInput source="author_id" choices={choices} optionText={optionRenderer} />
*
* `optionText` also accepts a React Element, that will be cloned and receive
* the related choice as the `record` prop. You can use Field components there.
* Note that you must also specify the `matchSuggestion` prop
* `optionText` also accepts a React Element, that can access
* the related choice through the `useRecordContext` hook. You can use Field components there.
* Note that you must also specify the `matchSuggestion` and `inputText` props
* @example
* const choices = [
* { id: 123, first_name: 'Leo', last_name: 'Tolstoi' },
* { id: 456, first_name: 'Jane', last_name: 'Austen' },
* ];
* const matchSuggestion = (filterValue, choice) => choice.first_name.match(filterValue) || choice.last_name.match(filterValue);
* const matchSuggestion = (filterValue, choice) => choice.first_name.match(filterValue) || choice.last_name.match(filterValue)
* const inputText = (record) => `${record.fullName} (${record.language})`;
* const FullNameField = ({ record }) => <span>{record.first_name} {record.last_name}</span>;
*
* <SelectInput source="gender" choices={choices} optionText={<FullNameField />} matchSuggestion={matchSuggestion} />
* const FullNameField = () => {
* const record = useRecordContext();
* return <span>{record.first_name} {record.last_name}</span>;
* }
*
* <AutocompleteArrayInput source="gender" choices={choices} optionText={<FullNameField />} matchSuggestion={matchSuggestion} />
*
* The choices are translated by default, so you can use translation identifiers as choices:
* @example
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
FormDataConsumer,
testDataProvider,
TestTranslationProvider,
useRecordContext,
} from 'ra-core';
import { AdminContext } from '../AdminContext';
import { SimpleForm } from '../form';
Expand Down Expand Up @@ -433,9 +434,10 @@ describe('<AutocompleteInput />', () => {
});

it('should allow customized rendering of suggesting item', () => {
const SuggestionItem = ({ record, ...rest }: { record?: any }) => (
<div {...rest} aria-label={record && record.name} />
);
const SuggestionItem = props => {
const record = useRecordContext();
return <div {...props} aria-label={record && record.name} />;
};

render(
<AdminContext dataProvider={testDataProvider()}>
Expand Down
25 changes: 10 additions & 15 deletions packages/ra-ui-materialui/src/input/AutocompleteInput.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import * as React from 'react';
import {
cloneElement,
isValidElement,
useCallback,
useEffect,
Expand Down Expand Up @@ -72,8 +71,8 @@ import { sanitizeInputRestProps } from './sanitizeInputRestProps';
* const optionRenderer = choice => `${choice.first_name} ${choice.last_name}`;
* <AutocompleteInput source="author_id" choices={choices} optionText={optionRenderer} />
*
* `optionText` also accepts a React Element, that will be cloned and receive
* the related choice as the `record` prop. You can use Field components there.
* `optionText` also accepts a React Element, that can access
* the related choice through the `useRecordContext` hook. You can use Field components there.
* Note that you must also specify the `matchSuggestion` and `inputText` props
* @example
* const choices = [
Expand All @@ -83,8 +82,11 @@ import { sanitizeInputRestProps } from './sanitizeInputRestProps';
* const matchSuggestion = (filterValue, choice) => choice.first_name.match(filterValue) || choice.last_name.match(filterValue)
* const inputText = (record) => `${record.fullName} (${record.language})`;
*
* const FullNameField = ({ record }) => <span>{record.first_name} {record.last_name}</span>;
* <SelectInput source="gender" choices={choices} optionText={<FullNameField />} matchSuggestion={matchSuggestion} inputText={inputText} />
* const FullNameField = () => {
* const record = useRecordContext();
* return <span>{record.first_name} {record.last_name}</span>;
* }
* <AutocompleteInput source="author" choices={choices} optionText={<FullNameField />} matchSuggestion={matchSuggestion} inputText={inputText} />
*
* The choices are translated by default, so you can use translation identifiers as choices:
* @example
Expand Down Expand Up @@ -496,16 +498,9 @@ If you provided a React element for the optionText prop, you must also provide t
onChange={handleAutocompleteChange}
onBlur={field.onBlur}
onInputChange={handleInputChange}
renderOption={(props, record) => {
if (isValidElement(optionText)) {
return cloneElement(optionText, {
record: record as RaRecord,
...props,
});
}

return <li {...props}>{getChoiceText(record)}</li>;
}}
renderOption={(props, record) => (
<li {...props}>{getChoiceText(record)}</li>
)}
/>
{createElement}
</Root>
Expand Down
15 changes: 10 additions & 5 deletions packages/ra-ui-materialui/src/input/CheckboxGroupInput.spec.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import * as React from 'react';
import expect from 'expect';
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
import { testDataProvider, TestTranslationProvider } from 'ra-core';
import {
testDataProvider,
TestTranslationProvider,
useRecordContext,
} from 'ra-core';

import { AdminContext } from '../AdminContext';
import { SimpleForm } from '../form';
Expand Down Expand Up @@ -138,15 +142,16 @@ describe('<CheckboxGroupInput />', () => {
});

it('should use optionText with an element value as text identifier', () => {
const Foobar = ({ record }) => (
<span data-testid="label">{record.foobar}</span>
);
const Foobar = () => {
const record = useRecordContext();
return <span data-testid="label">{record.foobar}</span>;
};
render(
<AdminContext dataProvider={testDataProvider()}>
<SimpleForm onSubmit={jest.fn}>
<CheckboxGroupInput
{...defaultProps}
optionText={<Foobar record={{}} />}
optionText={<Foobar />}
choices={[{ id: 'foo', foobar: 'Bar' }]}
/>
</SimpleForm>
Expand Down
16 changes: 4 additions & 12 deletions packages/ra-ui-materialui/src/input/CheckboxGroupInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,8 @@ import { LinearProgress } from '../layout';
* const optionRenderer = choice => `${choice.first_name} ${choice.last_name}`;
* <CheckboxGroupInput source="recipients" choices={choices} optionText={optionRenderer} />
*
* `optionText` also accepts a React Element, that will be cloned and receive
* the related choice as the `record` prop. You can use Field components there.
* `optionText` also accepts a React Element, that can access
* the related choice through the `useRecordContext` hook. You can use Field components there.
* @example
* const choices = [
* { id: 123, first_name: 'Leo', last_name: 'Tolstoi' },
Expand All @@ -81,7 +81,7 @@ import { LinearProgress } from '../layout';
* However, in some cases (e.g. inside a `<ReferenceArrayInput>`), you may not want
* the choice to be translated. In that case, set the `translateChoice` prop to false.
* @example
* <CheckboxGroupInput source="gender" choices={choices} translateChoice={false}/>
* <CheckboxGroupInput source="tags" choices={choices} translateChoice={false}/>
*
* The object passed as `options` props is passed to the material-ui <Checkbox> components
*/
Expand Down Expand Up @@ -236,15 +236,7 @@ const sanitizeRestProps = ({
}: any) => sanitizeInputRestProps(rest);

CheckboxGroupInput.propTypes = {
// @ts-ignore
choices: PropTypes.arrayOf(
PropTypes.shape({
id: PropTypes.oneOfType([
PropTypes.string.isRequired,
PropTypes.number.isRequired,
]).isRequired,
})
),
choices: PropTypes.arrayOf(PropTypes.any),
className: PropTypes.string,
source: PropTypes.string,
optionText: PropTypes.oneOfType([
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import * as React from 'react';
import expect from 'expect';
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
import { testDataProvider, TestTranslationProvider } from 'ra-core';
import {
testDataProvider,
TestTranslationProvider,
useRecordContext,
} from 'ra-core';

import { AdminContext } from '../AdminContext';
import { SimpleForm } from '../form';
Expand Down Expand Up @@ -238,9 +242,10 @@ describe('<RadioButtonGroupInput />', () => {
});

it('should use optionText with an element value as text identifier', () => {
const Foobar = ({ record }: { record?: any }) => (
<span>{record.longname}</span>
);
const Foobar = () => {
const record = useRecordContext();
return <span data-testid="label">{record.longname}</span>;
};
render(
<AdminContext dataProvider={testDataProvider()}>
<SimpleForm defaultValues={{ type: 'mc' }} onSubmit={jest.fn()}>
Expand Down
6 changes: 3 additions & 3 deletions packages/ra-ui-materialui/src/input/RadioButtonGroupInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,15 +58,15 @@ import { LinearProgress } from '../layout';
* const optionRenderer = choice => `${choice.first_name} ${choice.last_name}`;
* <CheckboxGroupInput source="recipients" choices={choices} optionText={optionRenderer} />
*
* `optionText` also accepts a React Element, that will be cloned and receive
* the related choice as the `record` prop. You can use Field components there.
* `optionText` also accepts a React Element, that can access
* the related choice through the `useRecordContext` hook. You can use Field components there.
* @example
* const choices = [
* { id: 123, first_name: 'Leo', last_name: 'Tolstoi' },
* { id: 456, first_name: 'Jane', last_name: 'Austen' },
* ];
* const FullNameField = ({ record }) => <span>{record.first_name} {record.last_name}</span>;
* <RadioButtonGroupInput source="gender" choices={choices} optionText={<FullNameField />}/>
* <RadioButtonGroupInput source="recipients" choices={choices} optionText={<FullNameField />}/>
*
* The choices are translated by default, so you can use translation identifiers as choices:
* @example
Expand Down
Loading

0 comments on commit 66197e9

Please sign in to comment.