treemate

https://treemate.vercel.app

Usage no npm install needed!

<script type="module">
  import treemate from 'https://cdn.skypack.dev/treemate';
</script>

README

treemate ยท Coverage Status

https://treemate.vercel.app

All in one solution for tree structure in component developing.

It helps you manipulate tree data structure for user interface. (Can be used in Tree, Select, Dropdown, Table, Menu components and ...)

  1. check & uncheck nodes in the tree
  2. expand & collapse nodes in the tree
  3. move along tree nodes
  4. get flattened nodes
  5. query nodes
  6. support group node (group a set of nodes in the same tree level)
  7. support ignored node (ignored by move)
  8. meta info of nodes
  9. get the original node data ref
  10. async check
  11. ...

Installation

npm i -D treemate

Basic Concepts

Before you started, I strongly recommend you to read the section to get the basic concepts of treemate.

Node, Group Node and Ignored Node

In treemate, a tree is composed of node (optional group node and optional ignored).

A node contains a key, and maybe a children prop which includes its child node.

Node

Node {
  key,
  children?
}

Group Node

If you don't need group node, you can pass the section.

A group node contains a key, a type prop with value group and a children of its child node (can be a group node any more).

GroupNode {
  key,
  type: 'group',
  children
}

The group node itself will be ignored when moving along the nodes, and the children of the group node is view as the same level of the group node.

For example, in the following tree, the group child 1 will be viewed as node 1's next node. node 2 will be viewed as group child 2's next node.

- node 1
- group node 1
  - group child 1
  - group child 2
- node 2

Ignored Node

Some time's you may want to put some render only nodes in the tree. For example:

- node 1
- divider (render only)
- node 2

In data aspect, the divider node is meaning less. You can make it a ignored node. The ignored node will be ignored when moving along the nodes. (node 2 will be view the next node as node 1.) Also, getNode method won't return the ignored node.

An ignored node should also contains a key (for modern frontend framework to do efficient diff)

IgnoredNode {
  key,
  type: 'ignored'
}

Usage

Create a Treemate

createTreeMate method accepts a array of node as data. It returns a treemate instance.

In javascript:

import { createTreeMate } from 'treemate'

const data = [
  // non-leaf node
  {
    key: 1,
    children: [
      {
        key: 2
      }
    ]
  },
  // leaf node
  {
    key: 3
  },
  // ignored node
  {
    key: 4,
    type: 'ignored'
  },
  // group node
  {
    key: 5,
    type: 'group',
    children: [
      {
        key: 6
      }
    ]
  }
]

const treeMate = createTreeMate(data)

In typescript, the data looks almost same. However createTreeMate accepts 3 optional generic parameter to specify the types of node, group node and ignored node.

interface BaseNode {
  key: string | number
  children?: Array<BaseNode | GroupNode | IgnoredNode>
}

interface GroupNode {
  key: string | number
  type: 'group'
  children: Array<BaseNode | IgnoredNode>
}

interface IgnoredNode {
  key: string | number
  type: 'ignored'
}

// 1. specify all node types
const treeMate = createTreeMate<BaseNode, GroupNode, IgnoredNode>(data)

// 2. equals to createTreeMate<BaseNode, GroupNode, BaseNode>()
//    which mean no ignored node is in the data.
const treeMate = createTreeMate<BaseNode, GroupNode>(data)

// 3. equals to createTreeMate<BaseNode, BaseNode, BaseNode>()
//    which mean no ignored node and group node is in the data.
const treeMate = createTreeMate<BaseNode>(data)

// 4. without generic parameter
//    it will use a builtin node type as basic node type.
//    RawNode {
//      key: string | number
//      children?: RawNode[]
//      disabled?: boolean
//      isLeaf? boolean
//    }
//    ignored node and group node should be in the data.
const treeMate = createTreeMate(data)

Custom createTreeMate Options

If you want another way to determine a way to specify if a node's key or its disabled, group, ignored status. You can pass an option when create treemate.

const treeMate = createTreeMate(data, {
  getKey: (node) => Key,
  getDisabled: (node) => boolean,
  getIsGroup: (node) => boolean,
  getIgnored: (node) => boolean
})

Get a Node from Tree

Now suppose we have a treemate instance.

const tmNode = treeMate.getNode(key) // if not exist return null

// Caveat: getNode won't return group node & ignored node!
// If you do need to get them, you can use the treeNodeMap, eg:
treeMate.treeNodeMap.get(key)

Props of a TreeNode

TreeNode {
  key,
  rawNode, // hold the ref to the original data node, can be very useful
  level, // from 0
  index, // its index in its parent node (or root array)
  siblings,
  isFirstChild,
  isLastChild,
  parent, // its parent TreeNode (not data node)
  isShallowLoaded, // use when on partial data is loaded
  isLeaf,
  isGroup,
  ignored, // boolean
  disabled, // disabled
  children?, // its child TreeNodes (not data node)
  getPrev, // method
  getNext, // method
  getParent, // method
  getChild // method
}

Do Check and Uncheck in the Tree

TreeMate.getCheckedKeys(checkedKeys, options?)

Get checked status of the tree.

Node has disabled = true will be block cascade check's propagation.

Param checkedKeys has two forms:

Key[] // 1. currently checked keys

// 2. merged checked status
interface InputMergedKeys {
  checkedKeys?: Key[] | null
  indeterminateKeys?: Key[] | null // half checked
}

// it can also be
null | undefined
// viewed as an empty array

Param options looks like

interface CheckOptions {
  cascade?: boolean // cascade check status, default is true
  leafOnly?: boolean // whether only allow leaf node being checked, default is false
  checkStrategy?: string // set show strategy when checked 'all' | 'parent' | 'child'
}

Return value looks like

interface MergedKeys {
  checkedKeys: Key[]
  indeterminateKeys: Key[] // half checked
}
Usage
const { checkedKeys, indeterminateKeys } = treeMate.getCheckedKeys([1])
const { checkedKeys, indeterminateKeys } = treeMate.getCheckedKeys([1], {
  cascade: false
})
const { checkedKeys, indeterminateKeys } = treeMate.getCheckedKeys({
  checkedKeys: [1],
  indeterminateKeys: [2]
})
// ...

TreeMate.check(keysToCheck, checkedKeys, options?)

Get checked status of the tree after some nodes are checked.

keysToCheck could be Key | Key[] | null | undefined.

For checkedKeys, options and return value, see getCheckedKeys(checkedKeys, options?).

TreeMate.uncheck(keysToUncheck, checkedKeys, options?)

Get checked status of the tree after some nodes are unchecked.

keysToCheck could be Key | Key[] | null | undefined.

For checkedKeys, options and return value, see getCheckedKeys(checkedKeys, options?).

Do Move in the Tree

TreeMate.getPrev(key, options?)

Get the first previous not disabled sibling TreeMateNode of the key's corresponding node. In the traverse process, the group | ignored node itself will be dismissed. If node doesn't exist, return null.

options look like { loop?: boolean }. By default, loop is false, it won't loop when touches the last node.

TreeMate.getNext(key, options?)

Get the first next not disabled sibling TreeMateNode of the key's corresponding node. In the traverse process, the group | ignored node itself will be dismissed. If node doesn't exist, return null.

options look like { loop?: boolean }. By default, loop is false, it won't loop when touches the last node.

TreeMate.getParent(key)

Get the parent node of the key's corresponding node. In the traverse process, the group node itself will be dismissed. If node doesn't exist, return null.

TreeMate.getChild(key)

Get the first not disabled child node of the key's corresponding node. In the traverse process, the group | ignored node itself will be dismissed. If node doesn't exist, return null.

TreeNode.getPrev(options?)

Get the first previous not disabled sibling TreeMateNode. In the traverse process, the group node itself will be dismissed. If node doesn't exist, return null.

options look like { loop?: boolean }. By default, loop is false, it won't loop when touches the last node.

TreeNode.getNext(options?)

Get the first next not disabled sibling TreeMateNode. In the traverse process, the group | ignored node itself will be dismissed. If node doesn't exist, return null.

options look like { loop?: boolean }. By default, loop is false, it won't loop when touches the last node.

TreeNode.getParent()

Get the parent node of TreeMateNode. In the traverse process, the group node itself will be dismissed. If node doesn't exist, return null.

TreeNode.getChild()

Get the first not disabled child TreeMateNode. In the traverse process, the group | ignored node itself will be dismissed. If node doesn't exist, return null.

Do Expand & Collapsed on the Tree

Expand status is will influence the flattened nodes of the tree. The flattened nodes is crucial for virtual list.

TreeMate.getFlattenedNodes(expandedKeys?)

Returns the flattened tree nodes with corresponding expandedKeys. If expandedKeys is not provided, treemate will treat it as all expanded.

createIndexGetter(flattenedNodes)

Create an index getter from the flattenedNodes.

import { createIndexGetter } from 'treemate'

const getIndex = createIndexGetter(flattenedNodes)

getIndex(flattenedNodes[0].key) === 0

Get Path of a Node

TreeMate.getPath(key, options?)

Get the path from root to the node corresponding to the key. The return value looks like

interface MergedPath {
  keyPath: Key[]
  treeNodePath: TreeMateNode[]
  treeNode: TreeMateNode | null
}

The keyPath is the key of the nodes in path. The treeNodePath is the node path. treeNode is the TreeMateNode corresponding to the key.

options looks like { includeSelf?: boolean, includeGroup?: boolean }, by default includeSelf is true and includeGroup is false.

Get First Available Node of the Tree

Can be used to get the default pending status of a select menu.

TreeMate.getFirstAvailableNode()

Get the first not disabled TreeMateNode of the tree. In the traverse process, the group | ignored node itself will be dismissed. If node doesn't exist, returns null.

Other Props in TreeMate Instance

TreeMate.treeNodes

Corresponding TreeMateNode Array of original data. The tree structure is identical to the original data.

TreeMate.treeNodeMap

A map of key to tree node. Contains all nodes, including group | ignored node.