ra-relationships

react-admin ≥ 4.1.2

A set of alternative inputs and fields to edit relationships, including many-to-many relationships using a join table.

Test it live in the Enterprise Edition Storybook.

Installation

npm install --save @react-admin/ra-relationships
# or
yarn add @react-admin/ra-relationships

Tip: ra-relationships is part of the React-Admin Enterprise Edition, and hosted in a private npm registry. You need to subscribe to one of the Enterprise Edition plans to access this package.

The package contains new translation messages (in English and French). You should add them to your i18nProvider:

import { Admin } from 'react-admin';
import polyglotI18nProvider from 'ra-i18n-polyglot';
import englishMessages from 'ra-language-english';
import frenchMessages from 'ra-language-french';

import {
    raRelationshipsLanguageEnglish,
    raRelationshipsLanguageFrench,
} from '@react-admin/ra-relationships';

const messages = {
    en: { ...englishMessages, ...raRelationshipsLanguageEnglish },
    fr: { ...frenchMessages, ...raRelationshipsLanguageFrench },
};

const i18nProvider = polyglotI18nProvider(locale => messages[locale], 'en');

const App = () => <Admin i18nProvider={is18nProvider}>{/* ... */}</Admin>;
import { Admin } from "react-admin";
import polyglotI18nProvider from "ra-i18n-polyglot";
import englishMessages from "ra-language-english";
import frenchMessages from "ra-language-french";

import { raRelationshipsLanguageEnglish, raRelationshipsLanguageFrench } from "@react-admin/ra-relationships";

const messages = {
    en: { ...englishMessages, ...raRelationshipsLanguageEnglish },
    fr: { ...frenchMessages, ...raRelationshipsLanguageFrench },
};

const i18nProvider = polyglotI18nProvider((locale) => messages[locale], "en");

const App = () => <Admin i18nProvider={is18nProvider}>{/* ... */}</Admin>;

Many-To-Many Relationships

Developers usually store many-to-many relationships in databases using an associative table (also known as join table, junction table or cross-reference table). For instance, if a Book can have many Authors, and an Author can write several Books, the normalized way to store this relationship in a relational database uses an intermediate table book_authors, as follows:

┌──────────────────┐       ┌──────────────┐      ┌───────────────┐
│ books            │       │ book_authors │      │ authors       │
│------------------│       │--------------│      │---------------│
│ id               │───┐   │ id           │   ┌──│ id            │
│ title            │   └──╼│ book_id      │   │  │ first_name    │
│ body             │       │ author_id    │╾──┘  │ last_name     │
│ publication_date │       │ is_public    │      │ date_of_birth │
└──────────────────┘       └──────────────┘      └───────────────┘

In the book_authors table, book_id and author_id are both foreign keys to books and authors.

A REST API closely following this data model exposes the three resources /books, /authors, and /book_authors. ra-relationships components rely on the associative table without ever showing it to the end-user. From the end user's point of view, the associative table is an implementation detail.

Out of scope

If the associative table uses a composite primary key, then ra-relationships does not work, as react-admin requires that all entities expose an identifier called id. For example, if user permissions are seen as a many-to-many relationship, they can be modeled in a relational database as follows:

users         user_permissions    permissions
----------    ----------------    -----------------
login         user_login          key
password      permission_key      description
first_name
last_name

Here, the associative table uses a composite primary key made of the tuple (user_login, permission_key). To allow react-admin to use this associative table, the related API route (/user_permissions) must include a unique id field for each record (which can simply be the concatenation of the two foreign keys).

Also, if your REST API can present that relationship through a list of related record ids (e.g. author_ids in books and book_ids in authors), you don't need ra-relationships. Just use <ReferenceArrayField> and <ReferenceArrayInput>, which are standard components in react-admin.

<ReferenceManyToManyField>

This component fetches a list of referenced records by lookup in an associative table and passes the records down to its child component, which must be an iterator component.

Note: The <ReferenceManyToManyField> cannot currently display multiple records with the same id from the end reference resource, even though they might have different properties in the associative table.

For instance, here is how to fetch the authors related to a book record by matching book.id to book_authors.post_id, then matching book_authors.author_id to authors.id, and then displaying the author last_name for each, in a <ChipField>:

import React from 'react';
import {
    Show,
    SimpleShowLayout,
    TextField,
    DateField,
    SingleFieldList,
    ChipField,
} from 'react-admin';
import { ReferenceManyToManyField } from '@react-admin/ra-relationships';

export const BookShow = () => (
    <Show>
        <SimpleShowLayout>
            <TextField source="title" />
            <DateField source="publication_date" />
            <ReferenceManyToManyField
                reference="authors"
                through="book_authors"
                using="book_id,author_id"
            >
                <SingleFieldList>
                    <ChipField source="last_name" />
                </SingleFieldList>
            </ReferenceManyToManyField>
            <EditButton />
        </SimpleShowLayout>
    </Show>
);
import React from "react";
import { Show, SimpleShowLayout, TextField, DateField, SingleFieldList, ChipField } from "react-admin";
import { ReferenceManyToManyField } from "@react-admin/ra-relationships";

export const BookShow = () => (
    <Show>
        <SimpleShowLayout>
            <TextField source="title" />
            <DateField source="publication_date" />
            <ReferenceManyToManyField reference="authors" through="book_authors" using="book_id,author_id">
                <SingleFieldList>
                    <ChipField source="last_name" />
                </SingleFieldList>
            </ReferenceManyToManyField>
            <EditButton />
        </SimpleShowLayout>
    </Show>
);

<ReferenceManyToManyField> expects an iterator component as child, i.e. a component working inside a ListContext. That means you can use a <Datagrid> instead of a <SingleFieldList> - but not inside another <Datagrid>! This is useful if you want to display a more detailed view of related records. For instance, to display the author first_name and last_name:

export const BookShow = (props) => (
    <Show {...props}>
        <SimpleShowLayout>
            <TextField source="title" />
            <DateField source="publication_date" />
            <ReferenceManyToManyField
                reference="authors"
                through="book_authors"
                using="book_id,author_id"
            >
-               <SingleFieldList>
-                   <ChipField source="last_name" />
-               </SingleFieldList>
+               <Datagrid>
+                   <TextField source="first_name" />
+                   <TextField source="last_name" />
+               </Datagrid>
            </ReferenceManyToManyField>
            <EditButton />
        </SimpleShowLayout>
    </Show>
);

dataProvider Calls

<ReferenceManyToManyField> fetches the dataProvider twice in a row:

  • once to get the records of the associative resource (book_authors in this case), using a getManyReference() call
  • once to get the records of the reference resource (books in this case), using a getMany() call.

For instance, if the user displays the book of id 123, <ReferenceManyToManyField> first issues the following query to the dataProvider:

dataProvider.getManyReference('book_authors', {
    target: 'book_id',
    id: 123,
});

Let's say that the dataProvider returns the following response:

{
    "data": [
        { "id": 667, "book_id": 123, "author_id": 732 },
        { "id": 895, "book_id": 123, "author_id": 874 }
    ],
    "total": 2
}

Then, <ReferenceManyToManyField> issues a second query to the dataProvider:

dataProvider.getMany('authors', {
    ids: [732, 874],
});

And receives the reference authors:

{
    "data": [
        { "id": 732, "first_name": "John", "last_name": "Doe" },
        { "id": 874, "first_name": "Jane", "last_name": "Doe" }
    ],
    "total": 2
}

Props

Prop Required Type Default Description
reference Required string - Name of the reference resource, e.g. 'authors'
through Required string - Name of the resource for the associative table, e.g. 'book_authors'
children Required element - An iterator element (e.g. <SingleFieldList> or <Datagrid>). The iterator element usually has one or more child <Field> components.
using Optional string '([resource]_id,[reference]_id)' Tuple (comma separated) of the two field names used as foreign keys, e.g 'book_id,author_id'. The tuple should start with the field pointing to the resource, and finish with the field pointing to the reference
source Optional string 'id' Name of the field containing the identity of the main resource. Used determine the value to look for in the associative table.
perPage Optional number 25 Limit for the number of results fetched from the associative table
sort Optional { field: string, order: 'ASC' or 'DESC' } { field: 'id', order: 'DESC' } Sort for the associative table (passed to the getManyReference() call)
filter Optional object {} Filter for the associative table (passed to the getManyReference() call)

Usage

Here is a usage example for these props:

// You can specify the associative table name using the `through` prop.
<ReferenceManyToManyField reference="authors" through="book_authors_assoc">
    {/* ... */}
</ReferenceManyToManyField>
// You can specify the associative table name using the `through` prop.
<ReferenceManyToManyField reference="authors" through="book_authors_assoc">
    {/* ... */}
</ReferenceManyToManyField>;
// You can specify the associative table columns using the `using` prop.
<ReferenceManyToManyField
    reference="authors"
    through="book_authors"
    using="b_id,a_id"
>
    {/* ... */}
</ReferenceManyToManyField>
// You can specify the associative table columns using the `using` prop.
<ReferenceManyToManyField reference="authors" through="book_authors" using="b_id,a_id">
    {/* ... */}
</ReferenceManyToManyField>;
// By default, react-admin restricts the possible values to 25.
// You can change the limit by setting the `perPage` prop:
<ReferenceManyToManyField
    reference="authors"
    through="book_authors"
    using="book_id,author_id"
    perPage={10}
>
    {/* ... */}
</ReferenceManyToManyField>
// By default, react-admin restricts the possible values to 25.
// You can change the limit by setting the `perPage` prop:
<ReferenceManyToManyField reference="authors" through="book_authors" using="book_id,author_id" perPage={10}>
    {/* ... */}
</ReferenceManyToManyField>;
// By default, react-admin orders the possible values by `id` desc.
// You can change this order by setting the `sort` prop (an object with `field` and `order` properties)
// to be applied to the associative resource.
<ReferenceManyToManyField
    reference="authors"
    through="book_authors"
    using="book_id,author_id"
    sort={{ field: 'id', order: 'DESC' }}
>
    {/* ... */}
</ReferenceManyToManyField>
// By default, react-admin orders the possible values by `id` desc.
// You can change this order by setting the `sort` prop (an object with `field` and `order` properties)
// to be applied to the associative resource.
<ReferenceManyToManyField
    reference="authors"
    through="book_authors"
    using="book_id,author_id"
    sort={{ field: "id", order: "DESC" }}
>
    {/* ... */}
</ReferenceManyToManyField>;
// Also, you can filter the records of the associative table using the `filter` prop.
<ReferenceManyToManyField
    reference="authors"
    through="book_authors"
    using="book_id,author_id"
    filter={{ is_public: true }}
>
    {/* ... */}
</ReferenceManyToManyField>
// Also, you can filter the records of the associative table using the `filter` prop.
<ReferenceManyToManyField
    reference="authors"
    through="book_authors"
    using="book_id,author_id"
    filter={{ is_public: true }}
>
    {/* ... */}
</ReferenceManyToManyField>;

<ReferenceManyToManyInput>

This component allows adding or removing relationships between two resources sharing an associative table. The changes in the associative table are sent to the dataProvider when the user submits the form so that they can cancel the changes before submission.

Note: The <ReferenceManyToManyInput> cannot currently display multiple records with the same id from the end reference resource even though they might have different properties in the associative table.

For instance, here is how to edit the events related to an artists record through a performances associative table.

┌────────────┐       ┌──────────────┐      ┌────────┐
│ artists    │       │ performances │      │ events │
│------------│       │--------------│      │--------│
│ id         │───┐   │ id           │   ┌──│ id     │
│ first_name │   └──╼│ artist_id    │   │  │ name   │
│ last_name  │       │ event_id     │╾──┘  │        │
└────────────┘       └──────────────┘      └────────┘

In this example, artists.id matches performances.artist_id, and performances.event_id matches events.id. The form displays the events name in a <SelectArrayInput>:

import React from 'react';
import {
    Edit,
    SelectArrayInput,
    SimpleForm,
    TextInput,
    required,
} from 'react-admin';

import { ReferenceManyToManyInput } from '@react-admin/ra-relationships';

const ArtistEdit = () => (
    <Edit>
        <SimpleForm>
            <TextInput disabled source="id" />
            <TextInput source="first_name" />
            <TextInput source="last_name" />
            <ReferenceManyToManyInput
                source="id"
                reference="events"
                through="performances"
                using="artist_id,event_id"
            >
                <SelectArrayInput
                    label="Performances"
                    // Validation must be set on this component
                    validate={required()}
                    optionText="name"
                    fullWidth
                />
            </ReferenceManyToManyInput>
        </SimpleForm>
    </Edit>
);

export default ArtistEdit;
import React from "react";
import { Edit, SelectArrayInput, SimpleForm, TextInput, required } from "react-admin";

import { ReferenceManyToManyInput } from "@react-admin/ra-relationships";

const ArtistEdit = () => (
    <Edit>
        <SimpleForm>
            <TextInput disabled source="id" />
            <TextInput source="first_name" />
            <TextInput source="last_name" />
            <ReferenceManyToManyInput source="id" reference="events" through="performances" using="artist_id,event_id">
                <SelectArrayInput
                    label="Performances"
                    // Validation must be set on this component
                    validate={required()}
                    optionText="name"
                    fullWidth
                />
            </ReferenceManyToManyInput>
        </SimpleForm>
    </Edit>
);

export default ArtistEdit;

<ReferenceManyToManyInput> expects an input allowing to select multiple values as child - like <SelectArrayInput> in the example above. Other possible children are <AutocompleteArrayInput>, <CheckboxGroupInput>, and <DualListInput>.

dataProvider Calls

When rendered, <ReferenceManyToManyInput> fetches the dataProvider three times in a row:

  • once to get the records of the associative resource (performances in this case), using a getManyReference() call
  • once to get the records of the reference resource (events in this case), using a getMany() call.
  • once to get the possible values of the reference resource (events in this case) to show as suggestions in the input, using a getList() call

For instance, if the user edits the artist of id 123, <ReferenceManyToManyInput> first issues the following query to the dataProvider:

dataProvider.getManyReference('performances', {
    target: 'artist_id',
    id: 123,
});

Let's say that the dataProvider returns the following response:

{
    "data": [
        { "id": 667, "artist_id": 123, "event_id": 732 },
        { "id": 895, "artist_id": 123, "event_id": 874 }
    ],
    "total": 2
}

Then, <ReferenceManyToManyInput> issues a second query to the dataProvider:

dataProvider.getMany('events', {
    ids: [732, 874],
});

Which returns the following:

{
    "data": [
        { "id": 732, "name": "Acme Rock Festival" },
        { "id": 874, "name": "Roll and Rock 2020" }
    ]
}

That's enough to display the current value in the input. But to display events suggestions, the component makes a final call:

dataProvider.getList('events', {
    sort: { field: 'id', order: 'DESC' },
    pagination: { page: 1, perPage: 25 },
    filter: {},
});
{
    "data": [
        { "id": 1, "name": "Rock Your Town" },
        { "id": 2, "name": "Aime le Rock" },
        { "id": 3, "name": "Breed Festival" },
        ...
    ],
    "total": 32
}

And that's it for the display phase.

When the user submits the form, the save function compares the value of the <ReferenceManyToManyInput> (the list of relationships edited by the user) with the value previously returned by the dataProvider. Using a diffing algorithm, it deduces a list of insertions and deletions in the associative table, that are executed all at once.

For instance, let's say that after displaying the events 732 and 874 where artist 123 performs, the user removes event 732, and adds events 2 and 3. Upon submission, the dataProvider will detect removals and additions, and send the following queries:

dataProvider.delete('performances', {
    id: 667,
    previousData: { id: 667, artist_id: 123, event_id: 732 },
});
dataProvider.create('performances', {
    data: { artist_id: 123, event_id: 2 },
});
dataProvider.create('performances', {
    data: { artist_id: 123, event_id: 3 },
});

Props

Prop Required Type Default Description
reference Required string - Name of the reference resource, e.g. 'authors'
through Required string - Name of the resource for the associative table, e.g. 'book_authors'
children Required element - A select array input element (e.g. <SelectArrayInput>).
perPage Optional number 25 Limit for the number of results fetched from the associative table
perPageEndResource Optional number 25 Limit for the number of results fetched from the end table
using Optional string '([resource]_id,[reference]_id)' Tuple (comma separated) of the two field names used as foreign keys, e.g 'book_id,author_id'. The tuple should start with the field pointing to the resource, and finish with the field pointing to the reference
source Optional string 'id' Name of the field containing the identity of the main resource. Used determine the value to look for in the associative table.
sort Optional { field: string, order: 'ASC' or 'DESC' } { field: 'id', order: 'DESC' } Sort for the associative table (passed to the getManyReference() call)
filter Optional object {} Filter for the associative table (passed to the getManyReference() call)
sortEndResource Optional { field: string, order: 'ASC' or 'DESC' } { field: 'id', order: 'DESC' } Sort for the end table (passed to the getList() call)
filterEndResource Optional object {} Filter for the end table (passed to the getList() call)

<DualListInput>

To let users choose multiple values by moving them from a list of available choices to a list of selected choices, use a <DualListInput>. It renders using two MUI's <List>. Set the choices prop to determine the options (with id, name tuples):

const choices = [
    { id: 'programming', name: 'Programming' },
    { id: 'lifestyle', name: 'Lifestyle' },
    { id: 'photography', name: 'Photography' },
];
<DualListInput source="tags" choices={choices} />;
const choices = [
    { id: "programming", name: "Programming" },
    { id: "lifestyle", name: "Lifestyle" },
    { id: "photography", name: "Photography" },
];
<DualListInput source="tags" choices={choices} />;

Props

Prop Required Type Default Description
choices Required Object[] - List of items to show as options
emptyText Optional string '' The text to display for the empty option
optionText Optional string | Function name Fieldname of record to display in the suggestion item, function which accepts the current record as argument ((record)=> {string}) or an element which will be cloned with a record prop
optionValue Optional string id Fieldname of record containing the value to use as input value
disableValue Optional string disabled Fieldname of record containing the value to use to determine if an item should be disabled
translateChoice Optional boolean true Whether the choices should be translated
addButton Optional 'outlined' | 'contained' | 'text' | element - A MUI variant value for the add button or a React element to replace it. See documentation below for more details
removeButton Optional 'outlined' | 'contained' | 'text' | element - A MUI variant value for the remove button or a React element to replace it. See documentation below for more details
addButtonLabel Optional string ra-relationships.duallistinput.select The text or translation key to use as the label for the add button
removeButtonLabel Optional string ra-relationships.duallistinput.unselect The text or translation key to use as the label for the remove button
availableItemsLabel Optional string ra-relationships.duallistinput.availableItems The text or translation key to use as the label for the list of available choices
selectedItemsLabel Optional string ra-relationships.duallistinput.selectedItems The text or translation key to use as the label for the list of selected choices
dense Optional boolean false Visual density of the list component
variant Optional outlined | filled | standard - Style variant for the input

Usage

You can customize the properties to use for the option name and value, thanks to the optionText and optionValue attributes:

const choices = [
    { _id: 123, full_name: 'Leo Tolstoi' },
    { _id: 456, full_name: 'Jane Austen' },
];
<DualListInput
    source="authors_ids"
    choices={choices}
    optionText="full_name"
    optionValue="_id"
/>;
const choices = [
    { _id: 123, full_name: "Leo Tolstoi" },
    { _id: 456, full_name: "Jane Austen" },
];
<DualListInput source="authors_ids" choices={choices} optionText="full_name" optionValue="_id" />;

optionText also accepts a function, so you can shape the option text at will:

const choices = [
    { id: 123, first_name: 'Leo', last_name: 'Tolstoi' },
    { id: 456, first_name: 'Jane', last_name: 'Austen' },
];
const optionRenderer = choice => `${choice.first_name} ${choice.last_name}`;
<DualListInput
    source="authors_ids"
    choices={choices}
    optionText={optionRenderer}
/>;
const choices = [
    { id: 123, first_name: "Leo", last_name: "Tolstoi" },
    { id: 456, first_name: "Jane", last_name: "Austen" },
];
const optionRenderer = (choice) => `${choice.first_name} ${choice.last_name}`;
<DualListInput source="authors_ids" 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.

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>
);
<DualListInput
    source="authors_ids"
    choices={choices}
    optionText={<FullNameField />}
/>;
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>
);
<DualListInput source="authors_ids" choices={choices} optionText={<FullNameField />} />;

The choices are translated by default, so you can use translation identifiers as choices:

const choices = [
    { id: 1, name: 'permissions.users.write' },
    { id: 2, name: 'permissions.users.read' },
    { id: 3, name: 'permissions.users.delete' },
];
const choices = [
    { id: 1, name: "permissions.users.write" },
    { id: 2, name: "permissions.users.read" },
    { id: 3, name: "permissions.users.delete" },
];

However, in some cases, you may not want the choice to be translated. In that case, set the translateChoice prop to false.

<DualListInput source="permissions" choices={choices} translateChoice={false} />
<DualListInput source="permissions" choices={choices} translateChoice={false} />;

Note that translateChoice is set to false when <DualListInput> is a child of <ReferenceArrayInput>.

Tip: If you want to populate the choices attribute with a list of related records, you should decorate <DualListInput> with <ReferenceArrayInput>, and leave the choices empty:

import { ReferenceArrayInput } from 'react-admin';
import { DualListInput } from '@react-admin/ra-relationships';

<ReferenceArrayInput label="Authors" source="authors_ids" reference="authors">
    <DualListInput optionText="last_name" />
</ReferenceArrayInput>;
import { ReferenceArrayInput } from "react-admin";
import { DualListInput } from "@react-admin/ra-relationships";

<ReferenceArrayInput label="Authors" source="authors_ids" reference="authors">
    <DualListInput optionText="last_name" />
</ReferenceArrayInput>;

You can set disabled values by setting the disabled property of one item:

const choices = [
    { _id: 123, full_name: 'Leo Tolstoi' },
    { _id: 456, full_name: 'Jane Austen' },
    { _id: 1, full_name: 'System Administrator', disabled: true },
];
<DualListInput
    source="authors_ids"
    choices={choices}
    optionText="full_name"
    optionValue="_id"
/>;
const choices = [
    { _id: 123, full_name: "Leo Tolstoi" },
    { _id: 456, full_name: "Jane Austen" },
    { _id: 1, full_name: "System Administrator", disabled: true },
];
<DualListInput source="authors_ids" choices={choices} optionText="full_name" optionValue="_id" />;

You can use a custom field name by setting disableValue prop:

const choices = [
    { _id: 123, full_name: 'Leo Tolstoi' },
    { _id: 456, full_name: 'Jane Austen' },
    { _id: 987, full_name: 'Jack Harden', not_available: true },
];
<DualListInput
    source="contacts_ids"
    choices={choices}
    optionText="full_name"
    optionValue="_id"
    disableValue="not_available"
/>;
const choices = [
    { _id: 123, full_name: "Leo Tolstoi" },
    { _id: 456, full_name: "Jane Austen" },
    { _id: 987, full_name: "Jack Harden", not_available: true },
];
<DualListInput
    source="contacts_ids"
    choices={choices}
    optionText="full_name"
    optionValue="_id"
    disableValue="not_available"
/>;

Buttons can be customized through the addButton and removeButton props to fit the look and feel of your application. You have 3 options:

  • provide a MUI button variant value (outlined, contained or text)
  • leave it undefined to use the theme defaults
  • pass a React element. In this case, react-admin will clone the element and provide it with a disabled and onClick prop.

CHANGELOG

v4.0.8

2022-08-29

  • (fix) Fix cropped content on DualListInput

v4.0.7

2022-08-10

  • (fix) Add variant support on DualListInput
  • (fix) Allow custom "Add" and "Remove" buttons in DualListInput
  • (fix) Fix <ReferenceManyToManyField> and <ReferenceManyToManyInput> do not set up the ResourceContext to the reference resource.

v4.0.6

2022-07-21

  • (fix) Fix useReferenceParams cleanup of filters debounce is too aggressive

v4.0.5

2022-07-20

  • (fix) Release useReferenceParams filters debounce on unmount

v4.0.4

2022-07-11

  • (fix) Add react-query to the dependencies

v4.0.3

2022-07-11

  • (fix) Fix useReferenceManyToManyInputController may return duplicates choices.

v4.0.2

2022-07-01

  • (fix) Many to many updates causes old references to show up while updating
  • (fix) Fixes how choices are provided to the <ReferenceManyToMany> input, making the <AutocompleteArrayInput> work correctly

v4.0.1

2022-06-08

  • (fix) Update peer dependencies ranges (support React 18)

v4.0.0

2022-06-07

  • Upgrade to react-admin v4
  • Add support for filterEndResource, perPageEndResource and sortEndResource props on ReferenceManyToManyInput and useReferenceManyToManyInputController. These props allows you to specify the sort, perPage and filter options applied to the queries targeting the end resource of the many-to-many relation (e.g, for a relation between bands and artists through a members table, these props targets the queries for artists).

Breaking Changes

  • <ManyToManyReferenceContextProvider> has been removed.
const ArtistEdit = () => (
    <Edit>
-        <ManyToManyReferenceContextProvider>
            <SimpleForm>
                <TextInput disabled source="id" />
                <TextInput source="first_name" />
                <TextInput source="last_name" />
                <ReferenceManyToManyInput
                    source="id"
                    reference="events"
                    through="performances"
                    using="artist_id,event_id"
                >
                    <SelectArrayInput optionText="name" />
                </ReferenceManyToManyInput>
            </SimpleForm>
-        </ManyToManyReferenceContextProvider>
    </Edit>
);
  • <ReferenceManyToManyInput> is no longer responsible for label, fullWidth and validation. Pass these props to its child instead.
const ArtistEdit = () => (
    <Edit>
            <SimpleForm>
                <TextInput disabled source="id" />
                <TextInput source="first_name" />
                <TextInput source="last_name" />
                <ReferenceManyToManyInput
                    source="id"
                    reference="events"
                    through="performances"
                    using="artist_id,event_id"
-                    label="Performances"
-                    validate={required()}
                >
                    <SelectArrayInput
+                        fullWidth
                         optionText="name"
+                        label="Performances"
+                        validate={required()}
                    />
                </ReferenceManyToManyInput>
            </SimpleForm>
    </Edit>
);

v2.1.15

2022-02-08

  • (fix) <DualListInput availableItemsLabel> and <DualListInput selectedItemsLabel> now work both as custom labels and translation messages

v2.1.14

2021-12-10

  • (fix) ManyToManyReferenceContextProvider is not using passed basePath

v2.1.13

2021-11-22

  • (fix) <DualListInput> Fix DualListInput throws error on double-clicking an available item if no items are selected while being child of a ReferenceArrayInput.

v2.1.12

2021-11-05

  • (fix) useReferenceManyToManyInputController should reload the possible choices when the input value changes and one of the value is not loaded. This ensures you can leverage the quick creation support of components like SelectArrayInput or AutocompleteArrayInput.

v2.1.11

2021-10-26

  • (fix) useReferenceManyToManyInputController should not load through references when record has no id

v2.1.10

2021-07-19

  • (fix) Fix <ReferenceManyToManyField> doesn't have a label in Show views

v2.1.9

2021-06-29

  • (fix) Update peer dependencies ranges (support react 17)

v2.1.8

2021-06-01

  • (doc) Fix <DualListInput> documentation about usage in <ReferenceArrayInput>.

v2.1.7

2021-05-12

  • (doc) Update setup instructions to includes translations
  • (doc) Update DualListInput screencast

v2.1.6

2021-05-05

  • (fix) Correctly handle validation on the <ReferenceManyToManyInput> component. The validate prop must be set on the <ReferenceManyToManyInput>, not its children.

v2.1.5

2021-04-22

  • (fix) Avoid updating main record when only the references have changed.

v2.1.4

2021-04-02

  • (fix) Fix Prop types for <ManyToManyField> marking the source prop as required.
  • (fix) Fix README includes imports from the wrong package name.

v2.1.3

2021-03-29

  • (fix) Fix documentation about limitations of the ReferenceManyToManyField and ReferenceManyToManyInput components

v2.1.2

2021-03-26

  • (fix) Fix ManyToManyReferenceContextProvider Props Interface

v2.1.1

2021-03-23

  • (fix) Fix ManyToManyReferenceInput props interface to include perPage like its controller hook.

v2.1.0

2021-02-16

  • (feat) Children of ManyToManyReferenceInput are now responsible for handling the loading state.
  • (feat) Add handling of LoadingState to the DualListInput

v2.0.1

2021-02-09

  • (fix) ReferenceManyToManyField does not pass the perPage prop to its controller hook.

v2.0.0

2020-11-19

  • (feat) Add ability to have multiple ReferenceManyToManyInput in a form.

BREAKING CHANGE

  • You don't need a custom form calling either the useReferenceManyToManyCreate or useReferenceManyToManyUpdate hooks anymore. These hooks have been removed of the ra-relationships package.
  • Forms must now be wrapped with a <ManyToManyReferenceContextProvider>:
import React from 'react';
import { Edit, EditProps, SelectArrayInput, SimpleForm, TextInput } from 'react-admin';

-import { ReferenceManyToManyInput, useReferenceManyToManyUpdate } from '@react-admin/ra-many-to-many';
+import { ReferenceManyToManyInput, ManyToManyReferenceContextProvider } from '@react-admin/ra-many-to-many';

const ArtistEditForm = (props: EditProps) => {
-    const save = useReferenceManyToManyUpdate({
-        basePath: props.basePath,
-        record: props.record,
-        redirect: props.redirect || 'list',
-        reference: 'events',
-        resource: props.resource,
-        source: 'id',
-        through: 'performances',
-        undoable: props.undoable,
-        using: 'artist_id,event_id',
-    });
-
-    return <SimpleForm {...props} save={save} />;
+    return (
+        <ManyToManyReferenceContextProvider>
+            <SimpleForm {...props}
+        </ManyToManyReferenceContextProvider>
+    );
};

const ArtistEdit = (props: EditProps) => (
    <Edit {...props}>
        <ArtistEditForm>
            <TextInput disabled source="id" />
            <TextInput source="first_name" />
            <TextInput source="last_name" />
            <ReferenceManyToManyInput
                source="id"
                reference="events"
                through="performances"
                using="artist_id,event_id"
                fullWidth
                label="Performances"
            >
                <SelectArrayInput optionText="name" />
            </ReferenceManyToManyInput>
        </ArtistEditForm>
    </Edit>
);

export default ArtistEdit;

Note that you don't even need a custom form anymore:

const ArtistEdit = (props: EditProps) => (
    <Edit {...props}>
        <ManyToManyReferenceContextProvider>
            <SimpleForm>
                <TextInput disabled source="id" />
                <TextInput source="first_name" />
                <TextInput source="last_name" />
                <ReferenceManyToManyInput
                    source="id"
                    reference="events"
                    through="performances"
                    using="artist_id,event_id"
                    fullWidth
                    label="Performances"
                >
                    <SelectArrayInput optionText="name" />
                </ReferenceManyToManyInput>
            </SimpleForm>
        </ManyToManyReferenceContextProvider>
    </Edit>
);
const ArtistEdit = (props) => (
    <Edit {...props}>
        <ManyToManyReferenceContextProvider>
            <SimpleForm>
                <TextInput disabled source="id" />
                <TextInput source="first_name" />
                <TextInput source="last_name" />
                <ReferenceManyToManyInput
                    source="id"
                    reference="events"
                    through="performances"
                    using="artist_id,event_id"
                    fullWidth
                    label="Performances"
                >
                    <SelectArrayInput optionText="name" />
                </ReferenceManyToManyInput>
            </SimpleForm>
        </ManyToManyReferenceContextProvider>
    </Edit>
);

v1.2.1

2020-12-08

  • (fix) Fix ManyToManyInput does not fetch the correct references

v1.2.0

2020-10-12

  • (fix) Update DualListInput button labels (select/unselect instead of add/remove)
  • (fix) Disable buttons when no item is selected

v1.1.0

2020-10-05

  • Upgrade to react-admin 3.9

v1.0.0

2020-09-15

  • First release