Knockoutjs. "Keeping" tree

mardi 22 janvier 2013


Judging by the frequency of occurrence of articles, KnockoutJS gaining popularity Habré. And I'll make a contribution. I want to highlight the theme of non-standard HTML controls and the "wood" in particular. Under the tree is understood analogue of the TreeView. The article assumes that the reader is already familiar with KnockoutJS at a basic level. The publication can be considered as an aid to learning KnockoutJS. On the other hand, I hope, and advanced users will be able to learn KnockoutJS for something new.

To display the TreeView written many libraries. A third-party libraries to be used in conjunction with traditional KnockoutJS creates corresponding binding (binding) for KnockoutJS. Unfortunately, often TreeView library enormous, contain an excess of functionality, it is often necessary to adjust their data model as a library. If you want to use TreeView with KnockoutJS, programmer is looking for the perfect library and binding. Not always found at the library is ready to tie-in, so you have to create their own binding, and the fun begins - the study of the internal library, which is not always pleasant. And you just want it work ... Here proposed an alternative approach - to make right on the TreeView KnockoutJS.

To start we will build an abstract tree, without reference to the data. Bind to the actual data, you can make yourself when you "grow" your trees. Either to solve this problem is my way of code reuse KnockoutJS, I will show in the end.

... And not a tree, and the list.


Traditionally trees build HTML using a set of nested unordered lists (tag UL) and CSS styles. Ie to build the tree need to generate something like the following HTML markup:
<ul> <li>Узел 1 <ul> <li>Узел 3</li> </ul> </li> <li>Узел 2 </li> <ul> 


Appearance of nodes configured with css classes.
Mapping model (ViewModel), obviously, can be constructed from two facilities - TreeViewNode and TreeView to start like this:
 function TreeViewNode(caption,children){ this.caption = caption; this.children = children; } function TreeView(children){ this.children = children; }} 

Here there is a temptation to do just one TreeViewNode, as TreeView differs only in the absence of the field caption. However, this should not hurry, because after these objects will be much more different.
Layout-bound model will use a recursive pattern:
 <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> 


Of course we need to complete our model with data and initialize the binding:
 var vm = { tree: new TreeView([ new TreeViewNode('Node 1',[ new TreeViewNode('Node 3') ]), new TreeViewNode('Node 2') ]) }; ko.applyBindings(vm); 


That's what we've got:
image
JSFiddle for experiments.

So far, it's not like TreeView. You need to assign CSS classes and add the appropriate styles.

Reflection of the situation.


Classes should reflect the position and condition of the tree node. Node status should be reflected in the model, so let's get to the model.
A node can be expanded or collapsed. One state from another can be a simple operation, but for convenience we define both:

 function TreeViewNode(caption,children){ ... this.isOpen = ko.observable(); this.isClosed = ko.computed(function(){ return !this.isOpen(); },this); ... } 

I made ​​properties .isOpen, .isClosed observed (observable), since they depend on each other, and their changes will automatically lead to changes in the DOM. Using these properties, we will disclose / collapse tree nodes.
I did .isClosed read-only property, not to produce too much code, and not to introduce extra circular dependency here, although it is possible to "razrulit." Thus, you can directly change the property only .isOpen .

To view it is also important to us to know whether the children of a node (if it can be expanded) and a node is the last on his level, so as not to draw a line from it to the next node.
 function TreeViewNode(caption,children){ ... this.children = children||[]; this.isLeaf = !this.children.length; this.isLast = false; ... } 

Because the site can not have children, and this means that the property of children or empty (null or undefined), or an array of length zero, I added the issue of uniqueness - initialize the property with an empty array if the child did not pass us.

As to property isLast, there are two options approach to its implementation. It can be made ko.computed c transfer to reference node to its parent, or have a parent isLast calculated property to their children. I chose the latter approach, because with him until less code. The first approach can also be useful as a reference to the parent node is useful in many scenarios. However, to move from one to the other then it will be easy.
So, add the processing properties isLast:

 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); ... } 


Now add in the template binding site for the corresponding classes:
 ... <li data-bind='css:{closed:isClosed,open:isOpen, leaf: isLeaf, last: isLast}'> ... </li> ... 


It remains to add the ability to hide and reveal the nodes:
 function TreeViewNode(caption,children){ var self = this; ... this.toggleOpen = function(){ self.isOpen(!self.isOpen()); }; ... } 

And add a binding:
 ... <li data-bind='css:{closed:isClosed,open:isOpen, leaf: isLeaf, last: isLast}, click: toggleOpen, clickBubble: false'> ... </li> ... 

clickBubble: false necessary so that the event does not pop up to the parents and had no effect on them.

Op-na new style.


You can navigate to CSS. I did not come up with a CSS, but just simplified styles from other libraries.

I present them below:
Styles


Full JavaScript


Full HTML


It turned out like this:

JSFiddle for experiments.

Not very nice, but quite functional. Now it is not difficult to add to each node icon or checkbox. You can make nodes allocated. But first of all I would like to make the code more generic to TreeView can be easily used with any hierarchical data structure. Moreover, it is necessary that the changes to the model are automatically reflected in the tree, and then in the DOM.

In FP, it is called «map».


To begin, we introduce an auxiliary function, which will be put in the appropriate array data array node model.
 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 no input node and the inscription "children" and some abstract data. Obviously, the inscription and the "children" he has to extract from the data. For a start, let us assume that the data is not entirely abstract, but it is an object, in which the signature is stored in the caption , and the "children" in the property children , 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 = []; ... } 


This is not the map, which map to the OP.


However, we easily give up a hard peg to the property name. Let us pass an object map , which indicates that the properties:

 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 = []; ... } 


And even better to be able to determine that the properties dynamically, based on the type of object data :
 function TreeViewNode(data){ ... this.data = data; var map = (typeof propMap == 'function') ? propMap(data):propMap, captionProp = (map && map.caption)||'caption', childrenProp = (map && map.children)||'children'; ... } 


Now you can hide ad TreeViewNode and support functions into the ad model TreeView , since copies TreeViewNode no longer be created by the user.
Full JavaScript

JSFiddle for experiments.

The magic begins here.


Now we need to do the latter requirement - automatically reflect changes to the model of "wood." We use the "magic» KnockoutJS with objects ko.observable, ko.computed, ko.observableArray . For this, we need only make the property children evaluated. Also, change the code to 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); }); } ... 

Function ko.utils.unwrapObservable the current value of the observed object, or, if this is not the observed object, the same value is passed to it as input. Use ko.utils.unwrapObservable inside ko.computed automatically creates dependency and .children will be automatically updated when we used as the data observed value. On the other hand, you can use just a JS array and then automatically track the changes will be.
Do the same for the 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); }); ... 

Now the changes in the data will automatically be reflected in the model of the "tree" and then automatically DOM.
You can experiment with JSFiddle .
There is only one annoying problem - adding nodes turns the tree. This is because every time we create a model upgrade TreeViewNode again. A more intelligent approach - to create models for new data, and to use the old old. This can be done in two ways:
  1. Store a reference to TreeViewNode in data;
  2. When updating a list of sites to search TreeViewNode in the old list.

I will show the first method, because it is shorter. However, it is limited - if you want to use the same object to different nodes of the tree, this method will not work. Rather it will lead to unexpected effects. But if you have the data to each object has only one node in the tree, then all will be fine.
So:
  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; } 


A choice - it is always nice.


Our "tree" almost grown. For a complete happiness we lack only the opportunity to choose the individual nodes' tree. "
 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); } } } 

Also be added to a template:
 ... <span class='caption' data-bind='text:caption, css: {selected:isSelected},click:toggleSelection, clickBubble: false'></span> ... 


Now we can construct a complete editor of "tree."
Full JavaScript

Full HTML

Full CSS


JSFiddle for experiments.

Results. Hidden PR. Distribution of elephants.


So we have quite efficient implementation TreeView adapted to work with Knockout, which is the JavaScript just over 60 lines of code that is clear, can be easily extended with new functions can be easily adapted to the data model. Now consider the possible scenarios for reuse:
  1. Copy & Paste functions TreeView your JS code or make it into a separate file. Insert styles in your styles, or import styles in a separate file. Insertion and adaptation patterns. This scenario is similar to the scenario of using code snippet.
  2. Make their binding (binding).
  3. Use my library knockout-component for converting a set of templates + model binding.


I took a third way. Now insert the tree is reduced to the HTML code:
 <div data-bind='kc.treeView: {data:data,map:{caption:"name",children:"list"}},kc.assign:tree'></div> 

0 commentaires:

Enregistrer un commentaire

 
© Copyright 2010-2011 GARMOBI All Rights Reserved.
Template Design by Herdiansyah Hamzah | Published by Borneo Templates | Powered by Blogger.com.