3 votes

Comment créer un tableau arborescent avec react js en utilisant la récursion ?

Je viens de commencer à apprendre React

Je travaille sur une solution où je veux créer un arbre comme un tableau en utilisant react.

En gros, la fonctionnalité est la suivante : il y a un tableau simple avec chaque ligne ayant une icône d'expansion conditionnelle, j'ai réalisé la fonctionnalité de base de rendre un autre composant de ligne lorsque je clique sur la ligne.

La solution de base que j'ai réalisée est la suivante : chaque fois que l'utilisateur clique sur une ligne, j'appelle la fonction expand row, j'ajoute les enfants statiques sous le tableau de la ligne cliquée et je les affiche en passant l'objet children au composant subrow.

Maintenant, je veux que chaque fois que l'utilisateur clique sur les enfants de la rangée étendue, elle doit s'étendre au niveau suivant en affichant les données liées à la deuxième extension.

Donc, en gros, cela ressemblera à

- Row 1
  - Child 1
    - Child 1.1
    - Child 1.2
  + Child 2
+ Row 2

J'ai créé un prototype de base utilisant des données json statiques à partir d'apis jsonplaceholder.

Voici le code

App.js

import React, { useState, Fragment } from "react";
import TableRowData from "./TableRowData";
import "./styles.css";

export default function App() {
  const [splits, setSplit] = useState(["campid", "appid", "os"]);
  return (
    <Fragment>
      <table>
        <thead>
          <tr>
            <th>Id</th>
            <th>Name</th>
          </tr>
        </thead>
        <tbody>
          <TableRowData avaliableSplits={splits} />
        </tbody>
      </table>
    </Fragment>
  );
}

TableRowData.js

import React, { useState, useEffect, useRef, Fragment } from "react";
import axios from "axios";
import SubRow from "./SubRow";

class TableRowData extends React.Component {
  state = { data: [] };
  constructor(props) {
    super(props);
  }

  componentDidMount() {
    axios.get("https://jsonplaceholder.typicode.com/users").then((res) => {
      this.setState({ data: res.data });
    });
  }
  render() {
    const updateState = (id, itemAttributes) => {
      var index = this.state.data.findIndex((x) => x.id === id);
      if (index !== -1) {
        this.setState({
          data: [
            ...this.state.data.slice(0, index),
            Object.assign({}, this.state.data[index], itemAttributes),
            ...this.state.data.slice(index + 1)
          ]
        });
      }
    };

    const expandRow = (user) => {
      user.children = [
        { id: "6656", name: "sfsdfds1" },
        { id: "66563", name: "sfsdfds2" }
      ];

      //    this.setState({data:[...this.state.data],})
      updateState(user.id, { isExpanded: true });
    };

    const collapseRow = (user) => {
      user.children = undefined;
      updateState(user.id, { isExpanded: false });
    };

    if (this.state.data) {
      const appData = this.state.data.map((user) => {
        return (
          <Fragment key={user.id}>
            <tr key={user.id}>
              <td>
                {user.isExpanded === true ? (
                  <button type="button" onClick={() => collapseRow(user)}>
                    -
                  </button>
                ) : (
                  <button type="button" onClick={() => expandRow(user)}>
                    +
                  </button>
                )}
                {user.id}
              </td>
              <td>{user.name}</td>
            </tr>
            {!!user.children && <SubRow rowData={user.children} />}
          </Fragment>
        );
      });
      return <Fragment>{appData}</Fragment>;
    } else {
      return null;
    }
  }
}

export default TableRowData;

SubRow.js

import React, { useState, useEffect, useRef, Fragment } from 'react';
import axios from 'axios';

const SubRow = (props) => {
    const appData = props.rowData.map((user) => {
            user.isExpanded = false;
            return (
                    <Fragment key={user.id}>
                        <tr key={user.id}>
                            <td><button type='button' onClick={()=>handleClick(user,props.reportData)}>+</button>{user.id}</td>
                            <td>{user.name}</td>
                        </tr>
                         {!!user.children && <SubRow rowData={user.children} />}
                    </Fragment>
                  )

             });

        return (
            <Fragment>{appData}</Fragment>
        )
}

 export default SubRow

Voici l'implémentation de codesandbox Table imbriquée

Je ne veux pas utiliser de paquets externes pour cela. Veuillez m'aider.

Ajout d'un scénario conditionnel d'expansion et d'effondrement

Je veux faire une expansion conditionnelle basée sur le tableau que je maintiens.

Disons que j'ai un tableau splits [a,b,c], si la valeur est fixée à ce tableau, la ligne de premier niveau aura des données liées à A. Maintenant, chaque fois que l'utilisateur clique sur B, je vais faire une requête AJAX avec les données de la ligne de A et afficher les lignes de B avec une icône d'expansion car ce sera un tableau arborescent à 3 niveaux, de même chaque fois que l'utilisateur clique sur C, je vais envoyer les données de b et récupérer les données de C. Maintenant, D n'aura pas d'expansion supplémentaire car la taille du tableau est de 3, chaque fois que l'utilisateur ajoute un 4e élément dans le tableau, je dois voir l'icône d'expansion sur C.

Attemp 1 :

import React, { useState, useEffect, useRef, Fragment } from "react";
import _ from 'lodash';
import axios from "axios";
import {connect} from 'react-redux';
import {getChildren} from '@src/redux/actions/reports';

class TableRowData extends React.Component {
  state = {
    showIcon: false,
    selection: [],
    data: []
  };
  constructor(props) {
    super(props);
  }

  componentDidMount() {
    axios.get("https://jsonplaceholder.typicode.com/users").then((res) => {
          const rowData = res.data.map((row) => {
              row.isExpanded = false;
              return row;
          });
          this.setState({ data: rowData });
    });

  }

  render() {
    const updateState = (id, itemAttributes) => {
      var index = this.state.data.findIndex((x) => x.id === id);
      if (index !== -1) {
        this.setState({
          data: [
            ...this.state.data.slice(0, index),
            Object.assign({}, this.state.data[index], itemAttributes),
            ...this.state.data.slice(index + 1)
          ]
        });
      }
    };

    const expandRow = (row) => {
      const index = _(this.state.data)
        .thru(function(coll) {
            return _.union(coll, _.map(coll, 'children') || []);
        })
        .flattenDeep()
        .findIndex({ id: row.id });

       if (index !== -1) {
        let prevState = [...this.state.data];
        let el = _(prevState)
          .thru(function(coll) {
              return _.union(coll, _.map(coll, 'children') || []);
          })
          .flattenDeep()
          .find({ id: row.id });
        el.children = [
          { id: '_' + Math.random().toString(36).substr(2, 5), name: row.id+"_ID1", isExpanded:false,parentId:row.id },
          { id: '_' + Math.random().toString(36).substr(2, 5), name: row.id+"_ID2ß",isExpanded:false,parentId:row.id },
        ];
        el.isExpanded=true;
        this.setState({data:[...this.state.data],prevState},()=>{})
      }
    };

    const collapseRow = (user) => {
      delete user.children
    //  updateState(user.id, { children: {id:1,name:'JANAK'} });
      updateState(user.id, { isExpanded: false });
    };

    const ExpandableTableRow = ({rows}) => {
      //console.log(rows);
      if (rows) {
          return rows.map((row) => {
          let children = null;

            return (
                <Fragment key={row.id}>
                  <tr key={row.id}>
                  <td>
                      <ExpandCollapsToggle row={row} /> {row.id}
                  </td>
                    <td>{row.name}</td>
                </tr>
                  <ExpandableTableRow rows={row.children} />
              </Fragment>
            )
           }
        );

        } else {
          return null;
        }
    };

    const ExpandCollapsToggle = ({row,actions}) => {
        if(row.isExpanded === true) {
              return (<button type="button" onClick={() => collapseRow(row)}>-</button>)
          } else {
              return (<button type="button" onClick={() => expandRow(row)}>+</button>)
          }
    }

    if (this.state.data) {
        return (
          <Fragment>
              <ExpandableTableRow rows={this.state.data} />
          </Fragment>
        );
    } else {
      return null;
    }
  }
}
const mapStateToProps = (state) => {
  return {"data":state.reportReducer.data};
//  return state;
}
export default connect(mapStateToProps,{getChildren})(TableRowData);

3voto

Chandan Points 5836

Solution 1

import React, { useRef, useState, useEffect } from "react";
import axios from 'axios';
import "./style.css";

function ExpandableRow({
  loadData = new Promise(resolve => resolve([])),
  onExpand,
  columns,
  id,
  row
  }) {
  /* added condition to test if not first render
   * if yes then added data through reference which
   * will update the state but because state is
   * updated directly render would not occur
   * to handle this issue `onExpand` function
   * is passed from parent to child which will update
   * state with new data which will intern cause rendering
   */
  const [rowState, setRowState] = useState(1);
  const didMount = useRef(false);

  useEffect(() => {
    if (didMount.current) {
      if (row.hasOwnProperty("children")) {
        delete row["children"];
        onExpand("remove", id, row);
      } else {
        setRowState(2);
        loadData(id, row).then(data => {
          row['children'] = data;
          onExpand("add", id, row);
          setRowState(3);
        });
      }
    } else didMount.current = true;
  }, [rowState]);

  return (
    <tr>
      <td>
        <button
          type="button"
          onClick={() => {
            setRowState(rowState == 3 ? 1 : 3);
          }}
        >
          {rowState == 2 ? "o" : rowState == 3 ? '-' : "+"}
        </button>
      </td>

      {columns.map((column, idx) => (
        <td key={idx}>{row[column]}</td>
      ))}
    </tr>
  );
}

// function for recursively creating table rows
function ExpandableRowTable({ loadData, onExpand, columns, rows }) {
  return rows.map((row, idx) => {
    const actions = {
      loadData,
      onExpand
    };

    return (
      <React.Fragment>
        <ExpandableRow
          id={idx}
          columns={columns}
          row={row}
          {...actions}
        />
        {row.children && (
          <ExpandableRowTable
            columns={columns}
            rows={row.children}
            {...actions}
          />
        )}
      </React.Fragment>
    );
  });
}

function apiData() {
  return axios.get("https://jsonplaceholder.typicode.com/users")
  .then((res) => {
    return res.data;
  });
}

let count = 0;
const columns = ["id", "name"];
export default function App() {
  const [rows, setRows] = useState([]);

  useEffect(() => {
    apiData().then((data) => {
      count = data.length;
      setRows(data)
    })
    .catch(err => {
      console.log(err);
    })
  }, []);

  return (
    <div>
      <table>
        <thead>
          <tr>
            <th />
            {columns.map((c, idx) => (
              <th key={idx}>{c}</th>
            ))}
          </tr>
        </thead>
        <tbody>
          <ExpandableRowTable
            loadData={(id, row) => {
              return new Promise((resolve) => {
                setTimeout(() => {
                  resolve([
                    {
                      id: count + 1,
                      name: `- ${row["name"]}`
                    },
                    {
                      id: count + 2,
                      name: `- ${row["name"]}`
                    }
                  ]);
                  count += 2
                }, 1000);
              })
            }}
            onExpand={(action, id, row) => {
              if (action == "remove") {
                count -= 2
              }
              setRows([...rows]);
            }}
            columns={columns}
            rows={rows}
          />
        </tbody>
      </table>
    </div>
  );
}

Solution 1 : travail exemple

Solution 2

import React, {
  createContext,
  useContext,
  useRef,
  useState,
  useReducer,
  useEffect
} from "react";
import axios from "axios";
import "./style.css";

const data = {
  rows: [
    {
      id: 1,
      name: "leanne graham"
    },
    {
      id: 2,
      name: "ervin howell"
    },
    {
      id: 3,
      name: "clementine bauch"
    }
  ],
  options: {
    "": null,
    cities: [
      {
        id: 1,
        name: "delhi"
      },
      {
        id: 2,
        name: "pune"
      },
      {
        id: 3,
        name: "hyderabad"
      }
    ],
    network: [
      {
        id: 1,
        name: "unknown"
      },
      {
        id: 2,
        name: "known"
      },
      {
        id: 3,
        name: "KNOWN"
      }
    ]
  }
};

const initialState = {
  showIcon: false,
  selection: [],
  rows: []
};

const store = createContext(initialState);
const { Provider, Consumer } = store;

const StateProvider = ({ children }) => {
  const [state, dispatch] = useReducer((state, action) => {
    // console.log(state.rows);
    switch (action.type) {
      case "ADD_SELECTION":
        if (state.selection.indexOf(action.payload) == -1) {
          const newState = {...state};
          newState['selection'].push(action.payload);
          return newState;
        }
        return state;
      case "SET_ROWS":
        return Object.assign({}, state, {
          rows: action.payload
        });
      case "TOGGLE_ICON":
        return Object.assign({}, state, {
          showIcon: !state.showIcon
        });
      default:
        return state;
    }
  }, initialState);

  return (
    <Provider
      value={{
        state,
        dispatch,
        toggleIcon: () => {
          dispatch({
            type: "TOGGLE_ICON"
          });
        },
        addSelection: value => {
          dispatch({
            type: "ADD_SELECTION",
            payload: value
          });
        },
        setRows: rows => {
          dispatch({
            type: "SET_ROWS",
            payload: rows
          });
        }
      }}
    >
      {children}
    </Provider>
  );
};
/*
1 - Loading (o)
2 - Plus    (+)
3 - Minus   (-)
*/
function ExpandableRow({
  showIcon,
  loadData = new Promise(resolve => resolve([])),
  onExpand,
  columns,
  id,
  row
}) {
  /* added condition to test if not first render
   * if yes then added data through reference which
   * will update the state but because state is
   * updated directly render would not occur
   * to handle this issue `onExpand` function
   * is passed from parent to child which will update
   * state with new data which will intern cause rendering
   */
  const [rowState, setRowState] = useState(2);
  const didMount = useRef(false);

  useEffect(() => {
    if (didMount.current) {
      if (row.hasOwnProperty("children")) {
        if (rowState == 2) {
          delete row["children"];
          onExpand("remove", id, row);
        }
      } else {
        setRowState(1);
        didMount.current = false;
        loadData(id, row).then(_data => {
          row["children"] = _data;
          onExpand("add", id, row);
          setRowState(3);
        });
      }
    } else didMount.current = true;
  }, [rowState]);

  return (
    <tr>
      <td>
        {showIcon && (
          <button
            type="button"
            onClick={() => {
              setRowState(rowState == 3 ? 2 : 3);
            }}
          >
            {rowState == 1 ? "o" : rowState == 3 ? "-" : "+"}
          </button>
        )}
      </td>

      {columns.map((column, idx) => (
        <td key={idx}>{row[column]}</td>
      ))}
    </tr>
  );
}

// function for recursively creating table rows
function ExpandableRowTable({ showIcon, loadData, onExpand, columns, rows }) {
  return rows.map((row, idx) => {
    const actions = {
      loadData,
      onExpand
    };

    return (
      <React.Fragment key={idx}>
        <ExpandableRow
          id={idx}
          showIcon={showIcon}
          columns={columns}
          row={row}
          {...actions}
        />
        {row.children && (
          <ExpandableRowTable
            showIcon={showIcon}
            columns={columns}
            rows={row.children}
            {...actions}
          />
        )}
      </React.Fragment>
    );
  });
}

function apiData(requestData) {
  console.log(requestData);
  return axios.get("https://jsonplaceholder.typicode.com/users").then(res => {
    return res.data;
  });
}

const columns = ["id", "name"];
function Select(props) {
  const { state, toggleIcon, addSelection } = useContext(store);
  const { selection } = state;
  return (
    <div>
      <div>{selection.join(", ")}</div>
      <select
        value={""}
        onChange={evt => {
          selection[selection.length - 1] != evt.target.value && toggleIcon();
          addSelection(evt.target.value);
        }}
      >
        {Object.keys(data.options).map((c, idx) => (
          <option key={idx}>{c}</option>
        ))}
      </select>
    </div>
  );
}

function Table(props) {
  const { state, toggleIcon, setRows } = useContext(store);
  const { rows, selection } = state;

  useEffect(() => {
    // apiData().then((data) => {
    setRows(data.rows);
    // })
    // .catch(err => {
    //   console.log(err);
    // })
  }, []);

  return (
    <table>
      <thead>
        <tr>
          <th />
          {columns.map((c, idx) => (
            <th key={idx}>{c}</th>
          ))}
        </tr>
      </thead>
      <tbody>
        <ExpandableRowTable
          showIcon={state.showIcon}
          loadData={(id, row) => {
            return new Promise(resolve => {
              const lastSelection = selection[selection.length - 1];
              setTimeout(() => {
                resolve(data.options[lastSelection]);
              }, 1000);
            });
          }}
          onExpand={(action, id, row) => {
            setRows(rows);
            toggleIcon();
          }}
          columns={columns}
          rows={rows}
        />
      </tbody>
    </table>
  );
}

export default function App() {
  return (
    <div>
      <StateProvider>
        <Select />
        <Table />
      </StateProvider>
    </div>
  );
}

Solution 2 : travail exemple

Solution 3

import React, { useState, useEffect, useRef, Fragment } from "react";
import axios from "axios";

class TableRowData extends React.Component {
  state = {
    showIcon: false,
    selection: [],
    data: []
  };
  constructor(props) {
    super(props);
  }

  componentDidMount() {
    axios.get("https://jsonplaceholder.typicode.com/users").then((res) => {
      const rowData = res.data.map((row) => {
        row.isExpanded = false;
        return row;
      });
      this.setState({ data: rowData });
    });
  }

  render() {
    const updateState = () => {
      this.setState(
        {
          data: [...this.state.data]
        },
        () => {}
      )
    }

    const ExpandableTableRow = ({ rows }) => {
      const expandRow = (row) => {
          row.children = [
            {
              id: "_" + Math.random().toString(36).substr(2, 5),
              name: row.id + "_ID1",
              isExpanded: false,
              parentId: row.id
            },
            {
              id: "_" + Math.random().toString(36).substr(2, 5),
              name: row.id + "_ID2ß",
              isExpanded: false,
              parentId: row.id
            }
          ];
          row.isExpanded = true;
          updateState();
      };

      const collapseRow = (row) => {
        delete row.children;
        row.isExpanded = false;
        updateState();
      };

      const ExpandCollapsToggle = ({ row, expandRow, collapseRow }) => {
        return (
          <button type="button" onClick={() => row.isExpanded ? collapseRow(row) : expandRow(row)}>
            {row.isExpanded ? '-' : '+'}
          </button>
        );
      };

      if (rows) {
        return rows.map((row) => {
          let children = null;
          return (
            <Fragment key={row.id}>
              <tr key={row.id}>
                <td>
                  <ExpandCollapsToggle
                    row={row}
                    expandRow={expandRow}
                    collapseRow={collapseRow}
                  />{" "}
                  {row.id}
                </td>
                <td>{row.name}</td>
              </tr>
              <ExpandableTableRow rows={row.children} />
            </Fragment>
          );
        });
        return null;
    };

    if (this.state.data) {
      return (
        <Fragment>
          <ExpandableTableRow rows={this.state.data} />
        </Fragment>
      );
    }
    return null;
  }
}
const mapStateToProps = (state) => {
  return { data: state.reportReducer.data };
};
export default TableRowData;

Solution 3 : travailler exemple

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X