Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Avoid cloning element for optionText #7215

Merged
merged 8 commits into from
Feb 10, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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