JSFiddle - React, Tailwind, and code Playground

by Doug Hill

HTML

<link rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.0.3/css/bootstrap.min.css">
<script src="//cdnjs.cloudflare.com/ajax/libs/knockout.mapping/2.4.1/knockout.mapping.js"></script>
<script src="//code.jquery.com/jquery-1.9.0.min.js"></script>
<script src="http://netdna.bootstrapcdn.com/bootstrap/3.0.3/js/bootstrap.min.js"></script>
<div id="nav-bar" data-bind="template: { name: 'tree-template', data: treeData }"></div>


<script type="text/html" id="tree-template">
 
    <ul data-bind="foreach: nodes">
        <li>
            <span data-bind="
                template: { name: 'node-name-template', data: $data },
                css: { 'pointer-icon': nodes().length > 0 },
                click: toggleVisibility"></span>
            <div data-bind="template: { name: 'folder-template', data: $data }, visible: isExpanded"></div>
        </li>
    </ul>
 
</script>
 
<script type="text/html" id="folder-template">
 
    <ul data-bind="foreach: nodes">
        <li>
 
            <div data-bind="template: { name: 'node-template', data: $data }"></div>
 
        </li>
    </ul>
 
</script>
 
<script type="text/html" id="node-template">
 
    <span data-bind="
        template: { name: 'node-name-template', data: $data },
        css: { 'pointer-icon': nodes().length > 0 },
        click: toggleVisibility"></span>
 
    <!-- ko if: nodes().length !== 0 -->
 
        <div data-bind="template: { name: 'folder-template', data: $data }, visible: isExpanded"></div>
 
    <!-- /ko -->
 
</script>
 
<script type="text/html" id="node-name-template">
 
    <span class="tree-item" data-bind="
    css: { 
        'glyphicon glyphicon-minus-sign': isExpanded() && nodes().length > 0, 
        'glyphicon glyphicon-plus-sign': !isExpanded() && nodes().length > 0,
        'glyphicon glyphicon-stop': nodes().length === 0 
    }
    "></span>
 
    <span data-bind="
    text: name, 
    attr: { 'title': description }, 
    tooltip: { delay: { show: 500, hide: 10 }...

CSS

#nav-bar ul {
	list-style-type: none;
}
 
#nav-bar {
	padding-left: 0;
}
 
#nav-bar > ul {
	padding-left: 20px;
}
 
.tree-item {
	font-family: 'Glyphicons Halflings';
	-webkit-font-smoothing: antialiased;
	font-size: 75%;
}
 
.pointer-icon {
	cursor: pointer;
}

JavaScript

ko.bindingHandlers.tooltip = {
    init: function(element, valueAccessor) {
        var local = ko.utils.unwrapObservable(valueAccessor()),
            options = {};

        ko.utils.extend(options, local);

        $(element).tooltip(options);

        ko.utils.domNodeDisposal.addDisposeCallback(element, function() {
            $(element).tooltip("destroy");
        });
    }
};

var NodeModel = function(data) {
	
	var self = this;
	
	self.isExpanded = ko.observable(true);
	self.description = ko.observable();
	self.name = ko.observable();
	self.nodes = ko.observableArray([]);
 
	self.toggleVisibility = function() {
		self.isExpanded(!self.isExpanded());
	};
 
	ko.mapping.fromJS(data, self.mapOptions, self);
 
};
 
NodeModel.prototype.mapOptions = {
	nodes: {
		create: function(args) {
			return new NodeModel(args.data);
		}
	}
};

var __sampleData = {
	navTree: {
		nodes: [
			{
				name: 'Root 1',
				description: 'test description!',
				objectId: '',
				nodes: [
					{
						name: 'Child 1',
						description: 'test description!',
						objectId: '',
						nodes: [
						]
					},
					{
						name: 'Child 2',
						description: 'test description! asd',
						objectId: '',
						nodes: [
							{
								name: 'Child Child 1',
								description: 'test description!',
								objectId: '',
								nodes: [
								]
							}
						]
					}
				]
			},
			{
				name: 'Root 2',
				description: 'this is a longer description and it is still fabulous',
				objectId: '',
				nodes: [
				]
			},
			{
				name: 'Root 3',
				description: '',
				objectId: '',
				nodes: [
				]
			}
		]
	}
};

var PageModel = function() {
		
	var self = this;
 
	self.treeData = ko.observable();
 
	self.loadData = function(data) {
		self.treeData(new NodeModel(data.navTree));
	};
}
 
var pageModel = new PageModel();
 
pageModel.loadData(__sampleData);
 
ko.applyBindings(pageModel);