HOME > ブログ > ExtJS入門その14『Ext.PagingToolbar』

ExtJS入門その14『Ext.PagingToolbar』

今回はページング処理ができるツールバーについてです。説明するまでもないですが、データを何件かにわけて次へ前へとページを分けて表示する処理です。
前回で作成したグリッドパネルにつけていくという形でいきましょう。

ページング処理ができるツールバーの表現には「Ext.PagingToolbar()」を使います。
ちょっと都合悪いのでグリッドパネルをウィンドウパネルの中に入れます。

[デモ]
var MyGridPanel = new Ext.grid.GridPanel({
	height: 400,
	store: MyDataStore,
	colModel: MyGridcolModel,
	viewConfig: {
		forceFit: true
	}
});

var MyWindow = new Ext.Window({
	title: 'グリッドパネル in ウィンドウ',
	width: 800,
	heigth: 500,
	layout: 'fit',
	items: MyGridPanel
});
MyWindow.show();
これでドラッグ可能なウィンドウの中にグリッドパネルが表示されました。

さて、まずはグリッドパネルの「bbar: 」にページングツールバーをコンフィグオプションなしで追記してみます。

[デモ]
var MyGridPanel = new Ext.grid.GridPanel({
	height: 400,
	store: MyDataStore,
	colModel: MyGridcolModel,
	bbar: new Ext.PagingToolbar(),
	viewConfig: {
		forceFit: true
	}
});
問題なく表示されました。どうやら表示に際しての必須のコンフィグはないようです。

次に実際のページング処理ですが、その前にこの機能はそもそもどういったものなのかを知っておく必要があります。
このページング処理の機能はサーバサイドプログラムがあってこその機能であるということをまずおさえておいた方がいいです。
というのも、データストアでは色々な場所・形式でデータを拾えてくることを確認しましたが、どんな形でもこのページング処理機能が使えるというわけではありません。私も最初勘違いしていて、この機能はそこらへんもやってくれるものだと思っていてせっせと静的JSONファイルに向かってクエリをなげてはでけへんなぁおかしいなぁと悩んでました(恥)
ExtJSがあまりによくできたRIAフレームワークである故の盲点でしょうか、当然やってくれるんだろうと思い込んでいたのです。全件取得して蓄えたデータに対して必要な部分のみ表示というのではページング処理の意味はありませんので、proxyあたりで取得先が静的なJSONファイルでもXMLファイルでも必要な部分のみを切り取って取得してくれるんだろうと思っていたのです。なんて便利なのだろうか、と。いつ何時も思い込みは怖いものですorz
まぁそんな話はどうでもいいですが、つまりはページング処理はサーバサイドプログラムとクライアントのAjax通信を簡単におこなってくれるようなものだということです。なのでプログラム側でlimitとoffsetによって限定的にデータを抽出し返してやるというお馴染みの処理が必ず必須というわけです。
(※ちなみにユーザーエクステンションにはメモリーデータでもページ処理を行える拡張プログラムがあるのでそれを使えば可能となりますが)

ということで今までのような固定データは使えないのでここいらで大変面倒くさいですが、データベースを用意してデータを返すプログラムも用意せねばなりません。メンドーだなぁ

データベース
CREATE TABLE members (
	id		serial NOT NULL,
	name		text,
	sex		text,
	tel		text,
	mail		text,
	address		text
);
ちなみに私のホームグラウンドはPostgreSQLなのでMySQLの方はセルフで置き換えてくださいませ。
ダミーのデータは2000件ほどあれば十分ですが、もちろん手作業ではやってられないのでジェネレータなどを使ってDBに放り込みましょう。
私は「テストデータ・ジェネレータ(21yamagata)」というサイトにお世話になりました。ありがとうございますm(_ _)m
PHPのソースは最後に記しますが、GETでlimit、offset、ORDER BYのソートカラム名、昇降順、コールバック関数の5つを指定可能な形にしています。(値がなければ既定の初期値を採用します-例えばlimitは20、offsetは0)あと諸事情でDBのエンコードはEUC-JPです。

クロスドメインでPHPが吐き出すJSONP形式を受け取るようにします。

[デモ]
Ext.onReady(function(){

	var MyDataStore = new Ext.data.Store({
		proxy: new Ext.data.ScriptTagProxy({
			url: 'http://www.jinlingren.com/extjs/eid73/kojinjoho.php',
			method: 'GET'
		}),
		reader: new Ext.data.JsonReader({
			root: 'humandata',
			totalProperty: 'totalCount',
			fields: [
				{ name:'name' },
				{ name:'sex' },
				{ name:'tel' },
				{ name:'mail' },
				{ name:'address' }
			]
		})
	});
	MyDataStore.load();

	var MyGridcolModel = new Ext.grid.ColumnModel({
		defaults: {
			sortable: true
		},
		columns: [
			new Ext.grid.RowNumberer({header:'No',width:32}),
			{
				header: '名前',
				width: 80,
				dataIndex: 'name'
			},{
				header: '性別',
				width: 40,
				dataIndex: 'sex'
			},{
				header: '電話番号',
				width: 110,
				dataIndex: 'tel'
			},{
				header: 'メール',
				width: 150,
				dataIndex: 'mail'
			},{
				header: '住所',
				width: 250,
				dataIndex: 'address'
			}
		]
	});

	var MyGridbbar = new Ext.PagingToolbar({
		store: MyDataStore,
		pageSize: 20,
		displayInfo: true,
		displayMsg: '{2}件中、{0}〜{1}件目表示中',
		emptyMsg: 'データなし'
	});

	var MyGridPanel = new Ext.grid.GridPanel({
		height: 400,
		store: MyDataStore,
		colModel: MyGridcolModel,
		bbar: MyGridbbar,
		viewConfig: {
			forceFit: true
		}
	});

	var MyWindow = new Ext.Window({
		title: 'グリッドパネル in ウィンドウ',
		width: 800,
		heigth: 500,
		layout: 'fit',
		items: MyGridPanel
	});
	MyWindow.show();

});

問題なく表示されたように見えますが、次のページをクリックしても表示はかわりません。
「stcCallback1001 is not defined」というエラーが出ています。上記ソースではコールバック関数を指定していませんので、PHP側では既定の初期値「stcCallback1001」が採用されているはずです。

う〜ん…接続を見てみると「http://〜&callback=stcCallback1002」となってボタンを押すたびに末尾の数字が加算されているじゃありませんか…

前の記事でコールバック関数は固定の「stcCallback1001」でいい!とか書いてしまいましたが、どうやら大ウソのようですね。このページング処理機能でソッコーでメッキがはがれましたよ。やはり決め付けはよくありませんな。
どうも初回時は「stcCallback1001」という固有値から始まるため最初は問題なく表示されるのでしょう。すっかりやられました。末尾が連番となっている時点でくさいと気付くべきでしたね。
なのでやはりこのコールバック関数は変数でもたせてプログラム側で動的に変更せねばならないというわけです。(ページ処理をしなければ「stcCallback1001」固定でいいのかな?)
「Ext.data.ScriptTagProxy()」のコンフィグオプション「callbackParam: 」で任意の文字列を指定し、「$_REQUEST['任意の文字列']」で受け取ります。

[デモ]
var MyDataStore = new Ext.data.Store({
	proxy: new Ext.data.ScriptTagProxy({
		url: 'http://www.jinlingren.com/extjs/eid73/kojinjoho.php',
		method: 'GET',
		callbackParam: '_callback'
	}),
	reader: new Ext.data.JsonReader({
		root: 'humandata',
		totalProperty: 'totalCount',
		fields: [
			{ name:'name' },
			{ name:'sex' },
			{ name:'tel' },
			{ name:'mail' },
			{ name:'address' }
		]
	})
});
MyDataStore.load();
任意の文字列を「_callback」としました。PHP側では「$_REQUEST['_callback']」となります。
(初期値は「callback」なので「callbackParam: 」を指定しなくてもPHP側で「$_REQUEST['callback']」で受け取ることもできます)
この状態で見てみます。OKですね。きちんとページ処理されて表示されます。

この時に送られているパラメーターは「http://〜/kojinjoho.php?start=20&limit=20&〜」となっています。つまりはlimitとoffsetのみですので、ORDER BYのソートカラム名、昇降順はPHP側の既定の初期値が採用されます。
SQLは
SELECT * FROM members ORDER BY name ASC LIMIT 20 OFFSET 20
となるはずです。
ExtJS側でどのカラムでもソートできないのであればこれでも問題ないのでしょうが、例えば仮に2ページ目で電話番号でソートした場合、
1.現在のページで表示されるデータはどういったものなのか?
2.そのまま1ページ目に遷移した場合そのページのデータはどういったものなのか?
を考えざるをえません。

実際にやってみるとこうなります。
1.2ページ目のデータの中(カラム「name」昇順でソートされた20件目から20件)のみでの電話番号ソートとなる。
2.1ページ目のデータの中(カラム「name」昇順でソートされた20件目から20件)のみでの電話番号ソートとなる。
この答えから推測するに、ソートはデータを取得しExtJS側にメモリしたものに対してかけているのだとわかります。ヘッダー部クリックでのソート時はサーバとの通信は行っていないはずです。実際Firebugを見ても接続要求はされていません。
ではサーバとの通信を行って電話番号でソートをかけるにはどうたらよいのでしょうか?
「Ext.data.Store()」コンフィグオプションの「remoteSort: 」を使うとローカルデータからかサーバからかを制御できます。
デフォルトはfalseとなっているのでローカルデータを使ってソートをかけるといわけです。
では設定してみます。

[デモ]
var MyDataStore = new Ext.data.Store({
	proxy: new Ext.data.ScriptTagProxy({
		url: 'http://www.jinlingren.com/extjs/eid73/kojinjoho.php',
		method: 'GET',
		callbackParam: '_callback'
	}),
	reader: new Ext.data.JsonReader({
		root: 'humandata',
		totalProperty: 'totalCount',
		fields: [
			{ name:'name' },
			{ name:'sex' },
			{ name:'tel' },
			{ name:'mail' },
			{ name:'address' }
		]
	}),
	remoteSort: true
});
MyDataStore.load();
どうですか?Firebugを見るときちんとサーバに接続要求を行っているのがわかりますね。パラメーターを見ると「http://〜/kojinjoho.php?start=20&limit=20&sort=tel&dir=ASC〜」となっていますのでこのページに表示されているデータのSQLは
SELECT * FROM members ORDER BY tel ASC LIMIT 20 OFFSET 20
となります。
この状態で次ページのボタンを押した場合、sortとdirのパラメーターは引き継がれるのでしょうか?
「http://〜/kojinjoho.php?start=40&limit=20&sort=tel&dir=ASC〜」となっていますので大丈夫のようですね。
ただここで一つ疑問が出てきますが、これらパラメータの変数「start、limit、sort、dir」などは予め決まっているものなのでしょうか?
今まで見てきたExtJSの性質を考えると多分任意の文字列を自由に設定できるんじゃないかな〜?とか予測できますが、ここはこの疑問をちょっと横においておいて…

ロードした時点で任意にソートをかけた状態

[デモ]
var MyDataStore = new Ext.data.Store({
	proxy: new Ext.data.ScriptTagProxy({
		url: 'http://www.jinlingren.com/extjs/eid73/kojinjoho.php',
		method: 'GET',
		callbackParam: '_callback'
	}),
	reader: new Ext.data.JsonReader({
		root: 'humandata',
		totalProperty: 'totalCount',
		fields: [
			{ name:'name' },
			{ name:'sex' },
			{ name:'tel' },
			{ name:'mail' },
			{ name:'address' }
		]
	}),
	remoteSort: true,
	sortInfo: {
		field: 'name',
		direction: 'ASC'
	}
});
MyDataStore.load();
「sortInfo: 」を使った場合、パラメーターは「http://〜/kojinjoho.php?start=0&limit=20&sort=name&dir=ASC〜」となっています。

ではロード時にパラメーターを付加した形

[デモ]
var MyDataStore = new Ext.data.Store({
	proxy: new Ext.data.ScriptTagProxy({
		url: 'http://www.jinlingren.com/extjs/eid73/kojinjoho.php',
		method: 'GET',
		callbackParam: '_callback'
	}),
	reader: new Ext.data.JsonReader({
		root: 'humandata',
		totalProperty: 'totalCount',
		fields: [
			{ name:'name' },
			{ name:'sex' },
			{ name:'tel' },
			{ name:'mail' },
			{ name:'address' }
		]
	}),
	remoteSort: true
});
MyDataStore.load({
	params: {
		start: 0,
		limit: 20,
		sort: 'name',
		dir: 'ASC'
	}
});
の場合も同じく、パラメーターは「http://〜/kojinjoho.php?start=0&limit=20&sort=name&dir=ASC〜」となっています。

ではではデータ取得時にベースパラメーターを付加する形

[デモ]
var MyDataStore = new Ext.data.Store({
	proxy: new Ext.data.ScriptTagProxy({
		url: 'http://www.jinlingren.com/extjs/eid73/kojinjoho.php',
		method: 'GET',
		callbackParam: '_callback'
	}),
	reader: new Ext.data.JsonReader({
		root: 'humandata',
		totalProperty: 'totalCount',
		fields: [
			{ name:'name' },
			{ name:'sex' },
			{ name:'tel' },
			{ name:'mail' },
			{ name:'address' }
		]
	}),
	remoteSort: true,
	baseParams: {
		start: 0,
		limit: 20,
		sort: 'name',
		dir: 'ASC'
	}
});
MyDataStore.load();
「baseParams: 」を使った場合も同じく、パラメーターは「http://〜/kojinjoho.php?start=0&limit=20&sort=name&dir=ASC〜」となっています。

一見全部同じじゃんと思うかもしれません。でもひとつずつ試してみるとわかりますが、それぞれの使い分けは明確に違います。
まず「sortInfo: 」は「load({})」とほぼ同じですが、パラメーターの変数がデフォルトで送られるということに気付くと思います。
一方「load({})」は変数と値がセットのJSONデータを指定できることからもわかるようにパラメーターの変数を任意に設定することができます。なおかつこれら4つの決まったパラメーターだけでなく全く別の新しい変数を設定することもできます。
この任意の文字列を設定できるという点では「baseParams: 」も「load({})」と同様ですが、違いは「baseParams=毎回」と「load({})=初回ロード時」というところにあります。
ただひとつ勘違いしてはいけないのは「baseParams=毎回」といってもどのページでもこのパラメータが採用され付与されるというわけではありません。
それではいつまでたっても「http://〜/kojinjoho.php?start=0&limit=20&sort=name&dir=ASC〜」となってページ処理もソートもありませんのでね。
ではどういうことなのかというと、あくまで挙動で確認する限りなのですが、なにも設定値がなければこの「baseParams: 」の値が採用されるという感じなのでしょう。
そもそも上記のようにすることが混乱をまねく恐れがあるのですが、このような形で使うものではなさそうです。例えば公開されているAPIを利用する時などに大抵IDやパスワードを変数として持たせて認証してデータを返してくれるというようなサービスだとすると、IDやパスワードは要求するクエリに毎回付与しなければなりません。この「baseParams: 」はまさにそのような場合に使うものだと思います。ベースとして付与する情報なんですよ、ということでしょう。

パラメータが任意の文字列で設定できるのか試してみます。
var MyDataStore = new Ext.data.Store({
	proxy: new Ext.data.ScriptTagProxy({
		url: 'http://www.jinlingren.com/extjs/eid73/kojinjoho.php',
		method: 'GET',
		callbackParam: '_callback'
	}),
	reader: new Ext.data.JsonReader({
		root: 'humandata',
		totalProperty: 'totalCount',
		fields: [
			{ name:'name' },
			{ name:'sex' },
			{ name:'tel' },
			{ name:'mail' },
			{ name:'address' }
		]
	}),
	remoteSort: true,
	baseParams: {
		starthoge: 0,
		limithoge: 20,
		sorthoge: 'name',
		dirhoge: 'ASC'
	}
});
MyDataStore.load();
としてみましょう。電話番号でソートして次のページ、のパラメーターは「http://〜/kojinjoho.php?starthoge=0&limithoge=20&sorthoge=name&dirhoge=ASC&stat=20&limit=20&sort=tel&dir=ASC〜」となりました。
あれ?どうやら基本的なパラメーターである「start、limit、sort、dir」に関しては決まっているようですね。確かにそらそうですよね〜新しい任意の文字列を設定できるのであればstarthogeなんかは新しい設定値ですものね。そりゃ「start」の意味にはなりません。

疲れたので「Ext.PagingToolbar()」のコンフィグオプションについては割愛します。
それにしてもこのページング処理機能はよくできていますね〜。

最後にPHPファイルのソースとJSの最終的なソースを載せておきます。

kojinjoho.php
<?php
$connect = pg_connect("host='localhost' port='5432' user='*' dbname='*' password='*'");

$pager_query = pg_exec($connect, "SELECT id FROM members");
$pager_max = pg_numrows($pager_query);

if($_REQUEST['start']<>""){
    $offset = $_REQUEST['start'];
}else{
    $offset = 0;
}

if($_REQUEST['limit']<>""){
    $limit = $_REQUEST['limit'];
}else{
    $limit = 20;
}

if($_REQUEST['sort']<>""){
    $sort = $_REQUEST['sort'];
}else{
    $sort = "name";
}

if($_REQUEST['dir']<>""){
    $dir = $_REQUEST['dir'];
}else{
    $dir = "ASC";
}

$members_sql = "SELECT * FROM members ".
"ORDER BY ".pg_escape_string($sort)." ".pg_escape_string($dir)." ".
"LIMIT ".pg_escape_string($limit)." OFFSET ".pg_escape_string($offset);
$members_query = pg_exec($connect, $members_sql);
$members_max = pg_numrows($members_query);
$members_array = pg_fetch_all($members_query);


if($_REQUEST['_callback']<>""){
    $callback = $_REQUEST['_callback'];
}else{
    $callback = "stcCallback1001";
}


header("Content-Type:text/javascript; charset=UTF-8");

echo $callback.'({';
echo ' "totalCount":"'.$pager_max.'",';
echo ' "humandata": [';

$lastnum = $members_max - 1;

for($members_row = 0; $members_row < $members_max ; $members_row++){
    echo '  {';
    echo '   "id": "'.$members_array[$members_row]['id'].'",';
    echo '   "name": "'.mb_convert_encoding($members_array[$members_row]['name'], "UTF-8", "EUC-JP").'",';
    echo '   "sex": "'.mb_convert_encoding($members_array[$members_row]['sex'], "UTF-8", "EUC-JP").'",';
    echo '   "tel": "'.$members_array[$members_row]['tel'].'",';
    echo '   "mail": "'.$members_array[$members_row]['mail'].'",';
    echo '   "address": "'.mb_convert_encoding($members_array[$members_row]['address'], "UTF-8", "EUC-JP").'"';
    echo '  }';
    if($members_row<>$lastnum){
        echo ',';
    }
}

echo ' ]';
echo '})';

?>
ベッタベタに作りました。データを出す以外何も考えていません。よい子は決してこのまま使ってはいけません×
受け取るGET変数は5つで、それぞれ値がない場合の初期値を設定しています。つまり単純にアクセスした場合は
SELECT * FROM members ORDER BY name ASC LIMIT 20 OFFSET 0
というSQL発行のJSONPデータが返るというわけです。
(※ちなみにこのPHPでのデータ取得はjinlingren.comドメインでしかできないようにしていますのでテストしたい場合は必ずDB+PHPは自身でご用意ください)



完成のJSソース

[デモ]
Ext.onReady(function(){

	var MyDataStore = new Ext.data.Store({
		proxy: new Ext.data.ScriptTagProxy({
			url: 'http://www.jinlingren.com/extjs/eid73/kojinjoho.php',
			method: 'GET',
			callbackParam: '_callback'
		}),
		reader: new Ext.data.JsonReader({
			root: 'humandata',
			totalProperty: 'totalCount',
			fields: [
				{ name:'name' },
				{ name:'sex' },
				{ name:'tel' },
				{ name:'mail' },
				{ name:'address' }
			]
		}),
		remoteSort: true
	});
	MyDataStore.load();

	var MyGridcolModel = new Ext.grid.ColumnModel({
		defaults: {
			sortable: true
		},
		columns: [
			new Ext.grid.RowNumberer({header:'No',width:32}),
			{
				header: '名前',
				width: 80,
				dataIndex: 'name'
			},{
				header: '性別',
				width: 40,
				dataIndex: 'sex'
			},{
				header: '電話番号',
				width: 110,
				dataIndex: 'tel'
			},{
				header: 'メール',
				width: 150,
				dataIndex: 'mail'
			},{
				header: '住所',
				width: 250,
				dataIndex: 'address'
			}
		]
	});

	var MyGridbbar = new Ext.PagingToolbar({
		store: MyDataStore,
		pageSize: 20,
		displayInfo: true,
		displayMsg: '{2}件中、{0}〜{1}件目表示中',
		emptyMsg: 'データなし'
	});

	var MyGridPanel = new Ext.grid.GridPanel({
		height: 400,
		store: MyDataStore,
		colModel: MyGridcolModel,
		bbar: MyGridbbar,
		viewConfig: {
			forceFit: true
		}
	});

	var MyWindow = new Ext.Window({
		title: 'グリッドパネル in ウィンドウ',
		width: 800,
		heigth: 500,
		layout: 'fit',
		items: MyGridPanel
	});
	MyWindow.show();

});

| ExtJS | Comment:3 |
コメント
ぺんぺん
お礼
大変わかりやすかったです。
cakephp + ExtJS で利用しました。

ありがとうございます。
2010/09/10 11:15:52
payday loans UK
payday loans UK
sqqvbq <a href="http://paydayloanscheckrates.co.uk/ ">payday loans UK</a> ZNvsk <a href="http://paydayloanscheckrates.com/ ">Payday loans</a> 7193 <a href="http://payday24loans.com/ ">payday loan</a> sZZHQ
| | 2012/01/30 08:50:08
generic cialis
generic cialis
sibqjtiq <a href="http://bcheap-cialis.com/ ">generic cialis</a> 4172 <a href="http://her-propecia.com/ ">propecia </a> 0406 <a href="http://bcheap-viagra.com/ ">viagra</a> 5997 <a href="http://accutane-skin.com/ ">accutane</a> UwXWVQ
| | 2012/02/04 04:21:28
コメント投稿












画像リロード
*半角の小英字、数字で構成されています