ra-tree

react-admin ≥ 4.14.0

Tree hooks and components for react-admin. Allows to display, edit, and rearrange tree structures like directories, categories, etc.

This module is agnostic as to how you store the tree structure in the backend side. Whether you use an array of children, a parent_id field, materialized paths or nested sets, this module will work. You'll just have to map the data structure expected by react-admin by the one returned by your API in your dataProvider.

Test it live in the Enterprise Edition Storybook and in the e-commerce demo.

Installation

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

Tip: ra-tree 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.

Usage

dataProvider

The dataProvider used by the <Admin> must support tree-specific methods:

Read methods

  • getTree(resource, { meta })
  • getRootNodes(resource, { meta })
  • getParentNode(resource, { childId, meta })
  • getChildNodes(resource, { parentId, meta })

These methods should return Promises for TreeRecord objects. A TreeRecord contains at least an id field and a children field (an array of child ids). For instance:

Write methods

  • moveAsNthChildOf(resource, { source, destination, position, meta }): source and destination are TreeRecord objects, and position a zero-based integer
  • moveAsNthSiblingOf(resource, { source, destination, position, meta }): source and destination are TreeRecord objects, and position a zero-based integer
  • addRootNode(resource, { data, meta })
  • addChildNode(resource, { parentId, data, meta })
  • deleteBranch(resource, { id, data, meta }): id is the identifier of the node to remove, and data its content

These methods should also return Promises for TreeRecord objects, except for the two moveAs... methods, which can return an empty data object if the move is successful.

Tip: All ra-tree dataProvider methods accept a meta parameter. it’s a good way to pass custom arguments or metadata to an API call.

Tip: moveAsNthChildOf is called when you drag and drop a node onto another node. The dragged node will then be inserted as the first child of the destination node. moveAsNthSiblingOf, on the other hand, is called when you drag and drop a node inbetween other existing nodes (not necessarily from the same branch). The dragged node will then be inserted right below the destination node.

Tip: With moveAsNthSiblingOf, the position parameter is a zero-based integer representing the target position in the destination branch, before the move has been performed. This means that the final position may not be equal to the position parameter, in case you are moving a node on the same branch with the source position being above the destination position. For instance, moving the first node of a branch under the second node will call moveAsNthSiblingOf with position=2, however its final position after the move has been performed will be position=1 (remember it's zero-based).

Here is an example of the expected syntax for the additional dataProvider methods:

dataProvider.getTree('posts').then(({ data }) => console.log(data));
// [
//   { id: 1, title: 'foo1', children: [3, 4] },
//   { id: 2, title: 'foo2', children: [] },
//   { id: 3, title: 'foo3', children: [5] },
//   { id: 4, title: 'foo4', children: [] },
//   { id: 5, title: 'foo5', children: [] },
// ]

dataProvider.getRootNodes('posts').then(({ data }) => console.log(data));
// [
//   { id: 1, title: 'foo1', children: [3, 4] },
//   { id: 2, title: 'foo2', children: [] },
// ]

dataProvider
    .getParentNode('posts', { childId: 5 })
    .then(({ data }) => console.log(data));
// { id: 3, title: 'foo3', children: [5] }

dataProvider
    .getChildNodes('posts', { parentId: 1 })
    .then(({ data }) => console.log(data));
// [
//   { id: 3, title: 'foo3', children: [5] },
//   { id: 4, title: 'foo4', children: [] },
// ]

dataProvider
    .moveAsNthChildOf('posts', {
        source: { id: 5, title: 'foo5', children: [] },
        destination: { id: 1, title: 'foo1', children: [3, 4] },
        position: 2,
    })
    .then(({ data }) => console.log(data));
// {}

dataProvider
    .moveAsNthSiblingOf('posts', {
        source: { id: 5, title: 'foo5', children: [] },
        destination: { id: 4, title: 'foo4', children: [] },
        position: 1,
    })
    .then(({ data }) => console.log(data));
// {}

dataProvider
    .addRootNode('posts', { data: { title: 'hello' } })
    .then(({ data }) => console.log(data));
// { id: 6, titl: 'hello', children: [] }

dataProvider
    .addChildNode('posts', { parentId: 2, data: { title: 'hello' } })
    .then(({ data }) => console.log(data));
// { id: 6, titl: 'hello', children: [] }

dataProvider
    .deleteBranch('posts', {
        id: 1,
        data: { id: 1, title: 'foo1', children: [3, 4] },
    })
    .then(({ data }) => console.log(data));
// { id: 1, title: 'foo1', children: [3, 4] }

ra-tree expects the dataProvider to return tree structures based on a children field, but chances are your API stores the tree in another data structure. In that case, you'll need to map the API data structure to the ra-tree data structure in your dataProvider.

dataProvider Builders

ra-tree provides helper functions to create a ra-tree-compatible dataProvider based on a regular react-admin dataProvider - provided the API returns the tree in any of the supported data structures. These helpers add the tree-specific methods described above (getTree(), getRootNodes(), etc.) by calling the regular methods (getList(), getOne(), etc.).

Be aware that these builders will call the regular dataProvider several times for each tree method call. We don't recommend using them in production - instead, you should modify your API to support the tree methods, and return data structures in the format expected by ra-tree.

addTreeMethodsBasedOnChildren

If the records returned by your API contain an array of children identifiers, use the addTreeMethodsBasedOnChildren builder.

Your API should return records with this format:

{
    id: 1234,
    name: 'hello',
    isRoot: false,
    children: [45, 356, 1],
}

Example:

import simpleRestProvider from 'ra-data-simple-rest';
import { addTreeMethodsBasedOnChildren } from '@react-admin/ra-tree';

const dataProvider = simpleRestProvider('http://path.to.my.api/');

const dataProviderWithTree = addTreeMethodsBasedOnChildren(
    dataProvider,
    'children',
    'isRoot',
    false
);

The builder accepts the following arguments:

  • dataProvider: The dataProvider to augment.
  • childrenField: The name of the field containing the children identifiers. Defaults to children.
  • isRootField: The name of the field containing the root status. Defaults to isRoot
  • apiSupportBranchDeletion: Indicates whether the API will handle children deletion when deleting a node as well as the parent update. If false, the dataProvider will handle it by making multiple requests in the right order. Defaults to false.

addTreeMethodsBasedOnParentAndPosition

If the records returned by your API contain a parent identifier and a position field, use the addTreeMethodsBasedOnParentAndPosition builder.

Your API should return records with this format:

{
    id: 1234,
    name: 'hello',
    parent_id: 35,
    position: 4, // zero-based
}

Example:

import simpleRestProvider from 'ra-data-simple-rest';
import { addTreeMethodsBasedOnParentAndPosition } from '@react-admin/ra-tree';

const dataProvider = simpleRestProvider('http://path.to.my.api/');

const dataProviderWithTree = addTreeMethodsBasedOnParentAndPosition(
    dataProvider,
    'parent_id',
    'position',
    false
);

The builder accepts the following arguments:

  • dataProvider: The dataProvider to augment.
  • parentIdField: The name of the field containing the parent identifier. Defaults to 'parent_id'
  • positionField: The name of the field containing the position of a node inside its parent. Defaults to 'position'
  • apiSupportBranchDeletion: Indicates whether the API will handle children deletion when deleting a node as well as the siblings update. If false, the dataProvider will handle it by making multiple requests in the right order. Defaults to false.

<Admin> Setup

This module comes with additional messages, as well as their translations in English and French. To use these messages, add them to your i18nProvider.

This means a typical ra-tree application begins like this:

// in src/App.js
import React from 'react';
import { Admin, Resource, mergeTranslations } from 'react-admin';
import polyglotI18nProvider from 'ra-i18n-polyglot';
import englishMessages from 'ra-language-english';
import { raTreeLanguageEnglish } from '@react-admin/ra-tree';

import dataProvider from './dataProvider';

const i18nProvider = polyglotI18nProvider(locale => {
    // Always fallback on english
    return mergeTranslations(englishMessages, raTreeLanguageEnglish);
}, 'en');

const App = () => (
    <Admin dataProvider={dataProvider} i18nProvider={i18nProvider} locale="en">
        ...
    </Admin>
);

<TreeWithDetails> Component

Once the dataProvider and <Admin> component are ready, you can start using the components of this package. The main one is a replacement for the <List> component, called <TreeWithDetails>.

// in src/category.js
import React from 'react';
import {
    Admin,
    Resource,
    Create,
    Edit,
    SimpleForm,
    TextInput,
} from 'react-admin';
import {
    CreateNode,
    EditNode,
    EditNodeToolbar,
    TreeWithDetails,
} from '@react-admin/ra-tree';

// a Create view for a tree uses <CreateNode> instead of the standard <Create>
const CategoriesCreate = () => (
    <CreateNode>
        <SimpleForm>
            <TextInput source="name" />
        </SimpleForm>
    </CreateNode>
);

// an Edit view for a tree uses <EditNode> instead of the standard <Edit>
const CategoriesEdit = () => (
    <EditNode>
        <SimpleForm toolbar={<EditNodeToolbar />}>
            <TextInput source="title" />
        </SimpleForm>
    </EditNode>
);

// a List view for a tree uses <TreeWithDetails>
export const CategoriesList = () => (
    <TreeWithDetails create={CategoriesCreate} edit={CategoriesEdit} />
);

// in src/App.js
import { CategoriesList } from './category';

const App = () => (
    <Admin dataProvider={dataProvider}>
        <Resource list={CategoriesList} />
    </Admin>
);

IMPORTANT: Note that in the Edition view, the <SimpleForm> must use the <EditNodeToolbar>. This toolbar replaces react-admin's default <DeleteButton> with a ra-tree version that deletes a branch instead of a record.

This also means that if you need to customize the Toolbar and includes a Delete Button, you must import the aternative button from @react-admin/ra-tree:

import { Toolbar, ToolbarProps } from 'react-admin';
import { DeleteBranchButton } from '@react-admin/ra-tree';

import MyCustomButton from './MyCustomButton';

export const MyToolbar = (props: ToolbarProps) => (
    <Toolbar>
        <MyCustomButton />
        <DeleteBranchButton />
    </Toolbar>
);
import { Toolbar, ToolbarProps } from 'react-admin';
import { DeleteBranchButton } from '@react-admin/ra-tree';

import MyCustomButton from './MyCustomButton';

export const MyToolbar = (props: ToolbarProps) => (
    <Toolbar>
        <MyCustomButton />
        <DeleteBranchButton />
    </Toolbar>
);

CreateNode and EditNode components accept a mutationOptions prop. So you can override the mutationOptions of the main mutation query.

const CategoriesCreate = () => (
    <CreateNode
        mutationOptions={{
            onSuccess: () => {
                console.log('Success!');
            },
            onError: () => {
                console.log('Error');
            },
            meta: { foo: 'bar' }, // The 'meta' object will be passed to the dataProvider methods
        }}
    >
        <SimpleForm>
            <TextInput source="name" />
        </SimpleForm>
    </CreateNode>
);

react-admin will fetch the entire tree on mount, and the TreeWithDetails component will show an interactive tree based on this data. By default, the <TreeWithDetails> component will use the title field of the records to display te tree. You can use an alternative field by setting the titleField prop in the <TreeWithDetails> component:

const CategoriesList = (props: ListProps) => (
    <TreeWithDetails titleField="name" edit={CategoriesEdit} {...props} />
);
const CategoriesList = (props: ListProps) => (
    <TreeWithDetails titleField="name" edit={CategoriesEdit} {...props} />
);

By default, this package allows only one root per tree. You can allow trees with multiple roots by setting the allowMultipleRoots prop:

export const CategoriesList = (props: ListProps) => (
    <TreeWithDetails
        create={CategoriesCreate}
        edit={CategoriesEdit}
        allowMultipleRoots
        {...props}
    />
);
export const CategoriesList = (props: ListProps) => (
    <TreeWithDetails
        create={CategoriesCreate}
        edit={CategoriesEdit}
        allowMultipleRoots
        {...props}
    />
);

addRootButton

When allowMultipleRoots is set to true or there are no root nodes in the tree, a button is displayed to allow the user to add root nodes. You can pass your own button component using addRootButton prop:

// in src/posts.js
import { CreateButton } from 'react-admin';

export const CategoriesList = () => (
    <TreeWithDetails addRootButton={<CreateButton label="Add Categories!" />}>
        ...
    </TreeWithDetails>
);
// in src/posts.js
import { CreateButton } from 'react-admin';

export const CategoriesList = () => (
    <TreeWithDetails addRootButton={<CreateButton label="Add Categories!" />}>
        ...
    </TreeWithDetails>
);

Tip: You can hide the add root button completly by passing false to addRootButton prop

Title

The default title for a tree view is “[resource] list” (e.g. “Posts list”). Use the title prop to customize the Tree view title:

// in src/posts.js
export const CategoriesList = () => (
    <TreeWithDetails title="List of categories">...</TreeWithDetails>
);
// in src/posts.js
export const CategoriesList = () => (
    <TreeWithDetails title="List of categories">...</TreeWithDetails>
);

The title can be either a string or an element of your own.

Node Actions

By default, every node has an action dropdown menu displayed after its name when hovered.

While this menu only has a delete action by default, it's possible to customize it.

import {
    NodeActions,
    DeleteMenuItem,
    TreeWithDetails,
} from '@react-admin/ra-tree';

const MyCustomActionMenuItem = forwardRef(
    ({ record, resource, parentId }, ref) => {
        const handleClick = () => {
            // Do something with dataProvider ?
        };
        return (
            <MenuItem ref={ref} onClick={handleClick}>
                Do something
            </MenuItem>
        );
    }
);

const MyActions = (props: NodeActionsProps) => (
    <NodeActions {...props}>
        <MyCustomActionMenuItem />
        <DeleteMenuItem />
    </NodeActions>
);

const CategoriesList = () => (
    <TreeWithDetails
        titleField="name"
        edit={CategoriesEdit}
        draggable
        showLine
        nodeActions={<MyActions />}
    />
);
import {
    NodeActions,
    DeleteMenuItem,
    TreeWithDetails,
} from '@react-admin/ra-tree';

const MyCustomActionMenuItem = forwardRef(
    ({ record, resource, parentId }, ref) => {
        const handleClick = () => {
            // Do something with dataProvider ?
        };
        return (
            <MenuItem ref={ref} onClick={handleClick}>
                Do something
            </MenuItem>
        );
    }
);

const MyActions = (props: NodeActionsProps) => (
    <NodeActions {...props}>
        <MyCustomActionMenuItem />
        <DeleteMenuItem />
    </NodeActions>
);

const CategoriesList = () => (
    <TreeWithDetails
        titleField="name"
        edit={CategoriesEdit}
        draggable
        showLine
        nodeActions={<MyActions />}
    />
);

The menu item will receive the current record and the resource.

Drag and Drop

If you want to allow user to reorder nodes in the tree, simply add the draggable prop to the <TreeWithDetails> component:

export const CategoriesList = () => <TreeWithDetails draggable />;
export const CategoriesList = () => <TreeWithDetails draggable />;

Hiding Root Nodes

Sometimes, a tree only has one root node for technical reasons and users should probably not see it at all. Use the hideRootNodes prop to hide all root nodes.

export const CategoriesList = () => <TreeWithDetails hideRootNodes />;
export const CategoriesList = () => <TreeWithDetails hideRootNodes />;

Lazy Load

If you have a tree with a lot of nodes, you may want to only load the root nodes at first and their children when they are expanded. To enable this mode, set the lazy prop to true.

IMPORTANT: When using the lazy mode, you cannot use the 'undoable' mutation mode. Hence, you need to set the mutationMode prop to 'pessimistic' or 'optimistic' on <EditNode>.

import React from 'react';
import { Admin, Resource, SimpleForm, TextField, TextInput } from 'react-admin';

import { EditNode, EditNodeToolbar, TreeWithDetails } from '@react-admin/ra-tree';
import CategoriesCreate from '../CategoriesCreate';
import i18nProvider from '../i18nProvider';
import dataProvider from './dataProvider';

const CategoriesEdit = () => (
    <EditNode mutationMode="pessimistic">
        <SimpleForm toolbar={<EditNodeToolbar />}>
            <TextField source="id" />
            <TextInput source="name" />
        </SimpleForm>
    </EditNode>
);

const CategoriesList = () => (
    <TreeWithDetails
        titleField="name"
        edit={CategoriesEdit}
        create={CategoriesCreate}
        lazy
    />
);

export const App = () => (
    <Admin dataProvider={dataProvider} i18nProvider={i18nProvider}>
        <Resource name="categories" list={CategoriesList} />
    </Admin>
);
import React from 'react';
import { Admin, Resource, SimpleForm, TextField, TextInput } from 'react-admin';

import { EditNode, EditNodeToolbar, TreeWithDetails } from '@react-admin/ra-tree';
import CategoriesCreate from '../CategoriesCreate';
import i18nProvider from '../i18nProvider';
import dataProvider from './dataProvider';

const CategoriesEdit = () => (
    <EditNode mutationMode="pessimistic">
        <SimpleForm toolbar={<EditNodeToolbar />}>
            <TextField source="id" />
            <TextInput source="name" />
        </SimpleForm>
    </EditNode>
);

const CategoriesList = () => (
    <TreeWithDetails
        titleField="name"
        edit={CategoriesEdit}
        create={CategoriesCreate}
        lazy
    />
);

export const App = () => (
    <Admin dataProvider={dataProvider} i18nProvider={i18nProvider}>
        <Resource name="categories" list={CategoriesList} />
    </Admin>
);

Transitions

rc-tree's <Tree> allows to customize the transition effect used when expanding or collapsing a node. However, by default, these transition effects are disabled in react-admin, because they are known to cause issues with the expand on click feature.

If you want to enable them, you can pass the motion prop to the <TreeWithDetails> component:

export const CategoriesList = () => <TreeWithDetails motion />;
export const CategoriesList = () => <TreeWithDetails motion />;

The motion prop also accepts a transition object, allowing you to customize the transition effect:

import { TreeWithDetails } from '@react-admin/ra-tree';
import { CSSProperties } from 'react';

const myMotion = {
    motionName: 'node-motion',
    motionAppear: false,
    onAppearStart: (): CSSProperties => ({ height: 0, width: 0 }),
    onAppearActive: (node: HTMLElement): CSSProperties => ({
        height: node.scrollHeight,
        width: node.scrollWidth,
    }),
    onLeaveStart: (node: HTMLElement): CSSProperties => ({
        height: node.offsetHeight,
        width: node.scrollWidth,
    }),
    onLeaveActive: (): CSSProperties => ({ height: 0, width: 0 }),
};

export const CategoriesList = () => (
    <TreeWithDetails
        motion={myMotion}
        sx={{
            '& .node-motion': {
                transition: 'all .7s',
                overflowX: 'hidden',
                overflowY: 'hidden',
            },
        }}
    />
);
import { TreeWithDetails } from '@react-admin/ra-tree';
import { CSSProperties } from 'react';

const myMotion = {
    motionName: 'node-motion',
    motionAppear: false,
    onAppearStart: (): CSSProperties => ({ height: 0, width: 0 }),
    onAppearActive: (node: HTMLElement): CSSProperties => ({
        height: node.scrollHeight,
        width: node.scrollWidth,
    }),
    onLeaveStart: (node: HTMLElement): CSSProperties => ({
        height: node.offsetHeight,
        width: node.scrollWidth,
    }),
    onLeaveActive: (): CSSProperties => ({ height: 0, width: 0 }),
};

export const CategoriesList = () => (
    <TreeWithDetails
        motion={myMotion}
        sx={{
            '& .node-motion': {
                transition: 'all .7s',
                overflowX: 'hidden',
                overflowY: 'hidden',
            },
        }}
    />
);

<Tree> Component

The <Tree> component is a wrapper for rc-tree's <Tree>, with Material Design style. It expects a treeData prop containing a tree of nodes with key, title, and children fields.

// example usage
import { Tree } from '@react-admin/ra-tree';
import treeData from './treeData';

export const SimpleTree = () => <Tree treeData={treeData} />;

// treeData format
[
    {
        key: '1',
        title: 'foo1',
        children: [
            {
                key: '3',
                title: 'foo3',
                children: [{ key: '5', title: 'foo5', children: [] }],
            },
            { key: '4', title: 'foo4', children: [] },
        ],
    },
    { key: '2', title: 'foo2', children: [] },
];
// example usage
import { Tree } from '@react-admin/ra-tree';
import treeData from './treeData';

export const SimpleTree = () => <Tree treeData={treeData} />;

// treeData format
[
    {
        key: '1',
        title: 'foo1',
        children: [
            {
                key: '3',
                title: 'foo3',
                children: [{ key: '5', title: 'foo5', children: [] }],
            },
            { key: '4', title: 'foo4', children: [] },
        ],
    },
    { key: '2', title: 'foo2', children: [] },
];

<TreeInput> Component

The <TreeInput> component allows to select one or several nodes from a tree.

Usage

Use <TreeInput> in a react-admin form, and pass the possible choices as the treeData prop in the format of the data returned by dataProvider.getTree() (i.e. an array of nodes with a children field).

import { TreeInput } from '@react-admin/ra-tree';
import { SimpleForm } from 'react-admin';
import treeData from './treeData';

export const SimpleTreeForm = () => (
    <SimpleForm>
        <TreeInput source="category" treeData={[
            { id: 1, title: 'Clothing', isRoot: true, children: [2, 6] },
            { id: 2, title: 'Men', children: [3] },
            { id: 3, title: 'Suits', children: [4, 5] },
            { id: 4, title: 'Slacks', children: [] },
            { id: 5, title: 'Jackets', children: [] },
            { id: 6, title: 'Women', children: [7, 10, 11] },
            { id: 7, title: 'Dresses', children: [8, 9] },
            { id: 8, title: 'Evening Gowns', children: [] },
            { id: 9, title: 'Sun Dresses', children: [] },
            { id: 10, title: 'Skirts', children: [] },
            { id: 11, title: 'Blouses', children: [] },
        ]} />
    </SimpleForm>
);
import { TreeInput } from '@react-admin/ra-tree';
import { SimpleForm } from 'react-admin';
import treeData from './treeData';

export const SimpleTreeForm = () => (
    <SimpleForm>
        <TreeInput source="category" treeData={[
            { id: 1, title: 'Clothing', isRoot: true, children: [2, 6] },
            { id: 2, title: 'Men', children: [3] },
            { id: 3, title: 'Suits', children: [4, 5] },
            { id: 4, title: 'Slacks', children: [] },
            { id: 5, title: 'Jackets', children: [] },
            { id: 6, title: 'Women', children: [7, 10, 11] },
            { id: 7, title: 'Dresses', children: [8, 9] },
            { id: 8, title: 'Evening Gowns', children: [] },
            { id: 9, title: 'Sun Dresses', children: [] },
            { id: 10, title: 'Skirts', children: [] },
            { id: 11, title: 'Blouses', children: [] },
        ]} />
    </SimpleForm>
);

Tip: You can use the <TreeInput> component in a <ReferenceNodeInput> to automatically fetch the treeData from a reference resource.

Props

Prop Required Type Default Description
source Required (*) string - The name of the source field - (*) Can be omitted when used inside <ReferenceNodeInput>
multiple - boolean false Set to true to allow selecting multiple nodes
hideRootNodes - boolean false Set to true to hide all root nodes
titleField - string 'title' The name of the field holding the node title

<TreeInput> also accepts the same props as <Tree>, as well as the common input props.

multiple

Use the multiple prop to allow selecting multiple nodes:

import { TreeInput } from '@react-admin/ra-tree';
import { SimpleForm } from 'react-admin';
import treeData from './treeData';

export const SimpleTreeForm = () => (
    <SimpleForm>
        <TreeInput source="category" treeData={treeData} multiple />
    </SimpleForm>
);
import { TreeInput } from '@react-admin/ra-tree';
import { SimpleForm } from 'react-admin';
import treeData from './treeData';

export const SimpleTreeForm = () => (
    <SimpleForm>
        <TreeInput source="category" treeData={treeData} multiple />
    </SimpleForm>
);

hideRootNodes

Use the hideRootNodes prop to hide all root nodes:

<TreeInput 
    source="category" 
    treeData={treeData} 
    hideRootNodes
/>
<TreeInput 
    source="category" 
    treeData={treeData} 
    hideRootNodes
/>

titleField

Use the titleField prop to specify the name of the field holding the node title:

<TreeInput 
    source="category" 
    treeData={treeData} 
    titleField="name" 
/>
<TreeInput 
    source="category" 
    treeData={treeData} 
    titleField="name" 
/>

checkStrictly

By default, <TreeInput> uses the checkStrictly prop from rc-tree's <Tree> component to allow selecting leaf and parent nodes independently. If you want to disable this feature, you can set the checkStrictly prop to false:

<TreeInput 
    source="category" 
    treeData={treeData} 
    multiple 
    checkStrictly={false} 
/>
<TreeInput 
    source="category" 
    treeData={treeData} 
    multiple 
    checkStrictly={false} 
/>

<ReferenceNodeInput> Component

Use the <ReferenceNodeInput> component to select one or several nodes from a tree of a reference resource. For instance, this is useful to select a category for a product.

Usage

Use <ReferenceNodeInput> in a react-admin form, and set the reference and source props just like for a <ReferenceInput>.

import { ReferenceNodeInput } from '@react-admin/ra-tree';
import { Edit, SimpleForm, TextInput } from 'react-admin';

const ProductEdit = () => (
    <Edit>
        <SimpleForm>
            <TextInput source="name" />
            <ReferenceNodeInput
                source="category_id"
                reference="categories"
            />
        </SimpleForm>
    </Edit>
);
import { ReferenceNodeInput } from '@react-admin/ra-tree';
import { Edit, SimpleForm, TextInput } from 'react-admin';

const ProductEdit = () => (
    <Edit>
        <SimpleForm>
            <TextInput source="name" />
            <ReferenceNodeInput
                source="category_id"
                reference="categories"
            />
        </SimpleForm>
    </Edit>
);

<ReferenceNodeInput> is a controller component, i.e. it fetches the tree from the reference resource, creates a tree choices context, and renders its child component.

By default <ReferenceNodeInput> will render a simple <TreeInput> as its child. If you need to customize the <TreeInput> props, e.g. set the multiple prop, you will need to pass the child explicitly:

import { ReferenceNodeInput, TreeInput } from '@react-admin/ra-tree';
import { Edit, SimpleForm, TextInput } from 'react-admin';

const ProductEdit = () => (
    <Edit>
        <SimpleForm>
            <TextInput source="name" />
            <ReferenceNodeInput
                source="category_id"
                reference="categories"
            >
                <TreeInput multiple />
            </ReferenceNodeInput>
        </SimpleForm>
    </Edit>
);
import { ReferenceNodeInput, TreeInput } from '@react-admin/ra-tree';
import { Edit, SimpleForm, TextInput } from 'react-admin';

const ProductEdit = () => (
    <Edit>
        <SimpleForm>
            <TextInput source="name" />
            <ReferenceNodeInput
                source="category_id"
                reference="categories"
            >
                <TreeInput multiple />
            </ReferenceNodeInput>
        </SimpleForm>
    </Edit>
);

Props

Prop Required Type Default Description
children - React Element <TreeInput /> The child component responsible for rendering the input
reference Required string - The reference resource
source Required string - The name of the source field
meta - object - An object containing metadata to be passed when calling the dataProvider

children

<ReferenceNodeInput> accepts only one child, which is responsible for rendering the input. By default, it renders a simple <TreeInput> with no props. If you need to pass additional props to <TreeInput>, you will need to pass them explicitely:

<ReferenceNodeInput
    source="category_id"
    reference="categories"
>
    <TreeInput multiple checkStrictly={false} />
</ReferenceNodeInput>
<ReferenceNodeInput
    source="category_id"
    reference="categories"
>
    <TreeInput multiple checkStrictly={false} />
</ReferenceNodeInput>

meta

Use the meta prop to pass metadata to the dataProvider when calling getTree():

<ReferenceNodeInput 
    source="category_id" 
    reference="categories" 
    meta={{ foo: 'bar' }}
/>
<ReferenceNodeInput 
    source="category_id" 
    reference="categories" 
    meta={{ foo: 'bar' }}
/>

reference

Use the reference prop to specify the reference resource:

<ReferenceNodeInput source="category_id" reference="categories" />
<ReferenceNodeInput source="category_id" reference="categories" />

source

Use the source prop to specify the name of the source field:

<ReferenceNodeInput source="category_id" reference="categories" />
<ReferenceNodeInput source="category_id" reference="categories" />

Customizing Translation Messages

This module uses specific translations for displaying buttons and other texts. As for all translations in react-admin, it's possible to customize the messages.

To create your own translations, you can use the TypeScript types to see the structure and see which keys are overridable.

Here is an example of how to customize translations in your app:

import polyglotI18nProvider from 'ra-i18n-polyglot';
import englishMessages from 'ra-language-english';
import frenchMessages from 'ra-language-french';
import {
    TranslationMessages as BaseTranslationMessages,
    raTreeEnglishMessages,
    raTreeFrenchMessages,
    RaTreeTranslationMessages,
} from '@react-admin/ra-tree';

/* TranslationMessages extends the defaut translation
 * Type from react-admin (BaseTranslationMessages)
 * and the ra-tree translation Type (RaTreeTranslationMessages)
 */
interface TranslationMessages
    extends RaTreeTranslationMessages,
        BaseTranslationMessages {}

const customEnglishMessages: TranslationMessages = mergeTranslations(
    englishMessages,
    raTreeEnglishMessages,
    {
        'ra-tree': {
            action: {
                add_child: 'Add a daughter',
                add_root: 'Add a god',
            },
        },
    }
);

const i18nCustomProvider = polyglotI18nProvider(locale => {
    if (locale === 'fr') {
        return mergeTranslations(frenchMessages, raTreeFrenchMessages);
    }
    return customEnglishMessages;
}, 'en');

export const MyApp = () => (
    <Admin
        dataProvider={myDataprovider}
        layout={myLayout}
        i18nProvider={i18nCustomProvider}
    >
        ...
    </Admin>
);
import polyglotI18nProvider from 'ra-i18n-polyglot';
import englishMessages from 'ra-language-english';
import frenchMessages from 'ra-language-french';
import {
    TranslationMessages as BaseTranslationMessages,
    raTreeEnglishMessages,
    raTreeFrenchMessages,
    RaTreeTranslationMessages,
} from '@react-admin/ra-tree';

/* TranslationMessages extends the defaut translation
 * Type from react-admin (BaseTranslationMessages)
 * and the ra-tree translation Type (RaTreeTranslationMessages)
 */
interface TranslationMessages
    extends RaTreeTranslationMessages,
        BaseTranslationMessages {}

const customEnglishMessages: TranslationMessages = mergeTranslations(
    englishMessages,
    raTreeEnglishMessages,
    {
        'ra-tree': {
            action: {
                add_child: 'Add a daughter',
                add_root: 'Add a god',
            },
        },
    }
);

const i18nCustomProvider = polyglotI18nProvider(locale => {
    if (locale === 'fr') {
        return mergeTranslations(frenchMessages, raTreeFrenchMessages);
    }
    return customEnglishMessages;
}, 'en');

export const MyApp = () => (
    <Admin
        dataProvider={myDataprovider}
        layout={myLayout}
        i18nProvider={i18nCustomProvider}
    >
        ...
    </Admin>
);

API

<Tree> Props

Property Description Type Default Version
autoExpandParent Whether to automatically expand a parent treeNode boolean true
blockNode Whether treeNode fill remaining horizontal space boolean false
checkable Adds a Checkbox before the treeNodes boolean false
checkedKeys (Controlled) Specifies the keys of the checked treeNodes (PS: When this specifies the key of a treeNode which is also a parent treeNode, all the children treeNodes of will be checked; and vice versa, when it specifies the key of a treeNode which is a child treeNode, its parent treeNode will also be checked. When checkable and checkStrictly is true, its object has checked and halfChecked property. Regardless of whether the child or parent treeNode is checked, they won't impact each other. string[] | {checked: string[], halfChecked: string[]} []
checkStrictly Check treeNode precisely; parent treeNode and children treeNodes are not associated boolean false
defaultCheckedKeys Specifies the keys of the default checked treeNodes string[] []
defaultExpandAll Whether to expand all treeNodes by default boolean false
defaultExpandedKeys Specify the keys of the default expanded treeNodes string[] []
defaultExpandParent auto expand parent treeNodes when init bool true
defaultSelectedKeys Specifies the keys of the default selected treeNodes string[] []
disabled whether disabled the tree bool false
draggable Specifies whether this Tree is draggable (IE > 8) boolean false
expandedKeys (Controlled) Specifies the keys of the expanded treeNodes string[] []
filterTreeNode Defines a function to filter (highlight) treeNodes. When the function returns true, the corresponding treeNode will be highlighted function(node) -
loadData Load data asynchronously function(node) -
loadedKeys (Controlled) Set loaded tree nodes. Need work with loadData string[] []
motion Allows to enable and customize the CSS transition effect boolean OR object false 4.2.0
multiple Allows selecting multiple treeNodes boolean false
selectable whether can be selected boolean true
selectedKeys (Controlled) Specifies the keys of the selected treeNodes string[] -
showIcon Shows the icon before a TreeNode's title. There is no default style; you must set a custom style for it if set to true boolean false
switcherIcon customize collapse/expand icon of tree node ReactNode -
showLine Shows a connecting line boolean false
treeData treeNodes data Array, if set it then you need not to construct children TreeNode. (key should be unique across the whole array) array<{ key, title, children, [disabled, selectable] }> -
virtual Disable virtual scroll when set to false boolean true 4.1.0
onCheck Callback function for when the onCheck event occurs function(checkedKeys, e:{checked: bool, checkedNodes, node, event, halfCheckedKeys}) -
onDragEnd Callback function for when the onDragEnd event occurs function({event, node}) -
onDragEnter Callback function for when the onDragEnter event occurs function({event, node, expandedKeys}) -
onDragLeave Callback function for when the onDragLeave event occurs function({event, node}) -
onDragOver Callback function for when the onDragOver event occurs function({event, node}) -
onDragStart Callback function for when the onDragStart event occurs function({event, node}) -
onDrop Callback function for when the onDrop event occurs function({event, node, dragNode, dragNodesKeys}) -
onExpand Callback function for when a treeNode is expanded or collapsed function(expandedKeys, {expanded: bool, node}) -
onLoad Callback function for when a treeNode is loaded function(loadedKeys, {event, node}) -
onRightClick Callback function for when the user right clicks a treeNode function({event, node}) -
onSelect Callback function for when the user clicks a treeNode function(selectedKeys, e:{selected: bool, selectedNodes, node, event}) -

CHANGELOG

v4.4.9

2023-10-25

  • Update lazy tree loading icon from hourglass to circular progress

v4.4.8

2023-09-21

  • Fix expanded child nodes automatically expand their parent on mount
  • Fix re-expanding a previously expanded then closed node in lazy mode put its children at root
  • TreeWithDetails now set the expandAction prop to false by default to prevent closing the children of an opened node when selecting it. If you need to, you can restore the previous behavior by setting expandAction="click".

v4.4.7

2023-09-05

  • Fix cannot exit node creation

v4.4.6

2023-09-01

  • Fix double loading icon in lazy mode

v4.4.5

2023-09-01

  • Fix TreeInput selected value is converted to string

v4.4.4

2023-08-29

  • (fix) Lazy mode: Fix tree renders incorrectly after adding a new node
  • (fix) Lazy mode: Fix tree is not updated after editing a node
  • (fix) Lazy mode: Tree data can contain duplicates after expanding the same node multiple times
  • (fix) Lazy mode: Don't offer to expand nodes when we already know they have no children

v4.4.3

2023-08-25

  • (fix) Lazy mode: Fix <Tree> should not load expanded nodes if their parent is collapsed
  • (fix) Lazy mode: Fix <Tree> should load all expanded nodes even when parent nodes are already cached

v4.4.2

2023-08-21

  • (fix) Fix missing useEditNodeController export

v4.4.1

2023-07-17

  • (fix) Fix usage of es2023 feature.

v4.4.0

2023-06-02

  • (feat) Introduce <TreeInput> and <ReferenceNodeInput> components
  • Reduce tree margins to improve information density and match material-ui default margins

v4.3.0

2023-05-24

  • Upgraded to react-admin 4.10.6

v4.2.0

2023-04-03

  • (fix) Fix <TreeWithDetails> can sometimes freeze collapsing/expanding of nodes after clicking on a leaf node

Breaking Change

CSS animations are now disabled by default. To re-enable them, you must pass the motion prop to the <TreeWithDetails> component.

export const CategoriesList = () => <TreeWithDetails motion />;
export const CategoriesList = () => <TreeWithDetails motion />;

v4.1.6

2023-01-25

  • Update rc-tree dependency

v4.1.5

2022-12-14

  • (feat) Add meta to TreeWithDetails props
  • (fix) Fix <TreeWithDetails> lazy loading with previously set expanded keys
  • (fix) Fix addTreeMethodsBasedOnChildren methods did not pass meta to all dataProvider methods
  • (fix) Fix addTreeMethodsBasedOnParentAndPosition methods did not pass meta to all dataProvider methods

v4.1.4

2022-11-21

  • (fix) Fix default success side effect when creating a new node causes an error with RA version 4.4.0 and above
  • (fix) Fix <NodeActions> should stop propagating the click event on close

v4.1.3

2022-09-29

  • (fix) Fix moving child outside its parent with addTreeMethodsBasedOnChildren

v4.1.2

2022-09-26

  • (fix) Fix addTreeMethodsBasedOnChildren and addTreeMethodsBasedOnParentAndPosition not allowing meta to be omitted in getTree and getRootNodes

v4.1.1

2022-09-19

  • (fix) Fix useMoveAsNthSiblingOf query cache update when moving an item from top to bottom on the same branch

v4.1.0

2022-09-15

  • (feat) Add support for meta in ra-tree hooks and components

v4.0.5

2022-09-08

  • (fix) Fix useEditNodeController is not using recordRepresentation
  • (fix) Fix moveAsNthSiblingOf reordering is wrong when source is below target in addTreeMethodsBasedOnParentAndPosition
  • (fix) Fix moveAsNthSiblingOf reordering is wrong when source is below target in addTreeMethodsBasedOnChildren
  • (fix) Fix moving at top position of the current branch with moveAsNthChildOf in addTreeMethodsBasedOnChildren
  • (doc) Improve documentation about moveAsNthChildOf and moveAsNthSiblingOf

v4.0.4

2022-07-01

  • (fix) UI corrections (alignement, width)

v4.0.3

2022-06-29

  • Fix: Replace classnames with clsx

v4.0.2

2022-06-08

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

v4.0.1

2022-06-08

  • (fix) <EditNode> form child must take a custom toolbar to have the <DeleteButton> delete a branch

v4.0.0

2022-06-07

  • Upgraded to react-admin v4
  • Upgraded to rc-tree v5.6.1

Breaking changes

The Signatures of all the DataProvider Hooks Have Changed

First, all hooks signatures now reflect their matching dataProvider method signature (so every hook now takes 3 arguments, resource, params and options). The params can also contain the newly supported meta property. This property could be used to pass additional parameters not supported by default by our hooks and default dataProviders.

import { useGetTree } from '@react-admin/ra-tree';

const Categories = () => {
-  const { data: tree, loading, loaded, error } = useGetTree('categories', options);
+    const { data: tree, isLoading, error } = useGetTree('categories', options);
-  if (!loaded) { return <Loading />; }
+    if (isLoading) { return <Loading />; }
    if (error) { return <p>ERROR</p>; }
    return <Tree tree={data} />;
};

Second, the loading property returned by these hooks has been renamed to isLoading. The loaded property has been removed.

import { useGetTree } from '@react-admin/ra-tree';

const Categories = () => {
-  const { data: tree, loading, loaded, error } = useGetTree('categories');
+    const { data: tree, isLoading, error } = useGetTree('categories');
-  if (!loaded) { return <Loading />; }
+    if (isLoading) { return <Loading />; }
    if (error) { return <p>ERROR</p>; }
    return <Tree tree={data} />;
};

Finally, these hooks are now powered by react-query, so the state argument contains way more than just isLoading (reset, status, refetch, etc.). Check the useQuery and the useMutation documentation on the react-query website for more details.

onSuccess And onFailure Props Have Moved

If you need to override the success or failure side effects of a component, you now have to use the queryOptions (for query side effects) or mutationOptions (for mutation side effects).

For instance, here is how to customize the side effects on the deleteBranch mutation in <DeleteMenuItem>:

const MyDeleteMenuItem = () => {
    const onSuccess = () => {
        // do something
    };
    const onFailure = () => {
        // do something
    };
    return (
        <DeleteMenuItem
-         onSuccess={onSuccess}
-         onFailure={onFailure}
+           mutationOptions={{
+               onSuccess: onSuccess,
+               onError: onFailure
+           }}
        />
    );
};

Note that the onFailure prop was renamed to onError in the options, to match the react-query convention.

The change concerns the following components:

  • useGetTree
  • useGetTreeCallback
  • useGetRootNodes
  • useGetChildNodesCallback
  • useAddRootNode
  • useAddChildNode
  • useDeleteBranch
  • useMoveAsNthChildOf
  • useMoveAsNthSiblingOf
  • useDeleteBranchWithConfirmController
  • useDeleteBranchWithUndoController

It also affects the save callback returned by the useSaveContext hook:

const MyButton = () => {
    const { save, saving } = useSaveContext();
    const notify = useNotify();

    const handleClick = () => {
        save({ value: 123 }, {
-          onFailure: (error) => {
+            onError: (error) => {
                notify(error.message, { type: 'error' });
            },
        });
    };

    return <button onClick={handleClick}>Save</button>
}

onSuccess Callback On DataProvider Hooks And Components Has A New Signature

The onSuccess callback used to receive the response from the dataProvider. On specialized hooks, it now receives the data property of the response instead.

const [deleteBranch] = useDeleteBranch();
const DeleteBranchButton = () => {
    const handleClick = () => {
        deleteBranch(
            'categories',
            { id: 123, data: { title: 'abc' } },
            {
-             onSuccess: ({ data }) => {
+               onSuccess: (data) => {
                    // do something with data
                }
            }
        );
    }
};

The change concerns the following components:

  • useGetTree
  • useGetTreeCallback
  • useGetRootNodes
  • useGetChildNodesCallback
  • useAddRootNode
  • useAddChildNode
  • useDeleteBranch
  • useMoveAsNthChildOf
  • useMoveAsNthSiblingOf
  • useDeleteBranchWithConfirmController
  • useDeleteBranchWithUndoController

<SimpleForm> Isn't Exported Anymore, Use <EditNodeToolbar> instead

In edition views, the previous version adised using an alternative <SimpleForm> to replace the <DeleteButton> by a version that deletes the current branch rather than the current node. In the new version, you have to use react-amin's <SimpleForm> and pass a custom toolbar instead:

import {
    Admin,
    Resource,
    Create,
    Edit,
+   SimpleForm,
    TextInput,
} from 'react-admin';
import {
    CreateNode,
    EditNode,
+   EditNodeToolbar,
- SimpleForm,
    TreeWithDetails,
} from '@react-admin/ra-tree';

const CategoriesEdit = () => (
    <EditNode>
-     <SimpleForm>
+       <SimpleForm toolbar={<EditNodeToolbar />}>
            <TextInput source="title" />
        </SimpleForm>
    </EditNode>
);

<DeleteButton> Was Renamed to <DeleteBranchButton>

To avoid conflicts, ra-tree no longer exports a <DeleteButton> commponent. Use <DeleteBranchButton> instead.

import { Toolbar, ToolbarProps } from 'react-admin';
-import { DeleteButton } from '@react-admin/ra-tree';
+import { DeleteBranchButton } from '@react-admin/ra-tree';

import MyCustomButton from './MyCustomButton';

export const MyToolbar = (props: ToolbarProps) => (
    <Toolbar>
        <MyCustomButton />
-     <DeleteButton />
+       <DeleteBranchButton />
    </Toolbar>
);

v1.6.14

2021-12-22

  • (feat) Add addRootButton to TreeWithDetails to customize the add root button.

v1.6.13

2021-12-22

  • (fix) useGetRootNodes doesn't return root nodes in the right order.

v1.6.12

2021-12-09

  • (fix) allow Toolbar to override its children.
  • (fix) deprecated material-ui symbols

v1.6.11

2021-11-10

  • (fix) pass all props in every view from TreeWithDetails.
  • (fix) ensure TreeWithDetails styles are overridable.
  • (fix) ensure EditNode actions are overridable.

v1.6.10

2021-10-26

  • (fix) builders now correctly apply their options (childrenField, parentField, etc.)

v1.6.9

2021-10-18

  • Update dataProvider usage for react-admin 3.19.0

v1.6.8

2021-09-21

  • (fix) Fix TreeWithDetails props type doesn't include classes

v1.6.7

2021-07-20

  • (fix) Fix nodeActions are invisible

v1.6.6

2021-07-20

  • (fix) Fix lazy loading support which required nodes to have their children identifiers populated

v1.6.5

2021-06-29

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

v1.6.4

2021-06-15

  • (fix) Fix useGetRootNodes and useGetTree are disabled by default.

v1.6.3

2021-05-03

  • (fix) Fix scrollbars appearing when expanding nodes

v1.6.2

2021-05-03

  • (fix) Fix reordering in the dataProviders builders.

v1.6.1

2021-04-26

  • (performances) Replace MUI boxes by div with styles.

v1.6.0

2021-04-22

  • (feat) Add lazy load mode on <TreeWithDetails> with the lazy prop which accept a boolean.

In lazy mode, the tree will only load the root nodes on mount and will load the child nodes of a node when it expands.

v1.5.0

2021-04-02

  • (feat) Allow to easily hide root nodes on <TreeWithDetails> with the hideRootNodes props.

v1.4.1

2021-04-01

  • (fix) Fix TreeWithDetails displays multiple titles after selecting a node

v1.4.0

2021-03-31

  • (feat) Add support for title in <TreeWithDetails>
  • (feat) Fix and export more component props interfaces

v1.3.4

2021-03-17

  • (fix) Fix moving a root node fails when using the addTreeMethodsBasedOnChildren builder

v1.3.3

2021-02-16

  • (fix) Update for react-admin 3.12

v1.3.2

2020-12-14

  • Remove unnecessary dependencies

v1.3.1

2020-11-18

  • Upgrade to react-admin 3.10

v1.3.0

2020-10-05

  • Upgrade to react-admin 3.9

v1.2.6

2020-09-30

  • (fix) Ensure we don't prevent non tree records from being deleted

v1.2.5

2020-09-30

  • Update Readme

v1.2.4

2020-09-28

  • (fix) Fix warnings regarding React refs
  • (fix) Fix warnings about unknown props in Toolbar

v1.2.3

2020-09-28

  • (fix) Fix redirection side effect for DeleteMenuItem components

v1.2.2

2020-09-25

  • (fix) Fix default redirect for DeleteMenuItem components

v1.2.1

2020-09-23

  • (fix) Fix the style of the default Toolbar.

v1.2.0

2020-09-18

  • (feat) Provides SimpleForm, TabbedForm, Toolbar and DeleteButton components for use inside a view (Edit and Show for example).

v1.1.1

2020-09-17

  • (fix) Fix Deletion components are not exported
  • (fix) Fix TypeScript types for Deletion components props
  • (fix) Fix Deletion components should have their redirect prop set to false by default

v1.1.0

2020-09-17

  • (feat) Add support for branch deletion

v1.0.2

2020-09-16

  • (deps) Upgrade dependencies

v1.0.1

2020-08-27

  • (fix) Fix dark mode support

v1.0.0

2020-08-03

  • First release