<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