LIDOR SYSTEMS

Advanced User Interface Controls and Components

Custom Item Templates in AngularJS TreeView

Created: 18 April 2016

By default, each item in AngularJS TreeView directive can display a checkbox, icon and a label. If we want to display custom content in specific item or in all items, we need to use a template. Templates allow us to add custom HTML elements and/or other AngularJS directives in each item. We can arrange them in custom layouts; this can be all set in your app code.

TreeView directive is part of IntegralUI Studio for Web
a suite of UI Components for development of web apps

In above example, we are using a custom template for each item in the TreeView. Expand boxes are replaced with custom folder icons, item label consists of two parts where the first part is editable and there are command buttons placed on the right side, which when clicked either edit or delete the corresponding item.

Create Custom Template for Tree Items

To create a template we will use the script tag so that it is available in template cache so that AngularJS can find it easily. Our template will consist of three parts, the first one showing a custom expand box, the middle one showing an editable item label and the last part showing custom command buttons on the right.

angular

.module("appModule", ["integralui"])

.controller("appCtrl", ["$scope", "IntegralUITreeViewService", function($scope, $treeService){

$scope.treeName = "treeSample";

$scope.data = [];

 

$scope.customItemTemplate = { url: "'item-template.html'" };

}]);

<!DOCTYPE html>

<html>

<head>

<link rel="stylesheet" href="css/integralui.css" />

<link rel="stylesheet" href="css/integralui.treeview.css" />

<link rel="stylesheet" href="css/themes/theme-flat-blue.css" />

<script type="text/javascript" src="external/angular.min.js"></script>

<script type="text/javascript" src="js/angular.integralui.min.js"></script>

<script type="text/javascript" src="js/angular.integralui.lists.min.js"></script>

<script type="text/javascript" src="js/angular.integralui.treeview.min.js"></script>

</head>

<body>

<div ng-app="appModule" ng-controller="appCtrl">

<script type="text/ng-template" id="'item-template.html'">

<div class="custom-item" ng-mouseenter="obj.showButtons = true" ng-mouseleave="obj.showButtons = !obj.showButtonsOnHover">

<div iui-class="{{obj.expandBox}}" ng-click="obj.events.expandBoxClick(obj)"></div>

<div style="display:inline-block;vertical-align:middle">

<span class="item-text" ng-show="!obj.edit">{{obj.text}}</span>

<input class="item-editor" ng-show="obj.edit" ng-model="obj.editText" ng-keydown="obj.events.editorKeyDown($event, obj)" iui-focus="{active:obj.edit}" onFocus="this.setSelectionRange(0, this.value.length)" ng-blur="obj.events.editorLostFocus(obj)" />

<span class="item-subtext" ng-show="!obj.edit">{{obj.subText}}</span>

</div>

<div style="display:inline-block;position:absolute;right:0;top:7px;padding-left:5px;" ng-show="obj.showButtons != false">

<span class="item-button item-button-trash" ng-click="obj.events.trashClick(obj)"></span>

<span class="item-button item-button-edit" ng-click="obj.events.editClick(obj)"></span>

</div>

</div>

</script>

<iui-treeview name="{{treeName}}" items="data" template-settings="customItemTemplate" allow-focus="false" after-collapse="onAfterCollapse(e)" after-expand="onAfterExpand(e)" update-complete="onUpdateComplete()" show-expand-boxes="false"v></iui-treeview>

</div>

</body>

</html>

.iui-treeview-item-hovered

{

background: #efefef;

}

.iui-treeview-item-selected

{

background-color: #dedede;

color: #008000;

}

.custom-item

{

padding: 5px;

position: relative;

}

In order for this template to be used by the TreeView directive, we need to set the value of url field in templateSettings property to point to the template. For this purpose, the template id needs to be set equal to the one in the url field.

Finally, each item has a templateObj field that needs to be set. In our case, we are using an object that holds the item id, text and some custom events.

var objEvents = {

expandBoxClick: function(obj){ return expandBoxClick(obj) },

editClick: function(obj){ return editClick(obj) },

editorKeyDown: function(e, obj){ return editorKeyDown(e, obj) },

editorLostFocus: function(obj){ return editorLostFocus(obj) },

trashClick: function(obj){ return trashClick(obj) }

}

 

$scope.customItems = [

{

id: 1,

templateObj: { itemId: 1, text: "Favorites", events: objEvents },

items: [

{ id: 11, pid: 1, templateObj: { itemId: 11, text: "Desktop", events: objEvents } },

{ id: 12, pid: 1, templateObj: { itemId: 12, text: "Downloads", events: objEvents } }

]

},

{

id: 2,

templateObj: { itemId: 2, text: "Libraries", events: objEvents },

items: [

{

id: 21,

pid: 2,

templateObj: { itemId: 21, text: "Documents", events: objEvents }, expanded: false,

items: [

{ id: 211, pid: 21, templateObj: { itemId: 211, text: "My Documents", events: objEvents } },

{ id: 212, pid: 21, templateObj: { itemId: 212, text: "Public Documents", events: objEvents } }

]

},

{ id: 22, pid: 2, templateObj: { itemId: 22, text: "Music", events: objEvents } },

{ id: 23, pid: 2, templateObj: { itemId: 23, text: "Pictures", events: objEvents } },

{ id: 24, pid: 2, templateObj: { itemId: 24, text: "Videos", events: objEvents } }

]

},

{

id: 3,

templateObj: { itemId: 3, text: "Computer", events: objEvents },

expanded: false,

items: [

{ id: 31, pid: 3, templateObj: { itemId: 31, text: "Local Disk (C:)", events: objEvents } },

{ id: 32, pid: 3, templateObj: { itemId: 32, text: "Storage (D:)", events: objEvents } }

]

},

{ id: 4, templateObj: { itemId: 4, text: "Network", events: objEvents } },

{ id: 5, templateObj: { itemId: 5, text: "Recycle Bin", events: objEvents } }

];

Change ExpandBox Appearance

In our example, we are using a custom <div> element to create our expand box, with some custom CSS modifications. We can place the expand box in any location within the item space, to keep it simple let us keep the default position at far left side.

<script type="text/ng-template" id="'item-template.html'">

<div class="custom-item" ng-mouseenter="obj.showButtons = true" ng-mouseleave="obj.showButtons = !obj.showButtonsOnHover">

<div iui-class="{{obj.expandBox}}" ng-click="obj.events.expandBoxClick(obj)"></div>

<div style="display:inline-block;vertical-align:middle">

<span class="item-text" ng-show="!obj.edit">{{obj.text}}</span>

<input class="item-editor" ng-show="obj.edit" ng-model="obj.editText" ng-keydown="obj.events.editorKeyDown($event, obj)" iui-focus="{active:obj.edit}" onFocus="this.setSelectionRange(0, this.value.length)" ng-blur="obj.events.editorLostFocus(obj)" />

<span class="item-subtext" ng-show="!obj.edit">{{obj.subText}}</span>

</div>

<div style="display:inline-block;position:absolute;right:0;top:7px;padding-left:5px;" ng-show="obj.showButtons != false">

<span class="item-button item-button-trash" ng-click="obj.events.trashClick(obj)"></span>

<span class="item-button item-button-edit" ng-click="obj.events.editClick(obj)"></span>

</div>

</div>

</script>

.item-expand-box

{

background: url(../../../resources/icons-x24.png) no-repeat 0 0;

display: inline-block;

width: 24px;

height: 24px;

margin-right: 2px;

vertical-align: top;

}

.item-expand-box-open

{

background-position: -120px -144px;

}

.item-expand-box-close

{

background-position: -144px -144px;

}

As visual representation of the expand box in this example we will use open/close folder icons. You can easily change this to any other custom icons that is better suitable for your application.

When this element is clicked, the icon will change showing whether the corresponding item is expanded or collapsed.

var changeExpandBox = function(item){

if (item && item.templateObj)

item.templateObj.expandBox = item.expanded != false ? 'item-expand-box item-expand-box-close' : 'item-expand-box item-expand-box-open';

}

 

var expandBoxClick = function(obj){

if (obj){

var item = $treeService.findItemById($scope.treeName, obj.itemId);

if (item){

$treeService.toggle($scope.treeName, item);

}

}

}

 

$scope.onAfterCollapse = function(e){

if (e.item)

changeExpandBox(e.item);

}

 

$scope.onAfterExpand = function(e){

if (e.item)

changeExpandBox(e.item);

}

Note As you can see from code, the ng-click value is set to point to a function that is set from templateObj. Because template is compiled using the item scope, and to link an outside action like a click, this is required.

Add Editable Label to Each Item

The item label is represented with two <span> elements and one <input> element for editing purpose. The input element by default is hidden and it will appear only when edit command button is clicked.

<script type="text/ng-template" id="'item-template.html'">

<div class="custom-item" ng-mouseenter="obj.showButtons = true" ng-mouseleave="obj.showButtons = !obj.showButtonsOnHover">

<div iui-class="{{obj.expandBox}}" ng-click="obj.events.expandBoxClick(obj)"></div>

<div style="display:inline-block;vertical-align:middle">

<span class="item-text" ng-show="!obj.edit">{{obj.text}}</span>

<input class="item-editor" ng-show="obj.edit" ng-model="obj.editText" ng-keydown="obj.events.editorKeyDown($event, obj)" iui-focus="{active:obj.edit}" onFocus="this.setSelectionRange(0, this.value.length)" ng-blur="obj.events.editorLostFocus(obj)" />

<span class="item-subtext" ng-show="!obj.edit">{{obj.subText}}</span>

</div>

<div style="display:inline-block;position:absolute;right:0;top:7px;padding-left:5px;" ng-show="obj.showButtons != false">

<span class="item-button item-button-trash" ng-click="obj.events.trashClick(obj)"></span>

<span class="item-button item-button-edit" ng-click="obj.events.editClick(obj)"></span>

</div>

</div>

</script>

.item-editor

{

border: thin solid gray;

padding: 3px;

}

.item-text

{

border: thin solid transparent;

display: inline-block;

padding-top: 2px;

}

.item-subtext

{

color: #ababab;

font-style: italic;

font-size: 0.875em;

}

The first <span> element will display the item text that is editable, and the second element shows the number of children present, if any. This information is divided on two numbers, whether child items are folders or files.

Position Command Buttons on Right Side

To add a specific action during run time, each item displays a set of command buttons to the right side of the item space. These command buttons are simply a <span> elements where we use a small icon as their background.

<script type="text/ng-template" id="'item-template.html'">

<div class="custom-item" ng-mouseenter="obj.showButtons = true" ng-mouseleave="obj.showButtons = !obj.showButtonsOnHover">

<div iui-class="{{obj.expandBox}}" ng-click="obj.events.expandBoxClick(obj)"></div>

<div style="display:inline-block;vertical-align:middle">

<span class="item-text" ng-show="!obj.edit">{{obj.text}}</span>

<input class="item-editor" ng-show="obj.edit" ng-model="obj.editText" ng-keydown="obj.events.editorKeyDown($event, obj)" iui-focus="{active:obj.edit}" onFocus="this.setSelectionRange(0, this.value.length)" ng-blur="obj.events.editorLostFocus(obj)" />

<span class="item-subtext" ng-show="!obj.edit">{{obj.subText}}</span>

</div>

<div style="display:inline-block;position:absolute;right:0;top:7px;padding-left:5px;" ng-show="obj.showButtons != false">

<span class="item-button item-button-trash" ng-click="obj.events.trashClick(obj)"></span>

<span class="item-button item-button-edit" ng-click="obj.events.editClick(obj)"></span>

</div>

</div>

</script>

.item-button

{

background-image: url(../../../resources/icons.png);

background-repeat: no-repeat;

display: inline-block;

overflow: hidden;

padding: 0;

margin: 3px 4px 0 4px;

width: 16px;

height: 16px;

float: right;

opacity: 0.6;

}

.item-button:hover

{

opacity: 1;

}

.item-button-trash

{

background-position: -80px -96px;

}

.item-button-edit

{

background-position: -128px -81px;

}

In our case, we have two buttons:

  • Edit button - will display a text editor so that you can change the item label
  • Trash button - will remove the corresponding item from tree hierarchy

When edit button is clicked, the <input> element from editable part of the template is shown with text equal to the item text. In this text editor we can enter a new item label, and confirm this change by pressing the ENTER key. If we want, we can also cancel the change by pressing the ESCAPE key or clicking anywhere outside the text editor. When change is confirmed, the text editor will close and the item label will be replaced by its new value.

var editClick = function(obj){

if (obj){

obj.edit = true;

obj.editText = obj.text;

}

}

 

var editorKeyDown = function(e, obj){

switch (e.keyCode){

case 13: // ENTER

obj.text = obj.editText;

obj.edit = false;

break;

 

case 27: // ESCAPE

obj.edit = false;

break;

}

}

 

var editorLostFocus = function(obj){

if (obj)

obj.edit = false;

}

When trash button is clicked, the item is removed from the tree hierarchy and the expand box appearance is also changed depending on whether there are children or not. Also second item label is updated showing correct number of folders and files.

var trashClick = function(obj){

if (obj){

var item = $treeService.findItemById($scope.treeName, obj.itemId);

if (item){

$treeService.removeItem($scope.treeName, item);

updateExpandBoxAppearance();

updateItemSubText();

}

}

}

 

var isThereVisibleChildren = function(){

var found = false;

 

var list = $treeService.getFlatList($scope.treeName, true);

if (list){

for (var i = 0; i < list.length; i++){

if (list[i].items && list[i].items.length > 0){

found = true;

break;

}

}

}

 

return found;

}

 

var updateExpandBoxAppearance = function(){

var isThereChildren = isThereVisibleChildren();

var list = $treeService.getFlatList($scope.treeName, true);

if (list){

for (var i = 0; i < list.length; i++){

if (list[i].items && list[i].items.length > 0)

changeExpandBox(list[i]);

else

list[i].templateObj.expandBox = isThereChildren ? 'item-expand-box' : '';

}

}

}

 

var updateItemSubText = function(){

var list = $treeService.getFlatList($scope.treeName, true);

if (list){

for (var i = 0; i < list.length; i++){

var currentItem = list[i];

var numFolders = 0;

var numFiles = 0;

 

if (currentItem.items && currentItem.items.length > 0){

for (var k = 0; k < currentItem.items.length; k++){

var childItem = currentItem.items[k];

if (childItem.items && childItem.items.length > 0)

numFolders++;

else

numFiles++;

}

 

var itemSubText = "( ";

if (numFolders > 0 && numFiles > 0){

itemSubText += numFolders + " folder";

if (numFolders > 1)

itemSubText += "s";

itemSubText += " and " + numFiles + " file";

if (numFiles > 1)

itemSubText += "s";

}

else if (numFolders > 0){

itemSubText += numFolders + " folder";

if (numFolders > 1)

itemSubText += "s";

}

else if (numFiles > 0){

itemSubText += numFiles + " file";

if (numFiles > 1)

itemSubText += "s";

}

 

itemSubText += " )";

 

list[i].templateObj.subText = itemSubText;

}

else

list[i].templateObj.subText = '';

}

}

}

The appearance of command buttons is further enhanced so that they are shown only when required. That is, by default they can be hidden, and will appear only when mouse cursor hovers over item space. This is simply done by setting a variable in app scope and using the ngShow directive.

Conclusion

Using templates allows you to enhance the appearance and functionality of TreeView directive for AngularJS in many ways. This example is only a small part of what is possible. You can customize every part of the TreeView using CSS styles and templates with custom HTML elements. You can use this sample as a guideline to create a tree hierarchy that is better suited to your application requirements.

Did you Like this Article?


Enter your e-mail address below and you will receive latest articles as well as news on upcoming events and special offers.