ツリー

ツリーパネルコンポーネントは Ext JS において最も有用なコンポーネントの1つであり、アプリケーションの階層データを表示する優れたツールでもあります。ツリーパネルはグリッドパネルと同じクラスから継承するため、グリッドパネルのすべての長所(機能、拡張性、プラグイン)を使用できます。列、列のサイズ変更、ドラッグとドロップ、描画、ソートとフィルタリングなどは、どちらのコンポーネントにおいても同様に作動します。

先ずはシンプルなツリーを作成してみましょう。

Editor Preview Open in Fiddle
Ext.create('Ext.tree.Panel', {
    renderTo: Ext.getBody(),
    title: 'Simple Tree',
    width: 300,
    height: 250,
    root: {
        text: 'Root',
        expanded: true,
        children: [
            {
                text: 'Child 1',
                leaf: true
            },
            {
                text: 'Child 2',
                leaf: true
            },
            {
                text: 'Child 3',
                expanded: true,
                children: [
                    {
                        text: 'Grandchild',
                        leaf: true
                    }
                ]
            }
        ]
    }
});

このツリーパネルは、ドキュメントの本文に対してそれ自身を描画します。定義したルートノードはデフォルトで拡張されます。ルートノードは3つの子を持っており、その内の最初の2つは子を持つことができないリーフノードです。3つめのノードはリーフノードではなく、子リーフノードを1つ持っています。textプロパティはノードのテキストラベルとして使用されます。

内部においては、ツリーパネルはそのデータを TreeStore にストアします。上記の例では、ルートコンフィグがストアを設定するショートカットとして使用されています。ストアを別々にストアする場合、コードは以下のようになります。

var store = Ext.create('Ext.data.TreeStore', {
    root: {
        text: 'Root',
        expanded: true,
        children: [
            {
                text: 'Child 1',
                leaf: true
            },
            {
                text: 'Child 2',
                leaf: true
            },
            ...
        ]
    }
});

Ext.create('Ext.tree.Panel', {
    title: 'Simple Tree',
    store: store,
    ...
});

ノードのインターフェース

上記の例では、別々のプロパティがいくつかツリーノードで設定されています。問題は、どれが正確にノードであるかです。前述のとおり、ツリーパネルはTreeStoreにバインドされます。Ext JSのStoreはモデルインスタンスのコレクションを管理します。ツリーノードはNodeInterfaceで装飾されるモデルインスタンスです。モデルをNodeInterfaceで装飾すると、ツリーで使用される際に必要なフィールド、メソッド、プロパティがモデルに与えられます。以下のスクリーンショットで、開発者ツールにおけるノードの構造体を示します。

A model instance decorated with the NodeInterface

ノードで利用できるフィールド、メソッド、プロパティの完全なセットを表示する場合は、NodeInterfaceクラスのAPIドキュメンテーションを参照してください。

ツリーの視覚的変更 シンプルなものを試してみましょう。useArrowsをtrueに設定すると、ツリーパネルは行を非表示にし、矢印を拡張アイコンや折りたたみアイコンとして使用します。

Arrows

rootVisibleプロパティをfalseに設定すると、ルートノードが仮想的に削除されます。これによって、ルートノードが自動的に拡張されます。以下の画像では、falseに設定されたrootVisibleとfalseに設定されたを持つ同じツリーが表示されています。

Root not visible and no lines

複数の列

ツリーパネルグリッドパネルと同じ基底クラスから継承するため、より多くの列を簡単に追加できます。

Editor Preview Open in Fiddle
var tree = Ext.create('Ext.tree.Panel', {
    renderTo: Ext.getBody(),
    title: 'TreeGrid',
    width: 300,
    height: 150,
    fields: ['name', 'description'],
    columns: [{
        xtype: 'treecolumn',
        text: 'Name',
        dataIndex: 'name',
        width: 150,
        sortable: true
    }, {
        text: 'Description',
        dataIndex: 'description',
        flex: 1,
        sortable: true
    }],
    root: {
        name: 'Root',
        description: 'Root description',
        expanded: true,
        children: [{
            name: 'Child 1',
            description: 'Description 1',
            leaf: true
        }, {
            name: 'Child 2',
            description: 'Description 2',
            leaf: true
        }]
    }
});

設定では、グリッドパネルに備わっているのと同じような Ext.grid.column.Column 設定の配列が求められます。唯一の違いは、ツリーパネルでは’treecolumn’のxtypeを持つ列が最低1つ求められることです。この種類の列には、深さ、行、拡張アイコン、折りたたみアイコンなどのツリー固有の視覚効果が備わっています。通常、ツリーパネルに備わっている’treecolumn’は1つのみです。

fields 設定は内部的に作成されたストアが使用する Ext.data.Model に渡されます。列のdataIndex設定が指定されたフィールド(名前と説明)にマッピングする方法に注意してください。

また、列が定義される場合には、‘text’に設定されたdataIndexを持つ単一のtreecolumnがツリーによって作成されます。このとき、ツリーのヘッダーも非表示になります。単一の列のみを使用する際にこのヘッダーを表示するには、hideHeadersを’false’に設定します。

ツリーへのノードの追加

ツリーパネルのルートノードは、最初の設定で指定される必要はありません。これは、常に後から追加できます。

var tree = Ext.create('Ext.tree.Panel');
tree.setRootNode({
    text: 'Root',
    expanded: true,
    children: [{
        text: 'Child 1',
        leaf: true
    }, {
        text: 'Child 2',
        leaf: true
    }]
});

これは、静的ノードを数個のみ持つ小さなツリーには有効ですが、ほとんどのツリーパネルには更に多くのノードが含まれています。それでは、新しいノードをツリーにプログラムで追加する方法を見てみましょう。

var root = tree.getRootNode();

var parent = root.appendChild({
    text: 'Parent 1'
});

parent.appendChild({
    text: 'Child 3',
    leaf: true
});

parent.expand();

リーフノードではないノードはすべて、ノードを受け入れるappendChildメソッド、または最初のパラメータとしてノードのコンフィグオブジェクトを備えており、追加されたノードを返します。また、上記の例では、拡張メソッドが呼び出され、新たに作成された親が拡張されます。

Appending to the tree

新たな親ノードの作成時に子を直列に定義する機能も有効です。以下のコードでも、同じ結果が得られます。

var parent = root.appendChild({
    text: 'Parent 1',
    expanded: true,
    children: [{
        text: 'Child 3',
        leaf: true
    }]
});

ノードを追加する代わりに、それを特定の場所に挿入する場合もあります。appendChildメソッド以外に、insertBefore メソッドと insertChild メソッドも、Ext.data.NodeInterface によって提供されます。

var child = parent.insertChild(0, {
    text: 'Child 2.5',
    leaf: true
});

parent.insertBefore({
    text: 'Child 2.75',
    leaf: true
}, child.nextSibling);

insertChildメソッドでは、子が挿入されるインデックスが要求されます。insertBeforeメソッドでは、参照ノードが要求されます。新しいノードは、参照ノードの前に挿入されます。

Inserting children into the tree

NodeInterfaceは、他のノードを参照できるプロパティを更にいくつかノード上に提供します。

プロキシによるツリーデータのロードと保存

ツリーデータのロードと保存は、平坦なデータを処理するよりも少々複雑です。これは、すべてのフィールドでツリーの階層構造を表現することが求められるためです。このセクションでは、ツリーデータを使って作業する複雑さについて説明します。

NodeInterfaceフィールド

ツリーデータを使って作業をする際に理解しておく最初にして最重要なものは、NodeInterfaceクラスのフィールドの動作方法です。ツリーのすべてのノードは、NodeInterfaceのフィールドとメソッドで装飾されたモデルインスタンスです。アプリケーションにPersonというモデルが備わっていると仮定してみます。Personは2つのフィールド(idおよび名前)を持っています。

Ext.define('Person', {
    extend: 'Ext.data.Model',
    fields: [
        { name: 'id', type: 'int' },
        { name: 'name', type: 'string' }
    ]
});

この段階では、Personは単純なモデルでしかありません。インスタンスが作成されると、fieldsコレクションを見ることによって、そのインスタンスが2つのフィールドを持っていることを簡単に検証できるようになります。

console.log(Person.prototype.fields.getCount()); // outputs '2'

PersonモデルがTreeStoreで使用されると、面白いことが起きます。以下のように、フィールドのカウントに注意してください。

var store = Ext.create('Ext.data.TreeStore', {
    model: 'Person',
    root: {
        name: 'Phil'
    }
});

console.log(Person.prototype.fields.getCount()); // outputs '24'

PersonモデルのプロトタイプをTreeStoreで使用して、フィールドが新たに22個追加されました。このような追加のフィールドはすべてNodeInterfaceで定義され、モデルのインスタンスがTreeStoreで最初に使用されるときに(それをルートノードとして設定することによって)、そのモデルのプロトタイプに追加されます。

これら22の追加のフィールドの正確な意味と役割とは一体何なのでしょうか。NodeInterfaceのソースコードをご覧ください。モデルが以下のフィールドで装飾されているのが分かります。これらのフィールドによって、ツリーの構造と状態に関連する情報が内部でストアされます。

{
    name: 'parentId',
    type: idType,
    defaultValue: null,
    useNull: idField.useNull
}, {
    name: 'index',
    type: 'int',
    defaultValue: -1,
    persist: false,
    convert: null
}, {
    name: 'depth',
    type: 'int',
    defaultValue: 0,
    persist: false,
    convert: null
}, {
    name: 'expanded',
    type: 'bool',
    defaultValue: false,
    persist: false,
    convert: null
}, {
    name: 'expandable',
    type: 'bool',
    defaultValue: true,
    persist: false,
    convert: null
}, {
    name: 'checked',
    type: 'auto',
    defaultValue: null,
    persist: false,
    convert: null
}, {
    name: 'leaf',
    type: 'bool',
    defaultValue: false
}, {
    name: 'cls',
    type: 'string',
    defaultValue: '',
    persist: false,
    convert: null
}, {
    name: 'iconCls',
    type: 'string',
    defaultValue: '',
    persist: false,
    convert: null
}, {
    name: 'icon',
    type: 'string',
    defaultValue: '',
    persist: false,
    convert: null
}, {
    name: 'root',
    type: 'boolean',
    defaultValue: false,
    persist: false,
    convert: null
}, {
    name: 'isLast',
    type: 'boolean',
    defaultValue: false,
    persist: false,
    convert: null
}, {
    name: 'isFirst',
    type: 'boolean',
    defaultValue: false,
    persist: false,
    convert: null
}, {
    name: 'allowDrop',
    type: 'boolean',
    defaultValue: true,
    persist: false,
    convert: null
}, {
    name: 'allowDrag',
    type: 'boolean',
    defaultValue: true,
    persist: false,
    convert: null
}, {
    name: 'loaded',
    type: 'boolean',
    defaultValue: false,
    persist: false,
    convert: null
}, {
    name: 'loading',
    type: 'boolean',
    defaultValue: false,
    persist: false,
    convert: null
}, {
    name: 'href',
    type: 'string',
    defaultValue: '',
    persist: false,
    convert: null
}, {
    name: 'hrefTarget',
    type: 'string',
    defaultValue: '',
    persist: false,
    convert: null
}, {
    name: 'qtip',
    type: 'string',
    defaultValue: '',
    persist: false,
    convert: null
}, {
    name: 'qtitle',
    type: 'string',
    defaultValue: '',
    persist: false,
    convert: null
}, {
    name: 'qshowDelay',
    type: 'int',
    defaultValue: 0,
    persist: false,
    convert: null
}, {
    name: 'children',
    type: 'auto',
    defaultValue: null,
    persist: false,
    convert: null
}

予約名であるNodeInterfaceフィールド

上記のフィールド名は、「予約」されている名前として必ず取り扱ってください。たとえば、モデルがツリー内で使用される場合、“parentId”というフィールドをモデル内に持つことはできません。これは、モデルのフィールドがNodeInterfaceフィールドをオーバーライドするためです。この規則に対する例外は、フィールドの永続性をオーバーライドするための合理的理由がある場合です。

永続型のフィールドvs非永続型のフィールドとフィールドの永続性のオーバーライド

NodeInterfaceフィールドのほとんどは、デフォルトのpersist: falseになります。これは、それらのフィールドがデフォルトでは非永続型であることを意味します。TreeStoreの同期メソッドを呼び出す場合、またはモデル上でsave()を呼び出す場合は、非永続型フィールドがプロキシから保存されることはありません。ほとんどの場合、これらのフィールドの大半はデフォルトの永続性設定に据え置かれますが、一部のフィールドの永続性をオーバーライドする必要がある場合もあります。以下の例において、NodeInterfaceフィールドの永続性をオーバーライドする方法を示します。NodeInterfaceフィールドをオーバーライドする際には、persistプロパティのみを変更してください。nametypedefaultValueは絶対に変更しないでください。

// overriding the persistence of NodeInterface fields in a Model definition
    Ext.define('Person', {
        extend: 'Ext.data.Model',
        fields: [
            // Person fields
            { name: 'id', type: 'int' },
            { name: 'name', type: 'string' }

            // override a non-persistent NodeInterface field to make it persistent
            { name: 'iconCls', type: 'string',  defaultValue: null, persist: true },

            // Make the index persistent, so that when reordering nodes, syncing to the server
            // passes the new index as well as the parentId.
            // (Note that if moved to the same index in a different parent, the index will
still be sent in order to fully describe the operation)
            { name: 'index', type: 'int', defaultValue: -1, persist: true}
        ]
    });

各NodeInterfaceフィールドと、そのpersistプロパティのオーバーライドが必要になるシナリオを更に掘り下げてみましょう。以下の各例では、特に断りがないかぎり、サーバープロキシが使用されていると仮定されます。

デフォルトでの永続型

  • parentId - ノードの親ノードのidをストアします。このフィールドは常に永続型であるため、オーバーライドしないでください。
  • leaf - ノードがリーフノードであるため子を追加できないことを示します。通常、このフィールドをオーバーライドする必要はありません。

デフォルトでの非永続型

  • index - ノードの順番を親の中にストアします。ノードが挿入または削除される場合、挿入ポイントまたは削除ポイントの後の兄弟ノードはすべてインデックスを更新します。必要に応じて、アプリケーションはこのフィールドを使用してノードの順番を永続させます。ただし、サーバーがストアする順番において別のメソッドを使う場合は、インデックスフィールドを非永続型に据え置くのがより適切です。WebStorageプロキシを使用する際にストアする順番が必要な場合は、このフィールドをオーバーライドして永続型にする必要があります。また、クライアントサイドのソートが使用されている場合には、インデックスフィールドを非永続型に据え置いてください。これは、ソートを行うと、ソートされた全ノードのインデックスが更新され、次の同期において永続化(persistプロパティがtrueの場合は保存)されるためです。

  • depth - ツリー階層内のノードの深さをストアします。サーバーが深さのフィールドをストアする必要がある場合、このフィールドをオーバーライドして永続型にします。WebStorage プロキシを使用する場合には、深さのフィールドの永続性をオーバーライドしないでください。このプロキシは、ツリーの構造を正しくストアする必要がなく、単に余分なスペースに対応するだけであるためです。

  • checked - ツリーがチェックボックス機能を使用している場合、このフィールドはオーバーライドされて永続型になります。
  • expanded - ノードの拡張/折りたたみ状態をストアします。通常、このフィールドをオーバーライドする必要はありません。
  • expandable - このノードが拡張可能であることを内部的に示します。このフィールドの永続性をオーバーライドしないでください。
  • cls - TreePanel で描画する際に、CSS クラスをノードに適用します。必要に応じて、このフィールドをオーバーライドして永続型にします。
  • iconCls - TreePanel で描画する際に、CSS クラスをノードのアイコンに適用します。必要に応じて、このフィールドをオーバーライドして永続型にします。
  • icon - TreePanel で描画する際に、カスタムのアイコンをノードに適用します。必要に応じて、このフィールドをオーバーライドして永続型にします。
  • root - このノードがルートノードであることを示します。このフィールドをオーバーライドしないでください。
  • isLast - このノードが兄弟の最後であることを示します。通常、このフィールドをオーバーライドする必要はありません。
  • isFirst - このノードが兄弟の最初であることを示します。通常、このフィールドをオーバーライドする必要はありません。
  • allowDrop - ノードでのドロップを内部的に拒否します。このフィールドの永続性をオーバーライドしないでください。
  • allowDrag - ノードのドラッグを内部的に拒否します。このフィールドの永続性をオーバーライドしないでください。
  • loaded - ノードの子がロードされていることを内部的に示します。このフィールドの永続性をオーバーライドしないでください。
  • loading - プロキシがノードの子をロード中であることを内部的に示します。このフィールドの永続性をオーバーライドしないでください。
  • href - ノードがリンクする必要があるURLを指定します。必要に応じて、オーバーライドして永続型にします。
  • hrefTarget - hrefのターゲットを指定します。必要に応じて、オーバーライドして永続型にします。
  • qtip - ツールチップテキストをノードに追加します。必要に応じて、オーバーライドして永続型にします。
  • qtitle - tooltipのタイトルを指定します。必要に応じて、オーバーライドして永続型にします。
  • children - 1度のリクエストでノードとその子をすべてロードする際に内部的に使用されます。このフィールドの永続性をオーバーライドしないでください。

データのロード

ツリーデータをロードする方法は2つあります。1つめは、プロキシがツリー全体を1度にフェッチするという方法です。すべてを1度にロードするのが理想的でない大きなツリーの場合、拡張時に各ノードの子を動的にロードする2つめのメソッドを利用するのが望ましいことがあります。

ツリー全体のロード

ツリーは拡張中のノードに応じて、データのロードのみ内部的に行います。ただし、プロキシがツリーの構造全体を含むネストされたオブジェクトを取得する場合には、階層全体をロードできます。これを実行するには、TreeStoreのルートノードをexpandedに初期化します。

Ext.define('Person', {
    extend: 'Ext.data.Model',
    fields: [
        { name: 'id', type: 'int' },
        { name: 'name', type: 'string' }
    ],
    proxy: {
        type: 'ajax',
        api: {
            create: 'createPersons',
            read: 'readPersons',
            update: 'updatePersons',
            destroy: 'destroyPersons'
        }
    }

});

var store = Ext.create('Ext.data.TreeStore', {
    model: 'Person',
    root: {
        name: 'People',
        expanded: true
    }
});

Ext.create('Ext.tree.Panel', {
    renderTo: Ext.getBody(),
    width: 300,
    height: 200,
    title: 'People',
    store: store,
    columns: [
        { xtype: 'treecolumn', header: 'Name', dataIndex: 'name', flex: 1 }
    ]
});

readPersonsのURLが以下のjsonオブジェクトを返すと仮定してみます。

{
    "success": true,
    "children": [
        { "id": 1, "name": "Phil", "leaf": true },
        { "id": 2, "name": "Nico", "expanded": true, "children": [
            { "id": 3, "name": "Mitchell", "leaf": true }
        ]},
        { "id": 4, "name": "Sue", "loaded": true }
    ]
}

ツリー全体をロードするために必要なものは以上です。

Tree with Bulk Loaded Data

重要な注意点

  • 子を持たないすべての非リーフノード(前述のSueという名のPersonなど)に対しては、サーバーのレスポンスは必ずloadedプロパティをtrueに設定する必要があります。それを行わない場合、プロキシは拡張時に、これらのノードの子のロードを試行します。
  • ここで疑問が挙がります。サーバーがJSONレスポンス内のノード上でloadedプロパティを設定できる場合、他の非永続型のフィールドのいずれかを設定できるのでしょうか。答えはイエス、である場合があります。上記のノードの例では、“Nico”の名前を持つノードはtrueに設定されたexpandedフィールドを持っているため、ツリーパネルでの拡張時には最初に表示されます。ルートノードではないノード上でrootプロパティを設定するなどの、深刻な問題を発生させかねない不適切な操作が行われる場合もあるため、注意が必要です。通常、loadedexpandedは、サーバーがJSONレスポンスにおいて非永続型フィールドを設定することが推奨される唯一のケースです。

ノード展開時の子ノードの動的ロード

ツリーが長い場合、親ノードの拡張時にのみ、子ノードをロードしてツリーの一部だけをロードするのが望ましいこともあります。上記の例で、“Sue”という名のノードがサーバーレスポンスでtrueに設定されたloadedフィールドを持っていないと仮定してみます。このツリーはノードの隣にExpanderアイコンを表示します。ノードの拡張時に、プロキシは、次のようなreadPersonsのURLに対して別のリクエストを作成します。

/readPersons?node=4

これは、4のidを持つノードの子ノードを取得するようサーバーに命じます。データは、ルートノードのロード時に使用されたデータと同じフォーマットで返されます。

{
    "success": true,
    "children": [
        { "id": 5, "name": "Evan", "leaf": true }
    ]
}

ツリーの外観は以下のとおりです。

Tree with Dynamically Loaded Node

データの保存

ノードの作成、更新、削除は、プロキシによって自動的に途切れなく処理されます。

新しいアプリケーションの作成

// Create a new node and append it to the tree:
var newPerson = Ext.create('Person', { name: 'Nige', leaf: true });
store.getNodeById(2).appendChild(newPerson);

プロキシはモデル上で直接定義されるため、モデルのsave()メソッドを使用してデータを永続化できます。

newPerson.save();

既存のノードの更新

store.getNodeById(1).set('name', 'Philip');

ノードの削除

store.getRootNode().lastChild.remove();

バルク操作

複数のノードの作成、更新、削除後、TreeStoreのsync()メソッドを呼び出せば、これらのノードをすべて1つの操作で永続化できます。

store.sync();
Last updated