Ext.grid.Panelからグループ化機能を解析してみた。
表題にあるとおり、グループ化機能を多段ネストさせたかったので、gridのソースコードから始めて現状がどのように実装されているのかを調べようとしました。
んが、実力が伴っていないせいで結局諦めましたw
いつか見合う実力がついてきたときに再開できるように、調査過程のメモだけでも掲載しておきます。
------------------------------------------------------------ ★注意★ 【未確認】は、自分で実装してみての確認をしていないということ。 ○×は、APIリファレンスを読んで推測している内容。 ------------------------------------------------------------ Ext.grid.Panel(Ext.data.Store) ・グループ ・ネスト →×【未確認】 ・1階級のネストしか作成できない。 ・グループサマリ→○【未確認】 ・列を超えて表示可能。 ・フィルタ →○【確認済】 ・filterByメソッドが用意されているため、任意のフィルタをかけることが可能。 ・ソート →○【確認済】 ・Ext.data.Storeのsortメソッドを利用して確認した。 ・列並び替え →○【未確認】 ・おそらくcolumnsプロパティを変更することで対応可能。 Ext.tree.Panel(Ext.data.TreeStore) ・グループ ・ネスト →○【確認済】 ・グループサマリ→×【確認済】 ・列を超えて表示不可能なため、列幅が狭いと途中で「...」の表記になってしまう。 ・フィルタ →△【未確認】 ・filterByメソッドが用意されていないため、そのままの状態では実装不可。 ・但し、Ext.data.TreeStoreを継承した自前クラスを定義し、そこにfilterBy他関連メソッドを実装することにより対応可能。 ・但し、上記実装時にノードのコピー処理を行うが、ExtJS 4.0.2aにおいてこの部分にバグが存在している為、 自前で修正する必要があるとのこと。 ・ソート →○【未確認】 ・treeのヘッダに組込まれているソート機能を利用して、動的にソート順が変更されたことは確認した。 ・Ext.data.TreeStoreにもsortメソッドが用意されているので、おそらく実現可能。 ・列並び替え →○【未確認】 ・おそらくcolumnsプロパティを変更することで対応可能。 Ext.data.TreeStoreに置けるfilterByを実装する必要はあるか?→ある。 サーバサイドで絞り込みをすることも可能だが、 Ext.data.StoreのfilterByはクライアントで実行されてるふう。 やはり同じような絞り込みが出来た方が便利。 Ext.grid.Panelに多段ネストできるグループ機能を実装する? 実装者からするとExt.grid.Panel(というかExt.data.Store)でのグループ化の仕方はわかりやすい。 自前で多段ネストできる機能をExt.data.Storeに実装するのもアリか? でもgridとstoreの連携部分の解析とか見せ方のカスタマイズとか大変そう。。 やっぱりExt.tree.PanelでTreeGrid式の見せ方する方が楽だよな〜。 とすると、Ext.tree.Panelのcolumnsの指定の仕方を検討する必要がある。 というのも、いくつか問題があるので。 1.多段ネストのグループ化は、「複数の列をグループ化指定する」にも関わらず、 「ツリー形式で見せる列は一つだけ」(厳密には複数指定可能だが複数指定すると見た目がカオスになる)なので、 グループ化列を増やせば増やすほど、インデントが大きくなっていく。 2.グループ化した列も、「列幅」の影響を受けるため、次の右側の列にはみ出して表示させることができない。 幅が短ければ途中で切れて「...」という表記になってしまう。 3.Ext.data.TreeStoreには、filterByメソッドが存在しない。 4.グループ化した行にサマリーを表示する機能がない。 但し、サーバ側で計算して該当列に表示することは可能。 が、これをやる場合、ビューの設定でうまいこと表示・・・というマネはできなくなるかも。。 columns設定の箇所で、「reaf: false」だった場合を区別できれば、renderをカスタマイズすることで対応できるか? (列データが変更された場合もちゃんと機能しないとダメよ。) 5.TreeStoreにaddっぽいメソッド(追加するとしかるべきところに勝手に追加される機能?)がないっぽい。 ツリーに対してツリーのどこに挿入するかを指定しないとダメかも。(未調査) ★★★解析したいのは、Ext.grid.Panelにおいてグルーピングを描画している箇所★★★ Ext.grid.Panelには実際に描画する為のロジックはなさげ。 なので基底クラスのExt.panel.Tableを見てみる。 Ext.panel.Tableにも描画ロジックはなさげ。 viewというものがあるみたい。これが描画を担当しているかも。 Ext.panel.Table.getView()について me.viewに割り当てられるviewは、次のviewConfigパラメータをわたされたビューになる。 xtypeには、'gridview'が指定されてる。(me.viewType) featuresには空の配列が渡されている。 gridviewは何者? →Ext.grid.View →Ext.view.Table Ext.view.Tableに、initFeatures();というメソッドがあるようなので、こいつを調べる感じ。 グループ化はgroupFeatureとかいうのを設定した気がするので、関連しているでしょきっと。 渡されたfeatureのコンフィグオプションを元に、インスタンス化したfeatureを生成している。 で、自分のfeatureMCって'Ext.util.MixedCollection'にインスタンスを入れてる。 'Ext.util.MixedCollection'ってなに? ★★★ Ext.util.MixedCollectionには引数一つのaddメソッドは無い。 Ext.util.AbstractMixedCollectionのgetKeyメソッド定義のところで引数一つのaddについての記述発見。 addされたitemの、item.idを自動的にキーとしてフェッチするらしい。 で、getKeyにカスタマイズしたfunctionを割り当てることでitemの任意のプロパティをキーにフェッチする事が可能だそうだ。 で、これに外部からアクセスするのはgetFeature(id)でとれる感じ。 じゃあfeatureってどんなん?あとidってのをキーにしてるけどこれは何に相当するんじゃ? ★★★の件を考えると、featureにidが割り当てられているはず。 featureを探して解析してみよう。 その前に現状のgridをグループ化するためのコードを実装してみよう。 できた。かなり簡単。 で、この状態でfetureに何が入ってきてるか、もクソもねぇな。 指定したfeatureは、「Ext.grid.feature.Grouping」。ソースファイルをみたところ、ftypeは'group'。 こちらを指定した遅延インスタンス化で実装してみた。 というわけで、「ひょっとして本丸?」なExt.grid.feature.Groupingを解析していってみよう。 コードを読むとファイルヘッダコメントに次のように書いてある。。 ------------------------------------------------------------ This feature allows to display the grid rows aggregated into groups as specified by the {@link Ext.data.Store#groupers} specified on the Store. ------------------------------------------------------------ マテ、Ext.data.Storeのコンフィグオプションで指定するのはgroupFieldではないのか? groupersってなんだっけ?てか、名前からして複数系だけど、もしかして多段ネストできたりする?? 先にこっちを調べてみよう。 やっぱダメそうじゃ〜ん。 元の作業に戻ろう。。 Ext.grid.feature.GroupingのgetFeatureTplにタグ的なものを設定している部分発見。 ------------------------------------------------------------ getFeatureTpl: function(values, parent, x, xcount) { var me = this; return [ '<tpl if="typeof rows !== \'undefined\'">', // group row tpl '<tr class="' + Ext.baseCSSPrefix + 'grid-group-hd ' + (me.startCollapsed ? me.hdCollapsedCls : '') + ' {hdCollapsedCls}"><td class="' + Ext.baseCSSPrefix + 'grid-cell" colspan="' + parent.columns.length + '" {[this.indentByDepth(values)]}><div class="' + Ext.baseCSSPrefix + 'grid-cell-inner"><div class="' + Ext.baseCSSPrefix + 'grid-group-title">{collapsed}' + me.groupHeaderTpl + '</div></div></td></tr>', // this is the rowbody '<tr id="{viewId}-gp-{name}" class="' + Ext.baseCSSPrefix + 'grid-group-body ' + (me.startCollapsed ? me.collapsedCls : '') + ' {collapsedCls}"><td colspan="' + parent.columns.length + '">{[this.recurse(values)]}</td></tr>', '</tpl>' ].join(''); }, ------------------------------------------------------------ ソースを見ると、ここではテンプレートを定義していて、内容はグループのヘッダ部分と中身の部分を生成している模様。 で、確認したい中身の部分はこんな風に定義されている。 '<td colspan="' + parent.columns.length + '">{[this.recurse(values)]}</td>' this.recurse(values)を呼び出して、テンプレートのtdのinnerHTMLを生成する感じ? じゃ、このメソッドは何をしてるのか。 おっと、Ext.grid.feature.Groupingには定義されてねぇがどこに定義されてるんだ? 基底クラス側かな? 見つからない。 じゃ、recurseをsrcフォルダ配下に対してgrepしてみよう。 view/TableChunker.jsにのコンストラクタに定義らしきモノ発見。 ------------------------------------------------------------ constructor: function() { Ext.XTemplate.prototype.recurse = function(values, reference) { return this.apply(reference ? values[reference] : values); }; }, ------------------------------------------------------------ って箇所だった。 引数を2つとる。値とリファレンス?正体はなんだろう。 デバッグで調べるしかないか? というか、そもそもこのメソッドが目当てのrecurseなのかどうか不明。 どうやって確認したら良い? あと、TableChunkerって何者? XTemplateが絡んできてる。長丁場になりそうだな〜。 話がそれるが、コンフィグオプションのmetaTableTplプロパティで、テーブルの全体の構造をタグでデザインしてる感じ。 ------------------------------------------------------------ metaTableTpl: [ '{[this.openTableWrap()]}', '<table class="' + Ext.baseCSSPrefix + 'grid-table ' + Ext.baseCSSPrefix + 'grid-table-resizer" border="0" cellspacing="0" cellpadding="0" {[this.embedFullWidth()]}>', '<tbody>', '<tr class="' + Ext.baseCSSPrefix + 'grid-header-row">', '<tpl for="columns">', '<th class="' + Ext.baseCSSPrefix + 'grid-col-resizer-{id}" style="width: {width}px; height: 0px;"></th>', '</tpl>', '</tr>', '{[this.openRows()]}', '{row}', '<tpl for="features">', '{[this.embedFeature(values, parent, xindex, xcount)]}', '</tpl>', '{[this.closeRows()]}', '</tbody>', '</table>', '{[this.closeTableWrap()]}' ], ------------------------------------------------------------ メインで行を書き出しているところは、embedFeatureってとこか。 定義はこれ。 ------------------------------------------------------------ embedFeature: function(values, parent, x, xcount) { var tpl = ''; if (!values.disabled) { tpl = values.getFeatureTpl(values, parent, x, xcount); } return tpl; }, ------------------------------------------------------------ valuesのgetFeatureTplを呼び出している。 このメソッドはあれか?「Ext.grid.feature.Grouping」の「getFeatureTpl」のことでOK? デバッガモードで確認してみよう。 あれ、ステップインできねー。 関数コピーしてちょっと整形してみる。 ------------------------------------------------------------ function (values, parent, x, xcount) { var me = this; return [ "<tpl if=\"typeof rows !== 'undefined'\">", "<tr class=\"" + Ext.baseCSSPrefix + "grid-group-hd " + (me.startCollapsed ? me.hdCollapsedCls : "") + " {hdCollapsedCls}\"><td class=\"" + Ext.baseCSSPrefix + "grid-cell\" colspan=\"" + parent.columns.length + "\" {[this.indentByDepth(values)]}><div class=\"" + Ext.baseCSSPrefix + "grid-cell-inner\"><div class=\"" + Ext.baseCSSPrefix + "grid-group-title\">{collapsed}" + me.groupHeaderTpl + "</div></div></td></tr>", "<tr id=\"{viewId}-gp-{name}\" class=\"" + Ext.baseCSSPrefix + "grid-group-body " + (me.startCollapsed ? me.collapsedCls : "") + " {collapsedCls}\"><td colspan=\"" + parent.columns.length + "\">{[this.recurse(values)]}</td></tr>", "</tpl>"].join(""); } ------------------------------------------------------------ ------------------------------------------------------------ getFeatureTpl: function(values, parent, x, xcount) { var me = this; return [ '<tpl if="typeof rows !== \'undefined\'">', // group row tpl '<tr class="' + Ext.baseCSSPrefix + 'grid-group-hd ' + (me.startCollapsed ? me.hdCollapsedCls : '') + ' {hdCollapsedCls}"><td class="' + Ext.baseCSSPrefix + 'grid-cell" colspan="' + parent.columns.length + '" {[this.indentByDepth(values)]}><div class="' + Ext.baseCSSPrefix + 'grid-cell-inner"><div class="' + Ext.baseCSSPrefix + 'grid-group-title">{collapsed}' + me.groupHeaderTpl + '</div></div></td></tr>', // this is the rowbody '<tr id="{viewId}-gp-{name}" class="' + Ext.baseCSSPrefix + 'grid-group-body ' + (me.startCollapsed ? me.collapsedCls : '') + ' {collapsedCls}"><td colspan="' + parent.columns.length + '">{[this.recurse(values)]}</td></tr>', '</tpl>' ].join(''); }, ------------------------------------------------------------ 同じですね。 ということで、ここでコールされている風。 さて、この中で呼び出されているrecurseメソッドだが。 TableChuncker.jsのコンストラクタの中で、 Ext.XTemplateのプロトタイプにrecurseメソッドを定義している。 これじゃあTableChunckerがインスタンス化される度にExt.XTemplate.prototype.recurseに関数再定義することになっちゃうじゃん。 と思ったら、TableChunckerはsingleton指定されていた。 てことは、TableChunckerが使われる場合だけ、Ext.XTemplate.prototype.recurseが定義される感じか。 で、本当にこのrecurseが呼び出されているんか? getFeatureTpl内のテンプレートの文字列は、「this.recurse」って感じで、thisスコープになってるが。。 あ、でもこのテンプレートが呼び出される箇所のthisスコープになるのか? ・・・だからテンプレートってのは気に入らねぇ。。デバッガで追えねぇんだよクソが。 getFeatureTpl内のテンプレート内で呼び出されているrecurseは引数一つだけだけど、 TableChuncker.jsのコンストラクタの中で定義されているExt.XTemplate.prototype.recurseは、引数2つなんだけど。 どういうこと? いったい誰が getTableTplのメソッド内で、 ------------------------------------------------------------ metaRowTpl = Ext.create('Ext.XTemplate', metaRowTpl.join(''), memberFns); cfg.row = metaRowTpl.applyTemplate(cfg); ------------------------------------------------------------ 的なことをやっていて、多分ここで行を生成してる感じ? いや、重要なのはgetTableTplだ。 こいつが生成するテンプレートの解析が必要だ。 得に、149行目で実行されている処理の第3引数。 こん中にrecurseに相当する何かが入ってるんじゃないか? ------------------------------------------------------------ // TODO: Investigate eliminating. if (!textOnly) { tpl = Ext.create('Ext.XTemplate', tpl, tplMemberFns); } ------------------------------------------------------------ この1行で生成されたtplの中身を見たら、recurse発見。 但し、functionという指定があるだけで、実体がないっぽ? てことはテンプレート内で記述されている「this.recurse(...)」の実行は行われていないのか? あくまでも自身の再起呼び出し以外では有効な関数が割り当てられないのかしら。 実験してみる必要があるな。 Ext.layout.container.Box.js 681行目 ★疑問★ デバッガでないともはや追うことは不可能な感じ。 動的型言語だから、ソースコード見ただけでは追えない。 →今見ている変数に入っているオブジェクトが何なのか分からないから。 動的言語のソースコードを解析する場合、デバッガで動かしながらというのは王道的な解析手法なのかも?