1. #1
    Ext JS Premium Member teqneers's Avatar
    Join Date
    Nov 2008
    Posts
    86
    Vote Rating
    1
    teqneers is on a distinguished road

      0  

    Lightbulb New approach to a tree-grid component

    New approach to a tree-grid component


    Hi all,

    We recently had to implement a user interface that included a editable grid displaying a tree-structure. Naturally we first turned our attention to the Ext.ux.tree.TreeGrid which was officially released with ExtJS 3.1. Sadly this component lacks the current high standard set by ExtJS components such as the Ext.grid.GridPanel, Ext.tree.TreePanel or the whole layout engine. Even worse it also lacks any documentation at all - just a side note (or a broad hint to de developers ;-)

    In our use-case, especially the column-model that's based on the Ext.list.ListView column-model, which made implementing editable fields quite hard and ugly, and some problems with calculating column widths as well as the lack of support for auto-expanding columns (we could correct the last two things by "hacking" the respective methods) proved to be a real show-stopper, so we started to think the other way 'round: why not use the current high-quality Ext.grid.GridPanel and Ext.grid.EditorGridPanel and extend them to be able to display tree-structures?

    On the basis of the componentized structure (panel component, store, column model, view and selection model) of Ext.grid.GridPanel we identified that it should be enough to just implement our own store that's able to read tree-like-structures and our own view that modifies row and cell rendering to handle alls those tree-features. That said, implementation went quite smooth, even though we had to copy some larger code fragments from the overridden base classes. The following code is by no means complete or perhaps it's not even production-ready outside our own application (as it doesn't support adding and removing or reordering nodes yet), but we think that this approach would be much more flexible and usable than using the current Ext.ux.tree.TreeGrid. Perhaps the ExtJS developers could give that approach a try...

    The first step was to turn a tree-structure into a set of records usable by the grid component:

    Code:
    Ext.ux.tree.TreeReader = Ext.extend(Ext.data.DataReader, {
    
    	tree: null,
    
    	constructor: function(meta, recordType) {
    		Ext.ux.tree.TreeReader.superclass.constructor.call(this, meta, recordType || meta.fields);
    	},
    
    	load: function(node){
            if (node.attributes.children){
    			var cs = node.attributes.children;
    			for (var i = 0, len = cs.length; i < len; i++){
    				var cn = node.appendChild(this.createNode(cs[i]));
    				this.load(cn);
    			}
    		}
        },
    
    	createNode: function(attr){
            var node = new Ext.data.Node(attr);
    		node.expanded = (attr.expanded === true);
    		return node;
        },
    	
        /**
         * Create a data block containing Ext.data.Records from a tree.
         */
        readRecords: function(o) {
    
    		var root	= this.createNode({
    			text: 'Root',
    			id: 'root',
    			children: o
    		});
    		this.tree	= new Ext.data.Tree(root);
    		this.load(root);
    
    		var f		= this.recordType.prototype.fields;
    		var records	= [];
    		root.cascade(function(node) {
    			if (node !== root) {
    				var record = new this.recordType(this.extractValues(node, f.items), node.id);
    				record.node		= node;
    				record.depth	= node.getDepth();
    				records.push(record);
    			}
    		}, this);
    
    		return {
                success : true,
                records : records,
                totalRecords : records.length
            };
    	},
    
    	/**
         * type-casts a single node
         */
        extractValues : function(node, fields) {
            var f, values = {};
            for(var j = 0; j < fields.length; j++){
                f = fields[j];
                var v = node.attributes[f.mapping];
                values[f.name] = f.convert((v !== undefined) ? v : f.defaultValue, node);
            }
    		return values;
        }
    });
    This allows us to simple hand over a tree-like structure within the data attribute of our grid store. The major work of rendering the tree was handed over to our own implementation of a grid view:

    Code:
    Ext.ux.tree.GridView = Ext.extend(Ext.grid.GridView, {
    
    	useArrows: true,
    
    	staticTree: false,
    
        constructor: function(config) {
    		this.emptyIcon	= Ext.BLANK_IMAGE_URL;
    
    		Ext.ux.tree.GridView.superclass.constructor.call(this, config);
    		this.templates			= {};
    		this.templates.master	= new Ext.Template(
    			'<div class="x-grid3 tq-treegrid" hidefocus="true">',
    				'<div class="x-grid3-viewport">',
    					'<div class="x-grid3-header">',
    						'<div class="x-grid3-header-inner">',
    							'<div class="x-grid3-header-offset" style="{ostyle}">{header}</div>',
    						'</div>',
    						'<div class="x-clear"></div>',
    					'</div>',
    					'<div class="x-grid3-scroller">',
    						'<div class="x-grid3-body ', (this.useArrows ? 'x-tree-arrows' : this.lines ? 'x-tree-lines' : 'x-tree-no-lines') , '" style="{bstyle}">{body}</div>',
    						'<a href="#" class="x-grid3-focus" tabIndex="-1"></a>',
    					'</div>',
    				'</div>',
    				'<div class="x-grid3-resize-marker">&#160;</div>',
    				'<div class="x-grid3-resize-proxy">&#160;</div>',
    			'</div>'
    		);
    
    		this.templates.row	= new Ext.Template(
    			'<div class="x-grid3-row {alt}" style="{tstyle}">',
    				'<table class="x-grid3-row-table" border="0" cellspacing="0" cellpadding="0" style="{tstyle}">',
    					'<tbody class="x-tree-node">',
    						'<tr>{cells}</tr>',
    						(this.enableRowBody ? '<tr class="x-grid3-row-body-tr" style="{bodyStyle}"><td colspan="{cols}" class="x-grid3-body-cell" tabIndex="0" hidefocus="on"><div class="x-grid3-row-body">{body}</div></td></tr>' : ''),
    					'</tbody>',
    				'</table>',
    			'</div>'
    		);
    		this.templates.treeCell	= new Ext.Template(
    			'<td class="tq-treegrid-col x-grid3-col x-grid3-cell x-grid3-td-{id} {css}" style="{style}" tabIndex="0" {cellAttr}>',
    				'<div ext:tree-node-id="{nodeId}" class="x-grid3-col-{id} x-tree-node-el x-unselectable" unselectable="on" {attr}>',
    					'<div class="tq-treegrid-icons">',
    						'<span class="x-tree-node-indent">{nodeIndent}</span>',
    						'<img src="', this.emptyIcon, '" class="x-tree-ec-icon x-tree-elbow {nodeTreeIconCls}" />',
    						'<img src="', this.emptyIcon, '" class="x-tree-node-icon {nodeIconCls}" unselectable="on" />',
    					'</div>',
    					'<div class="x-grid3-cell-inner" unselectable="on" {attr}>',
    						'{value}',
    					'</div>',
    				'</div>',
    			'</td>'
    		);
    	},
    
    	getChildIndentUI: function(node) {
    		var indentBuffer	= [];
    		var parentNode		= node.parentNode;
    		while (parentNode){
    			if (!parentNode.isRoot){
    				if (!parentNode.isLast()) {
    					indentBuffer.unshift('<img src="'+this.emptyIcon+'" class="x-tree-elbow-line" />');
    				} else {
    					indentBuffer.unshift('<img src="'+this.emptyIcon+'" class="x-tree-icon" />');
    				}
    			}
    			parentNode = parentNode.parentNode;
    		}
    		return indentBuffer.join("");
    	},
    
    	getTreeNodeIcon: function(node) {
    		var treeIcon	= node.isLast() ? "x-tree-elbow-end" : "x-tree-elbow";
    		if (node.hasChildNodes() && !this.staticTree){
    			if (node.expanded){
    				treeIcon += "-minus";
    			} else {
    				treeIcon += "-plus";
    			}
    			treeIcon += ' tq-tree-node-control';
    		}
    		return treeIcon;
    	},
    
    	// private
        doRender: function(cs, rs, ds, startRow, colCount, stripe){
            // buffers
            var rowBuffer = [];
    		var cellBuffer;
    		var cell;
    		var cellTemplate;
    		var cellProperties = {};
    		var rowProperties = { 
    			tstyle: 'width:'+this.getTotalWidth()+';'
    		};
    		var record;
    		var depthBuffer	= [];
    		var hasTreeCol;
    
            for (var j = 0, len = rs.length; j < len; j++){
    			record			= rs[j];
    			cellBuffer		= [];
    			hasTreeCol		= false;
                var rowIndex	= (j+startRow);
    
    
                for (var i = 0; i < colCount; i++){
                    cell					= cs[i];
                    cellProperties.id		= cell.id;
                    cellProperties.css		= (i === 0) ? 'x-grid3-cell-first ' : (i == (colCount - 1) ? 'x-grid3-cell-last ' : '');
                    cellProperties.attr		= cellProperties.cellAttr = '';
                    cellProperties.value	= cell.renderer.call(cell.scope, record.data[cell.name], cellProperties, record, rowIndex, i, ds);
                    cellProperties.style	= cell.style;
                    if (Ext.isEmpty(cellProperties.value)){
                        cellProperties.value = '&#160;';
                    }
                    if (this.markDirty && record.dirty && Ext.isDefined(record.modified[cell.name])){
                        cellProperties.css += ' x-grid3-dirty-cell';
                    }
    
    				if (cell.scope.treeCol && !hasTreeCol && record.node) {
    					hasTreeCol	= true;
    					var node	= record.node;
    					
    					cellProperties.nodeIndent	= this.getChildIndentUI(node);
    					cellProperties.nodeIconCls	= node.attributes.iconCls || '';
    				
    					cellProperties.nodeTreeIconCls	= this.getTreeNodeIcon(node);
    
    					cellTemplate = this.templates.treeCell;
    				} else {
    					cellTemplate = this.templates.cell;
    				}
    				cellBuffer[cellBuffer.length] = cellTemplate.apply(cellProperties);
    
                }
                var alt = [];
    
    			if (record.depth && record.node) {
    				if (depthBuffer.length < record.depth) {
    					rowBuffer.push('<div class="x-tree-node-ct">');
    					depthBuffer.push('</div>');
    				} else {
    					while (depthBuffer.length > record.depth) {
    						rowBuffer.push(depthBuffer.pop());
    					}
    				}
    
    				if (this.staticTree) {
    					alt.push('tq-treegrid-static');
    				}
    
    				if (node.isLeaf()) {
    					alt.push('x-tree-node-leaf');
    				} else if (node.expanded){
    					alt.push('x-tree-node-expanded');
    				} else {
    					alt.push('x-tree-node-collapsed');
    				}
    			}
    
                if(stripe && ((rowIndex+1) % 2 === 0)){
                    alt.push('x-grid3-row-alt');
                }
                if(record.dirty){
                    alt.push(' x-grid3-dirty-row');
                }
    
                rowProperties.cols		= colCount;
    			rowProperties.nodeId	= record.node.id;
                rowProperties.cells		= cellBuffer.join('');
                if (this.getRowClass){
                    alt.push(this.getRowClass(record, rowIndex, rowProperties, ds));
                }
    			rowProperties.alt	= alt.join(' ');
    			
                rowBuffer[rowBuffer.length] =  this.templates.row.apply(rowProperties);
            }
    
    		while (depthBuffer.length) {
    			rowBuffer.push(depthBuffer.pop());
    		}
            return rowBuffer.join('');
        },
    
    	afterRender: function() {
    		Ext.ux.tree.GridView.superclass.afterRender.call(this);
    
    		if (!this.staticTree) {
    			this.mainBody.on('click', function(ev, el) {
    				this.toggleNode(el);
    			}, this, {
    				delegate: '.tq-tree-node-control'
    			});
    			this.mainBody.on('dblclick', function(ev, el) {
    				this.toggleNode(el);
    			}, this, {
    				delegate: '.x-tree-node-el '
    			});
    		}
    	},
    
    	toggleNode: function(el) {
    		var row		= Ext.get(this.findRow(el));
    		var node	= this.grid.getStore().getAt(row.dom.rowIndex).node;
    
    		var childCnt	= row.next('.x-tree-node-ct', false);
    		var nodeCtrl	= row.child('.tq-tree-node-control', false);
    
    		if (childCnt && nodeCtrl) {
    			if (node.expanded) {
    				childCnt.enableDisplayMode('block');
    				childCnt.stopFx();
    
    				row.removeClass('x-tree-node-expanded');
    				nodeCtrl.removeClass(node.isLast() ? "x-tree-elbow-end-minus" : "x-tree-elbow-minus");
    				row.addClass('x-tree-node-collapsed');
    				nodeCtrl.addClass(node.isLast() ? "x-tree-elbow-end-plus" : "x-tree-elbow-plus");
    
    				childCnt.slideOut('t', {
    					callback : function(){
    					   row.highlight();
    					   node.expanded	= false;
    					},
    					scope: this,
    					duration: .25
    				});
    				
    			} else {
    				childCnt.stopFx();
    
    				row.addClass('x-tree-node-expanded');
    				nodeCtrl.addClass(node.isLast() ? "x-tree-elbow-end-minus" : "x-tree-elbow-minus");
    				row.removeClass('x-tree-node-collapsed');
    				nodeCtrl.removeClass(node.isLast() ? "x-tree-elbow-end-plus" : "x-tree-elbow-plus");
    
    				childCnt.slideIn('t', {
    				   callback : function(){
    						childCnt.highlight();
    						node.expanded	= true;
    					},
    					scope: this,
    					duration: .25
    				});
    			}
    		}
    	},
    
        getRows: function() {
            return this.hasRows() ? this.mainBody.query(this.rowSelector) : [];
        }
    
    });
    That's where the hard work is done (event though a decent amount of code had to be copied over from the original implementation). Be aware that we currently use CSS classes from the grid itself, from the tree and from the treegrid - that's also not really production ready outside our environment.
    Together with some CSS additions we can easily set up our own tree grid:

    Code:
    /**
     * Tree grid
     */
    .tq-treegrid .tq-treegrid-col {
    	border: none;
    }
    
    .tq-treegrid .tq-treegrid-icons {
    	float: left;
    }
    
    .tq-treegrid .x-tree-node-el {
    	line-height: 13px;
    	padding: 1px 3px 1px 5px;
    }
    
    .tq-treegrid .tq-treegrid-static .x-tree-ec-icon {
    	display: none;
    }
    
    .tq-treegrid .tq-treegrid-static .x-tree-node-el {
    	cursor: default;
    }
    Code:
    var treeGrid	= new Ext.grid.EditorGridPanel({
    	renderTo: Ext.getBody(),
    	autoHeight: true,
    	width: 400,
    	columnLines: true,
    	autoExpandColumn: 'col-name',
    	view: new Ext.ux.tree.GridView({
    		useArrows: true,
    		staticTree: false
    	}),
    	store: {
    		xtype: 'store',
    		autoDestroy: true,
    		reader: new Ext.ux.tree.TreeReader({
    			fields: [{
    				name: 'id',
    				mapping: 'id',
    				type: 'int'
    			}, {
    				name: 'name',
    				mapping: 'name'
    			}, {
    				name: 'cost',
    				mapping: 'cost',
    				type: 'float'
    			}]
    		}),
    		data: [{
    			id: 1,
    			name: 'Company A',
    			cost: 1000000.00,
    			leaf: false,
    			expanded: true,
    			children: [{
    				id: 11,
    				name: 'Department AA',
    				cost: 800000.00,
    				leaf: false,
    				expanded: true,
    				children: [{
    					id: 111,
    					name: 'Thing AAA',
    					cost: 300000.00,
    					leaf: true
    				}, {
    					id: 112,
    					name: 'Thing AAB',
    					cost: 500000.00,
    					leaf: true
    				}]
    			}, {
    				id: 12,
    				name: 'Department AB',
    				cost: 200000.00,
    				leaf: false,
    				expanded: true,
    				children: [{
    					id: 121,
    					name: 'Thing ABA',
    					cost: 50000.00,
    					leaf: true
    				}, {
    					id: 122,
    					name: 'Thing ABB',
    					cost: 150000.00,
    					leaf: true
    				}]
    			}]
    		}, {
    			id: 2,
    			name: 'Company B',
    			cost: 200000.00,
    			leaf: false,
    			expanded: true,
    			children: [{
    				id: 21,
    				name: 'Department BA',
    				cost: 100000.00,
    				leaf: false,
    				expanded: true,
    				children: [{
    					id: 211,
    					name: 'Thing BAA',
    					cost: 50000.00,
    					leaf: true
    				}, {
    					id: 212,
    					name: 'Thing BAB',
    					cost: 50000.00,
    					leaf: true
    				}]
    			}, {
    				id: 22,
    				name: 'Department BB',
    				cost: 100000.00,
    				leaf: false,
    				expanded: true,
    				children: [{
    					id: 221,
    					name: 'Thing BBA',
    					cost: 90000.00,
    					leaf: true
    				}, {
    					id: 222,
    					name: 'Thing BBB',
    					cost: 10000.00,
    					leaf: true
    				}]
    			}]
    		}]
    	},
    	columns: [{
    		header: 'Id',
    		dataIndex: 'id',
    		width: 30
    	}, {
    		header: 'Name',
    		id: 'col-name',
    		dataIndex: 'name',
    		treeCol: true
    	}, {
    		header: 'Cost',
    		dataIndex: 'cost',
    		width: 80
    	}]
    });
    Fell free to use this base work at your will.
    Best regards to all of you!

    Stefan

    Stefan Gehrig
    TEQneers GmbH & Co. KG, Stuttgart, Germany
    Attached Images

  2. #2
    Ext User
    Join Date
    Jan 2008
    Posts
    17
    Vote Rating
    0
    buenavida is on a distinguished road

      0  

    Default IE 8 looks funny

    IE 8 looks funny


    Hi Stefan,

    Many thanks for this work.
    I saw that the column with icons (collapsible), appears funny in IE 8. (the column entry is separated into 2 lines, one line for icon and the other for the description). Do you see the same thing in IE8?

    Best Regards
    Marty

  3. #3
    Ext JS Premium Member teqneers's Avatar
    Join Date
    Nov 2008
    Posts
    86
    Vote Rating
    1
    teqneers is on a distinguished road

      0  

    Default


    Hi Marty,

    I just checked with all available browsers (IE6, IE7, IE8, Opera, Firefox and Chrome) and the tree-column looks OK in all of those. Most likely you're missing some CSS rules that we borrowed from the Ext.ux.tree.TreeGrid. DO you have the same issue in other browsers?

    Best regards

    Stefan

    Stefan Gehrig
    TEQneers GmbH & Co. KG, Stuttgart, Germany
    Last edited by teqneers; 14 Mar 2010 at 11:58 PM. Reason: added name

  4. #4
    Ext User
    Join Date
    Mar 2010
    Posts
    2
    Vote Rating
    0
    aldanco is on a distinguished road

      0  

    Default


    Work very fine in all browsers I've tried. Thank you so much for this wonderful work.

  5. #5
    Sencha User
    Join Date
    Mar 2007
    Location
    London, UK
    Posts
    143
    Vote Rating
    0
    albeva is infamous around these parts albeva is infamous around these parts

      0  

    Default


    hmm very nice work and integrates very seemlessly with different themes unlike the tree grid plugin shipped with Ext. I really like how this is setup. Only big problem is sorting

  6. #6
    Ext JS Premium Member teqneers's Avatar
    Join Date
    Nov 2008
    Posts
    86
    Vote Rating
    1
    teqneers is on a distinguished road

      0  

    Default


    Hi albeva,

    thanks a lot...
    You're right about sorting. We currently don't need the grid's sorting abilities with our tree grid so I haven't looked into this topic yet (furthermore the code currently does not support adding, removing or reordering nodes yet). That's why I carefully called the code "not even production-ready". It should be considered a "proof-of-concept" for a "better" or more integrative implementation of a tree grid.

    Best regards

    Stefan

    Stefan Gehrig
    TEQneers GmbH & Co. KG, Stuttgart, Germany

  7. #7
    Sencha User masterix's Avatar
    Join Date
    Aug 2007
    Location
    Catanzaro - Calabria - ITALY
    Posts
    6
    Vote Rating
    0
    masterix is on a distinguished road

      0  

    Default


    Hi teqneers,

    i'm trying it, but don't work with a JsonStore. This is my code:

    Code:
    var store = new Ext.data.JsonStore({
    		url: 'json/data.php',
    		totalProperty: 'results',
    		idProperty: 'id',
    		root: 'rows',
    		autoLoad: true,
    		remoteSort: true,
    		autoDestroy: true,
    		reader: new Ext.ux.tree.TreeReader({
    			fields: [{
    				name: 'id',
    				mapping: 'id',
    				type: 'int'
    			}, {
    				name: 'name',
    				mapping: 'name'
    			}]
    		})
    	});
    	store.setDefaultSort('id', 'asc');
    	
    	var treeGrid = new Ext.grid.GridPanel({
    		renderTo: Ext.getBody(),
    		width: 400,
    		height: 400,
    		columnLines: true,
    		autoExpandColumn: 'col-name',
    		view: new Ext.ux.tree.GridView({
    			useArrows: true,
    			staticTree: false
    		}),
    		store: store,
    		columns: [{
    			header: 'Id',
    			dataIndex: 'id',
    			width: 30
    		}, {
    			header: 'Name',
    			id: 'col-name',
    			dataIndex: 'name',
    			treeCol: true
    		}]
    	});
    can you help me? thank you

  8. #8
    jay@moduscreate.com's Avatar
    Join Date
    Mar 2007
    Location
    Frederick MD, NYC, DC
    Posts
    16,360
    Vote Rating
    81
    jay@moduscreate.com is a name known to all jay@moduscreate.com is a name known to all jay@moduscreate.com is a name known to all jay@moduscreate.com is a name known to all jay@moduscreate.com is a name known to all jay@moduscreate.com is a name known to all

      0  

    Default


    Quote Originally Posted by teqneers View Post
    Hi albeva,

    thanks a lot...
    You're right about sorting. We currently don't need the grid's sorting abilities with our tree grid so I haven't looked into this topic yet (furthermore the code currently does not support adding, removing or reordering nodes yet). That's why I carefully called the code "not even production-ready". It should be considered a "proof-of-concept" for a "better" or more integrative implementation of a tree grid.

    Best regards

    Stefan

    Stefan Gehrig
    TEQneers GmbH & Co. KG, Stuttgart, Germany
    You don't need to continuously paste your signature. add it to your profile. http://www.extjs.com/forum/usercp.php

  9. #9
    Ext JS Premium Member teqneers's Avatar
    Join Date
    Nov 2008
    Posts
    86
    Vote Rating
    1
    teqneers is on a distinguished road

      0  

    Default


    Nice idea... Thanks ;-)

  10. #10
    Ext JS Premium Member teqneers's Avatar
    Join Date
    Nov 2008
    Posts
    86
    Vote Rating
    1
    teqneers is on a distinguished road

      0  

    Default


    Can you please post how your data, which is returned by json/data.php, looks?

    It's most likely that the data is not correctly formatted (the TreeReader is not as sophisticated as the other readers yet, so it requires a quite strict data format).

    Best regards

    Stefan