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

Leverage SourceContext in fields #9620

Merged
merged 14 commits into from
Feb 26, 2024
11 changes: 6 additions & 5 deletions docs/Fields.md
Original file line number Diff line number Diff line change
Expand Up @@ -501,17 +501,18 @@ export const UserList = () => (

**Tip**: In such custom fields, the `source` is optional. React-admin uses it to determine which column to use for sorting when the column header is clicked. In case you use the `source` property for additional purposes, the sorting can be overridden by the `sortBy` property on any `Field` component.

If you build a reusable field accepting a `source` props, you will probably want to support deep field sources (e.g. source values like `author.name`). Use [lodash/get](https://www.npmjs.com/package/lodash.get) to replace the simple object lookup. For instance, for a Text field:
If you build a reusable field accepting a `source` props, you will probably want to support deep field sources (e.g. source values like `author.name`). Use the [`useFieldValue` hook](/useFieldValue.md) to replace the simple object lookup. For instance, for a Text field:

```diff
import * as React from 'react';
+import get from 'lodash/get';
import { useRecordContext } from 'react-admin';
-import { useRecordContext } from 'react-admin';
+import { useFieldValue } from 'react-admin';

const TextField = ({ source }) => {
const record = useRecordContext();
fzaninotto marked this conversation as resolved.
Show resolved Hide resolved
- const record = useRecordContext();
+ const value = useFieldValue(props);
- return record ? <span>{record[source]}</span> : null;
slax57 marked this conversation as resolved.
Show resolved Hide resolved
+ return record ? <span>{get(record, source)}</span> : null;
+ return record ? <span>{value}</span> : null;
fzaninotto marked this conversation as resolved.
Show resolved Hide resolved
}

fzaninotto marked this conversation as resolved.
Show resolved Hide resolved
export default TextField;
Expand Down
3 changes: 3 additions & 0 deletions docs/Reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,9 @@ title: "Index"
* [`useEditContext`](./useEditContext.md)
* [`useEditController`](./useEditController.md)

**- F -**
* [`useFieldValue`](./useFieldValue.md)

**- G -**
* [`useGetIdentity`](./useGetIdentity.md)
* [`useGetList`](./useGetList.md)
Expand Down
1 change: 1 addition & 0 deletions docs/navigation.html
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,7 @@
<li {% if page.path == 'TranslatableFields.md' %} class="active" {% endif %}><a class="nav-link" href="./TranslatableFields.html"><code>&lt;TranslatableFields&gt;</code></a></li>
<li {% if page.path == 'UrlField.md' %} class="active" {% endif %}><a class="nav-link" href="./UrlField.html"><code>&lt;UrlField&gt;</code></a></li>
<li {% if page.path == 'WrapperField.md' %} class="active" {% endif %}><a class="nav-link" href="./WrapperField.html"><code>&lt;WrapperField&gt;</code></a></li>
<li {% if page.path == 'useFieldValue.md' %} class="active" {% endif %}><a class="nav-link" href="./useFieldValue.html"><code>useFieldValue</code></a></li>
</ul>

<ul><div>Inputs</div>
Expand Down
32 changes: 32 additions & 0 deletions docs/useFieldValue.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
---
layout: default
title: "useFieldValue"
---

# `useFieldValue`

A hook that gets the value of a field of the current record. It gets the current record from the context or use the one provided as a prof. It supports deep sources such as `name.fr`.
djhi marked this conversation as resolved.
Show resolved Hide resolved

## Usage

Here is an example `TextField` component:

```jsx
import * as React from 'react';
+import { useFieldValue } from 'react-admin';

const TextField = ({ source }) => {
djhi marked this conversation as resolved.
Show resolved Hide resolved
const value = useFieldValue(props);
return record ? <span>{value}</span> : null;
}
```

fzaninotto marked this conversation as resolved.
Show resolved Hide resolved
## Options

### `source`

The name of the property on the record object that contains the value to display. Can be a deep path.
fzaninotto marked this conversation as resolved.
Show resolved Hide resolved

### `record`

The record from which to read the value. Read from the `RecordContext` by default.
fzaninotto marked this conversation as resolved.
Show resolved Hide resolved
2 changes: 0 additions & 2 deletions packages/ra-core/src/i18n/TranslatableContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@ export const TranslatableContext = createContext<
>(undefined);

export interface TranslatableContextValue {
getLabel: GetTranslatableLabel;
fzaninotto marked this conversation as resolved.
Show resolved Hide resolved
getSource: GetTranslatableSource;
locales: string[];
selectedLocale: string;
selectLocale: SelectTranslatableLocale;
Expand Down
12 changes: 1 addition & 11 deletions packages/ra-core/src/i18n/useTranslatable.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import { useState, useMemo } from 'react';
import { useResourceContext } from '../core';
import { TranslatableContextValue } from './TranslatableContext';
import { useLocaleState } from './useLocaleState';
import { useTranslateLabel } from './useTranslateLabel';

/**
* Hook supplying the logic to translate a field value in multiple languages.
Expand All @@ -25,22 +23,14 @@ export const useTranslatable = (
const [localeFromUI] = useLocaleState();
const { defaultLocale = localeFromUI, locales } = options;
const [selectedLocale, setSelectedLocale] = useState(defaultLocale);
const resource = useResourceContext({});
const translateLabel = useTranslateLabel();

const context = useMemo<TranslatableContextValue>(
() => ({
// TODO: remove once fields use SourceContext
getSource: (source: string, locale: string = selectedLocale) =>
`${source}.${locale}`,
// TODO: remove once fields use SourceContext
getLabel: (source: string, label?: string) =>
translateLabel({ source, resource, label }) as string,
locales,
selectedLocale,
selectLocale: setSelectedLocale,
}),
[locales, resource, selectedLocale, translateLabel]
[locales, selectedLocale]
);

return context;
Expand Down
1 change: 1 addition & 0 deletions packages/ra-core/src/util/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { getMutationMode } from './getMutationMode';
export * from './getFieldLabelTranslationArgs';
export * from './mergeRefs';
export * from './useEvent';
export * from './useFieldValue';

export {
escapePath,
Expand Down
64 changes: 64 additions & 0 deletions packages/ra-core/src/util/useFieldValue.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import * as React from 'react';
import { render, screen } from '@testing-library/react';
import { useFieldValue, UseFieldValueOptions } from './useFieldValue';
import { RecordContextProvider } from '../controller';
import { SourceContextProvider } from '..';

describe('useFieldValue', () => {
const Component = (props: UseFieldValueOptions) => {
return <div>{useFieldValue(props) ?? 'None'}</div>;
};

it('should return undefined if no record is available', async () => {
fzaninotto marked this conversation as resolved.
Show resolved Hide resolved
render(<Component source="name" />);

await screen.findByText('None');
});

it('should return the field value from the record in RecordContext', async () => {
render(
<RecordContextProvider value={{ name: 'John Wick' }}>
<Component source="name" />
</RecordContextProvider>
);

await screen.findByText('John Wick');
});

it('should return the field value from the record in props', async () => {
render(
<RecordContextProvider value={{ id: 2, name: 'John Wick' }}>
<Component
source="name"
record={{ id: 1, name: 'Johnny Silverhand' }}
slax57 marked this conversation as resolved.
Show resolved Hide resolved
/>
</RecordContextProvider>
);

await screen.findByText('Johnny Silverhand');
});

it('should return the field value from the record inside a SourceContext', async () => {
render(
<RecordContextProvider
value={{
id: 2,
name: { fr: 'Neuromancien', en: 'Neuromancer' },
}}
>
<SourceContextProvider
value={{
getSource(source) {
return `${source}.fr`;
},
getLabel: source => source,
}}
>
<Component source="name" />
</SourceContextProvider>
</RecordContextProvider>
);

await screen.findByText('Neuromancien');
});
});
slax57 marked this conversation as resolved.
Show resolved Hide resolved
32 changes: 32 additions & 0 deletions packages/ra-core/src/util/useFieldValue.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import get from 'lodash/get';
import { RaRecord } from '../types';
import { useRecordContext } from '../controller';
import { useSourceContext } from '../core';

/**
* A hook that gets the value of a field of the current record.
* @param options The hook options
* @param options.source The field source
* @param options.record The record to use. Uses the record from the RecordContext if not provided
* @returns The field value
*
* @example
* const MyField = (props: { source: string }) => {
* const value = useFieldValue(props);
* return <span>{value}</span>;
* }
*/
export const useFieldValue = <RecordType = RaRecord>(
options: UseFieldValueOptions<RecordType>
djhi marked this conversation as resolved.
Show resolved Hide resolved
) => {
const { source } = options;
const sourceContext = useSourceContext();
const record = useRecordContext<RecordType>(options);

return get(record, sourceContext?.getSource(source) ?? source);
};

export interface UseFieldValueOptions<RecordType = RaRecord> {
source?: string;
fzaninotto marked this conversation as resolved.
Show resolved Hide resolved
record?: RecordType;
}
fzaninotto marked this conversation as resolved.
Show resolved Hide resolved
8 changes: 2 additions & 6 deletions packages/ra-ui-materialui/src/field/BooleanField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,10 @@ import * as React from 'react';
import { styled } from '@mui/material/styles';
import { SvgIconComponent } from '@mui/icons-material';
import PropTypes from 'prop-types';
import get from 'lodash/get';
import DoneIcon from '@mui/icons-material/Done';
import ClearIcon from '@mui/icons-material/Clear';
import { Tooltip, Typography, TypographyProps } from '@mui/material';
import { useTranslate, useRecordContext } from 'ra-core';
import { useTranslate, useFieldValue } from 'ra-core';
import { genericMemo } from './genericMemo';
import { FieldProps, fieldPropTypes } from './types';
import { sanitizeFieldRestProps } from './sanitizeFieldRestProps';
Expand All @@ -19,18 +18,15 @@ const BooleanFieldImpl = <
const {
className,
emptyText,
source,
valueLabelTrue,
valueLabelFalse,
TrueIcon = DoneIcon,
FalseIcon = ClearIcon,
looseValue = false,
...rest
} = props;
const record = useRecordContext<RecordType>(props);
const translate = useTranslate();

const value = get(record, source);
const value = useFieldValue(props);
const isTruthyValue = value === true || (looseValue && value);
let ariaLabel = value ? valueLabelTrue : valueLabelFalse;

Expand Down
8 changes: 3 additions & 5 deletions packages/ra-ui-materialui/src/field/ChipField.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import * as React from 'react';
import { styled } from '@mui/material/styles';
import get from 'lodash/get';
import Chip, { ChipProps } from '@mui/material/Chip';
import Typography from '@mui/material/Typography';
import clsx from 'clsx';
import { useRecordContext, useTranslate } from 'ra-core';
import { useFieldValue, useTranslate } from 'ra-core';

import { sanitizeFieldRestProps } from './sanitizeFieldRestProps';
import { FieldProps, fieldPropTypes } from './types';
Expand All @@ -15,9 +14,8 @@ const ChipFieldImpl = <
>(
props: ChipFieldProps<RecordType>
) => {
const { className, source, emptyText, ...rest } = props;
const record = useRecordContext<RecordType>(props);
const value = get(record, source);
const { className, emptyText, ...rest } = props;
const value = useFieldValue(props);
const translate = useTranslate();

if (value == null && emptyText) {
Expand Down
11 changes: 2 additions & 9 deletions packages/ra-ui-materialui/src/field/DateField.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import * as React from 'react';
import PropTypes from 'prop-types';
import get from 'lodash/get';
import { Typography, TypographyProps } from '@mui/material';
import { useRecordContext, useTranslate } from 'ra-core';
import { useFieldValue, useTranslate } from 'ra-core';

import { sanitizeFieldRestProps } from './sanitizeFieldRestProps';
import { FieldProps, fieldPropTypes } from './types';
Expand Down Expand Up @@ -44,7 +43,6 @@ const DateFieldImpl = <
options,
showTime = false,
showDate = true,
source,
transform = defaultTransform,
...rest
} = props;
Expand All @@ -56,12 +54,7 @@ const DateFieldImpl = <
);
}

const record = useRecordContext<RecordType>(props);
if (!record) {
return null;
}

const value = get(record, source) as any;
const value = useFieldValue(props);
if (value == null || value === '') {
return emptyText ? (
<Typography
Expand Down
8 changes: 3 additions & 5 deletions packages/ra-ui-materialui/src/field/EmailField.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import * as React from 'react';
import get from 'lodash/get';
import Typography from '@mui/material/Typography';
import { Link, LinkProps } from '@mui/material';
import { useRecordContext, useTranslate } from 'ra-core';
import { useFieldValue, useTranslate } from 'ra-core';

import { sanitizeFieldRestProps } from './sanitizeFieldRestProps';
import { FieldProps, fieldPropTypes } from './types';
Expand All @@ -13,9 +12,8 @@ const EmailFieldImpl = <
>(
props: EmailFieldProps<RecordType>
) => {
const { className, source, emptyText, ...rest } = props;
const record = useRecordContext(props);
const value = get(record, source);
const { className, emptyText, ...rest } = props;
const value = useFieldValue(props);
const translate = useTranslate();

if (value == null) {
Expand Down
13 changes: 7 additions & 6 deletions packages/ra-ui-materialui/src/field/FileField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { styled } from '@mui/material/styles';
import PropTypes from 'prop-types';
import get from 'lodash/get';
import Typography from '@mui/material/Typography';
import { useRecordContext, useTranslate } from 'ra-core';
import { useFieldValue, useTranslate } from 'ra-core';

import { sanitizeFieldRestProps } from './sanitizeFieldRestProps';
import { FieldProps, fieldPropTypes } from './types';
Expand Down Expand Up @@ -31,7 +31,6 @@ export const FileField = <
const {
className,
emptyText,
source,
title,
src,
target,
Expand All @@ -40,8 +39,12 @@ export const FileField = <
rel,
...rest
} = props;
const record = useRecordContext(props);
const sourceValue = get(record, source);
const sourceValue = useFieldValue(props);
const titleValue =
useFieldValue({
...props,
source: title,
}) ?? title;
const translate = useTranslate();

if (!sourceValue) {
Expand Down Expand Up @@ -87,8 +90,6 @@ export const FileField = <
);
}

const titleValue = get(record, title)?.toString() || title;
fzaninotto marked this conversation as resolved.
Show resolved Hide resolved

return (
<Root className={className} {...sanitizeFieldRestProps(rest)}>
<Link
Expand Down
Loading
Loading