Skip to content

Commit 4def05b

Browse files
committed
Initial commit
0 parents  commit 4def05b

25 files changed

+8958
-0
lines changed

.babelrc

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
{
2+
"env": {
3+
"build": {
4+
"presets": [["env", { "modules": false }], "stage-3", "react"],
5+
"plugins": ["external-helpers", "transform-class-properties"]
6+
},
7+
"buildProd": {
8+
"presets": [["env", { "modules": false }], "stage-3", "react"],
9+
"plugins": [
10+
"external-helpers",
11+
"transform-class-properties",
12+
[
13+
"transform-react-remove-prop-types",
14+
{ "mode": "remove", "removeImport": true }
15+
]
16+
]
17+
},
18+
"es": {
19+
"presets": [["env", { "modules": false }], "stage-3", "react"],
20+
"plugins": ["transform-class-properties"]
21+
},
22+
"commonjs": {
23+
"plugins": [
24+
["transform-es2015-modules-commonjs", { "loose": true }],
25+
"transform-class-properties"
26+
],
27+
"presets": ["stage-3", "react"]
28+
},
29+
"test": {
30+
"presets": ["env", "stage-3", "react"],
31+
"plugins": ["transform-class-properties"]
32+
}
33+
}
34+
}

.eslintrc

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
{
2+
"parser": "babel-eslint",
3+
"extends": [
4+
"eslint:recommended",
5+
"plugin:import/recommended",
6+
"plugin:react/recommended"
7+
],
8+
"parserOptions": {
9+
"ecmaVersion": 6,
10+
"sourceType": "module",
11+
"ecmaFeatures": {
12+
"jsx": true,
13+
"experimentalObjectRestSpread": true
14+
}
15+
},
16+
"env": {
17+
"browser": true,
18+
"node": true,
19+
"jest": true
20+
},
21+
"rules": {
22+
"valid-jsdoc": 2,
23+
"react/prop-types": 0,
24+
"react/jsx-uses-react": 1,
25+
"react/jsx-no-undef": 2,
26+
"react/display-name": 0,
27+
"import/no-unresolved": ["error", { "ignore": ["^react$"] }],
28+
"import/unambiguous": 0,
29+
"react/jsx-key": 0
30+
},
31+
"plugins": [
32+
"import",
33+
"react"
34+
]
35+
}

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
*.log
2+
node_modules
3+
lib
4+
es
5+
dist

.prettierrc

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"trailingComma": "es5",
3+
"singleQuote": true
4+
}

README.md

Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
# Treefold
2+
3+
[![styled with prettier](https://img.shields.io/badge/styled_with-prettier-ff69b4.svg)](https://github.com/prettier/prettier)
4+
5+
A renderless tree React component for your hierarchical views.
6+
7+
## The problem
8+
9+
You need to show hierarchical data in different ways. You know how you want to show the information for each individual data item. But you don't want to repeat over and over again the logic about how to traverse the data, how to assemble it all to make it look like a tree, how to expand/collapse nodes, etc.
10+
11+
## This solution
12+
13+
This is a component that abstracts away some of these repetitive details during a tree-rendering process. You just specify how you want to render each individual data item, and `Treefold` takes care of everything else.
14+
15+
The component itself is stateless and controlled by default, so you can control any aspects of it from the outside. The expand/collapse state of each individual node is normally also controlled, but if no props for controlling it are specified, the component auto-manages the state for this on its own.
16+
17+
## Installation
18+
19+
This module is distributed via [npm](https://www.npmjs.com/) which is bundled with [node](https://nodejs.org) and should be installed as one of your project's `dependencies`:
20+
21+
```
22+
npm install --save react-treefold
23+
```
24+
25+
or if you use [yarn](https://yarnpkg.com/):
26+
27+
```
28+
yarn add react-treefold
29+
```
30+
31+
> This package also depends on `react` and `prop-types`. Please make sure you have those installed as well.
32+
33+
## Usage
34+
35+
Use the `Treefold` component by passing to it the hierarchical data (`nodes` prop) and how to render each node in the tree (`renderNode` prop):
36+
37+
```jsx
38+
import Treefold from 'react-treefold';
39+
40+
const MyTreeView = ({ nodes }) => (
41+
<div className="treeview">
42+
<Treefold
43+
nodes={nodes}
44+
renderNode={({
45+
node,
46+
level,
47+
isFolder,
48+
isExpanded,
49+
getToggleProps,
50+
renderChildNodes,
51+
}) => (
52+
<Fragment>
53+
<div className="item" style={{ paddingLeft: `${level * 20}px` }}>
54+
{isFolder && (
55+
<span className="toggle-icon" {...getToggleProps()}>
56+
<i
57+
className={isExpanded ? 'caret-down' : 'caret-right'}
58+
aria-hidden="true"
59+
/>
60+
</span>
61+
)}
62+
{node.name}
63+
</div>
64+
{isFolder && renderChildNodes()}
65+
</Fragment>
66+
)}
67+
/>
68+
</div>
69+
);
70+
```
71+
72+
The `renderNode` function receives the data for the node it needs to render, in addition to a set of extra props describing several different aspects of how the node needs to be rendered.
73+
74+
## Treefold props
75+
76+
### nodes
77+
78+
> `array<object>` | required
79+
80+
The list of root nodes in the tree. Each node in the list should be an object with a unique `id` attribute (string or numeric), and a `children` array attribute containing the child nodes of that node. If no `children` is specified, the node is assumed to be a leaf.
81+
82+
Note: the names of these node attributes is customizable. See [getNodeId](#getNodeId) and [getNodeChildren](#getNodeChildren).
83+
84+
### renderNode
85+
86+
> `function({/* see below */}): element` | required
87+
88+
A function that renders a single node of the tree.
89+
90+
This is called with an object. Read more about the properties of this object in the section "[Rendering a single node](#rendering-a-single-node)".
91+
92+
### renderEmptyFolder
93+
94+
> `function({ level: number }): element?` | optional, defaults to `() => null`
95+
96+
A function that renders whatever needs to be rendered as content for an empty folder.
97+
98+
### isNodeExpanded
99+
100+
> `function(node: object): boolean` | optional
101+
102+
A function that receives a node and returns `true` if that node should be expanded, or `false` otherwise.
103+
104+
It must be provided alongside [onToggleExpand](#onToggleExpand), in order to control the tree from the outside (i.e. to make it behave as a controlled component).
105+
106+
If these two props are not provided, the tree controls its expand/collapse state of nodes on its own (i.e. it behaves as an uncontrolled component).
107+
108+
### onToggleExpand
109+
110+
> `function(node: object)` | optional
111+
112+
A function that receives a node and toggles the state of that node being expanded or collapsed.
113+
114+
It must be provided alongside [isNodeExpanded](#isNodeExpanded), in order to control the tree from the outside. If these two props are not provided, the tree controls its expand/collapse state on its own.
115+
116+
### getNodeId
117+
118+
> `function(node: object): number | string` | optional, defaults to `node => node.id`
119+
120+
A function that receives a node in the tree and returns what to use as a unique id for that node.
121+
122+
### getNodeChildren
123+
124+
> `function(node: object): array?` | optional, defaults to `node => node.children`
125+
126+
A function that receives a node in the tree and returns the array of child nodes of that node, or nothing if the node is a leaf.
127+
128+
Note that there can be non-leaf nodes that have no child nodes. These are the ones with an empty array of child nodes. For a node to really be considered a leaf, the resultof this function must be `null` or `undefined`.
129+
130+
## Rendering a single node
131+
132+
The most important thing you have to tell to `Treefold` besides the actual tree data to render, is how to render it. You do so primarily by providing a prop called `renderNode` which receives all the information necessary about that node, and returns the jsx element that represents it.
133+
134+
```jsx
135+
<Treefold
136+
nodes={treeData}
137+
renderNode={props => ({
138+
/* you render the node here */
139+
})}
140+
/>
141+
```
142+
143+
`Treefold` takes care of calling this function as it traverses the tree, and passes to it an object (`props` in the example just above) with the properties documented below:
144+
145+
### node
146+
147+
> `object` | required
148+
149+
The data for the node that is being rendered.
150+
151+
### level
152+
153+
> `number` | required
154+
155+
The level of depth of the node in the tree. Root nodes have level 0. Child nodes of a given node have the level of their parent node plus 1.
156+
157+
### isFolder
158+
159+
> `boolean` | required
160+
161+
Wether the node is a folder (meaning it has or may have child nodes under it) or not.
162+
163+
A node is considered to be a folder if it has a collection of child nodes, even if that collection is empty. See [getNodeChildren](#getNodeChildren) to see how `Treefold` determines what is the collection of child nodes for a given node.
164+
165+
### isExpanded
166+
167+
> `boolean` | required
168+
169+
Wether the node is to be rendered expanded, with its child nodes visible, or not.
170+
171+
This property will always be `false` for leaf nodes. It may be `true` for non-leaf nodes that have an empty list of child nodes.
172+
173+
### hasChildNodes
174+
175+
> `boolean` | required
176+
177+
Wether the node to be rendered has a non-empty list of child nodes or not.
178+
179+
This property will always be `false` for leaf nodes. It may also be `false` for non-leaf nodes that have an empty list of child nodes.
180+
181+
### getToggleProps
182+
183+
> function(props: object): object | required for folder nodes, `null` for leaf nodes
184+
185+
This is a [prop getter](prop-getters) that returns all the props that you need to apply to any expand/collapse toggle elements in your node. These are the elements that are meant to be activated by the user to toggle that folder node expand/collapse state.
186+
187+
By definition this function is provided only to non-leaf nodes, **even if the node has an empty list of child nodes**. For leaf nodes this prop is `null`.
188+
189+
### renderChildNodes
190+
191+
> `function(): element?` | required for folder nodes, `null` for leaf nodes
192+
193+
A function that **you need to call for non-leaf nodes** so that its child nodes are rendered.
194+
195+
#### Why do I need to take care of this?
196+
197+
You might think that this is something that `Treefold` should do itself. But if it did so, it would hinder your freedom in achieving different outcomes.
198+
199+
> TODO: Document this more in depth
200+
201+
## TO-DO
202+
203+
* Ability to load children asynchronously.
204+
* Full WAI-ARIA compliance.
205+
* Improve customization to expand the use cases where this can be applied.
206+
207+
Feature requests and suggestions are very welcome. This is a very young project not yet applied to a wide variety of situations, so I know it has a long road of changes and improvements ahead.
208+
209+
## LICENCE
210+
211+
MIT
212+
213+
[prop-getters]: https://blog.kentcdodds.com/how-to-give-rendering-control-to-users-with-prop-getters-549eaef76acf

package.json

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
{
2+
"name": "react-treefold",
3+
"version": "0.0.1",
4+
"description": "A renderless tree component for your hierarchical React views",
5+
"author": "Ernesto Garcia <[email protected]> (http://github.com/gnapse)",
6+
"license": "MIT",
7+
"main": "lib/index.js",
8+
"module": "es/index.js",
9+
"scripts": {
10+
"clean": "rimraf lib es",
11+
"build": "npm run clean && npm run build:es && npm run build:commonjs",
12+
"build:commonjs": "cross-env BABEL_ENV=commonjs babel src --out-dir lib",
13+
"build:es": "cross-env BABEL_ENV=es babel src --out-dir es",
14+
"test": "jest --watch",
15+
"test:all": "jest",
16+
"lint": "eslint src",
17+
"precommit": "yarn lint && lint-staged",
18+
"prettier": "prettier --write *.{md,js}",
19+
"storybook": "start-storybook -p 9001 -c storybook"
20+
},
21+
"lint-staged": {
22+
"*.{js,md}": [
23+
"prettier --write",
24+
"git add"
25+
]
26+
},
27+
"jest": {
28+
"setupTestFrameworkScriptFile": "./tests/setup.js",
29+
"transformIgnorePatterns": [
30+
"/node_modules/",
31+
"/lib/"
32+
]
33+
},
34+
"repository": {
35+
"type": "git",
36+
"url": "https://github.com/gnapse/react-treefold.git"
37+
},
38+
"files": [
39+
"lib",
40+
"src",
41+
"es"
42+
],
43+
"keywords": [
44+
"react",
45+
"reactjs",
46+
"components",
47+
"tree",
48+
"treeview",
49+
"hierarchy",
50+
"render-props",
51+
"renderless",
52+
"accessibility",
53+
"WAI-ARIA"
54+
],
55+
"bugs": {
56+
"url": "https://github.com/gnapse/react-treefold/issues"
57+
},
58+
"homepage": "https://github.com/gnapse/react-treefold#readme",
59+
"devDependencies": {
60+
"@storybook/react": "^3.3.13",
61+
"babel-cli": "^6.26.0",
62+
"babel-core": "^6.26.0",
63+
"babel-eslint": "^8.2.1",
64+
"babel-jest": "^22.2.2",
65+
"babel-plugin-external-helpers": "^6.22.0",
66+
"babel-plugin-transform-class-properties": "^6.24.1",
67+
"babel-plugin-transform-react-remove-prop-types": "^0.4.13",
68+
"babel-preset-env": "^1.6.1",
69+
"babel-preset-react": "^6.24.1",
70+
"babel-preset-stage-3": "^6.24.1",
71+
"cross-env": "^5.1.3",
72+
"enzyme": "^3.3.0",
73+
"enzyme-adapter-react-16": "^1.1.1",
74+
"eslint": "^4.17.0",
75+
"eslint-plugin-import": "^2.8.0",
76+
"eslint-plugin-react": "^7.6.1",
77+
"husky": "^0.14.3",
78+
"jest": "^22.2.2",
79+
"lint-staged": "^6.1.0",
80+
"prettier": "^1.10.2",
81+
"prop-types": "^15.6.0",
82+
"react": "^16.2.0",
83+
"react-dom": "^16.2.0",
84+
"react-test-renderer": "^16.2.0",
85+
"rimraf": "^2.6.2",
86+
"rollup": "^0.55.5",
87+
"rollup-plugin-babel": "^3.0.3",
88+
"rollup-plugin-commonjs": "^8.3.0",
89+
"rollup-plugin-node-resolve": "^3.0.2",
90+
"rollup-plugin-replace": "^2.0.0",
91+
"rollup-plugin-uglify": "^3.0.0"
92+
},
93+
"peerDependencies": {
94+
"prop-types": ">=15",
95+
"react": ">=15"
96+
}
97+
}

0 commit comments

Comments
 (0)