Package detail

treetabular

reactabular4.1kMIT3.6.0

Tree utilities

react, reactjs, table, tables

readme

build status bitHound Score codecov

Treetabular - Tree utilities

treetabular provides tree helpers for Reactabular. It allows you to set up collapsible rows that can contain more collapsible ones while remaining within a table format.

To achieve this, treetabular relies on a flat structure that contains the hierarchy:

const tree = [
  {
    _index: 0,
    id: 123,
    name: 'Demo'
  },
  {
    _index: 1,
    id: 456,
    name: 'Another',
    parent: 123
  },
  {
    _index: 2,
    id: 789,
    name: 'Yet Another',
    parent: 123
  },
  {
    _index: 3,
    id: 532,
    name: 'Foobar'
  }
];

If there's a parent relation, the children must follow their parent right after it (you might use fixOrder helper function if your data does not meet that criteria).

You can find suggested default styling for the package at style.css in the package root.

API

import * as tree from 'treetabular';

// Or you can cherry-pick
import { filter } from 'treetabular';
import { filter as filterTree } from 'treetabular';

Transformations

tree.collapseAll = ({ property = 'showingChildren' }) => (rows) => [<collapsedRow>]

Collapses rows by setting showingChildren of each row to false.

tree.expandAll = ({ property = 'showingChildren' }) => (rows) => [<expandedRow>]

Expands rows by setting showingChildren of each row to true.

tree.filter = ({ fieldName, idField = 'id', parentField = 'parent' }) => (rows) => [<filteredRow>]

Filters the given rows using fieldName. This is handy if you want only rows that are visible assuming visibility logic has been defined.

Queries

tree.getLevel = ({ index, idField = 'parentId', parentField = 'parent' }) => (rows) => <level>

Returns the nesting level of the row at the given index within rows.

tree.getChildren = ({ index, idField = 'id', parentField = 'parent' }) => (rows) => [<child>]

Returns children based on given rows and index. This includes children of children.

tree.getImmediateChildren = ({ index, idField = 'id', parentField = 'parent' }) => (rows) => [<child>]

Returns immediate children based on given rows and index.

tree.getParents = ({ index, idField = 'parentId', parentField = 'parent' }) => (rows) => [<parent>]

Returns parents based on given rows and index.

tree.hasChildren = ({ index, idField = 'id', parentField = 'parent '}) => (rows) => <boolean>

Returns a boolean based on whether or not the row at the given index has children.

tree.search = ({ operation: (rows) => [<row>], idField = 'id', parentField = 'parent' }) => (rows) => [<searchedRow>]

Searches against a tree structure using operation while matching against children too. If children are found, associated parents are returned as well. This has been designed to searchtabular multipleColumns and singleColumn, but as long as the passed operation follows the interface, it should fit in.

This depends on resolve.resolve!

tree.wrap = ({ operations: [rows => rows], idField = 'id' }) => (rows) => [<operatedRow>]

If you want to perform an operation, such as sorting, against the root rows of a tree, use tree.wrap.

Example:

wrap({
  operations: [
    sorter({
      columns,
      sortingColumns,
      sort: orderBy
    })
  ]
})(rows);

Packing

tree.pack = ({ parentField = 'parent', childrenField = 'children', idField = 'id' }) => (rows) => [<packedRow>]

Packs children inside root level nodes. This is useful with sorting and filtering.

tree.unpack = ({ parentField = 'parent', childrenField = 'children', idField = 'id', parent }) => (rows) => [<unpackedRow>]

Unpacks children from root level nodes. This is useful with sorting and filtering.

Drag and Drop

tree.moveRows = ({ operation: (rows) => [<row>], retain = [], idField = 'id', parentField = 'parent' }) => (rows) => [<movedRow>]

Allows moving tree rows while retaining given fields at their original rows. You should pass an operation that performs actual moving here. reactabular-dnd moveRows is one option.

UI

tree.toggleChildren = ({ getIndex, getRows, getShowingChildren, toggleShowingChildren, props, idField = 'id', parentField, toggleEvent = 'DoubleClick' }) => (value, extra) => <React element>

Makes it possible to toggle node children through a user interface. Pass "indent":false inside props object if you want to disable automatic indentation.

The default implementation of getIndex(rowData) depends on resolve.resolve as it looks for index of the row to toggle based on that. This can be customized though.

Helpers

tree.fixOrder = ({ parentField = 'parent', idField = 'id' }) => (rows) => [<rows in correct order>]

If children in your rows don't follow their parents you can use that helper method so they will be moved into right place.

Basically it converts [ parent, x, y, z, children ] into [ parent, children, x, y, z ].

Example

/*
import React from 'react';
import cloneDeep from 'lodash/cloneDeep';
import orderBy from 'lodash/orderBy';
import { compose } from 'redux';
import * as resolve from 'table-resolver';
import VisibilityToggles from 'reactabular-visibility-toggles';
import * as Table from 'reactabular-table';
import * as tree from 'treetabular';
import * as search from 'searchtabular';
import * as sort from 'sortabular';

import {
  generateParents, generateRows
} from './helpers';
*/

const schema = {
  type: 'object',
  properties: {
    id: {
      type: 'string'
    },
    name: {
      type: 'string'
    },
    age: {
      type: 'integer'
    }
  },
  required: ['id', 'name', 'age']
};

class TreeTable extends React.Component {
  constructor(props) {
    super(props);

    const columns = this.getColumns();
    const rows = resolve.resolve({ columns })(
      generateParents(generateRows(100, schema))
    );

    this.state = {
      searchColumn: 'all',
      query: {},
      sortingColumns: null,
      rows,
      columns
    };

    this.onExpandAll = this.onExpandAll.bind(this);
    this.onCollapseAll = this.onCollapseAll.bind(this);
    this.onToggleColumn = this.onToggleColumn.bind(this);
  }
  getColumns() {
    const sortable = sort.sort({
      // Point the transform to your rows. React state can work for this purpose
      // but you can use a state manager as well.
      getSortingColumns: () => this.state.sortingColumns || {},

      // The user requested sorting, adjust the sorting state accordingly.
      // This is a good chance to pass the request through a sorter.
      onSort: selectedColumn => {
        const sortingColumns = sort.byColumns({
          sortingColumns: this.state.sortingColumns,
          selectedColumn
        });

        this.setState({ sortingColumns });
      }
    });

    return [
      {
        property: 'name',
        props: {
          style: { width: 200 }
        },
        header: {
          label: 'Name',
          transforms: [sortable]
        },
        cell: {
          formatters: [
            tree.toggleChildren({
              getRows: () => this.state.rows,
              getShowingChildren: ({ rowData }) => rowData.showingChildren,
              toggleShowingChildren: rowIndex => {
                const rows = cloneDeep(this.state.rows);

                rows[rowIndex].showingChildren = !rows[rowIndex].showingChildren;

                this.setState({ rows });
              },
              // Inject custom class name per row here etc.
              props: {}
            })
          ]
        },
        visible: true
      },
      {
        property: 'age',
        props: {
          style: { width: 300 }
        },
        header: {
          label: 'Age',
          transforms: [sortable]
        },
        visible: true
      }
    ];
  }
  render() {
    const {
      searchColumn, columns, sortingColumns, query
    } = this.state;
    const visibleColumns = columns.filter(column => column.visible);
    const rows = compose(
      tree.filter({ fieldName: 'showingChildren' }),
      tree.wrap({
        operations: [
          sort.sorter({
            columns,
            sortingColumns,
            sort: orderBy
          })
        ]
      }),
      tree.search({
        operation: search.multipleColumns({ columns, query })
      })
    )(this.state.rows);

    return (
      <div>
        <VisibilityToggles
          columns={columns}
          onToggleColumn={this.onToggleColumn}
        />

        <button onClick={this.onExpandAll}>Expand all</button>
        <button onClick={this.onCollapseAll}>Collapse all</button>

        <div className="search-container">
          <span>Search</span>
          <search.Field
            column={searchColumn}
            query={query}
            columns={visibleColumns}
            rows={rows}
            onColumnChange={searchColumn => this.setState({ searchColumn })}
            onChange={query => this.setState({ query })}
          />
        </div>

        <Table.Provider
          className="pure-table pure-table-striped"
          columns={visibleColumns}
        >
          <Table.Header />

          <Table.Body rows={rows} rowKey="id" />
        </Table.Provider>
      </div>
    );
  }
  onExpandAll() {
    this.setState({
      rows: tree.expandAll()(this.state.rows)
    });
  }
  onCollapseAll() {
    this.setState({
      rows: tree.collapseAll()(this.state.rows)
    });
  }
  onToggleColumn({ columnIndex }) {
    const columns = cloneDeep(this.state.columns);

    columns[columnIndex].visible = !columns[columnIndex].visible;

    this.setState({ columns });
  }
}

<TreeTable />

License

MIT. See LICENSE for details.

changelog

treetabular

3.6.0 / 2020-01-27

  • Feature - Improve filtering performance #24

3.5.2 / 2019-01-02

  • Fix - Fix JSX error when hasAutomaticIndentation is false #23

3.5.1 / 2018-06-25

  • Fix - Include Redux 5 to peer dep. #22

3.5.0 / 2017-10-05

  • Chore - Support React 16. #21

3.4.0 / 2017-09-13

  • Fix - tree.fixOrder works with deeply nested data now. #13, #20

3.3.0 / 2017-03-14

  • Feature - Expose toggleChildren toggleEvent as a prop. Defaults to DoubleClick. #12

3.2.1 / 2017-02-25

  • Fix - tree.getParents allows undefined parents now. #10

3.2.0 / 2017-02-25

  • Feature - Add indent prop to tree.toggleChildren. Set to false to default indentation behavior. #8

3.1.3 / 2017-02-22

  • Chore - Fix tree.filter API signature at documentation. #7

3.1.2 / 2017-02-14

  • Chore - Update readme example. Now sorting should work.

3.1.1 / 2017-02-11

  • Chore - Update readme example.

3.1.0 / 2017-02-09

  • Feature - Expose tree.toggleChildren indexing through a getIndex callback. It looks into rowData._index by default.

3.0.1 / 2017-02-09

  • Fix - Make sure tree.hasChildren does not crash if index is negative.

3.0.0 / 2017-02-09

  • Breaking fix - Allow children that have sub-children to be displayed. #4
  • Fix - Allow tree.hasChildren to receive parentField. #4

2.1.0 / 2017-02-06

  • Feature - Add tree.fixOrder - If children in your rows don't follow their parents you can use that helper method so they will be moved into right place. #2

2.0.2 / 2017-02-03

  • Docs - Improve intro.

2.0.1 / 2017-02-02

  • Docs - Fix example column toggling.

2.0.0 / 2016-12-16

  • Feature - Add tree.wrap = ({ operations: [rows => rows], idField = 'id' }) => (rows) => [<operatedRow>].
  • Breaking tree.moveRows does not depend on reactabular-dnd directly anymore. Instead you have to pass the move operation to it. The new signature is tree.moveRows = ({ operation: (rows) => [<row>], retain = [], idField = 'id', parentField = 'parent' }) => (rows) => [<movedRow>].
  • Breaking - tree.search does not depend on selectabular directly anymore. Instead you have to pass the search operation to it. The new signature is tree.search = ({ operation: (rows) => [<row>], idField = 'id', parentField = 'parent' }) => (rows) => [<searchedRow>].
  • Breaking - tree.sort has been dropped. Use tree.wrap instead. Example:
wrap({
  operations: [
    sorter({
      columns,
      sortingColumns,
      sort: orderBy
    })
  ]
})(rows);

1.0.6 / 2016-11-30

  • Bug fix - Bump reactabular-dnd peer dependency range.

1.0.0 / 2016-11-26

  • Initial re-release under a different name.
  • Bug fix - Respect idField properly at tree.moveRows.
  • Breaking - Make tree.filter throw if fieldName is not passed. Without this it would fail silently.
  • Feature - Attach _isParent to parents when using tree.unpack.
  • Bug fix - tree.moveRows will return the original rows now if moving fails for some reason.

reactabular-tree

7.0.0 / 2016-11-03

  • Bug fix - Allow tree.toggleChildren to work without column props defined.
  • Feature - Add tree.getImmediateChildren.
  • Feature - Add tree.moveRows.
  • Breaking - Refactor tree.filter as ({ fieldName, parentField = 'parent' }) => (rows) => filteredRows.

6.1.1 / 2016-10-27

  • Bug fix - Allow tree.filter parent to be zero.

6.1.0 / 2016-10-25

  • Feature - Allow idField to be passed to tree.sort.

6.0.3 / 2016-10-19

  • Bug fix - Bump peer version ranges to avoid npm warnings.

6.0.0 / 2016-10-14

  • Breaking - Merge tree.flatten with tree.unpack. The new signature for tree.unpack is tree.unpack = ({ parentField = 'parent', parent, idField = 'id'}) => (rows) => <unpackedRows>.
  • Breaking - Rework API so that all functions except tree.toggleChildren work in curry format ((...) => (rows) => <new rows>). This way the API is consistent and easy to extend.
  • Breaking - Expose childrenField for tree.pack and tree.unpack. It defaults to children.
  • Breaking - Make tree.pack to work in a recursive manner (packs children within children).
  • Breaking - Make tree.search match against children as well. If children as matched, it will return parents as well.
  • Feature - Add tree.getChildren utilities for getting node children.

5.2.1 / 2016-09-30

  • Bug fix - If className is not provided to tree.toggleChildren, do not render undefined as a class. Also dropped extra console.log.

5.2.0 / 2016-09-30

  • Bug fix - Calculate tree.getParents correctly for root level nodes without parents. Previously that gave false positives.
  • Feature - Annotate tree.toggleChildren with has-children and has-parent classes. Easier to style this way.

5.1.0 / 2016-09-29

  • Feature - Add tree.flatten to allow transforming a nested tree structure into a flat structure used by the algorithms.

4.3.0 / 2016-09-27

  • Feature - Let toggleChildren toggle when cell is clicked. If you want the old behavior, override onClick through props.
  • Feature - Add collapseAll and expandAll helpers.

4.2.0 / 2016-09-23

  • Feature - Allow toggleChildren to accept props for customization.

3.0.5 / 2016-09-02

  • Feature - Allow id to be passed to filter.

3.0.4 / 2016-09-02

  • Feature - Allow toggleChildren id to be customized (not just "id" anymore).

3.0.2 / 2016-09-01

  • Feature - Include suggested default styling for the toggle arrow.

3.0.1 / 2016-09-01

  • Bug fix - Pass strategy to sorter at tree.sort.

3.0.0 / 2016-09-01

  • Breaking - Rewrite API. Now most parts accept objects and you can also customize field names.
  • Feature - Add tree.sort to wrap toggling row children.

2.0.0 / 2016-08-16

  • Initial release.