React用のWindowsライクなListViewをTypeScriptで作成する
1.WindowsライクなListViewが欲しい
Webアプリケーション作成中に、データの一覧表示やそれらの選択に、WindowsなどのデスクトップOSでよく使われる形式のListViewが欲しいことがあります。そんなコンポーネントを探そうとすると出てくるのは、モバイルOSで良く使われる形式の、単純な縦スクロール系ListViewばかりです。欲しいのはそういうのではなく、上部にヘッダが付いていて列サイズを調整できる奴を探しているのです。スクロールしたらヘッダが一緒にどこかにいってしまうタイプももちろん論外です。探し方が悪いのかもしれませんが、該当するものは見つかりませんでした。
Reactをまともに使い始めて一ヶ月も経っておらず、チュートリアルすらやっていなかったので、スキルアップを兼ねて早速作ることにしました。せっかくReactを使って実装するので、データを仮想DOMで記述できるようにします。
<ListList dataSource={data}/>
前提として上記のようなものはアウトです。オブジェクトでデータを与える選択肢を与えるにしても、まずは仮想DOMでデータを記述できるようにしたいところです。理想的なのは以下のような形です。
<ListView>
<ListHeaders>
<ListHeader>No</ListHeader>
<ListHeader>武器の<br />名前</ListHeader>
<ListHeader>攻撃力</ListHeader>
<ListHeader>価格</ListHeader>
</ListHeaders>
<ListRow>
<ListItem>1</ListItem>
<ListItem>竹槍</ListItem>
<ListItem>5</ListItem>
<ListItem>10</ListItem>
</ListRow>
<ListRow>
<ListItem>2</ListItem>
<ListItem>銅の剣</ListItem>
<ListItem>18</ListItem>
<ListItem>120</ListItem>
</ListRow>
<ListRow>
<ListItem>3</ListItem>
<ListItem>棍棒</ListItem>
<ListItem>10</ListItem>
<ListItem>40</ListItem>
</ListRow>
<ListRow>
<ListItem>4</ListItem>
<ListItem>鉄の槍</ListItem>
<ListItem>30</ListItem>
<ListItem>380</ListItem>
</ListRow>
<ListRow>
<ListItem>5</ListItem>
<ListItem>鉄の剣</ListItem>
<ListItem>40</ListItem>
<ListItem>700</ListItem>
</ListRow>
</ListView>
これを自動的にWindowsライクなListViewに変換していこうと思います。
2.ReactNodeというポリモーフィズムの塊
仮想DOMでデータを受け付ける場合、props.childrenに入っているのはデータを利用することになります。この中身を使えるように整理し、ListViewで表示するデータとして加工していきます。
このprops.childrenは文字列だったり数値だったり配列だったりReactElementだったりする、凄まじい多態性を持つReactNodeで出来ています。また、[ReactElement,ReactElement[]]が並んでいた場合、ReactElement[]はその場で展開される仕様になっていたりと、仕様を理解しないとトラップにハマることになります。
Reactでコンポーネントを作るなら以下の仕様は覚えておいた方が良いと思われます。
type ReactText = string | number;
type ReactChild = ReactElement | ReactText;
interface ReactNodeArray extends Array<ReactNode> {}
type ReactFragment = {} | ReactNodeArray;
type ReactNode = ReactChild | ReactFragment | ReactPortal | boolean | null | undefined;
これに言及した記事がほとんどありませんでした。ネットで見つけた一般的なコンポーネント作成記事はprops.childrenを使わないか、特に加工を加えず子ノードとしてそのまま放り込んで終わる感じです。
props.childrenの中からデータを取り出す場合は、以下の命令を使うことになります。重要なので使い方を覚えておくと役に立ちます。
React.Children.map
React.Children.toArray
3.ListViewの実装はやってみるとかなり複雑
WindowsライクなListViewは、実際にやってみるとかなり複雑です。
3.1 ヘッダを固定したスクロール
これ、大半の人がヘッダ表示領域とアイテム表示領域を分けて、アイテム領域にスクロールを設置すれば終わりだと思うでしょう。縦スクロールはそれで問題ありません。しかし横スクロールでヘッダが取り残されて終わります。
じゃあ、横スクロール用に上の階層にスクロールエリアを用意すればいいじゃんと、やはり大半の人が思うでしょう。実際それで横スクロールすると、今度は緑の部分の縦スクロールバーが一緒に横へ移動していきます。
これを解決するには、横スクロールに対して緑の部分を反対方向へ移動させ、ピンクの部分をスクロール方向へ移動させるというトリックを使います。
3.2 ヘッダをスライドさせる
ヘッダ領域をスライドさせ、同時にアイテム領域の列サイズを変更する処理です。これはまず、ヘッダに透明ブロックを設置します。スライドそのものは、その透明ブロックのドラッグ量を計算するだけです。そしてそれに合わせてアイテムの列サイズを変更してやるわけですが、アイテム一つ一つの幅を設定していたら、タダでさえ重いDOMの再計算処理なのでアイテム数が増えたときにシャレになりません。再計算を最小限にするためにはアイテムの親領域を横では無く、縦に区切るという技を使います。緑の部分のように列ごとにアイテムを収納するグループを作り、その中にアイテムを入れていきます。それによってリサイズは列グループのノードを変更するだけで終わりです。欠点は場所が列グループにまたがっているので、アイテムを行レベルで操作するときに面倒になることです。
3.3 スクロールバーの状態に応じた修正
横スクロール強制移動法を使っているアイテムは、縦スクロールバーが表示されたときに最後尾が引っかかって見えなくなります。スクロールバーが表示されたら位置を修正するという細かい調整が必要となります。
3.4 ヘッダクリックによるソート
クリックイベントを受け取ったらソートするだけなので大した処理ではありませんが、列に文字列と数値という型の概念を持たせておかないと、意図通りのソートになりません。
4.三分間クッキング的なノリで完成
こちらが出来上がりです。メソッドによるデータの追加やドラッグドロップも可能です。
ListViewが仮想Window化されているのは今回の本題では無いので気にしないでください。
実際に試したい場合は**こちら**のListViewSimpleを選択してください。
サンプル選択メニュー自体がListViewになっているという突っ込みは勘弁してください。選択したサンプルのソースが背面に出るようになっています。
これを作るのに必要なコードは以下のようになります。
import * as React from "react";
import {
JSWindow,
ListView,
ListHeaders,
ListHeader,
ListRow,
ListItem,
} from "@jswf/react";
export function ListViewSimple() {
let count = 1;
return (
<JSWindow width={600} title="ListViewSimple">
<ListView>
<ListHeaders>
<ListHeader type="number">No</ListHeader>
<ListHeader width={100}>武器の<br />名前</ListHeader>
<ListHeader type="number">攻撃力</ListHeader>
<ListHeader type="number">価格</ListHeader>
</ListHeaders>
<ListRow>
<ListItem>{count++}</ListItem>
<ListItem>竹槍</ListItem>
<ListItem>5</ListItem>
<ListItem>10</ListItem>
</ListRow>
<ListRow>
<ListItem>{count++}</ListItem>
<ListItem>銅の剣</ListItem>
<ListItem>18</ListItem>
<ListItem>120</ListItem>
</ListRow>
<ListRow>
<ListItem>{count++}</ListItem>
<ListItem>棍棒</ListItem>
<ListItem>10</ListItem>
<ListItem>40</ListItem>
</ListRow>
<ListRow>
<ListItem>{count++}</ListItem>
<ListItem>鉄の槍</ListItem>
<ListItem>30</ListItem>
<ListItem>380</ListItem>
</ListRow>
<ListRow>
<ListItem>{count++}</ListItem>
<ListItem>鉄の剣</ListItem>
<ListItem>40</ListItem>
<ListItem>700</ListItem>
</ListRow>
</ListView>
</JSWindow>
);
}
5.リンク
今回作ったコンポーネントはnpmに登録してあります
https://www.npmjs.com/package/@jswf/react
コンポーネントの使い方はこちら
https://ttis.croud.jp/?uuid=b292d429-dbad-49b5-8fed-6d268f4feaf0
コンポーネントのソースコード
https://github.com/JavaScript-WindowFramework/jswf-react
実際の動き
https://javascript-windowframework.github.io/jwf-react-sample01/dist/
サンプルソース
https://github.com/JavaScript-WindowFramework/jwf-react-sample01
6.まとめ
無事ListViewを作ることが出来ました。姉妹品で仮想DOMでデータが記述可能なTreeViewも作ってあります。この系統を作る時に役に立ったのは、あらゆるフレームワークを使用せずにオレオレパワーでフロントエンドのプログラムを組み続けてきたことです。フレームワークを使用しない素のJavaScriptでのDOM操作は、事前に徹底的にやっておいた方が良いと思います。
ということで基本コンポーネントの整備が進んだので、そろそろシステム作りをしようと思っています。