ツリー
The TreePanel コンポーネントは、Ext JS 内で最も用途の広いコンポーネントの一つで、アプリケーションで階層的なデータを表示する素晴らしいツールです。TreePanel は、 Grid Panel と同じクラスから拡張するため、機能、拡張性、プラグインなどの GridPanel の利点は TreePanel でも使用可能です。カラム、カラムのサイズ変更、ドラッグ&ドロップ、renderer、sorter、filter は、どちらのコンポーネントで同じように動作します。
では、とてもシンプルなツリーを生成してみましょう。
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 } ] } ] } }); |
この TreePanel はドキュメントボディに自身を描画します。まずは root コンフィグでデフォルトで展開された状態のルートノードを定義しました。ルートノードには三つの子があり、最初の二つはリーフノード (leaf: true
) なので、子を持つことはできません。三つ目のノードはリーフノードではなく、一つの子リーフノードがあります。ノードの文字ラベルには、text
プロパティが使用されます。
内部的に、TreePanel は、
TreeStore
にデータを格納します。上記のサンプルは、Store を構成するために、
root
コンフィグをショートカットとして使用しています。もし Store を別に構成する場合には、そのコードは下記のようになります。
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, ... }); |
NodeInterface
上記のサンプルで、ツリーノードに二つのプロパティが設定されています。しかし、ノードとは一体何でしょう? 前に説明した通り、TreePanel は TreeStore. にバインドされます。Ext JS の Store は Model のインスタンスを管理します。ツリーノードは、 NodeInterface NodeInterface. がついた Model のインスタンスです。Model に NodeInterface をセットすると、その Model をツリーで使う時に必要な、フィールド、メソッド、プロパティを与えます。次のスクリーンショットは、デベロッパーツールの中でノードの構造を表示させたところです。
ノードの使用可能なフィールド、メソッド、プロパティを全て見るには、 NodeInterface クラスの API ドキュメントをご覧ください。
ツリーの見た目を変更する
さて、簡単なことをやってみましょう。
useArrows
コンフィグを true
に設定すと、TreePanel に線が表示されなくなり、展開と折りたたみのアイコンが矢印になります。
rootVisible
プロパティを false に設定すると、ルートノードを表示しなくなります。こうすると、ルートノードは自動的に展開されます。次のイメージは、rootVisible を false に、
lines
も false に設定されたツリーを表示しています。
複数カラム
Tree Panel は Grid Panel と同じベースクラスから拡張するため、とても簡単にカラムを追加できます。
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 }] } }); |
columns
コンフィグは、
Grid Panel
と同様、
Ext.grid.column.Column
コンフィグの配列を期待しています。ただ一つの違いは、TreePanel には xtyep が ‘treecolumn’ のカラムが必要です。このカラムは、階層、線、展開、折りたたみ等のツリー特有の視覚効果があります。通常の TreePanel では、一つの treecolumn だけがあります。
fields
コンフィグは、内部で生成された Store が利用する
Ext.data.Model
に渡されます。
カラムの
dataIndex
コンフィグで指定したフィールド (name と description) にマップする方法をご覧ください。
また、columns 定義されない場合、ツリーは、dataIndex
を 'text'
に設定した treecolumn
を一つ自動的に生成します。ツリーのヘッダーは非表示になります。カラムが一つだけの時にヘッダーを表示するためには、hideHeaders
コンフィグを false
に設定して下さい。
ツリーにノードを追加する
TreePanel のルートノードは、初期コンフィグに指定しなければならないわけではありません。後で追加する事もできます。
var tree = Ext.create('Ext.tree.Panel'); tree.setRootNode({ text: 'Root', expanded: true, children: [{ text: 'Child 1', leaf: true }, { text: 'Child 2', leaf: true }] }); |
いくつかの静的なノードがある小さいツリーには便利ですが、ほとんどの TreePanel には、より多くのノードがあります。次は、プログラムによってツリーに新しいノードを追加する方法を説明します。
var root = tree.getRootNode(); var parent = root.appendChild({ text: 'Parent 1' }); parent.appendChild({ text: 'Child 3', leaf: true }); parent.expand(); |
リーフノードでは無いノードには、第 1パラメータに追加するノードまたはノードのコンフィグオブジェクトを受け取る
appendChild
メソッドがあり、戻り値に追加されたノードを返します。上記のサンプルでは、
expand
メソッドを使って新しく生成された parent を展開しています。
もう一つの便利な方法は、親ノードを生成する時に、子をインラインで定義する事です。次のコードは前のコードと同じ結果になります。
var parent = root.appendChild({ text: 'Parent 1', expanded: true, children: [{ text: 'Child 3', leaf: true }] }); |
ツリーを追加するのではなく、ノードを特定な場所に挿入したい場合もあります。
appendChild メソッド以外に、
Ext.data.NodeInterface
は
insertBefore
と
insertChild
メソッドを提供しています。
var child = parent.insertChild(0, { text: 'Child 2.5', leaf: true }); parent.insertBefore({ text: 'Child 2.75', leaf: true }, child.nextSibling); |
insertChild
メソッドには、子が挿入されるインデックスを渡します。insertBefore
メソッドはノードへの参照を渡します。新しいノードは、指定したノードの前に挿入されます。
NodeInterface also provides several more properties on nodes that can be used to reference other nodes. NodeInterface は、他のノードを参照するために使われるノードのプロパティをいくつか提供します。
プロキシを利用したツリーデータの読み込みと保存
ツリーの階層構造を表現するために必要なフィールドがあるため、ツリーデータの読み込みと保存は、フラットデータより複雑になります。このセクションは、ツリーのデータを取り扱うことにかんする詳細を説明します。
NodeInterface フィールド
ツリーデータの扱いにおいて、 NodeInterface クラスのフィールドの動作を理解する事は重要です。ツリーの各ノードは、NodeInterface のフィールドやメソッドが付加された Model インスタンスです。あるアプリケーションが Person という Medel があるとしましょう。一つの Person には、id と name という 2つのフィールドがあります。
Ext.define('Person', { extend: 'Ext.data.Model', fields: [ { name: 'id', type: 'int' }, { name: 'name', type: 'string' } ] }); |
この時点での、Person は、単なる通常の Model です。インスタンスが生成して、 fields
コレクションを見ると、フィールドが 2つだけある事が検証できます。
console.log(Person.prototype.fields.getCount()); // outputs '2' |
この Person Model が TreeStore
で使われると、面白い事が起こります。現在のフィールド数に注目して下さい。
var store = Ext.create('Ext.data.TreeStore', { model: 'Person', root: { name: 'Phil' } }); console.log(Person.prototype.fields.getCount()); // '24' と出力 |
TreeStore で使用するだけで、Person Model のプロトタイプには、22個のフィールドが追加されました。追加されたフィールドは、 NodeInterface クラスで定義され、その Model のインスタンスが (ルートノードとして設定されて) 初めて TreeStore で使用されたときに、その Model のプロトタイプに追加されます。
さて、この追加された 22 フィールドは何でしょう? どんな役割があるのでしょう? NodeInterface のソースコードを見ると、Model には次のフィールドが追加する事がわかります。これらのフィールドは、ツリーの構造と状態に関係する情報を内部的に格納するために使用されています。
{ 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フィールドは予約名です
上記のフィールド名は、予約名と同じ扱いをします。例えば、Model をツリーで使うのであれば、その Model では parentId という名前のフィールドは許可されません。なぜなら Model のフィールドは、NodeInterface のフィールドをオーバーライドしてしまうからです。フォールドの永続性をオーバーライドする正当な理由がある時は例外です。
永続的なフィールド/非永続的なフィールドと永続的なフィールドのオーバーライド
ほとんどの NodeInterface のフィールドは、デフォルトで persist: false
になっています。このため、デフォルトでは非永続的です。非永続的なフィールドは、TreeStore での sync()
メソッド呼び出しや Model で save()
を呼び出した時にプロキシ経由で保存されません。多くの場合、このフィールドは、ほとんどの場合デフォルトの永続性の設定のままででいいですが、時にフィールドの永続性をオーバーライドする必要があることもあります。次のサンプルは、NodeInterface フィールドの永続性をオーバーライドする方法を示します。NodeInterface フィールドをオーバーライドする時に、persist プロパティだけを変更する事が大事です。name
、type
、defaultValue
は、一切変更しません。
// Model 定義で NodeInterface フィールドの永続性をオーバーライドする Ext.define('Person', { extend: 'Ext.data.Model', fields: [ // Person のフィールド { name: 'id', type: 'int' }, { name: 'name', type: 'string' } // NodeInterface フィールドを永続的にする { name: 'iconCls', type: 'string', defaultValue: null, persist: true }, // index を家族化する、ノードの順番を変えると、サーバーと同期する // 新しい index や parrentId も渡される // (違う親の同じインデックスに移動しても、 // 操作を完全に伝えるために index は送信されます) { name: 'index', type: 'int', defaultValue: -1, persist: true} ] }); |
さて、それぞれの NodeInterface フィールドと persist プロパティをオーバーライドする必要があるシナリオについてより細かく検討しましょう。下記の各サンプルで、特に記載されていない場合には、 Server Proxy が使われていると思って下さい。
デフォルトで永続的なもの。
parentId
– ノードの親ノードのIDを格納するために使用されます。このフィールドは、常に永続的である必要がありますので、オーバーライドしないで下さい。leaf
– ノードはリーフノードであるという事を示すために使われ、子を付加する事はできません。このフィールドは、通常オーバーライドする必要はありません。
デフォルトで非永続的なもの:
index
– 親の中でのノードの順番を格納するために使用されます。ノードが挿入あるいは削除された場合、その位置以降の兄弟ノードの index はアップデートされます。必要であれば、アプリケーションがノードの順番を永続化するために、このフィールドを使用できます。しかし、もしサーバーが、保存する順序に別な方法を使っていたら、index フィールドを非永続的なままにするほうが良いでしょう。 WebStorage Proxy を使っていて順序の保存が必要な場合には、このフィールドを永続的になるようにオーバーライドする必要があります。
また、クライアントサイドの ソート が使われている場合には、index フィールドを非永続的なままにすることを推奨します。というのは、ソートすると、全てのソートされたノードの index がアップデートされるため、次の sync で永続されるか、
persist
プロパティがtrue
に設定されていたら保存されます。depth
– ノードのツリー階層内の深さを格納するために使用されます。サーバーにフィールドの深さを格納する必要がある場合、このフィールドをオーバーライドし、永続性を有効にして下さい。 WebStorage Proxy を利用している場合は、depth フィールドの永続性をオーバーライドしない事を推奨します。depth フィールドは、ツリー構造を正確に格納するために必要なく、余分なスペースを取るだけだからです。checked
– ツリーが チェックボックス機能を使用している場合には、このフィールドをオーバーライドします。expanded
– ノードの展開・折りたたみの状態を格納するために使用されます。このフィールドは通常オーバーライドする必要はありません。expandable
– このノードは展開できる事を示すために、内部的に使用されます。このフィールドの永続性はオーバーライドしないで下さい。cls
– ノードが TreePanel に描画される時に、CSS クラスを反影するために使われます。必要に応じて、フィールドの永続性をオーバーライドして下さい。iconCls
– ノードが TreePanel に描画される時に、ノードのアイコンに CSS クラスを反影するために使われます。必要に応じて、このフィールドの永続性をオーバーライドして下さい。icon
– ノードが TreePanel に描画される時に、カスタムアイコンをノードに反影するために使われます。必要に応じて、このフィールドの永続性をオーバーライドして下さい。root
– このノードはルートノードである事を示すために使用されます。このフィールドはオーバーライドしないで下さい。isLast
– このノードは兄弟の最後のノードである事を示すために使用されます。このフィールドは通常オーバーライドする必要はありません。isFirst
– このノードは兄弟の最初のノードである事を示すために使用されます。このフィールドは通常オーバーライドする必要はありません。allowDrop
– ノードにドロップする事を禁じるために内部的に使用されます。このフィールドの永続性をオーバーライドしないで下さい。allowDrag
– ノードをドラッグする事を禁じるために内部的に使用されます。このフィールドの永続性をオーバーライドしないで下さい。loaded
– ノードの子ノードがロードされた事を示すために、内部的に使用されます。このフィールドの永続性をオーバーライドしないで下さい。loading
– プロキシがノードの子がロードしている最中であることを示すために、内部的に使用されます。このフィールドの永続性をオーバーライドしないで下さい。href
– ノードがリンクされる URL を指定するために使用されます。必要に応じて、このフィールドの永続性をオーバーライドして下さい。hrefTarget
–href
のターゲットを指定するために使用されます。必要に応じて、このフィールドを永続性をオーバーライドして下さい。qtip
– ノードにツールチップ文字列を追加するために使用されます。必要に応じて、このフィールドの永続性をオーバーライドして下さい。qtitle
– ツールチップのタイトルを指定するために使用されます。必要に応じて、このフィールドの永続性をオーバーライドして下さい。children
– ノードとその子を一つのリクエストでロードする時に、内部的に使用されます。このフィールドの永続性をオーバーライドしないで下さい。
データの読み込み
ツリーのデータを読み込む方法は二つあります。一つは、プロキシが一度にツリー全体を取得する方法です。より大きなツリーの場合には、一度に全てを読み込むのが最適とはいえませんので、二つ目の方法、ノードが展開される度に動的にロードする、を使うのが適しているでしょう。
ツリー全体を読み込む
内部的にいうと、ツリーはノードが展開された事に反応し、データを読み込みます。しかし、プロキシがツリー構造全体を持ったネストしたオブジェクトを取得すると、階層構造全体をロードできます。
これを実現するためには、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 } ] } |
これだけで、ツリー全体を読み込めます。
注意点:
- リーフではないノードで、子もないものに対して (例えば、上記の name が Sue の Person) 、サーバーのレスポンスは、
loaded
プロパティをtrue
に設定する必要があります。そうしないと、ノードが展開された時点で、プロキシが子ノードを読み込もうとします。 - ここで疑問が浮かびます – サーバーが、JSON レスポンスでノードに
loaded
プロパティを設定できるのであれば、他の非永続的なフィールドも設定できるのでしょうか? その答えは – はい、時々 – です。 上のサンプルでは、name が “Nico” のノードは、expanded
フィールドがtrue
に設定されているため、Tree Panel で展開されている状態で表示されます。しかし、例えばルートノードではないノードにroot
プロパティを設定するなど、この行為が適切ではない場合もありますので、ご注意ください。 通常は、サーバーが JSON レスポンスに 非永続的なフィールドを設定するのは、loaded
とexpanded
だけにすることを推奨します。
ノードが展開された時に子ノードを動的にロードする
大規模なツリーの場合には、親ノードが展開された時に子ノードを読み込む方法が理想的かもしれません。上記のサンプルでサーバーのレスポンスで loaded
フィールドが true
に設定されてないとしましょう。ツリーは、ノードの横に展開アイコンを表示します。ノードが展開されたら、プロキシは次のようなリクエストを readPersons の URL に送信します。
/readPersons?node=4 |
これは ID が 4 のノードの子ノードを取得したいという事をサーバーに伝えています。そのデータは、ルートノードを読み込む時に使用されたデータと同じ形式で返されます。
{ "success": true, "children": [ { "id": 5, "name": "Evan", "leaf": true } ] } |
そのツリーは次のようになります。
データの保存
ノードの生成、更新、削除は、プロキシで自動的に管理されます。
新しいノードを生成する
// 新しいノードを生成し、ツリーに追加する var newPerson = Ext.create('Person', { name: 'Nige', leaf: true }); store.getNodeById(2).appendChild(newPerson); |
プロキシは Model に直接定義されているので、
Model の
save()
メソッドを使ってデータを永続化できます。
newPerson.save(); |
既存のノードの更新
store.getNodeById(1).set('name', 'Philip'); |
ノードの削除
store.getRootNode().lastChild.remove(); |
一括操作
いくつかのノードの生成、更新、削除を行った後、
全てを
TreeStore の
sync()
メソッドを呼び出すと、全てを一つの操作で永続化する事ができます。
store.sync(); |