Serenity: How to implement TreeView by SlickGrid in Serenity

Created on 1 Nov 2016  路  17Comments  路  Source: serenity-is/Serenity

Hi,

I had read previous issues, and as you told, SlickGrid can support this. I tried several times, but not worked. Could you please advise how to implement TreeView by SlickGrid in your template? Thank you.

SlickGrid Standard Sample as below:
http://6pac.github.io/SlickGrid/examples/example5-collapsing.html

Most helpful comment

It took me an hour to figure out how to turn default EntityGrid (generated by sergen) to a Tree List with minimum code modification and keep other grid functions (like quick search, quick filter, sorting...) still working.
Hope that the code below helps others.

Assume you have a table named "Organization" with primary key Id and foreign key ParentId. Run sergen to generate default grid, then add following functions to the class OrganizationGrid in OrganizationGrid.ts. That's all.

    // Process tree data
    protected onViewProcessData(response: ListResponse<OrganizationRow>) {
        response = super.onViewProcessData(response);

        // Get grouped and sorted rows
        var results: OrganizationRow[] = [];
        let roots = response.Entities.filter(x => !x.ParentId || x.ParentId == 0);
        roots.forEach(x => this.GetTreeData(x, response.Entities, results));
        response.Entities = results;

        // Set indents of tree nodes
        Serenity.SlickTreeHelper.setIndents(response.Entities, x => x.Id, x => x.ParentId, false);

        return response;
    }

    // Add tree toggle to the first column
    protected getColumns() {
        let columns = super.getColumns();
        var col = columns[0];
        col.format = Janus.SlickFormatting.treeToggle(() => this.view, x => x.Id, col.format ? col.format : ctx => { return ctx.value });            
        col.formatter = SlickHelper.convertToFormatter(col.format);     
        return columns;
    }

    // Add filter by parent id
    protected onViewFilter(item: OrganizationRow): boolean {
        if (!super.onViewFilter(item)) {
            return false;
        }

        if (!Serenity.SlickTreeHelper.filterById(item, this.view, x => x.ParentId))
            return false;

        return true;
    }

    // Handle click event on tree node
    protected onClick(e, row, cell): void {
        super.onClick(e, row, cell);

        if (!e.isDefaultPrevented()) {
            Serenity.SlickTreeHelper.toggleClick(e, row, cell, this.view, x => x.Id);
        }
    }

    // Recursively get tree data
    private GetTreeData(item: OrganizationRow, items: OrganizationRow[], results: OrganizationRow[]) {
        results.push(item);
        let children = items.filter(x => x.ParentId == item.Id);
        children.forEach(x => this.GetTreeData(x, items, results));
    }

organiztiontreelist

All 17 comments

You need to override onViewFilter method in your grid, and apply similar code to myFilter method in that sample. I will add a tree example to Serene eventually, but it might take time.

Thank you, and looking forward for your professional sample. 馃憤

It took me an hour to figure out how to turn default EntityGrid (generated by sergen) to a Tree List with minimum code modification and keep other grid functions (like quick search, quick filter, sorting...) still working.
Hope that the code below helps others.

Assume you have a table named "Organization" with primary key Id and foreign key ParentId. Run sergen to generate default grid, then add following functions to the class OrganizationGrid in OrganizationGrid.ts. That's all.

    // Process tree data
    protected onViewProcessData(response: ListResponse<OrganizationRow>) {
        response = super.onViewProcessData(response);

        // Get grouped and sorted rows
        var results: OrganizationRow[] = [];
        let roots = response.Entities.filter(x => !x.ParentId || x.ParentId == 0);
        roots.forEach(x => this.GetTreeData(x, response.Entities, results));
        response.Entities = results;

        // Set indents of tree nodes
        Serenity.SlickTreeHelper.setIndents(response.Entities, x => x.Id, x => x.ParentId, false);

        return response;
    }

    // Add tree toggle to the first column
    protected getColumns() {
        let columns = super.getColumns();
        var col = columns[0];
        col.format = Janus.SlickFormatting.treeToggle(() => this.view, x => x.Id, col.format ? col.format : ctx => { return ctx.value });            
        col.formatter = SlickHelper.convertToFormatter(col.format);     
        return columns;
    }

    // Add filter by parent id
    protected onViewFilter(item: OrganizationRow): boolean {
        if (!super.onViewFilter(item)) {
            return false;
        }

        if (!Serenity.SlickTreeHelper.filterById(item, this.view, x => x.ParentId))
            return false;

        return true;
    }

    // Handle click event on tree node
    protected onClick(e, row, cell): void {
        super.onClick(e, row, cell);

        if (!e.isDefaultPrevented()) {
            Serenity.SlickTreeHelper.toggleClick(e, row, cell, this.view, x => x.Id);
        }
    }

    // Recursively get tree data
    private GetTreeData(item: OrganizationRow, items: OrganizationRow[], results: OrganizationRow[]) {
        results.push(item);
        let children = items.filter(x => x.ParentId == item.Id);
        children.forEach(x => this.GetTreeData(x, items, results));
    }

organiztiontreelist

Wow @ducthanhnguyen! I'm really suprised how you managed to find all these helper methods and where to use them. Nice skills.

Thank you @ducthanhnguyen !

@volkanceylan : I'm new to Serenity so just spade its code to see if it's suiltable for my next project. And the answer is "definitely YES". I am willing to contribute to Serenity, perhaps starting with this todo list

Sometimes you may need a simple Tree View instead of a Tree List, ie. only tree and no column, search, filter, paging... For example, you'd like to put a organization tree on the left side of user list or employee list. This can be done by creating a new typescript file "OrganizationTree.ts" like this

namespace Serene1.OrgStructure {

    @Serenity.Decorators.registerClass()
    export class OrganizationTree extends Serenity.DataGrid<OrganizationRow, any> {
        protected getIdProperty() { return OrganizationRow.idProperty; }

        constructor(container: JQuery) {
            super(container);

            OrganizationService.List({}, response => {                
                // Get grouped and sorted rows
                var items: OrganizationRow[] = [];
                let roots = response.Entities.filter(x => !x.ParentId || x.ParentId == 0).sort(x => x.DisplayOrder);
                roots.forEach(x => this.GetTreeData(x, response.Entities, items));

                // Set indents of tree nodes
                Serenity.SlickTreeHelper.setIndents(items, x => x.Id, x => x.ParentId, false);

                // set view items
                this.view.setItems(items, true);
            });            
        }

        // Overide to remove toolbar container
        protected createToolbar() {
        }

        protected onViewSubmit() {
            return false;
        }

        // Manual create columns
        protected getColumns() {
            let columns: Slick.Column[] = [
            {
                field: 'Name',
                format: Serenity.SlickFormatting.treeToggle(() => this.view, x => x.Id, ctx => { return ctx.value; }),
                width: 250,                
                sortable: false
            }];

            return columns;
        }

        protected onViewFilter(item: OrganizationRow): boolean {
            if (!super.onViewFilter(item)) {
                return false;
            }

            // Add filter by parent id
            if (!Serenity.SlickTreeHelper.filterById(item, this.view, x => x.ParentId))
                return false;

            return true;
        }

        // Handle click event on tree node
        protected onClick(e, row, cell): void {
            super.onClick(e, row, cell);

            if (!e.isDefaultPrevented()) {
                Serenity.SlickTreeHelper.toggleClick(e, row, cell, this.view, x => x.Id);
            }
        }

        // Recursively get tree data
        private GetTreeData(item: OrganizationRow, items: OrganizationRow[], results: OrganizationRow[]) {
            results.push(item);            
            let children = items.filter(x => x.ParentId == item.Id).sort(x => x.DisplayOrder);
            children.forEach(x => this.GetTreeData(x, items, results));
        }
    }
}

Note that the OrganizationTree extends from Serenity.DataGrid instead of Serenity.EntityGrid.
Then, you can use that tree in cshtml view, like the code below (UserIndex.cshtml):

<div class="box box-solid">   
    <div class="box-body">
        <div class="row">
            <div class="col-md-3">
                <div class="grid-title"><div class="title-text">Organization</div></div>
                <div id="OrgTreeDiv" class="tree-view"></div>
            </div>
            <div class="col-md-9">
                <div id="GridDiv"></div>
            </div>
        </div>
    </div>
</div>

@* Use css to remove grid column header, background and border *@
<style>   
    .tree-view .slick-header{display:none}
    .tree-view .grid-canvas{width:100%!important}
    .tree-view .slick-row{background:none}
    .tree-view .slick-cell{border-bottom: none;}
</style>

<script type="text/javascript">
    jQuery(function () {
        new Serene1.Administration.UserGrid($('#GridDiv'), {});
        Q.initFullHeightGridPage($('#GridDiv'));

        new Serene1.OrgStructure.OrganizationTree($('#OrgTreeDiv'), {}).init();
        Q.initFullHeightGridPage($('#OrgTreeDiv'));
    });
</script>

Here is the result:
organiztiontreeview

Another nice one that should go into basic samples

I've added a TreeGrid sample to Serene, which uses a simple mixin i've used internally:

http://serenity.is/demo/BasicSamples/TreeGrid

Great! That mixin keeps the column's original format, flexible and easy to implement

@volkanceylan @DucThanhNguyen Thank you very much, and it works very well.

@freddiezhou I've just updated the code to keep the orginal column format of the tree list. However, if you updated to the latest version of Senerity, please use the TreeGridMixin instead

The TreeGridMixin caused an exception when the column associated with toggleField had no formatter
Line 3689 in Serenity.Script.UI.js
var text = formatter(ctx);
It should be:
var text = formatter ? formatter(ctx) : ctx.value;

I also tried to use TreeGridMixin in a GridEditorBase but it didn't work. Please fix it.

Yes also noticed that

Formatter issue is fixed in 2.6.1.

It won't work with GridEditorBase, because that uses another fake id, e.g. __id, so your getParentId should also return that fake ID.

If you want it to work with original ID column, get latest GridEditorBase.ts from Serene repo and return your original ID property name from getIdProperty() method.

Hi, I'm using Serenity 3.3.9 and I have the latest version of _GridEditorBase.ts_ in Serene.
However, I can not get TreeGridMixin to work in my _xyzEditor.ts_, although I added as recommended above:

protected getIdProperty () {return xyzRow.idProperty; }

For information, the TreeGridMixin works fine in _xyzGrid.ts_.

Can you tell me what to do to make it work in _xyzEditor.ts_ ?

thanks in advance

Hello,

Do not consider my previous message. I found the solution to run the TreeGridMixin with a MasterDetailRelation.

In fact, do not add the getIdProperty. The one from _GridEditorBase.ts_ makes it work. You just have to make the master-detail relation on xyzRow["__ id"] yourself and all is well.

But the problem was that the update of the records is virtual. Thus there is no view.onProcessData (onViewProcessData) event that is generated. But it is on this event that the grid is reworked by the class TreeGridMixin.

So, now this class works fine after I added the following code in the overlay of the save() method:

protected save(opt: Serenity.ServiceOptions<any>, callback: (r: Serenity.ServiceResponse) => void) {
   ...
   super.save(opt, callback);
   ...

  // For莽age de l'appel de onProcessData (pour l'茅v猫nement onViewProcessData) car il n'est pas appel茅 en ma卯tre-d茅tail et est requis pour l'affichage avec la liste arborescente
  var loRetour: Serenity.ListResponse<vte_VteCmptRow> = {
    Entities: this.view.getItems(),
    Error: null,
    Skip: 0,
    Take: 0,
    TotalCount: this.view.getItems().length
  };
  loRetour = this.view.onProcessData(loRetour, this.view);
  this.view.setItems(loRetour.Entities, true);
}

Hoping that it helps other people who were also stuck on this problem.

Good day to all

Was this page helpful?
0 / 5 - 0 ratings

Related issues

kilroyFR picture kilroyFR  路  3Comments

StefanTheiner picture StefanTheiner  路  3Comments

Akarsh03 picture Akarsh03  路  3Comments

newyearsoft picture newyearsoft  路  3Comments

GitHubOrim picture GitHubOrim  路  3Comments