Implementing an Editable Auto-Sorting Ext JS 4.2 Tree

taxonomy

Ext JS 4 has some fantastic components, although it’s not always clear as to how to configure a particular component to achieve a desired result.

Recently we were tasked with implementing a custom metadata/taxonomy editing GUI for a customer and decided to create an editable, drag-and-drop tree control to accomplish this task.

Here’s the code, which was cobbled together from various blog posts, forum messages, and a little bit of ingenuity on my part. Note that it was developed using Sencha Architect 2.x, which is, in itself, noteworthy:

Here’s the code for the Tree Panel:

Ext.define('MyApp.view.admin.taxonomy.Taxonomy', {
    extend: 'Ext.tree.Panel',
    alias: 'widget.edittaxonomy',

    requires: [
        'MyApp.view.override.admin.taxonomy.Taxonomy',
        'Ext.grid.plugin.CellEditing'
    ],
    plugins: [
       Ext.create('Ext.grid.plugin.CellEditing', {clicksToEdit:2})
    ]

    height: 500,
    width: 200,
    autoScroll: true,
    header: false,
    title: 'Taxonomy',
    hideHeaders: true,
    store: 'Taxonomy',

    initComponent: function() {
        var me = this;

        Ext.applyIf(me, {
            viewConfig: {
                toggleOnDblClick: false,
                plugins: [
                    Ext.create('Ext.tree.plugin.TreeViewDragDrop', {
                        appendOnly: true,
                        nodeHighlightOnDrop: true,
                        nodeHighlightOnRepair: true
                    })
                ],
                listeners: {
                    drop: {
                        fn: me.onTreeViewDragDropDrop,
                        scope: me
                    }
                }
            },
            dockedItems: [
                {
                    xtype: 'toolbar',
                    dock: 'top',
                    items: [
                        {
                            xtype: 'button',
                            handler: function(button, event) {
                                var tree = this.up('treepanel');
                                var selModel = tree.getSelectionModel();

                                // Could also use:
                                // var node = selModel.getSelection()[0];
                                var node = selModel.getLastSelected();

                                if (!node) {
                                    node = tree.getStore().getRootNode();
                                }

                                // Feels like this should happen automatically
                                node.set('leaf', false);

                                node.appendChild({
                                    leaf: true,
                                    text: 'New Child'
                                });

                                // Not strictly required but...
                                node.expand();
                            },
                            flex: 1,
                            text: 'Add Term'
                        },
                        {
                            xtype: 'button',
                            handler: function(button, event) {
                                var tree = this.up('treepanel');
                                var selModel = tree.getSelectionModel();

                                // Could also use:
                                // var node = selModel.getSelection()[0];
                                var node = selModel.getLastSelected();
                                node.destroy();
                            },
                            flex: 1,
                            text: 'Del Term'
                        }
                    ]
                },
                {
                    xtype: 'toolbar',
                    dock: 'bottom',
                    items: [
                        {
                            xtype: 'button',
                            handler: function(button, event) {
                                Ext.getStore('Taxonomy').sync();
                            },
                            flex: 1,
                            text: 'Save'
                        }
                    ]
                }
            ],
            columns: [
                {
                    xtype: 'treecolumn',
                    dataIndex: 'text',
                    text: 'Categories/Keywords',
                    flex: 1,
                    editor: {
                        xtype: 'textfield',
                        allowBlank: false,
                        allowOnlyWhitespace: false,
                        maxLength: 25
                    }
                }
            ],
            listeners: {
                beforerender: {
                    fn: me.onTreepanelBeforeRender,
                    scope: me
                }
            }
        });

        me.callParent(arguments);
    },

    onTreeViewDragDropDrop: function(node, data, overModel, dropPosition, eOpts) {

        this.getStore().sortChildren(overModel);

    },

    onTreepanelBeforeRender: function(component, eOpts) {
        component.getStore().load();
    }

});

And here’s the code for the Store:

Ext.define('MyApp.store.Taxonomy', {
    extend: 'Ext.data.TreeStore',

    constructor: function(cfg) {
        var me = this;
        cfg = cfg || {};
        me.callParent([Ext.apply({
            lazyFill: true,
            proxy: {
                type: 'ajax',
                url: 'resources/sampledata/taxonomy.json'
            },
            fields: [
                {
                    name: 'text'
                },
                {
                    name: 'bitmask'
                }
            ],
            listeners: {
                update: {
                    fn: me.onTreeStoreUpdate,
                    scope: me
                }
            }
        }, cfg)]);
    },

    onTreeStoreUpdate: function(store, record, operation, modifiedFieldNames, eOpts) {

        if (modifiedFieldNames && modifiedFieldNames[0] === 'text') {
            this.sortChildren(record.parentNode);
        }
    },

    sortChildren: function(parentNode) {
        parentNode.sort(function(n1, n2) {
            n1 = n1.get('text');
            n2 = n2.get('text');
            if (n1 < n2) {
                return -1;
            } else if (n1 === n2) {
                return 0;
            }
            return 1;
        });
    }
});

And here’s the sample data:

[{
    "text": "Category 1", 
    "id" : 1,
    "cls": "folder",
    "children": [{
        "text": "Keyword 1",
        "leaf": true,
         "id" : 2,
    },{
        "text": "Keyword 2",
        "leaf": true,
         "id" : 3,
    },{
        "text": "Keyword 3",
        "leaf": true,
        "id" : 4,
    }]
},{
    "text": "Category 2",
    "cls": "folder",
    "children": [{
        "text": "Keyword A",
        "leaf": true,
        "id" : 5,
    },{
        "text": "Keyword B",
        "leaf": true,
        "id" : 6,
    },{
        "text": "Keyword C",
        "leaf": true,
        "id" : 7
    }]
}]

5 thoughts on “Implementing an Editable Auto-Sorting Ext JS 4.2 Tree

  1. George White

    Don’t use delays!

    The problem with a store update event listener is that you are hearing about all kinds of internal updates. Tree nodes maintain their state in “fields”. As well as “text”, there’s “leaf”, etc. All kinds of internal values are fields, and update events will fire as the tree takes care of itself.

    Check that the field name that’s been updated is “text” before you sort, and all will be well.

    I have a little example working at https://fiddle.sencha.com/#fiddle/14d

    Reply

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s