業務アプリ向けUIコンポーネント
Wijmoには業務システム向けのUIコンポーネントが豊富に用意されています。ここでは、本サンプルアプリケーションで使用する4つのコンポーネントを紹介します。ダウンロードサンプルでは、それぞれ以下のパスで実装を確認できます。
-
FlexGrid(商品一覧):
src/components/wijmo/ProductsGrid.tsx -
MultiRow(受発注入力):
src/components/wijmo/OrdersMultiRow.tsx -
TreeView(商品分類):
src/components/wijmo/CategoriesTreeView.tsx -
Accordion(管理メニュー):
src/components/wijmo/AdminAccordion.tsx
FlexGridはExcelライクな操作感を持つデータグリッド、MultiRowは1レコードを複数行で表現する伝票形式のグリッド、TreeViewは階層構造のツリー表示、Accordionは折りたたみ可能なパネルUIです。各コンポーネントの詳しいプロパティ設定やカスタマイズについては、次回以降Firestoreとの連携を進める中で随時解説します。
FlexGrid:商品一覧
FlexGridは、Excelライクな操作感を持つデータグリッドです。ソート、フィルタ、インライン編集といった機能を標準で備えています。データソースにはWijmoのCollectionViewを使用します。CollectionViewは配列データをラップし、ソート・フィルタ・変更追跡といった機能をグリッドに提供するオブジェクトです。
リスト4では、useEffect内でCollectionViewのインスタンス(cv)を生成し、setView(cv)でReactのstate変数viewにセットしています。FlexGridのitemsSourceプロパティにはこのviewをバインドすることで、Reactのライフサイクルに沿ったデータ管理が実現できます(リスト4)。
import { useState, useEffect } from "react";
import { FlexGrid, FlexGridColumn } from "@mescius/wijmo.react.grid";
import { FlexGridFilter } from "@mescius/wijmo.react.grid.filter";
import { CollectionView } from "@mescius/wijmo";
const [view, setView] = useState<CollectionView | null>(null);
useEffect(() => {
// CollectionViewでデータソースを作成
const cv = new CollectionView(products, {
trackChanges: true, // データの変更を追跡
});
setView(cv); // state変数viewにセット
}, [products]);
…中略…
<FlexGrid
itemsSource={view} // CollectionViewをバインド
alternatingRowStep={1} // 1行おきに背景色を変える
…中略…
>
{/* フィルタ機能を追加 */}
<FlexGridFilter />
{/* カラム定義 */}
<FlexGridColumn binding="code" header="商品コード" width={100} />
{/* widthに*を指定することで、残り幅を自動調整 */}
<FlexGridColumn binding="name" header="商品名" width="*" />
…中略…
{/* 金額バインディングのとき、format属性で通貨形式を指定できる */}
<FlexGridColumn binding="price" header="単価" format="c0" />
…中略…
</FlexGrid>
FlexGridFilterを子コンポーネントとして配置するだけで、カラムヘッダーにフィルタ機能が追加されます。bindingプロパティでデータソースのフィールド名を、formatプロパティで表示形式を指定します(図1)。
図1のように、各カラムヘッダーにフィルタアイコンが表示されます。カラムヘッダーをクリックするとソートが、フィルタアイコンをクリックすると条件指定による絞り込みが可能です。これらの機能は追加のコードなしで利用できます。
図2は、商品名カラムのフィルタアイコンをクリックした際に表示されるフィルタダイアログの例です。値フィルタと条件フィルタの2種類が用意されており、チェックボックスの切り替えや条件式の指定で直感的にデータを絞り込めます。
図2:FlexGridのフィルタダイアログ
FlexGridでは、DataMapを使ってセルにコンボボックス形式のカスタムエディタを設定できます。DataMapはキーと表示名のマッピングを提供するオブジェクトで、FlexGridColumnのdataMapプロパティに指定します。これにより、ユーザーはドロップダウンから値を選択でき、内部的にはキー値(ID)が保持されます(リスト5)。
import { DataMap } from "@mescius/wijmo.grid";
// カテゴリマスタからDataMapを作成(idをキー、nameを表示名に)
const categoryMap = useMemo(
() => new DataMap(categories, "id", "name"),
[categories]
);
// 文字列配列を渡すだけでもDataMapとして機能する
const unitOptions = ["個", "箱", "本", "袋", "缶", "切", "冊", "台", "セット", "100g", "kg"];
…中略…
{/* dataMap属性でコンボボックスエディタを設定 */}
<FlexGridColumn binding="categoryId" header="分類" width={120} dataMap={categoryMap} />
<FlexGridColumn binding="unit" header="単位" width={80} dataMap={unitOptions} />
分類列のセルをクリックすると、図3のようにカテゴリ一覧がドロップダウンで表示されます。DataMapによりIDと表示名が自動変換されるため、表示用の変換処理を別途書く必要がありません。
図3:FlexGridのカスタムエディタ(コンボボックス)
FlexGridのカスタムエディタには、コンボボックスの他にもInputMaskによるマスク入力があります。取引先一覧(PartnersGrid.tsx)の電話番号列を例に見てみましょう(リスト6)。
import { InputMask } from "@mescius/wijmo.input";
// InputMaskエディタを作成(数字3桁-4桁-4桁の電話番号マスク)
useEffect(() => {
const editor = new InputMask(document.createElement("div"), {
mask: "000-0000-0000", // 0は数字1桁、ハイフンはリテラル文字
});
setPhoneEditor(editor);
return () => editor.dispose();
}, []);
…中略…
<FlexGridColumn binding="phone" header="電話番号" width={130} editor={phoneEditor} />
InputMaskのmaskプロパティで0は数字1桁を表し、ハイフンはリテラル文字として自動挿入されます。InputMaskもInputDateと同様に@mescius/wijmo.inputパッケージに含まれるコントロールです。
図4:FlexGridのカスタムエディタ(マスク入力)
MultiRow:受発注入力
MultiRowは、1レコードを複数行で表示するコンポーネントです。受発注データのように項目が多い場合に有効です。layoutDefinitionで行のレイアウトを定義します。各グループのcolspanプロパティで、そのグループが何列分の幅を占めるかを指定します。グループ内のcells配列に定義したセルは、colspanの列数に収まるように自動的に複数行に折り返されます。個々のセルにもcolspanを指定でき、備考欄のように複数列にまたがるセルを作れます(リスト7)。
const layoutDefinition = [
{
header: "伝票情報", // グループヘッダー
colspan: 3, // このグループは3列分の幅
cells: [ // 3列×2行に自動配置される
{ binding: "orderNumber", header: "伝票番号", width: 120 },
{ binding: "type", header: "区分", width: 70 },
{ binding: "status", header: "ステータス", width: 90 },
{ binding: "orderDate", header: "発注日", width: 100, format: "yyyy/M/d" },
{ binding: "deliveryDate", header: "納期", width: 100, format: "yyyy/M/d" },
// colspanで2列分の幅を指定
{ binding: "notes", header: "備考", width: 150, colspan: 2 },
],
},
{
header: "取引先・金額",
colspan: 2, // このグループは2列分の幅
cells: [
{ binding: "partnerName", header: "取引先名", width: 180 },
{ binding: "itemCount", header: "明細数", width: 70, align: "right" },
{ binding: "totalAmount", header: "合計金額", width: 120, format: "c0", align: "right" },
],
},
];
…中略…
<MultiRow
itemsSource={view}
layoutDefinition={layoutDefinition} // 複数行レイアウトを適用
…中略…
/>
1レコードが2行で表示され、伝票情報と金額情報が見やすく配置されます(図5)。
通常のグリッドでは横スクロールが必要になる項目数でも、MultiRowなら1画面に収まります。業務システムでよくある「伝票形式」の表示に最適です。
MultiRowでも、FlexGridと同様にDataMapを使ったコンボボックスや、InputDateを使ったカレンダーピッカーをカスタムエディタとして設定できます。layoutDefinitionの各セル定義にdataMapやeditorプロパティを追加するだけで、セルごとに適切な入力UIを提供できます(リスト8)。
import { DataMap } from "@mescius/wijmo.grid";
import { InputDate } from "@mescius/wijmo.input";
// 区分・ステータスのDataMapを定義
const typeDataMap = useMemo(() => new DataMap(
[{ key: "purchase", name: "発注" }, { key: "sales", name: "受注" }],
"key", "name"
), []);
const statusDataMap = useMemo(() => new DataMap([
{ key: "draft", name: "下書き" }, { key: "confirmed", name: "確定" },
{ key: "shipped", name: "出荷済" }, { key: "completed", name: "完了" },
{ key: "cancelled", name: "キャンセル" },
], "key", "name"), []);
// InputDateエディタを作成
useEffect(() => {
const editor = new InputDate(document.createElement("div"), {
format: "yyyy/M/d",
});
setDateEditor(editor);
return () => editor.dispose();
}, []);
// layoutDefinitionのセルにdataMapとeditorを設定
{ binding: "type", header: "区分", width: 70, dataMap: typeDataMap },
{ binding: "status", header: "ステータス", width: 90, dataMap: statusDataMap },
{ binding: "orderDate", header: "発注日", width: 100, format: "yyyy/M/d", editor: dateEditor },
{ binding: "deliveryDate", header: "納期", width: 100, format: "yyyy/M/d", editor: dateEditor },
DataMapにより、内部値("purchase"や"draft")が自動的に表示名(「発注」「下書き」)に変換されるため、表示用のデータ変換処理が不要になります。また、日付列ではカレンダーピッカーから直感的に日付を選択できます(図6)。
TreeView:商品分類
TreeViewは、階層構造のデータを表示するコンポーネントです。まず、表示するデータの構造を確認しましょう。商品分類データは、以下のように親子関係を持つ再帰的なツリー構造で定義されています(リスト9)。
// CategoryTree型: Categoryを継承し、items?: CategoryTree[] を追加した再帰構造
const mockCategoryTree: CategoryTree[] = [
{
id: "cat1",
name: "食品",
…中略…
items: [
{
id: "cat1-1",
name: "生鮮食品",
…中略…
items: [
{ id: "cat1-1-1", name: "野菜", …中略… },
{ id: "cat1-1-2", name: "果物", …中略… },
…中略…
],
},
{ id: "cat1-2", name: "加工食品", …中略…, items: [ …中略… ] },
{ id: "cat1-3", name: "飲料", …中略…, items: [ …中略… ] },
],
},
{ id: "cat2", name: "日用品", …中略…, items: [ …中略… ] },
{ id: "cat3", name: "事務用品", …中略…, items: [ …中略… ] },
];
このツリーデータをTreeViewのdisplayMemberPathとchildItemsPathで表示します。displayMemberPathで表示する項目名(ここではname)、childItemsPathで子要素の配列(ここではitems)を指定します(リスト10)。
<TreeView
itemsSource={categories}
displayMemberPath="name" // 表示するプロパティ名
childItemsPath="items" // 子要素の配列プロパティ名
selectedItemChanged={handleSelectedItemChanged} // 選択変更時のコールバック
…中略…
isAnimated={true} // 展開/折りたたみをアニメーション
/>
階層構造がツリー形式で表示され、クリックで展開・折りたたみができます。図7の左側の表示がTreeViewです。
商品分類のような親子関係を持つデータを、視覚的に分かりやすく表示できます。分類を選択したときに呼び出されるhandleSelectedItemChangedコールバックの実装は以下の通りです(リスト11)。
const [selectedCategory, setSelectedCategory] = useState<CategoryTree | null>(null);
const handleSelectedItemChanged = (sender: TreeView) => {
const item = sender.selectedItem as CategoryTree | null;
setSelectedCategory(item);
};
sender.selectedItemで現在選択されているアイテムを取得し、Reactのstateに保存しています。これにより、図7の右側パネルのように選択した分類の詳細情報(分類ID、分類名、子分類数)を表示できます。このパターンは、マスタ選択UIとして幅広く活用できます。
Accordion:マスタ管理メニュー
Accordionは、折りたたみ可能なメニューを作成するコンポーネントです。まず、メニューデータの構造を見てみましょう(リスト12)。
interface MenuItem {
label: string;
href: string;
description: string;
}
interface MenuSection {
title: string;
icon: string;
items: MenuItem[];
}
const menuSections: MenuSection[] = [
{
title: "マスタ管理",
icon: "📁",
items: [
{ label: "商品マスタ", href: "/products", description: "商品の登録・編集・削除" },
{ label: "取引先マスタ", href: "/partners", description: "取引先の登録・編集・削除" },
{ label: "商品分類マスタ", href: "/categories", description: "商品カテゴリの階層管理" },
],
},
…中略… // 受発注管理、帳票出力、システム設定の3セクションが続く
];
allowExpandManyをfalseにすると、一度に開けるパネルを1つに制限できます(リスト13)。
<Accordion
allowExpandMany={false} // 同時に1つのパネルのみ展開可能
isAnimated={true}
>
{menuSections.map((section) => (
<AccordionPane key={section.title}>
<div>{section.title}</div> {/* パネルヘッダー */}
<ul>{/* メニュー項目 */}</ul> {/* パネルコンテンツ */}
</AccordionPane>
))}
</Accordion>
セクションをクリックすると、該当するメニューが展開されます(図8)。
マスタ管理、受発注管理、帳票出力といった機能カテゴリをAccordionで整理することで、多機能なシステムでもナビゲーションが煩雑になりません。

