SHOEISHA iD

※旧SEメンバーシップ会員の方は、同じ登録情報(メールアドレス&パスワード)でログインいただけます

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

ComponentZine(ActiveReports)byメシウス(AD)

Firebase × Next.js × Wijmo × ActiveReportsJSでつくる、現場が自走できる業務システム

業務アプリ向け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)。

[リスト4]src/components/wijmo/ProductsGrid.tsx(抜粋)
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:FlexGridによる商品一覧画面
図1:FlexGridによる商品一覧画面

 図1のように、各カラムヘッダーにフィルタアイコンが表示されます。カラムヘッダーをクリックするとソートが、フィルタアイコンをクリックすると条件指定による絞り込みが可能です。これらの機能は追加のコードなしで利用できます。

 図2は、商品名カラムのフィルタアイコンをクリックした際に表示されるフィルタダイアログの例です。値フィルタと条件フィルタの2種類が用意されており、チェックボックスの切り替えや条件式の指定で直感的にデータを絞り込めます。

図2:FlexGridのフィルタダイアログ

図2:FlexGridのフィルタダイアログ

 FlexGridでは、DataMapを使ってセルにコンボボックス形式のカスタムエディタを設定できます。DataMapはキーと表示名のマッピングを提供するオブジェクトで、FlexGridColumndataMapプロパティに指定します。これにより、ユーザーはドロップダウンから値を選択でき、内部的にはキー値(ID)が保持されます(リスト5)。

[リスト5]src/components/wijmo/ProductsGrid.tsx(カスタムエディタ設定抜粋)
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のカスタムエディタ(コンボボックス)

図3:FlexGridのカスタムエディタ(コンボボックス)

 FlexGridのカスタムエディタには、コンボボックスの他にもInputMaskによるマスク入力があります。取引先一覧(PartnersGrid.tsx)の電話番号列を例に見てみましょう(リスト6)。

[リスト6]src/components/wijmo/PartnersGrid.tsx(マスクエディタ設定抜粋)
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} />

 InputMaskmaskプロパティで0は数字1桁を表し、ハイフンはリテラル文字として自動挿入されます。InputMaskInputDateと同様に@mescius/wijmo.inputパッケージに含まれるコントロールです。

図4:FlexGridのカスタムエディタ(マスク入力)

図4:FlexGridのカスタムエディタ(マスク入力)

MultiRow:受発注入力

 MultiRowは、1レコードを複数行で表示するコンポーネントです。受発注データのように項目が多い場合に有効です。layoutDefinitionで行のレイアウトを定義します。各グループのcolspanプロパティで、そのグループが何列分の幅を占めるかを指定します。グループ内のcells配列に定義したセルは、colspanの列数に収まるように自動的に複数行に折り返されます。個々のセルにもcolspanを指定でき、備考欄のように複数列にまたがるセルを作れます(リスト7)。

[リスト7]src/components/wijmo/OrdersMultiRow.tsx(抜粋)
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)。

図5:MultiRowによる受発注入力画面
図5:MultiRowによる受発注入力画面

 通常のグリッドでは横スクロールが必要になる項目数でも、MultiRowなら1画面に収まります。業務システムでよくある「伝票形式」の表示に最適です。

 MultiRowでも、FlexGridと同様にDataMapを使ったコンボボックスや、InputDateを使ったカレンダーピッカーをカスタムエディタとして設定できます。layoutDefinitionの各セル定義にdataMapeditorプロパティを追加するだけで、セルごとに適切な入力UIを提供できます(リスト8)。

[リスト8]src/components/wijmo/OrdersMultiRow.tsx(カスタムエディタ設定抜粋)
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)。

図6:MultiRowのカスタムエディタ(カレンダーピッカー)
図6:MultiRowのカスタムエディタ(カレンダーピッカー)

TreeView:商品分類

 TreeViewは、階層構造のデータを表示するコンポーネントです。まず、表示するデータの構造を確認しましょう。商品分類データは、以下のように親子関係を持つ再帰的なツリー構造で定義されています(リスト9)。

[リスト9]src/lib/mockData.ts(商品分類データ抜粋)
// 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: [ …中略… ] },
];

 このツリーデータをTreeViewdisplayMemberPathchildItemsPathで表示します。displayMemberPathで表示する項目名(ここではname)、childItemsPathで子要素の配列(ここではitems)を指定します(リスト10)。

[リスト10]src/components/wijmo/CategoriesTreeView.tsx(抜粋)
<TreeView
  itemsSource={categories}
  displayMemberPath="name"                    // 表示するプロパティ名
  childItemsPath="items"                      // 子要素の配列プロパティ名
  selectedItemChanged={handleSelectedItemChanged}  // 選択変更時のコールバック
  …中略…
  isAnimated={true}                           // 展開/折りたたみをアニメーション
/>

 階層構造がツリー形式で表示され、クリックで展開・折りたたみができます。図7の左側の表示がTreeViewです。

図7:TreeViewによる商品分類画面
図7:TreeViewによる商品分類画面

 商品分類のような親子関係を持つデータを、視覚的に分かりやすく表示できます。分類を選択したときに呼び出されるhandleSelectedItemChangedコールバックの実装は以下の通りです(リスト11)。

[リスト11]src/components/wijmo/CategoriesTreeView.tsx(コールバック抜粋)
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)。

[リスト12]src/components/wijmo/AdminAccordion.tsx(メニューデータ抜粋)
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セクションが続く
];

 allowExpandManyfalseにすると、一度に開けるパネルを1つに制限できます(リスト13)。

[リスト13]src/components/wijmo/AdminAccordion.tsx(抜粋)
<Accordion
  allowExpandMany={false}  // 同時に1つのパネルのみ展開可能
  isAnimated={true}
>
  {menuSections.map((section) => (
    <AccordionPane key={section.title}>
      <div>{section.title}</div>     {/* パネルヘッダー */}
      <ul>{/* メニュー項目 */}</ul>  {/* パネルコンテンツ */}
    </AccordionPane>
  ))}
</Accordion>

 セクションをクリックすると、該当するメニューが展開されます(図8)。

図8:Accordionによるマスタ管理メニュー
図8:Accordionによるマスタ管理メニュー

 マスタ管理、受発注管理、帳票出力といった機能カテゴリをAccordionで整理することで、多機能なシステムでもナビゲーションが煩雑になりません。

次のページ
ActiveReportsJS Viewerの組み込み

この記事は参考になりましたか?

ComponentZine(ActiveReports)byメシウス連載記事一覧

もっと読む

この記事の著者

WINGSプロジェクト 中川 幸哉(ナカガワ ユキヤ)

WINGSプロジェクトについて>有限会社 WINGSプロジェクトが運営する、テクニカル執筆コミュニティ(代表 山田祥寛)。主にWeb開発分野の書籍/記事執筆、翻訳、講演等を幅広く手がける。2018年11月時点での登録メンバは55名で、現在も執筆メンバを募集中。興味のある方は、どしどし応募頂きたい。著書記事多数。 RSS X: @WingsPro_info(公式)、@WingsPro_info/wings(メンバーリスト) Facebook

※プロフィールは、執筆時点、または直近の記事の寄稿時点での内容です

山田 祥寛(ヤマダ ヨシヒロ)

静岡県榛原町生まれ。一橋大学経済学部卒業後、NECにてシステム企画業務に携わるが、2003年4月に念願かなってフリーライターに転身。Microsoft MVP for Visual Studio and Development Technologies。執筆コミュニティ「WINGSプロジェクト」代表。主な著書に「独習シリーズ(Java・C#・Python・PHP・Ruby・JSP&サーブレットなど)」「速習シリーズ(ASP.NET Core・Vue.js・React・TypeScript・ECMAScript、Laravelなど)」「改訂3版JavaScript本格入門」「これからはじめるReact実践入門」「はじめてのAndroidアプリ開発 Kotlin編 」他、著書多数

※プロフィールは、執筆時点、または直近の記事の寄稿時点での内容です

提供:メシウス株式会社

【AD】本記事の内容は記事掲載開始時点のものです 企画・制作 株式会社翔泳社

この記事は参考になりましたか?

この記事をシェア

CodeZine(コードジン)
https://codezine.jp/article/detail/23423 2026/03/30 12:00

おすすめ

アクセスランキング

アクセスランキング

イベント

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

新規会員登録無料のご案内

  • ・全ての過去記事が閲覧できます
  • ・会員限定メルマガを受信できます

メールバックナンバー

アクセスランキング

アクセスランキング