react-tree-menu
Version:
A tree menu component for React
590 lines (493 loc) • 19.1 kB
JSX
var React = require('react'),
TreeMenu = require('../index'),
TreeNode = require('../index').TreeNode,
TreeMenuUtils = require('../index').Utils,
Immutable = require('immutable'),
_ = require('lodash');
var CSSTransitionGroup = require('react-addons-css-transition-group');
var App = React.createClass({
getInitialState: function() {
return {
lastAction1: null,
lastAction2: null,
lastAction3: null,
staticTreeData: {
"option_2.a" : {
checked: false
},
"option_2.b" : {
checked: false
}
},
dynamicTreeData: [
{
label : "Option 1"
},
{
label : "Option 2",
children : [
{
checkbox: true,
label: "Sub Option A",
children: [
{
label: "Third Level Option 1",
checkbox : true
},
{
label: "Third Level Option 2",
checkbox : true
}
]
},
{
checkbox: true,
label: "Sub Option B"
}
]
}
],
dynamicTreeDataMap: {
"Option 1" : {
checked: true,
checkbox: true,
children: {
"Sub Option 1" : {
checked: false
},
"Sub Option 2" : {
checked: false,
checkbox: true,
children: {
"Sub-Sub Option 1" : {
checked: false,
checkbox: true
},
"Sub-Sub Option 2" : {
checked: false,
checkbox: true
}
}
}
}
},
"Option 2" : {
checked: false,
checkbox: true
}
},
dynamicTreeDataMap2: {
"Option 1" : {
checkbox: false,
children: {
"Sub Option 1" : {
checkbox: false
},
"Sub Option 2" : {
checkbox: false,
children: {
"Sub-Sub Option 1" : {
selected: true,
checkbox: false
},
"Sub-Sub Option 2" : {
checkbox: false
}
}
}
}
},
"Option 2" : {
checkbox: false
}
}
};
},
render: function() {
var dynamicExample1 = this._getExamplePanel("Dynamic", this._getDynamicTreeExample()),
declarativeExample = this._getExamplePanel("Declarative", this._getDeclarativeTreeExample()),
unboundExample = this._getExamplePanel("Unbound", this._getUnboundTreeExample()),
statefulExample = this._getExamplePanel("Stateful", this._getStatefulTreeExample()),
dynamicExample2 = this._getExamplePanel("Dynamic (Object)", this._getDynamicTreeExample2()),
dynamicExample3 = this._getExamplePanel("Selection w/o Checkboxes", this._getDynamicTreeExample3()),
labelFilterExample = this._getExamplePanel("Label Filter", this._getLabelFilterExample()),
rendererExample = this._getExamplePanel("Label/Checkbox Renderers", this._getRendererExample());
return <div className="container">
<div className="row">
<div className="col-lg-12 hero">
<h1><code>{"<TreeMenu/>"}</code></h1>
</div>
</div>
<div className="row">
<div className="col-lg-12 hero">
<h3>Check out the code for this demo <a href="https://github.com/MandarinConLaBarba/react-tree-menu/blob/master/example/App.jsx">here</a>.</h3>
</div>
</div>
<div className="row">
<div className="col-lg-3">
<h2>Dynamic (Array)</h2>
<ul>
<li>This menu is built dynamically using the data prop (array)</li>
<li>It is collapsible (the default), so expand/collapse icons show</li>
<li>It has no identifier prop for the TreeMenu, so it uses the array index when emitting events</li>
</ul>
</div>
<div className="col-lg-3">
<h2>Declarative</h2>
<ul>
<li>This menu is built w/ nested TreeNode components</li>
<li>It has collapsible=false, so no expand/collapse icons show</li>
<li>It has identifier="id", so it uses the 'id' prop when emitting events</li>
</ul>
</div>
<div className="col-lg-3">
<h2>{"Unbound: Won't Update UI State"}</h2>
<ul>
<li>This menu doesn't have any handlers applied that update UI state, so it's static.</li>
</ul>
</div>
<div className="col-lg-3">
<h2>{"Stateful, manages its own UI state"}</h2>
<ul>
<li>This menu has stateful=true, so it manages its own state w/o help from the Controller-View.</li>
</ul>
</div>
</div>
<div className="row">
<div className="col-lg-3">
{dynamicExample1}
</div>
<div className="col-lg-3">
{declarativeExample}
</div>
<div className="col-lg-3">
{unboundExample}
</div>
<div className="col-lg-3">
{statefulExample}
</div>
</div>
<div className="row">
<div className="col-lg-3">
<CSSTransitionGroup
transitionEnterTimeout={500}
transitionName="last-action" transitionLeave={false}>
{this._getLastActionNode("1")}
</CSSTransitionGroup>
</div>
<div className="col-lg-3">
<CSSTransitionGroup transitionEnterTimeout={500} transitionName="last-action" transitionLeave={false}>
{this._getLastActionNode("2")}
</CSSTransitionGroup>
</div>
<div className="col-lg-3">
<CSSTransitionGroup transitionEnterTimeout={500} transitionName="last-action" transitionLeave={false}>
{this._getLastActionNode("3")}
</CSSTransitionGroup>
</div>
<div className="col-lg-3">
<CSSTransitionGroup transitionEnterTimeout={500} transitionName="last-action" transitionLeave={false}>
{this._getLastActionNode("4")}
</CSSTransitionGroup>
</div>
</div>
<div className="row">
<div className="col-lg-3">
<h2>Dynamic (Object)</h2>
<ul>
<li>This menu is built dynamically using the data prop (object)</li>
<li>It is collapsible (the default), so expand/collapse icons show</li>
<li>It has no identifier prop for the TreeMenu, so it uses the object key when emitting events</li>
</ul>
</div>
<div className="col-lg-3">
<h2>Dynamic - No Checkboxes</h2>
<ul>
<li>This menu is built dynamically using the data prop (object)</li>
<li>It doesn't have checkboxes, but does have selection state</li>
</ul>
</div>
<div className="col-lg-3">
<h2>Label Filter</h2>
<ul>
<li>This menu has a labelFilter prop that truncates the displayed label</li>
<li>NOTE: no event handlers attached for this example</li>
</ul>
</div>
<div className="col-lg-3">
<h2>Renderers</h2>
<ul>
<li>Using renderer (node factory) methods as arguments</li>
<li>NOTE: no event handlers attached for this example</li>
</ul>
</div>
</div>
<div className="row">
<div className="col-lg-3">
{dynamicExample2}
</div>
<div className="col-lg-3">
{dynamicExample3}
</div>
<div className="col-lg-3">
{labelFilterExample}
</div>
<div className="col-lg-3">
{rendererExample}
</div>
</div>
<div className="row">
<div className="col-lg-3">
<CSSTransitionGroup transitionEnterTimeout={500} transitionName="last-action" transitionLeave={false}>
{this._getLastActionNode("5")}
</CSSTransitionGroup>
</div>
<div className="col-lg-3">
<CSSTransitionGroup transitionEnterTimeout={500} transitionName="last-action" transitionLeave={false}>
{this._getLastActionNode("6")}
</CSSTransitionGroup>
</div>
</div>
</div>;
},
_getLastActionNode: function (col) {
var lastActionNode = <div className="text-center alert alert-success tree-event-alert">{"Waiting for interaction"}</div>,
key = "lastAction" + col;
var action = this.state[key];
if (action) {
lastActionNode = (
<div className="text-center alert alert-success tree-event-alert" key={"lastAction_" + col + "_" + action.time}>
<h3>Event Received:</h3>
<div><strong>{action.event}</strong></div>
<h3><code>{"<TreeNode/>"}</code> Affected: </h3>
<div><strong>{action.node}</strong></div>
</div>);
} else {
}
return lastActionNode;
},
_getStatefulTreeExample: function () {
return (
<TreeMenu
identifier="id"
stateful={true}
onTreeNodeClick={this._setLastActionState.bind(this, "clicked", "4")}
onTreeNodeCollapseChange={this._setLastActionState.bind(this, "collapsed", "4")}
onTreeNodeCheckChange={this._setLastActionState.bind(this, "checked", "4")}
expandIconClass="fa fa-chevron-right"
collapseIconClass="fa fa-chevron-down">
<TreeNode label="Option 1" checkbox={true} id="option_1"/>
<TreeNode label="Option 2" checkbox={true} id="option_2">
<TreeNode label="Option A" checkbox={true} id="option_2.a"/>
<TreeNode label="Option B" checkbox={true} id="option_2.b">
<TreeNode label="Option B.x" checkbox={true} id="option_2.b.x"/>
<TreeNode label="Option B.y" checkbox={true} id="option_2.b.y"/>
</TreeNode>
</TreeNode>
</TreeMenu>
);
},
_getUnboundTreeExample: function () {
return (
<TreeMenu
identifier="id"
onTreeNodeClick={this._setLastActionState.bind(this, "clicked", "3")}
onTreeNodeCollapseChange={this._setLastActionState.bind(this, "collapsed", "3")}
onTreeNodeCheckChange={this._setLastActionState.bind(this, "checked", "3")}
expandIconClass="fa fa-chevron-right"
collapseIconClass="fa fa-chevron-down">
<TreeNode label="Option 1" checkbox={true} id="option_1"/>
<TreeNode label="Option 2" checkbox={true} id="option_2">
<TreeNode label="Option A" checkbox={true} checked={true} id="option_2.a"/>
<TreeNode label="Option B" checkbox={true} id="option_2.b"/>
</TreeNode>
<TreeNode label="Option 3" checkbox={true} id="option_3"/>
<TreeNode label="Option 3" checkbox={true} id="option_3"/>
</TreeMenu>
);
},
_getDeclarativeTreeExample: function () {
return (
<TreeMenu
sort
identifier="id"
onTreeNodeClick={this._setLastActionState.bind(this, "clicked", "2")}
onTreeNodeCollapseChange={this._handleDeclarativeTreeNodePropChange.bind(this, "collapsed")}
onTreeNodeCheckChange={this._handleDeclarativeTreeNodePropChange.bind(this, "checked")}
collapsible={false}
expandIconClass="fa fa-chevron-right"
collapseIconClass="fa fa-chevron-down">
<TreeNode label="Option 1" id="option_1"/>
<TreeNode label="Option 2" id="option_2">
<TreeNode label="Option B" checkbox={true} checked={this.state.staticTreeData["option_2.b"].checked} id="option_2.b"/>
<TreeNode label="Option A" checkbox={true} checked={this.state.staticTreeData["option_2.a"].checked} id="option_2.a"/>
</TreeNode>
<TreeNode label="Option 3" id="option_3"/>
<TreeNode label="Option 4" id="option_4"/>
</TreeMenu>
);
},
_getLabelFilterExample: function () {
return (
<TreeMenu
identifier="id"
onTreeNodeClick={function() {}}
onTreeNodeCollapseChange={function() {}}
onTreeNodeCheckChange={function() {}}
collapsible={false}
labelFilter={function(label) {
var max = 10;
if (label.length <= max) return label;
return label.substring(label, max).concat("...");
}}
expandIconClass="fa fa-chevron-right"
collapseIconClass="fa fa-chevron-down">
<TreeNode label="Option 1 with a long name" id="option_1"/>
<TreeNode label="Option 2 also has a long name" id="option_2">
<TreeNode label="Option A with a long name" checkbox={true} id="option_2.a"/>
<TreeNode label="Option B with a long name" checkbox={true} id="option_2.b"/>
</TreeNode>
<TreeNode label="Option 3 with a long name" id="option_3"/>
<TreeNode label="Option 4 with a long name" id="option_4"/>
</TreeMenu>
);
},
_getRendererExample: function () {
return (
<TreeMenu
identifier="id"
onTreeNodeClick={function() {}}
onTreeNodeCollapseChange={function() {}}
onTreeNodeCheckChange={function() {}}
collapsible={false}
labelFilter={function(label) {
var max = 10;
if (label.length <= max) return label;
return label.substring(label, max).concat("...");
}}
labelFactory={function(labelClassName, displayLabel) {
return <span className={labelClassName}>{displayLabel} {"<span/>"}</span>;
}}
checkboxFactory={function(checkboxClassName, isChecked) {
return <span className={checkboxClassName + " fa " + (isChecked ? "fa-check-circle" : "fa-circle-o")}></span>;
}}
expandIconClass="fa fa-chevron-right"
collapseIconClass="fa fa-chevron-down">
<TreeNode label="Option 1 with a long name" id="option_1"/>
<TreeNode label="Option 2 also has a long name" id="option_2" checkbox={true} checked={false}>
<TreeNode label="Option A with a long name" checkbox={true} id="option_2.a"/>
<TreeNode label="Option B with a long name" checkbox={true} checked={true} id="option_2.b"/>
</TreeNode>
<TreeNode label="Option 3 with a long name" id="option_3"/>
<TreeNode label="Option 4 with a long name" id="option_4"/>
</TreeMenu>
);
},
_getDynamicTreeExample: function () {
return (
<TreeMenu
expandIconClass="fa fa-chevron-right"
collapseIconClass="fa fa-chevron-down"
onTreeNodeClick={this._setLastActionState.bind(this, "clicked", "1")}
onTreeNodeCollapseChange={this._handleDynamicTreeNodePropChange.bind(this, "collapsed")}
onTreeNodeCheckChange={this._handleDynamicTreeNodePropChange.bind(this, "checked")}
data={this.state.dynamicTreeData} />
);
},
_getDynamicTreeExample2: function () {
return (
<TreeMenu
expandIconClass="fa fa-chevron-right"
collapseIconClass="fa fa-chevron-down"
onTreeNodeClick={this._setLastActionState.bind(this, "clicked", "5")}
onTreeNodeCollapseChange={this._handleDynamicObjectTreeNodePropChange.bind(this, 5, "dynamicTreeDataMap","collapsed")}
onTreeNodeCheckChange={this._handleDynamicObjectTreeNodePropChange.bind(this, 5, "dynamicTreeDataMap","checked")}
data={this.state.dynamicTreeDataMap} />
);
},
_getDynamicTreeExample3: function () {
return (
<TreeMenu
expandIconClass="fa fa-chevron-right"
collapseIconClass="fa fa-chevron-down"
onTreeNodeCollapseChange={this._handleDynamicObjectTreeNodePropChange.bind(this, 6, "dynamicTreeDataMap2", "collapsed")}
onTreeNodeCheckChange={this._handleDynamicObjectTreeNodePropChange.bind(this, 6, "dynamicTreeDataMap2","checked")}
onTreeNodeSelectChange={this._handleDynamicObjectTreeNodePropChange.bind(this, 6, "dynamicTreeDataMap2","selected")}
data={this.state.dynamicTreeDataMap2} />
);
},
_getExamplePanel: function (title, treeMenuNode) {
return <div>
<div className="panel panel-default">
<div className="panel-heading">{title + " Menu"}</div>
<div className="panel-body">
{treeMenuNode}
</div>
</div>
</div>;
},
_handleDeclarativeTreeNodePropChange: function (propName, lineage) {
this._setLastActionState(propName, "2", lineage);
var nodeId = lineage.pop();
var treeState = this.state.staticTreeData;
treeState[nodeId][propName] = !treeState[nodeId][propName];
this.setState({
staticTreeData : treeState
});
},
_handleDynamicTreeNodePropChange: function (propName, lineage) {
this._setLastActionState(propName, "1", lineage);
this.setState(TreeMenuUtils.getNewTreeState(lineage, this.state.dynamicTreeData, propName));
},
_handleDynamicObjectTreeNodePropChange: function (messageWindowKey, stateKey, propName, lineage) {
this._setLastActionState(propName, messageWindowKey, lineage);
//Get a node path that includes children, given a key
function getNodePath(nodeKey) {
if (nodeKey.length === 1) return nodeKey;
return _(nodeKey).zip(nodeKey.map(function () {
return "children";
})).flatten().initial().value();
}
var oldState = Immutable.fromJS(this.state[stateKey]);
var nodePath = getNodePath(lineage),
keyPaths = [nodePath.concat([propName])];
//Build a list of key paths for all children
function addChildKeys(state, parentPath) {
var childrenPath = parentPath.concat('children'),
children = state.getIn(childrenPath);
if (!children || children.size === 0) return;
children.map(function (value, key) {
keyPaths.push(childrenPath.concat([key, propName]))
addChildKeys(state, childrenPath.concat([key]));
});
}
addChildKeys(oldState, nodePath);
//Get the new prop state
var newPropState = !oldState.getIn(keyPaths[0]);
//Now create a new map w/ all the changes
var newState = oldState.withMutations(function(state) {
keyPaths.forEach(function (keyPath) {
state.setIn(keyPath, newPropState);
})
});
var mutation = {};
mutation[stateKey] = newState.toJS();
this.setState(mutation);
},
_setLastActionState: function (action, col, node) {
var toggleEvents = ["collapsed", "checked", "selected"];
if (toggleEvents.indexOf(action) >= 0) {
action += "Changed";
}
console.log("Controller View received tree menu " + action + " action: " + node.join(" > "));
var key = "lastAction" + col;
var mutation = {};
mutation[key] = {
event: action,
node: node.join(" > "),
time: new Date().getTime()
};
this.setState(mutation)
}
});
module.exports = App;