React-admin Datagrid: expand all rows by default

Viewed 873

I have a react-admin (3.14.1) List using a Datagrid, where each row is expandable.

Does anyone know how to expand all the rows by default?

Or expand a row programmatically?

I've checked the Datagrid code in node_modules/ra-ui-materialui/lib/list/datagrid/Datagrid.js, no obvious props...

Datagrid.propTypes = {
    basePath: prop_types_1.default.string,
    body: prop_types_1.default.element,
    children: prop_types_1.default.node.isRequired,
    classes: prop_types_1.default.object,
    className: prop_types_1.default.string,
    currentSort: prop_types_1.default.shape({
        field: prop_types_1.default.string,
        order: prop_types_1.default.string,
    }),
    data: prop_types_1.default.any,
    // @ts-ignore
    expand: prop_types_1.default.oneOfType([prop_types_1.default.element, prop_types_1.default.elementType]),
    hasBulkActions: prop_types_1.default.bool,
    hover: prop_types_1.default.bool,
    ids: prop_types_1.default.arrayOf(prop_types_1.default.any),
    loading: prop_types_1.default.bool,
    onSelect: prop_types_1.default.func,
    onToggleItem: prop_types_1.default.func,
    resource: prop_types_1.default.string,
    rowClick: prop_types_1.default.oneOfType([prop_types_1.default.string, prop_types_1.default.func]),
    rowStyle: prop_types_1.default.func,
    selectedIds: prop_types_1.default.arrayOf(prop_types_1.default.any),
    setSort: prop_types_1.default.func,
    total: prop_types_1.default.number,
    version: prop_types_1.default.number,
    isRowSelectable: prop_types_1.default.func,
    isRowExpandable: prop_types_1.default.func,
};
3 Answers

I have a solution that does something similar, not using jquery. It's a custom hook that makes the first id of a resource expand, which in my case is the first item in the List.

// useExpandFirst.ts

import * as React from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { Identifier, ReduxState, toggleListItemExpand } from 'react-admin';

type AboutExpansion = { noneExpanded: boolean; firstId: Identifier };

const useExpandFirst = (props) => {
  const { resource } = props;
  const once = React.useRef(false);

  const dispatch = useDispatch();
  const { noneExpanded, firstId } = useSelector<ReduxState, AboutExpansion>((state) => {
    const list = state?.admin?.resources?.[resource]?.list;

    return {
      noneExpanded: list?.expanded?.length === 0,
      firstId: list?.ids[0],
    };
  });

  React.useEffect(() => {
    if (noneExpanded && firstId && !once.current) {
      once.current = true;
      const action = toggleListItemExpand(resource, firstId);
      dispatch(action);
    }
  }, [noneExpanded, firstId, dispatch, resource]);
};

Instead of using the hook in the component that actually renders the List, I'm using it in some other (not so) random component, for example the app's Layout. That causes way less rerenders of the component that renders the List.

// MyLayout.tsx

const MyLayout: React.FC<LayoutProps> = (props) => {
  // expand the first company record as soon as it becomes available
  useExpandFirst({ resource: 'companies' });

  return (
    <Layout
      {...props}
      appBar={MyAppBar}
      sidebar={MySidebar}
      menu={MyMenu}
      // notification={MyNotification}
    />
  );
};

It's not perfect, but it does the job. With just a few modifications, you can alter it to expand all id's. That would mean that you have to dispatch the action for each id (in the useEffect hook function).

I found this question and used this answer to solve the same problem.

export const useExpandDefaultForAll = (resource) => {
    const [ids, expanded]  = useSelector(
        (state) => ([state.admin.resources[resource].list.ids, state.admin.resources[resource].list.expanded])
    );
    const dispatch = useDispatch();
    useEffect(() => {
        for (let i = 0; i < ids.length; i++) {
            if (!expanded.includes(ids[i])){
                dispatch(toggleListItemExpand(resource, ids[i]));
            }
        }
    }, [ids]);
};

And i also call it in my List component:

const OrderList = (props) => {
    useExpandDefaultForAll(props.resource);
... 

I will be glad if it is useful to someone. If you know how to do it better then please fix it.

Hacked it with jquery, dear dear.

import $ from 'jquery'
import React, {Fragment} from 'react';
import {List, Datagrid, TextField, useRecordContext} from 'react-admin';

export class MyList extends React.Component {

    gridref = React.createRef()

    ensureRowsExpanded(ref) {
        // Must wait a tick for the expand buttons to be completely built
        setTimeout(() => {
            if (!ref || !ref.current) return;

            const buttonSelector = "tr > td:first-child > div[aria-expanded=false]"
            const buttons = $(buttonSelector, ref.current)
            buttons.click()
        }, 1)
    }

    /** 
     * This runs every time something changes significantly in the list,
     * e.g. search, filter, pagination changes.
     * Surely there's a better way to do it, i don't know!
    */
    Aside = () => {
        this.ensureRowsExpanded(this.gridref)
        return null;
    }

    render = () => {
        return <List {...this.props} aside={<this.Aside/>} >
                    <Datagrid 
                        expand={<MyExpandedRow />}
                        isRowExpandable={row => true}  
                        ref={this.gridref}
                    >
                        <TitleField source="title" />
                    </Datagrid>
               </List>
    }
}

const MyExpandedRow = () => {
    const record = useRecordContext();
    if (!record) return "";
    return <div>Hello from {record.id}</div>;
}

Relies on particular table structure and aria-expanded attribute, so not great. Works though.

Related