
<ul> <li> 1 <ul> <li> 3</li> </ul> </li> <li> 2 </li> <ul>  function TreeViewNode(caption,children){ this.caption = caption; this.children = children; } function TreeView(children){ this.children = children; }}  <div data-bind='with: tree'> <ul data-bind='template: {name:"treeNode", foreach: children}'> </ul> </div> <script id='treeNode' type='text/html'> <li> <span data-bind='text:caption'></span> <ul data-bind='template: {name:"treeNode", foreach: children}'> </ul> </li> </script>  var vm = { tree: new TreeView([ new TreeViewNode('Node 1',[ new TreeViewNode('Node 3') ]), new TreeViewNode('Node 2') ]) }; ko.applyBindings(vm); 
 function TreeViewNode(caption,children){ ... this.isOpen = ko.observable(); this.isClosed = ko.computed(function(){ return !this.isOpen(); },this); ... } .isOpen, .isClosed observable, because they depend on each other, and changing them will automatically lead to changes in the DOM. By changing these properties, we will expand / collapse tree nodes..isClosed property read-only, in order not to produce extra code and not to introduce an extra cyclic dependency here, although it can be “fixed”. Thus, only the .isOpen property can be directly changed. function TreeViewNode(caption,children){ ... this.children = children||[]; this.isLeaf = !this.children.length; this.isLast = false; ... } children property is either empty (null or undefined), or is an array of zero length, I added unambiguity to this question — I initialized the property with an empty array if children were not passed to us. function setIsLast(children){ for(var i=0,l=children.length;i<l;i++){ children[i].isLast = (i==(l-1)); } } function TreeViewNode(caption,children){ ... setIsLast(this.children); ... } function TreeView(children){ ... setIsLast(this.children); ... }  ... <li data-bind='css:{closed:isClosed,open:isOpen, leaf: isLeaf, last: isLast}'> ... </li> ...  function TreeViewNode(caption,children){ var self = this; ... this.toggleOpen = function(){ self.isOpen(!self.isOpen()); }; ... }  ... <li data-bind='css:{closed:isClosed,open:isOpen, leaf: isLeaf, last: isLast}, click: toggleOpen, clickBubble: false'> ... </li> ... clickBubble: false needed so that the event does not pop up to the parents and does not affect them. .tree li, .tree ins{ background-image:url("http://habrastorage.org/storage2/0eb/507/98d/0eb50798dca00f5cc8e153e6da9a87f9.png"); background-repeat:no-repeat; background-color:transparent; } .tree li { background-position:-90px 0; background-repeat:repeat-y; } .tree li { display:block; min-height:18px; line-height:18px; white-space:nowrap; margin-left:18px; min-width:18px; } .tree ul, .tree li { display:block; margin:0 0 0 0; padding:0 0 0 0; list-style-type:none; } .tree li { display:block; min-height:18px; line-height:18px; white-space:nowrap; margin-left:18px; min-width:18px; } .tree > ul > li { margin-left:0px; } .tree li.last { background:transparent; } .tree .open > ins { background-position:-72px 0;} .tree .closed > ins { background-position:-54px 0;} .tree .leaf > ins { background-position:-36px 0;} .tree ins { display:inline-block; text-decoration:none; width:18px; height:18px; margin:0 0 0 0; padding:0; } li.open > ul { display:block; } li.closed > ul { display:none; }  function setIsLast(children){ for(var i=0,l=children.length;i<l;i++){ children[i].isLast = (i==(l-1)); } } function TreeViewNode(caption,children){ var self = this; this.caption = caption; this.children = children||[]; this.isOpen = ko.observable(); this.isClosed = ko.computed(function(){ return !this.isOpen(); },this); this.isLeaf = !this.children.length; this.isLast = false; setIsLast(this.children); this.toggleOpen = function(){ self.isOpen(!self.isOpen()); }; } function TreeView(children){ this.children = children; setIsLast(this.children); } var vm = { tree: new TreeView([ new TreeViewNode('Node 1',[ new TreeViewNode('Node 3') ]), new TreeViewNode('Node 2') ]) }; ko.applyBindings(vm);  <div class='tree' data-bind='with: tree'> <ul data-bind='template: {name:"treeNode", foreach: children}'> </ul> </div> <script id='treeNode' type='text/html'> <li data-bind='css:{closed:isClosed,open:isOpen, leaf: isLeaf, last: isLast}, click: toggleOpen, clickBubble: false'> <ins></ins> <span data-bind='text:caption'></span> <ul data-bind='template: {name:"treeNode", foreach: children}'> </ul> </li> </script> 
 function dataToNodes(dataArray){ var res = []; for(var i=0,l=dataArray.length;i<l;i++){ res.push(new TreeViewNode(dataArray[i])); } return res; } TreeViewNode now takes as input not a label for the node and "children", but some abstract data. Obviously, the inscription and the "children" he must himself extract from the data. And first, suppose that the data is not completely abstract, and this is an object in which the signature is stored in the caption property, and “children” in the children property, which is an array. function TreeViewNode(data){ ... this.data = data; this.caption = data.caption; if(data.children) this.children = dataToNodes(data.children); else this.children = []; ... } map object be passed to us, in which the corresponding properties are indicated: function TreeViewNode(data){ ... this.data = data; var captionProp = (map && map.caption)||'caption', childrenProp = (map && map.children)||'children'; this.caption = data[captionProp]; if(data[childrenProp]) this.children = dataToNodes(data[childrenProp]); else this.children = []; ... } data object: function TreeViewNode(data){ ... this.data = data; var map = (typeof propMap == 'function') ? propMap(data):propMap, captionProp = (map && map.caption)||'caption', childrenProp = (map && map.children)||'children'; ... } TreeViewNode declaration and support functions inside the TreeView model declaration, since the TreeViewNode instances should no longer be created by the user on their own. function TreeView(data, propMap){ this.data = data; this.children = dataToNodes(data); setIsLast(this.children); function dataToNodes(dataArray){ var res = []; for(var i=0,l=dataArray.length;i<l;i++){ res.push(new TreeViewNode(dataArray[i])); } return res; } function setIsLast(children){ for(var i=0,l=children.length;i<l;i++){ children[i].isLast = (i==(l-1)); } } function TreeViewNode(data){ var self = this; this.data = data; var map = (typeof propMap == 'function') ? propMap(data):propMap; captionProp = (map && map.caption)||'caption', childrenProp = (map && map.children)||'children'; this.caption = data[captionProp]; if(data[childrenProp]) this.children = dataToNodes(data[childrenProp]); else this.children = []; this.isOpen = ko.observable(); this.isClosed = ko.computed(function(){ return !this.isOpen(); },this); this.isLeaf = !this.children.length; this.isLast = false; setIsLast(this.children); this.toggleOpen = function(){ self.isOpen(!self.isOpen()); }; } } var vm = { data: [ { name:'Node 1', list: [{ name: 'Node 3' }] }, { name:'Node2', list: [{ name: 'Node 6', list: [{ name: 'Node 5' }] }] } ] }; vm.tree = new TreeView(vm.data,{caption:'name',children:'list'}); ko.applyBindings(vm); ko.observable, ko.computed, ko.observableArray . To do this, we just need to make the children property computable. And also change the code for other properties that depend on it:  function TreeViewNode(data){ ... if(data[childrenProp]) this.children = ko.computed(function(){ return dataToNodes(ko.utils.unwrapObservable(data[childrenProp])); }); else this.children = null; ... this.isLeaf = ko.computed(function(){ return !(this.children && this.children().length); },this); this.isLast = ko.observable(false); if(this.children){ setIsLast(this.children()); this.children.subscribe(function(newVal){ setIsLast(newVal); }); } ... ko.utils.unwrapObservable function returns the current value of the observed object, or, if it is not an observed object, the same value that was passed to it on input. Using ko.utils.unwrapObservable inside ko.computed will automatically create a dependency and .children will be automatically updated if we used the observed value as data. On the other hand, you can simply use the JS array, then there will be no automatic change tracking.TreeView function TreeView(data, propMap){ ... this.children = ko.computed(function(){ return dataToNodes(ko.utils.unwrapObservable(data)); }); setIsLast(this.children()); this.children.subscribe(function(newVal){ setIsLast(newVal); }); ... TreeViewNode models. A smarter approach is needed - create models only for new data, and for old ones, use old ones. This can be implemented in two ways:TreeViewNode in the data;TreeViewNode in the old list.  function TreeViewNode(data){ ... data._treeviewNode = this; //        ... } function dataToNodes(dataArray,old){ var res = []; for(var i=0,l=dataArray.length;i<l;i++){ res.push(dataArray[i]._treeviewNode || new TreeViewNode(dataArray[i])); //        } return res; }  function TreeView(data, propMap){ var treeView = this; //    TreeView this.selectedNode = ko.observable(); //   ... function TreeViewNode(data){ ... this.isSelected = ko.computed(function(){ //      return (treeView.selectedNode()===this) },this); this.toggleSelection = function(){ //     if(this.isSelected()) treeView.selectedNode(null); else treeView.selectedNode(this); } } }  ... <span class='caption' data-bind='text:caption, css: {selected:isSelected},click:toggleSelection, clickBubble: false'></span> ...  function TreeView(data, propMap){ var treeView = this; this.data = data; this.selectedNode = ko.observable(); this.children = ko.computed(function(){ return dataToNodes(ko.utils.unwrapObservable(data)); }); setIsLast(this.children()); this.children.subscribe(function(newVal){ setIsLast(newVal); }); function dataToNodes(dataArray,old){ var res = []; for(var i=0,l=dataArray.length;i<l;i++){ res.push(dataArray[i]._treeviewNode || new TreeViewNode(dataArray[i])); } return res; } function setIsLast(children){ for(var i=0,l=children.length;i<l;i++){ children[i].isLast(i==(l-1)); } } function TreeViewNode(data){ var self = this; this.data = data; data._treeviewNode = this; var map = (typeof propMap == 'function') ? propMap(data):propMap; captionProp = (map && map.caption)||'caption', childrenProp = (map && map.children)||'children'; this.caption = data[captionProp]; if(data[childrenProp]) this.children = ko.computed(function(){ return dataToNodes(ko.utils.unwrapObservable(data[childrenProp])); }); else this.children = null; this.isOpen = ko.observable(); this.isClosed = ko.computed(function(){ return !this.isOpen(); },this); this.isLeaf = ko.computed(function(){ return !(this.children && this.children().length); },this); this.isLast = ko.observable(false); if(this.children){ setIsLast(this.children()); this.children.subscribe(function(newVal){ setIsLast(newVal); }); } this.toggleOpen = function(){ self.isOpen(!self.isOpen()); }; this.isSelected = ko.computed(function(){ return (treeView.selectedNode()===this) },this); this.toggleSelection = function(){ if(this.isSelected()) treeView.selectedNode(null); else treeView.selectedNode(this); } } } function SomeObject(col){ this.name = ko.observable('New SomeObject'); this.list = ko.observableArray(); this.collection = col; } var vm = { data:ko.observableArray(), AddRootNode: function(){ this.data.push(new SomeObject(this.data)); }, AddChildNode: function(){ var data = this.tree.selectedNode().data; data.list.push(new SomeObject(data.list)); }, RemoveNode:function(){ var data = this.tree.selectedNode().data; this.tree.selectedNode(null); data.collection.remove(data); } }; vm.tree = new TreeView(vm.data,{caption:'name',children:'list'}); ko.applyBindings(vm);  <button data-bind='click:AddRootNode'>Add New Root Node</button> <button data-bind='click:AddChildNode,visible: tree.selectedNode'>Add Child toSelected Node</button> <button data-bind='click:RemoveNode,visible: tree.selectedNode'>Delete Selected Node</button> <div class='tree' data-bind='with: tree'> <ul data-bind='template: {name:"treeNode", foreach: children}'> </ul> </div> <div data-bind='with: tree.selectedNode'> <!-- ko with: data --> <input data-bind='value:name'> <!-- /ko --> </div> <script id='treeNode' type='text/html'> <li data-bind='css:{closed:isClosed,open:isOpen, leaf: isLeaf, last: isLast}, click: toggleOpen, clickBubble: false'> <ins></ins> <span class='caption' data-bind='text:caption, css: {selected:isSelected},click:toggleSelection, clickBubble: false'></span> <ul data-bind='template: {name:"treeNode", foreach: children}'> </ul> </li> </script>  .tree li, .tree ins{ background-image:url("http://habrastorage.org/storage2/0eb/507/98d/0eb50798dca00f5cc8e153e6da9a87f9.png"); background-repeat:no-repeat; background-color:transparent; } .tree li { background-position:-90px 0; background-repeat:repeat-y; } .tree li { display:block; min-height:18px; line-height:18px; white-space:nowrap; margin-left:18px; min-width:18px; } .tree ul, .tree li { display:block; margin:0 0 0 0; padding:0 0 0 0; list-style-type:none; } .tree li { display:block; min-height:18px; line-height:18px; white-space:nowrap; margin-left:18px; min-width:18px; } .tree > ul > li { margin-left:0px; } .tree li.last { background:transparent; } .tree .open > ins { background-position:-72px 0;} .tree .closed > ins { background-position:-54px 0;} .tree .leaf > ins { background-position:-36px 0;} .tree ins { display:inline-block; text-decoration:none; width:18px; height:18px; margin:0 0 0 0; padding:0; } li.open > ul { display:block; } li.closed > ul { display:none; } .selected {background-color: #ccc; } span.caption {cursor: pointer}  <div data-bind='kc.treeView: {data:data,map:{caption:"name",children:"list"}},kc.assign:tree'></div> Source: https://habr.com/ru/post/165565/
All Articles