Skip to content

Commit

Permalink
Avoid cloning element for optionText
Browse files Browse the repository at this point in the history
  • Loading branch information
Travis CI committed Feb 9, 2022
1 parent ebd6651 commit cf93765
Show file tree
Hide file tree
Showing 13 changed files with 111 additions and 50 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 />}/>
}
```

## `useListContext` No Longer Returns An `ids` Prop

The `ListContext` used to return two props for the list data: `data` and `ids`. To render the list data, you had to iterate over the `ids`.
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
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
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 @@ -429,9 +430,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
22 changes: 13 additions & 9 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 All @@ -23,6 +22,7 @@ import {
ChoicesProps,
FieldTitle,
RaRecord,
RecordContextProvider,
useInput,
useSuggestions,
UseSuggestionsOptions,
Expand Down Expand Up @@ -71,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
* 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 @@ -82,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>;
* }
* <SelectInput 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 @@ -463,10 +466,11 @@ If you provided a React element for the optionText prop, you must also provide t
onInputChange={handleInputChange}
renderOption={(props, record) => {
if (isValidElement(optionText)) {
return cloneElement(optionText, {
record: record as RaRecord,
...props,
});
return (
<RecordContextProvider value={record}>
{optionText}
</RecordContextProvider>
);
}

return <li {...props}>{getChoiceText(record)}</li>;
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
8 changes: 4 additions & 4 deletions packages/ra-ui-materialui/src/input/CheckboxGroupInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,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
* 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 @@ -75,7 +75,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 @@ -223,7 +223,7 @@ const sanitizeRestProps = ({
}: any) => sanitizeInputRestProps(rest);

CheckboxGroupInput.propTypes = {
choices: PropTypes.arrayOf(PropTypes.object),
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 @@ -52,15 +52,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
* 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
11 changes: 9 additions & 2 deletions packages/ra-ui-materialui/src/input/SelectArrayInput.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 @@ -170,7 +174,10 @@ describe('<SelectArrayInput />', () => {
});

it('should use optionText with an element value as text identifier', () => {
const Foobar = ({ record = undefined }) => <span>{record.foobar}</span>;
const Foobar = () => {
const record = useRecordContext();
return <span>{record.foobar}</span>;
};
render(
<AdminContext dataProvider={testDataProvider()}>
<SimpleForm onSubmit={jest.fn()}>
Expand Down
4 changes: 2 additions & 2 deletions packages/ra-ui-materialui/src/input/SelectArrayInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,8 @@ import {
* const optionRenderer = choice => `${choice.first_name} ${choice.last_name}`;
* <SelectArrayInput source="authors" 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
* 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
15 changes: 10 additions & 5 deletions packages/ra-ui-materialui/src/input/SelectInput.spec.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import * as React from 'react';
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
import { required, testDataProvider, TestTranslationProvider } from 'ra-core';
import {
required,
testDataProvider,
TestTranslationProvider,
useRecordContext,
} from 'ra-core';

import { AdminContext } from '../AdminContext';
import { SimpleForm } from '../form';
Expand Down Expand Up @@ -249,10 +254,10 @@ describe('<SelectInput />', () => {
});

it('should use optionText with an element value as text identifier', () => {
const Foobar = ({ record }: { record?: any }) => (
<span data-value={record.id} aria-label={record.foobar} />
);

const Foobar = () => {
const record = useRecordContext();
return <span data-value={record.id} aria-label={record.foobar} />;
};
render(
<AdminContext dataProvider={testDataProvider()}>
<SimpleForm onSubmit={jest.fn()}>
Expand Down
13 changes: 8 additions & 5 deletions packages/ra-ui-materialui/src/input/SelectInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,15 +60,18 @@ import {
* const optionRenderer = choice => `${choice.first_name} ${choice.last_name}`;
* <SelectInput 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
* 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>;
* <SelectInput source="gender" choices={choices} optionText={<FullNameField />}/>
* const FullNameField = () => {
* const record = useRecordContext();
* return <span>{record.first_name} {record.last_name}</span>;
* }
* <SelectInput source="author" choices={choices} optionText={<FullNameField />}/>
*
* The choices are translated by default, so you can use translation identifiers as choices:
* @example
Expand Down Expand Up @@ -171,7 +174,7 @@ export const SelectInput = (props: SelectInputProps) => {

const renderEmptyItemOption = useCallback(() => {
return React.isValidElement(emptyText)
? React.cloneElement(emptyText)
? emptyText
: emptyText === ''
? ' ' // em space, forces the display of an empty line of normal height
: translate(emptyText, { _: emptyText });
Expand Down

0 comments on commit cf93765

Please sign in to comment.