Wijmo

TreeView 101

This page shows how to get started with Wijmo's TreeView control.

Getting Started

Steps for getting started with Input controls in AngularJS applications:

  1. Add references to AngularJS, Wijmo, and Wijmo's AngularJS directives.
  2. Include the Wijmo directives in the app module:
    var app = angular.module('app', ['wj']);
  3. Add a controller to provide data and logic.
  4. Add a Wijmo Input control to the page and bind it to your data.
  5. (Optional) Add some CSS to customize the input control's appearance.

The examples below demonstrate all this.

Creating Trees

To create trees, you will normally have to set three properties:

  1. itemsSource defines the array that contains the hierarchical data. Each item in the array contains information about a node and (optionally) an array of child nodes.
  2. displayMemberPath defines the name of the property in the items that contains the text to be displayed in the tree nodes. By default, this property is set to the string 'header'.
  3. childItemsPath defines the name of the property in the items that contains the array of child nodes. By default, this property is set to the string 'items'.

There are also properties for binding node images, checkboxes, and collapsed state to the itemsSource array.

By default, the TreeView expands the first node of each level when it loads the tree. You can customize that behavior using the collapsedMemberPath property to control the collapsed state of each node, or call the collapseToLevel method after the tree is loaded to collapse all nodes deeper than the level you want to show.

Once the tree is loaded, you can select, collapse, or expand nodes using the mouse or the keyboard. You can also use the keyboard to search for nodes.

The TreeView control uses animations to expand and collapse nodes by default. You can turn this feature off by setting the isAnimated property to false.

It also collapses sibling nodes automatically when a node is expanded. You can turn this feature off by setting the autoCollapse property to false.

By default, the TreeView control expands collapsed nodes when the user clicks anywhere on the node. You can change this by setting the expandOnClick property to false, in which case only clicks on the collapsed/expanded glyph will affect the collapsed state.

HTML
<!DOCTYPE html> <html> <head> <link rel="stylesheet" href="css/bootstrap.css"/> <link rel="stylesheet" href="css/wijmo.css"/> <link href="css/app.css" rel="stylesheet"/> <script src="scripts/angular.js"></script> <script src="scripts/wijmo.js"></script> <script src="scripts/wijmo.nav.js"></script> <script src="scripts/wijmo.angular.js"></script> <script src="scripts/app.js"></script> </head> <body ng-app="app" ng-controller="appCtrl"> <!-- this is the InputNumber directive --> <wj-tree-view control="tv" items-source="items" display-member-path="'header'" child-items-path="'items'" is-animated="isAnimated" auto-collapse="autoCollapse" expandOn-click="expandOnClick"> </wj-tree-view> <div> <button id="btnCollapse" class="btn btn-default" ng-click="tv.collapseToLevel(0);">Collapse All</button> <button id="btnExpand" class="btn btn-default" ng-click="tv.collapseToLevel(1000);">Expand All</button> </div> <br /> <label> <input id="chkIsAnimated" type="checkbox" ng-model="tv.isAnimated" /> isAnimated </label> <br /> <label> <input id="chkAutoCollapse" type="checkbox" ng-model="tv.autoCollapse" /> autoCollapse </label> <br /> <label> <input id="chkExpandOnClick" type="checkbox" ng-model="tv.expandOnClick" /> expandOnClick </label> </body> </html>
JS
// declare app module var app = angular.module('app', ['wj']); // app controller provides data app.controller('appCtrl', function appCtrl($scope) { $scope.items = [ { header: 'Electronics', img: 'resources/electronics.png', items: [ { header: 'Trimmers/Shavers' }, { header: 'Tablets' }, { header: 'Phones', img: 'resources/phones.png', items: [ { header: 'Apple' }, { header: 'Motorola', newItem: true }, { header: 'Nokia' }, { header: 'Samsung' } ] }, { header: 'Speakers', newItem: true }, { header: 'Monitors' } ] }, { header: 'Toys', img: 'resources/toys.png', items: [ { header: 'Shopkins' }, { header: 'Train Sets' }, { header: 'Science Kit', newItem: true }, { header: 'Play-Doh' }, { header: 'Crayola' } ] }, { header: 'Home', img: 'resources/home.png', items: [ { header: 'Coffeee Maker' }, { header: 'Breadmaker', newItem: true }, { header: 'Solar Panel', newItem: true }, { header: 'Work Table' }, { header: 'Propane Grill' } ] } ]; $scope.isAnimated = true; $scope.autoCollapse = true; $scope.expandOnClick = true; });
CSS
/* default trees on this sample */ .wj-treeview { height: 350px; font-size: 120%; margin-bottom: 8px; background: white; box-shadow: 0 3px 6px rgba(0,0,0,0.16), 0 3px 6px rgba(0,0,0,0.23); }

Result (live):




Styling and CSS

You can customize the appearance of the TreeView using CSS.

This example changes the collapse/expand icons, uses different font sizes depending on node level, and adds a vertical bar to the left of the level one nodes.

Use the checkbox below the TreeView to toggle the custom style and see the difference.

HTML
<wj-tree-view control="tvCss" ng-class="useCustomCss?'custom-tree':''" items-source="items" display-member-path="'header'" child-items-path="'items'"> </wj-tree-view> <label> <input id="tvCssCheck" type="checkbox" checked="checked" ng-model="useCustomCss" /> Use custom CSS </label>
JS
$scope.useCustomCss = true;
CSS
/* custom tree styles */ .custom-tree.wj-treeview { color: #80044d; } /* level 0 and deeper nodes */ .custom-tree.wj-treeview .wj-nodelist > .wj-node { font-size: 120%; } /* level 1 and deeper nodes (larger font, vertical line along the left) */ .custom-tree.wj-treeview .wj-nodelist > .wj-nodelist > .wj-node, .custom-tree.wj-treeview .wj-nodelist > .wj-nodelist > .wj-nodelist { font-size: 110%; border-left: 4px solid rgba(128, 4, 77, 0.3); } /* level 2 and deeper nodes (smaller font, thinner border) */ .custom-tree.wj-treeview .wj-nodelist > .wj-nodelist > .wj-nodelist > .wj-node, .custom-tree.wj-treeview .wj-nodelist > .wj-nodelist > .wj-nodelist > .wj-nodelist { font-size: 100%; border-left: 2px solid rgba(128, 4, 77, 0.3); } /* expanded node glyph */ .custom-tree.wj-treeview .wj-nodelist .wj-node:before { content: "\e114"; font-family: 'Glyphicons Halflings'; top: 4px; border: none; opacity: .3; transition: all .3s cubic-bezier(.4,0,.2,1); } /* collapsed node glyph */ .custom-tree.wj-treeview .wj-nodelist .wj-node.wj-state-collapsed:before, .custom-tree.wj-treeview .wj-nodelist .wj-node.wj-state-collapsing:before { transform: rotate(-180deg); transition: all .3s cubic-bezier(.4,0,.2,1); }

Result (live):

Navigation Trees

The simplest and most common use for the TreeView control is navigation. The TreeView's hierarchical structure and auto-search functionality make it easy for users to drill-down and find the items they are interested in.

You can use the selectedItemChanged or itemClicked events for navigation. The difference is that selectedItemChanged occurs when the user moves the selection with the keyboard, and itemClicked occurs when the user clicks an item or presses the Enter key.

This example uses the itemClicked event:

HTML
<wj-tree-view control="tvNav" items-source="items" display-member-path="'header'" child-items-path="'items'" item-clicked="navTo(tvNav)"> </wj-tree-view> <div id="tvNavItem" ng-bind-html="navStr" ></div>
JS
$scope.navStr = ''; $scope.navTo = function (treeView) { $scope.navStr = $sce.trustAsHtml('Navigating to <b>*** ' + treeView.selectedItem.header + ' ***</b>'); $scope.$apply(); }

Result (live):

Accordion Trees

Accordions are multi-pane panels that keep only one panel expanded at a time. They are commonly used for navigation.

You can use the TreeView control to implement accordions.

Use CSS to customize the header display and to hide the collapse/expand glyphs, and make sure the autoCollapse property is set to true (the default), so non-active panels are automatically collapsed.

HTML
<wj-tree-view control="tvAccordion" class="accordion-tree" items-source="accordionItems" is-Content-html="true" auto-Collapse="true"> </wj-tree-view> <div id="tvAccordionItem" ng-bind-html="tvAccordionStr"></div>
JS
$scope.tvAccordionStr = ''; $scope.$watch('tvAccordion', function () { /* handle clicks on accordion items */ if (!$scope.tvAccordion) { return; } $scope.tvAccordion.hostElement.addEventListener('click', function (e) { if (e.target.tagName == 'A') { $scope.tvAccordionStr = $sce.trustAsHtml('Navigating to <b>*** ' + treeView.selectedItem.header + ' ***</b>'); $scope.$apply(); e.preventDefault(); } }); });
CSS
/* accordion tree styles */ .accordion-tree.wj-treeview { background: transparent; box-shadow: none; height: auto; } /* hide collapse/expand glyphs */ .accordion-tree.wj-treeview .wj-nodelist .wj-node:before { display: none; } /* level 0 nodes (headers) */ .accordion-tree.wj-treeview .wj-nodelist > .wj-node { font-size: 120%; font-weight: bold; padding: 6px 10px; color: white; background: #106cc8; margin-bottom: 4px; box-shadow: 0 1px 3px rgba(0,0,0,0.12), 0 1px 2px rgba(0,0,0,0.24); } /* level 1 nodes (navigation items) */ .accordion-tree.wj-treeview .wj-nodelist > .wj-nodelist > .wj-node { font-size: inherit; font-weight: normal; padding: 4px 1em; color: inherit; background: inherit; box-shadow: none; } .accordion-tree.wj-treeview .wj-nodelist { padding-bottom: 6px; }

Result (live):

Checkboxes

Set the showCheckboxes property to true and the TreeView will add checkboxes to each node.

When checkboxes are displayed, the TreeView manages their hierarchy so that when a checkbox is checked or cleared, the new value is automatically applied to all child nodes, and reflected on the state of the parent nodes.

When items are checked or unchecked, the checkedItemsChanged event is raised, and the checkedItems property is updated with a list of the items that are currently checked.

HTML
<wj-tree-view control="tvChk" items-source="items" display-member-path="'header'" child-items-path="'items'" show-checkboxes="true" checked-items-changed="checkedItems(tvChk)"> </wj-tree-view> <button id="btnCheckAll" class="btn btn-default" ng-click="tvChk.checkAllItems(true);"> Check All </button> <button id="btnUncheckAll" class="btn btn-default" ng-click="tvChk.checkAllItems(false);"> Uncheck All </button>      <button id="btnSaveState" class="btn btn-default" ng-click="saveCheckedItems(tvChk)"> Save State </button> <button id="btnRestoreState" class="btn btn-default" ng-click="restoreCheckedItems(tvChk)"> Restore State </button> <br/> <div id="tvChkStatus" ng-bind-html="tvChkStatusStr"> </div>
JS
$scope.tvChkStatusStr = ''; $scope.checkedItems = function (treeView) { var items = treeView.checkedItems, msg = ''; if (items.length) { msg = '<p><b>Checked Items: </b></p><ol>\r\n'; for (var i = 0; i < items.length; i++) { msg += '<li>' + items[i].header + '</li>\r\n'; } msg += '</ol>'; } $scope.tvChkStatusStr = $sce.trustAsHtml(msg); $scope.$apply(); } // save checked items var checkedItems = []; $scope.saveCheckedItems = function(treeView) { checkedItems = treeView.checkedItems; } // restore checked items $scope.restoreCheckedItems = function (treeView) { treeView.checkedItems = checkedItems || []; }

Result (live):

    

Images

Use the imageMemberPath property to add images to nodes by specifying the name of a property on the data items that contains an image URL.

For example, some our sample items array have an "img" property set to image URLs:

HTML
<wj-tree-view control="tvImg" items-source="items" display-member-path="'header'" child-items-path="'items'" image-member-path="'img'"> </wj-tree-view>

Result (live):

Disabled Nodes

You can disable nodes using the TreeNode's isDisabled property. Disabled nodes cannot be selected using the mouse or keyboard.

HTML
<wj-tree-view control="tvDisable" items-source="items" display-member-path="'header'" child-items-path="'items'"> </wj-tree-view> <button id="btnDisableNode" class="btn btn-default" ng-click="disableNode(tvDisable)">Disable Selected Node</button> <button id="btnEnableAllNodes" class="btn btn-default" ng-click="enableAllNodes(tvDisable)">Enable All Nodes</button>
JS
// disable node $scope.disableNode= function (treeView) { var nd = treeView.selectedNode; if (nd) { nd.isDisabled = true; } } // enable all nodes $scope.enableAllNodes = function (treeView) { for (var nd = treeView.getFirstNode(); nd; nd = nd.next()) { nd.isDisabled = false; } }

Result (live):

Custom Node Content

You can customize the content of the TreeView nodes using the formatItem event. The event handler parameters include the element that represents the node and the data item being rendered.

The example below uses the formatItem event to add a "new" badge to the right of new items on the tree.

HTML
<wj-tree-view control="tvFmtItem" items-source="items" display-member-path="'header'" child-items-path="'items'" format-item="formatItem(s, e)"> </wj-tree-view>
JS
// enable all nodes $scope.formatItem = function (s, e) { if (e.dataItem.newItem) { e.element.innerHTML += ' <img style="margin-left:6px;transform:rotate(-30deg)" src="resources/new.png"/>'; } }

Result (live):

Lazy Loading

Lazy loading is useful when you are dealing with large hierarchical data sources and would like to avoid the delays involved in loading the entire data set at once.

The TreeView control makes lazy-loading super easy. Only two steps are required:

  1. Set the items property in the parent node data item to an empty array.
  2. Set the TreeView's lazyLoadFunction property to a function to be called when the user expands the node. This function takes two parameters: the parent node and a callback function to be invoked when the data becomes available.

The tree in example below starts with three lazy-loaded nodes. When you expand them, the lazyLoadFunction is invoked. The function uses a setTimeout to simulate an http delay and returns data for three child nodes, one of which is also a lazy-loaded node.

HTML
<wj-tree-view control="tvLazyLoad" items-source="lazyItems" display-member-path="'header'" child-items-path="'items'" lazy-load-function="lazyLoadFunction"> </wj-tree-view>
JS
$scope.lazyItems = [ // start with three lazy-loaded nodes { header: 'Lazy Node 1', items: [] }, { header: 'Lazy Node 2', items: [] }, { header: 'Lazy Node 3', items: [] } ]; $scope.lazyLoadFunction =function (node, callback) { setTimeout(function () { // simulate http delay var result = [ // simulate result { header: 'Another lazy node...', items: [] }, { header: 'A non-lazy node without children' }, { header: 'A non-lazy node with child nodes', items: [ { header: 'hello' }, { header: 'world' } ]} ]; callback(result); // return result to control }, 2500); // 2.5sec http delay } }

Result (live):

Lazy Loading and OData

This example shows how you can use the TreeView control to display hierarchical data from OData sources.

The sample starts by loading the Northwind employees table. When the data is loaded, the code adds an "Orders" empty array to each employee. The lazyLoadFunction is used to load the orders when an employee node is expanded.

The orders table also adds an "Order_Details" empty array to each order. The lazyLoadFunction is used to load the order details when an order node is expanded.

HTML
<wj-tree-view control="tvLazyLoadOData" display-member-path="['FullName', 'ShipName', 'Summary' ]" child-items-path="['Orders', 'Order_Details']" lazy-load-function="lazyLoadODataFunction"> </wj-tree-view>
JS
$scope.tvLazyLoadOData = null; var nwindService = 'http://services.odata.org/V4/Northwind/Northwind.svc'; $scope.lazyLoadFunction = function (node, callback) { switch (node.level) { // load orders for an employee case 0: var url = 'Employees(' + node.dataItem.EmployeeID + ')/Orders'; var orders = new wijmo.odata.ODataCollectionView(nwindService, url, { fields: 'OrderID,ShipName,ShipCountry'.split(','), loaded: function () { var items = orders.items.map(function (e) { e.Order_Details = []; // lazy-order details return e; }); callback(items); } }); break; // load extended details for an order case 1: var url = "Order_Details_Extendeds/?$filter=OrderID eq " + node.dataItem.OrderID; var details = new wijmo.odata.ODataCollectionView(nwindService, url, { fields: 'ProductName,ExtendedPrice'.split(','), loaded: function () { var items = details.items.map(function (e) { e.Summary = wijmo.format('{ProductName}: {ExtendedPrice:c}', e); return e; }); callback(items); } }); break; // default default: callback(null); } } $scope.$watch('tvLazyLoadOData', function () { var employees = new wijmo.odata.ODataCollectionView(nwindService, 'Employees', { fields: 'EmployeeID,FirstName,LastName'.split(','), loaded: function () { var items = employees.items.map(function (e) { e.FullName = e.FirstName + ' ' + e.LastName; e.Orders = []; // lazy-load orders return e; }); $scope.tvLazyLoadOData.itemsSource = items; } }); });
CSS
/* level 0 nodes and deeper (employees...) */ #tvLazyLoadOData.wj-treeview .wj-nodelist > .wj-node { font-weight: bold; } /* level 1 nodes and deeper (orders...) */ #tvLazyLoadOData.wj-treeview .wj-nodelist > .wj-nodelist > .wj-node { font-weight: normal; font-size: 95%; color: darkblue; } /* level 2 nodes and deeper (order details...) */ #tvLazyLoadOData.wj-treeview .wj-nodelist > .wj-nodelist > .wj-nodelist > .wj-node { font-size: 90%; color: darkslategrey; }

Result (live):

Drag and Drop

Set the allowDragging property to true to allow users to drag nodes to new positions within the TreeView.

When dragging is allowed, users may drag any node to any position within the tree. You can customize this behavior by handling the TreeView drag/drop events:

HTML
<wj-tree-view control="tvDragDrop" items-source="items" display-member-path="'header'" child-items-path="'items'" show-checkboxes="true" image-member-path="'img'" allow-dragging="allowDragging" drag-start="dragStart(s,e)" drag-over="dragOver(s,e)"> </wj-tree-view> <label> <input id="allowDragging" type="checkbox" checked="checked" ng-model="tvDragDrop.allowDragging" > allowDragging </label> <br /> <label> <input id="allowDraggingParentNodes" type="checkbox" checked="checked" ng-model="allowDraggingParentNodes"> allow dragging parent nodes </label> <br /> <label> <input id="allowDroppingIntoEmpty" type="checkbox" checked="checked" ng-model="allowDroppingIntoEmpty"> allow dropping into empty nodes </label>
JS
$scope.allowDraggingParentNodes = true; $scope.allowDroppingIntoEmpty = true; // use dragStart event to honor the allowDraggingParentNodes setting // by setting the 'cancel' event parameter to true $scope.dragStart = function (s, e) { if (e && e.node.hasChildren) { if (!$scope.allowDraggingParentNodes) { e.cancel = true; // prevent dragging parent nodes } else { e.node.isCollapsed = true; // collapse parent nodes when dragging } } } // use dragOver event to honor the allowDroppingIntoEmpty setting // by changing the 'position' event parameter to 'Before' $scope.dragOver = function (s, e) { if (!$scope.allowDroppingIntoEmpty && !e.dropTarget.hasChildren && e.position == wijmo.input.DropPosition.Into) { e.position = wijmo.input.DropPosition.Before; } }

Result (live):



Drag and Drop Between Trees

Setting the allowDrag property to true allows users to drag and drop nodes within the same TreeView.

To allow dragging and dropping nodes between different TreeView controls, you must handle the dragOver event and set the cancel parameter to true if the move is not valid, or to false if it is valid.

In the example below, users can drag nodes within and between the two trees:

HTML
<wj-tree-view control="tvDragDrop1" class="short" items-source="dragItems1" display-member-path="'header'" child-items-path="'items'" allow-dragging="true" drag-Over="dragOverBetweenTrees(s,e)"> </wj-tree-view> <wj-tree-view control="tvDragDrop2" class="short" items-source="dragItems2" display-member-path="'header'" child-items-path="'items'" allow-dragging="true" drag-over="dragOverBetweenTrees(s,e)"> </wj-tree-view>
JS
$scope.dragItems1 = [ { header: 'Item 1.1' }, { header: 'Item 1.2' }, { header: 'Item 1.3' } ]; $scope.dragItems2 = [ { header: 'Item 2.1' }, { header: 'Item 2.2' }, { header: 'Item 2.3' } ]; $scope.tvDragDrop1 = null; $scope.tvDragDrop2 = null; // allow drag/drop between tvDragDrop1 and tvDragDrop2 $scope.dragOverBetweenTrees = function (sender, e) { var t1 = e.dragSource.treeView; var t2 = e.dropTarget.treeView; if (t1 == $scope.tvDragDrop1 || t1 == $scope.tvDragDrop2) { if (t2 == $scope.tvDragDrop1 || t2 == $scope.tvDragDrop2) { e.cancel = false; } } }

Result (live):

Editing Nodes

The TreeView control provides editing support. Set the isReadOnly property to false and users will be able to edit the content of the nodes by pressing the F2 key.

Edits made to node contents are automatically applied to the items in the itemsSource array using the properties specified by the displayMemberPath property.

You may customize the editing behavior using the following events: nodeEditStarting, nodeEditStarted, nodeEditEnding, and nodeEditEnded.

In the example below, we enable editing only for nodes that contain no children. To edit, select a node and press F2:

HTML
<wj-tree-view control="tvEdit" items-source="editableItems" display-member-path="'header'" child-items-path="'items'" image-member-path="'img'" show-checkboxes="true" is-read-only="false" node-edit-starting="nodeEditStarting(s,e)"> </wj-tree-view>
JS
$scope.nodeEditStarting = function (s, e) { if (e.node.hasChildren) { e.cancel = true; } }

Result (live):

RTL support

Some languages render content from the right to the left of the page (Arabic and Hebrew are typical examples) . HTML accommodates this with the 'dir' attribute. Setting 'dir' to 'rtl' on any element causes the element's content to flow from right to left.

The TreeView supports this automatically. If the element hosting the tree has the 'dir' attribute set to 'rtl', the tree will render with nodes extending from right to left. You don't have to set any properties on the control.

Note that the 'dir' attribute value is inherited, so if you set it on the body tag for example, the entire page will be rendered from right to left, including the tree.

Note also that CSS has a 'direction' attribute that performs the same function as the 'dir' element attribute. The 'dir' attribute is generally considered more appropriate for several reasons, including the fact that it can be used in CSS rules.

HTML
<wj-tree-view control="tvRtl" items-source="items" display-member-path="'header'" child-items-path="'items'" image-member-path="'img'" show-checkboxes="true"> </wj-tree-view>

Result (live):

My parent element has a dir="rtl" attribute!