Skip to content

Commit

Permalink
Merge pull request #9788 from marmelab/feat/add-default-unique-id-for…
Browse files Browse the repository at this point in the history
…-useInput

Add a default unique id for `useInput`
  • Loading branch information
djhi committed May 2, 2024
2 parents 2ae79e7 + df41f11 commit 3239c64
Show file tree
Hide file tree
Showing 9 changed files with 103 additions and 18 deletions.
32 changes: 32 additions & 0 deletions cypress/e2e/create.cy.js
Expand Up @@ -15,6 +15,38 @@ describe('Create Page', () => {
CreatePage.waitUntilVisible();
});

it('should validate unique fields', () => {
CreatePage.logout();
LoginPage.login('admin', 'password');

UserCreatePage.navigate();
UserCreatePage.setValues([
{
type: 'input',
name: 'name',
value: 'Annamarie Mayer',
},
]);
cy.get(UserCreatePage.elements.input('name')).blur();

cy.get(CreatePage.elements.nameError)
.should('exist')
.contains('Must be unique', { timeout: 10000 });

UserCreatePage.setValues([
{
type: 'input',
name: 'name',
value: 'Annamarie NotMayer',
},
]);
cy.get(UserCreatePage.elements.input('name')).blur();

cy.get(CreatePage.elements.nameError)
.should('exist')
.should('not.contain', 'Must be unique', { timeout: 10000 });
});

it('should show the correct title in the appBar', () => {
cy.get(CreatePage.elements.title).contains('Create Post');
});
Expand Down
2 changes: 1 addition & 1 deletion cypress/support/CreatePage.js
Expand Up @@ -23,7 +23,7 @@ export default url => ({
title: '#react-admin-title',
userMenu: 'button[aria-label="Profile"]',
logout: '.logout',
nameError: '#name-helper-text',
nameError: '.MuiFormHelperText-root',
},

navigate() {
Expand Down
8 changes: 8 additions & 0 deletions docs/Upgrade.md
Expand Up @@ -932,6 +932,14 @@ The `BulkActionProps` has been removed as it did not contain any prop. You can s

The `data-generator-retail` package has been updated to provide types for all its records. In the process, we renamed the `commands` resource to `orders`. Accordingly, the `nb_commands` property of the `customers` resource has been renamed to `nb_orders` and the `command_id` property of the `invoices` and `reviews` resources has been renamed to `order_id`.

## Inputs default ids are auto-generated

In previous versions, the input default id was the source of the input. In v5, inputs defaults ids are auto-generated with [React useId()](https://react.dev/reference/react/useId).

**Tip:** You still can pass an id as prop of any [react-admin input](./Inputs.md) or use a [reference](https://fr.react.dev/reference/react/useRef).

If you were using inputs ids in your tests, you should pass your own id to the dedicated input.

## `<SimpleFormIterator>` No Longer Clones Its Buttons

`<SimpleFormIterator>` used to clones the add, remove and reorder buttons and inject some props to them such as `onClick` and `className`.
Expand Down
20 changes: 10 additions & 10 deletions docs/useInput.md
Expand Up @@ -41,16 +41,16 @@ const TitleInput = ({ source, label }) => {

## Props

| Prop | Required | Type | Default | Description |
|----------------|----------|--------------------------------|---------|-------------------------------------------------------------------|
| `source` | Required | `string` | - | The name of the field in the record |
| `defaultValue` | Optional | `any` | - | The default value of the input |
| `format` | Optional | `Function` | - | A function to format the value from the record to the input value |
| `parse` | Optional | `Function` | - | A function to parse the value from the input to the record value |
| `validate` | Optional | `Function` &#124; `Function[]` | - | A function or an array of functions to validate the input value |
| `id` | Optional | `string` | - | The id of the input |
| `onChange` | Optional | `Function` | - | A function to call when the input value changes |
| `onBlur` | Optional | `Function` | - | A function to call when the input is blurred |
| Prop | Required | Type | Default | Description |
|----------------|----------|--------------------------------|----------------- |-------------------------------------------------------------------|
| `source` | Required | `string` | - | The name of the field in the record |
| `defaultValue` | Optional | `any` | - | The default value of the input |
| `format` | Optional | `Function` | - | A function to format the value from the record to the input value |
| `parse` | Optional | `Function` | - | A function to parse the value from the input to the record value |
| `validate` | Optional | `Function` &#124; `Function[]` | - | A function or an array of functions to validate the input value |
| `id` | Optional | `string` | `auto-generated` | The id of the input |
| `onChange` | Optional | `Function` | - | A function to call when the input value changes |
| `onBlur` | Optional | `Function` | - | A function to call when the input is blurred |

Additional props are passed to [react-hook-form's `useController` hook](https://react-hook-form.com/docs/usecontroller).

Expand Down
2 changes: 1 addition & 1 deletion packages/ra-core/src/form/useInput.spec.tsx
Expand Up @@ -58,7 +58,7 @@ describe('useInput', () => {
</CoreAdminContext>
);

expect(inputProps.id).toEqual('title');
expect(inputProps.id).toEqual(':r0:');
expect(inputProps.isRequired).toEqual(true);
expect(inputProps.field).toBeDefined();
expect(inputProps.field.name).toEqual('title');
Expand Down
43 changes: 43 additions & 0 deletions packages/ra-core/src/form/useInput.stories.tsx
@@ -0,0 +1,43 @@
import * as React from 'react';
import { CoreAdminContext } from '../core';
import { Form } from './Form';
import { useInput } from './useInput';

export default {
title: 'ra-core/form/useInput',
};

const Input = ({ source }) => {
const { id, field, fieldState } = useInput({ source });

return (
<label htmlFor={id}>
{id}: <input id={id} {...field} />
{fieldState.error && <span>{fieldState.error.message}</span>}
</label>
);
};

export const Basic = () => {
const [submittedData, setSubmittedData] = React.useState<any>();
return (
<CoreAdminContext>
<Form onSubmit={data => setSubmittedData(data)}>
<div
style={{
display: 'flex',
flexDirection: 'column',
gap: '1em',
marginBottom: '1em',
}}
>
<Input source="field1" />
<Input source="field2" />
<Input source="field3" />
</div>
<button type="submit">Submit</button>
</Form>
<pre>{JSON.stringify(submittedData, null, 2)}</pre>
</CoreAdminContext>
);
};
5 changes: 3 additions & 2 deletions packages/ra-core/src/form/useInput.ts
@@ -1,4 +1,4 @@
import { ReactElement, useEffect } from 'react';
import { ReactElement, useEffect, useId } from 'react';
import {
ControllerFieldState,
ControllerRenderProps,
Expand Down Expand Up @@ -44,6 +44,7 @@ export const useInput = <ValueType = any>(
const formGroupName = useFormGroupContext();
const formGroups = useFormGroups();
const record = useRecordContext();
const defaultId = useId();

if (
!source &&
Expand Down Expand Up @@ -132,7 +133,7 @@ export const useInput = <ValueType = any>(
};

return {
id: id || finalSource,
id: id || defaultId,
field,
fieldState,
formState,
Expand Down
4 changes: 2 additions & 2 deletions packages/ra-input-rich-text/src/RichTextInput.spec.tsx
Expand Up @@ -9,7 +9,7 @@ describe('<RichTextInput />', () => {
const { container, rerender } = render(<Basic record={record} />);

await waitFor(() => {
expect(container.querySelector('#body')?.innerHTML).toEqual(
expect(container.querySelector('.ProseMirror')?.innerHTML).toEqual(
'<h1>Hello world!</h1>'
);
});
Expand All @@ -18,7 +18,7 @@ describe('<RichTextInput />', () => {
rerender(<Basic record={newRecord} />);

await waitFor(() => {
expect(container.querySelector('#body')?.innerHTML).toEqual(
expect(container.querySelector('.ProseMirror')?.innerHTML).toEqual(
'<h1>Goodbye world!</h1>'
);
});
Expand Down
Expand Up @@ -86,9 +86,10 @@ describe('<RadioButtonGroupInput />', () => {
);
expect(screen.queryByText('People')).not.toBeNull();
const input1 = screen.getByLabelText('Leo Tolstoi');
expect(input1.id).toBe('type_123');
expect(input1.id).toMatch(/:r\d:/);
const input2 = screen.getByLabelText('Jane Austen');
expect(input2.id).toBe('type_456');
expect(input2.id).toMatch(/:r\d:/);
expect(input2.id).not.toEqual(input1.id);
});

it('should trigger custom onChange when clicking radio button', async () => {
Expand Down

0 comments on commit 3239c64

Please sign in to comment.