Giter VIP home page Giter VIP logo

treetabular's Introduction

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.

treetabular's People

Contributors

bebraw avatar dependabot[bot] avatar googol7 avatar jackbrown avatar lmeikle avatar pzmudzinski avatar sapegin avatar softage0 avatar vijayst avatar wmertens avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar

treetabular's Issues

Typo on the docs

Wrong: This can be customized through.
Correct: This can be customized though.

There no way to show multiple children which have sub-children

I want to show the following hierarchy data:

  • 001
    • 101
    • 102
      • 201
    • 103
      • 202

I know you already mentioned on the README.md:

If there’s a parent relation, the children must follow their parent right after it.

But there's no way to place the children right after their parents on the above hierarchy, and treetabular shows wrong hierarchy like the below image:
screen shot 2017-02-06 at 3 02 11 pm

Do you have plan to improve it?

The following code is my sample data:

      [
        {
          "id": "MNU00001",
          "parent": null,
          "name": "Configuration",
        },
        {
          "id": "MNU00101",
          "parent": "MNU00001",
          "name": "UserMangement",
        },
        {
          "id": "MNU00102",
          "parent": "MNU00001",
          "name": "MenuMangement",
        },
        {
          "id": "MNU00201",
          "parent": "MNU00102",
          "name": "TEST",
        },
        {
          "id": "MNU00103",
          "parent": "MNU00001",
          "name": "RoleMangement",
        },
        {
          "id": "MNU00202",
          "parent": "MNU00103",
          "name": "TEST",
        },
      ]

parentId as default value for idField in the documentation.

Some of the methods use default value of 'parentId' for the idField in the docs. For example, filter method is shown below:

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

I have not supplied a non-default idField of 'id'. Is it an error with the documentation? Or should I specify a non-default idField - in my case, it is 'id'? Thanks.

Give users a hint to include _index in their datastructure

I spent quite some time to find out that my rows got to have an _index property to show the expand / collapse icon. So I think it would be very helpful for beginners if the datastructure example at the beginning of the REAMDE.md would include it:

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'
  }
];

I found out by looking at the sourcecode at https://github.com/reactabular/treetabular/blob/master/src/toggle-children.js#L7

getIndex = rowData => rowData._index, // Look for index based on convention

Performance issue with fixOrder

Using fixOrder on a list of 3000 items took 30 seconds. Knowing the nature of data (only parent-child relation), an alternative fix took only 50 ms.

Random list of 3000 items:

     const rows = [];
     let j = 0;
     for (let i = 0; i < 3000; i++) {
         if (i % 3 === 0) {
             j++;
             rows.push({
                 id: j,
                 name: `Item ${j}`,
             });
        } else {
            const parent = Math.round(Math.random() * 1000);
            rows.push({
                id: 10000 + i,
                name: `Item ${10000 + i}`,
                parent
            });
        }
     }

An alternative fixOrder which works in 50 ms (not generic):

function fixOrder2(rows) {
    const parents = rows.filter(r => !r.parent);
    const children = rows.filter(r => r.parent);
    children.forEach(child => {
        const parentIndex = parents.findIndex(p => p.id === child.parent);
        parents.splice(parentIndex + 1, 0, child);
    });
    return parents;
}

Selecting checkbox on child row, toggles the row and shows parent row

Hi,

So far, I've been able to use the functionalities of reactabular and it's components for my use case.
Thanks for Reactabular!

I noticed this issue when I added a select check box for each row (parent and child).
When I try to select the checkbox of the child, it removes the row and displays the parent.
And if the parent's checkbox is selected and is expanded to show the child, unselecting the parent's checkbox, closes the child row.
How can I disable the toggle if I select the checkbox?

I used the below onRow code (after seeing this reactabular (reactabular/reactabular#242)

onRow(row, { rowIndex }) {
    return {
      className: classnames(
        rowIndex % 2 ? 'odd-row' : 'even-row',
        row.selected && 'selected-row'
      ),
      onClick: (e) => this.onSelectRow(e, row, rowIndex)
    };
  }
  onSelectRow({ target: {tagName}}, row, selectedRowIndex) {
    console.log('selected row' + tagName);
    if (tagName === 'INPUT') {
      this.props.onHandleSelect(row);
    } else {
      const { rows } = this.state;
      const selectedRowId = rows[selectedRowIndex].id;
      this.setState(
          compose(
            select.rows(r => r.selected === selectedRowId),
            select.none
          )(rows)
        );
    }
  }

screen shot 2017-04-03 at 3 54 26 pm

tree.fixOrder does not work for deep hierarchical data

tree.fixOrder does not work for hierarchical data (more than 2 levels deep).

For (id, parent) combinations below.

100, null
110, 100
120, 100
111, 110
112, 110

following is the arrangement after tree.fixOrder

111, 110
112, 110
100, null
110, 100
120, 100

Expand / collapse animations

I am wondering if there's any way to add animations when rows are toggled.
Also great work with reactabular! Best grid library I've seen in React and tried many of them.

Treetabular not working correctly for 'undefined' parents.

There is a minor bug in the getParents utility. This line expects the parent field to be null. If the parent is undefined, the tree does not work correctly.

if ((cell[parentField] === null) || (typeof cell[parentField] === 'undefined')) {
        break;
 }

With the second OR clause added, the tree expands correctly.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.