material-tableで日付や数値をセレクトリストでフィルタリングしてみる

2020.08.16

こんにちは、CX事業本部の若槻です。

今回は、以前の記事で作成したReact + Material-UI + material-tableのアプリで、テーブルの各データの日付や数値の値をもとにセレクトリストでフィルタリングをできるようにしてみます。

実現したいこと

管理者が販売商品の一覧をテーブルビューで確認できるページで、テーブルの商品を以下の条件でセレクトリストによりフィルタリング表示できるようにしたいです。

  • 予約のあり・なし(予約数01以上か)
  • 予約期限の超過・未超過(予約期限が現在日を過ぎているか否か)

スクリーンショット_2020-08-16_0_33_10.png

そこで上記の両フィルターをmaterial-tableのテーブルに対して実装してみます。

実装

セレクトリストの共通コンポーネント

まず今回実装したい両フィルターにて共通で使用するコンポーネントSelectList.tsxsrc/components/atoms配下に作成します。

SelectList.tsx

import React from 'react';
import MenuItem from '@material-ui/core/MenuItem';
import FormControl from '@material-ui/core/FormControl';
import Select from '@material-ui/core/Select';

const SelectList = (props: {
  columnDef: any;
  onFilterChanged: (rowId: string, filterValue: string) => void;
  items: [string, string][];
}) => {
  const { columnDef, onFilterChanged, items } = props;

  const handleChange = (event: React.ChangeEvent<{ value: unknown }>) => {
    onFilterChanged(columnDef.tableData.id, event.target.value as string);
  };

  return (
    <FormControl>
      <Select onChange={handleChange}>
        {items.map((item) => (
          <MenuItem value={item[0]}>{item[1]}</MenuItem>
        ))}
      </Select>
    </FormControl>
  );
};

export default SelectList;

上記コードでは、SelectListコンポーネントの引数としてmaterial-tableの仕様として渡されるcolumnDefonFilterChangedに加えて、items[string, string][]形式で渡し、

        {items.map((item) => (
          <MenuItem value={item[0]}>{item[1]}</MenuItem>
        ))}

上記箇所で<MenuItem>をMapして作成することにより、セレクトリストのメニュー項目をコンポーネントの呼び出し側で動的に指定できるようにしています。

予約のあり・なしによるフィルタリング

src/components/pages/ProductPage.tsxのコードを次のように変更します。

ProductPage.tsx

import React from 'react';
import MaterialTable from 'material-table';
import GenericTemplate from '../templates/GenericTemplate';
import { ArrowUpward } from '@material-ui/icons';
import * as colors from '@material-ui/core/colors';
import SelectList from '../atoms/SelectList';

const ProductPage: React.FC = () => {
  interface Product {
    itemName: string;
    price: number;
    reserveDuedate: string;
    reserveCount: number;
  }
  return (
    <GenericTemplate title={'商品ページ'}>
      <MaterialTable
        icons={{
          SortArrow: React.forwardRef((props, ref) => (
            <ArrowUpward
              {...props}
              ref={ref}
              style={{ color: colors.blue[800] }}
            />
          )),
        }}
        columns={[
          {
            title: '商品名',
            field: 'itemName',
            defaultSort: 'asc',
            filtering: false,
          },
          {
            title: '予約期限',
            field: 'reserveDuedate',
            filtering: false,
          },
          {
            title: '価格',
            field: 'price',
            type: 'numeric',
            filtering: false,
          },
          {
            title: '予約数',
            field: 'reserveCount',
            type: 'numeric',
            filterCellStyle: { textAlign: 'right' },
            filterComponent: (props) => (
              <SelectList
                columnDef={props.columnDef}
                onFilterChanged={props.onFilterChanged}
                items={[
                  ['all', 'すべて'],
                  ['reserved', '予約あり'],
                  ['notReserved', '予約なし'],
                ]}
              />
            ),
            customFilterAndSearch: (filterValue: string, rowData: Product) => {
              if (filterValue === 'reserved') {
                return rowData.reserveCount > 0;
              } else if (filterValue === 'notReserved') {
                return rowData.reserveCount === 0;
              }
              return true;
            },
          },
        ]}
        data={[
          {
            itemName: 'ペンライトセット',
            price: 20000,
            reserveDuedate: '2020/08/10',
            reserveCount: 20,
          },
          {
            itemName: 'パンフレット',
            price: 4000,
            reserveDuedate: '2020/09/15',
            reserveCount: 0,
          },
          {
            itemName: 'タオル',
            price: 3000,
            reserveDuedate: '2020/08/30',
            reserveCount: 5,
          },
          {
            itemName: 'Tシャツ',
            price: 4500,
            reserveDuedate: '2020/08/30',
            reserveCount: 10,
          },
        ]}
        options={{
          showTitle: false,
          filtering: true,
        }}
      />
    </GenericTemplate>
  );
};

export default ProductPage;

columnsのfilterComponentプロパティにSelectListコンポーネントを指定し、その引数itemsにメニューの要素としてvalue属性(allreservednotReserved)と値(すべて予約あり予約なし)の組の配列を渡して、予約数列で使用するセレクトリストを作成しています。

            filterComponent: (props) => (
              <SelectList
                columnDef={props.columnDef}
                onFilterChanged={props.onFilterChanged}
                items={[
                  ['all', 'すべて'],
                  ['reserved', '予約あり'],
                  ['notReserved', '予約なし'],
                ]}
              />
            ),

これにより下記のようなセレクトリストが予約数列で使用可能となります。

そして、フィルター条件をcolumnsのcustomFilterAndSearchプロパティにて定義します。セレクトリストで選択したメニューのvalue属性の値(allreservednotReservedのいずれか)がcustomFilterAndSearchの第一引数、テーブルの各行ごとのデータが第二引数となるので、2つの引数をもとにフィルター条件を定義します。ここでtrueが返された行のみがフィルター表示されます。

            customFilterAndSearch: (filterValue: string, rowData: Product) => {
              if (filterValue === 'reserved') {
                return rowData.reserveCount > 0;
              } else if (filterValue === 'notReserved') {
                return rowData.reserveCount === 0;
              }
              return true;
            },

予約数列にて予約のあり・なしによるフィルタリングが実装できました。操作時の画面は以下のようになります。

  • フィルター適用前 スクリーンショット 2020-08-16 3.20.54.png

  • セレクトリストで予約ありを選択 スクリーンショット 2020-08-16 3.22.49.png

  • セレクトリストで予約なしを選択 スクリーンショット 2020-08-16 3.24.23.png

  • セレクトリストですべてを選択 スクリーンショット 2020-08-16 3.27.08.png

予約期限の超過・未超過によるフィルタリング

日付の比較によるフィルタリングを可能とするためmomentをインストールします。

% npm install moment --save

src/components/pages/ProductPage.tsxのコードを次のように変更します。

ハイライトされた行が前項との変更部分(行追加)となります。

ProductPage.tsx

import React from 'react';
import MaterialTable from 'material-table';
import GenericTemplate from '../templates/GenericTemplate';
import { ArrowUpward } from '@material-ui/icons';
import * as colors from '@material-ui/core/colors';
import SelectList from '../atoms/SelectList';
import moment from 'moment';

const ProductPage: React.FC = () => {
  interface Product {
    itemName: string;
    price: number;
    reserveDuedate: string;
    reserveCount: number;
  }
  return (
    <GenericTemplate title={'商品ページ'}>
      <MaterialTable
        icons={{
          SortArrow: React.forwardRef((props, ref) => (
            <ArrowUpward
              {...props}
              ref={ref}
              style={{ color: colors.blue[800] }}
            />
          )),
        }}
        columns={[
          {
            title: '商品名',
            field: 'itemName',
            defaultSort: 'asc',
            filtering: false,
          },
          {
            title: '予約期限',
            field: 'reserveDuedate',
            filterComponent: (props) => (
              <SelectList
                columnDef={props.columnDef}
                onFilterChanged={props.onFilterChanged}
                items={[
                  ['all', 'すべて'],
                  ['notOverDue', '未超過'],
                  ['overDue', '超過'],
                ]}
              />
            ),
            customFilterAndSearch: (filterValue: string, rowData: Product) => {
              const jstNow = new Date().toLocaleString('ja', {});
              if (filterValue === 'notOverDue') {
                return moment(rowData.reserveDuedate).isSameOrAfter(jstNow);
              } else if (filterValue === 'overDue') {
                return moment(rowData.reserveDuedate).isBefore(jstNow);
              }
              return true;
            },
          },
          {
            title: '価格',
            field: 'price',
            type: 'numeric',
            filtering: false,
          },
          {
            title: '予約数',
            field: 'reserveCount',
            type: 'numeric',
            filterCellStyle: { textAlign: 'right' },
            filterComponent: (props) => (
              <SelectList
                columnDef={props.columnDef}
                onFilterChanged={props.onFilterChanged}
                items={[
                  ['all', 'すべて'],
                  ['reserved', '予約あり'],
                  ['notReserved', '予約なし'],
                ]}
              />
            ),
            customFilterAndSearch: (filterValue: string, rowData: Product) => {
              if (filterValue === 'reserved') {
                return rowData.reserveCount > 0;
              } else if (filterValue === 'notReserved') {
                return rowData.reserveCount === 0;
              }
              return true;
            },
          },
        ]}
        data={[
          {
            itemName: 'ペンライトセット',
            price: 20000,
            reserveDuedate: '2020/08/10',
            reserveCount: 20,
          },
          {
            itemName: 'パンフレット',
            price: 4000,
            reserveDuedate: '2020/09/15',
            reserveCount: 0,
          },
          {
            itemName: 'タオル',
            price: 3000,
            reserveDuedate: '2020/08/30',
            reserveCount: 5,
          },
          {
            itemName: 'Tシャツ',
            price: 4500,
            reserveDuedate: '2020/08/30',
            reserveCount: 10,
          },
        ]}
        options={{
          showTitle: false,
          filtering: true,
        }}
      />
    </GenericTemplate>
  );
};

export default ProductPage;

実装の仕方は前項の「予約のあり・なし」と同じです。

columnsのfilterComponentプロパティにSelectListコンポーネントを指定し、その引数itemsにメニューの要素を渡して、予約期限列で使用するセレクトリストを作成しています。

            filterComponent: (props) => (
              <SelectList
                columnDef={props.columnDef}
                onFilterChanged={props.onFilterChanged}
                items={[
                  ['all', 'すべて'],
                  ['notOverDue', '未超過'],
                  ['overDue', '超過'],
                ]}
              />
            ),

これにより下記のようなセレクトリストが予約期限列で使用可能となります。

そして、フィルター条件をcolumnsのcustomFilterAndSearchプロパティにて定義します。セレクトリストの選択状態および、各行ごとに日本時間の現在時刻とreserveDuedatemomentオブジェクトの比較によってフィルタリングをしています。

            customFilterAndSearch: (filterValue: string, rowData: Product) => {
              const jstNow = new Date().toLocaleString('ja', {});
              if (filterValue === 'notOverDue') {
                return moment(rowData.reserveDuedate).isSameOrAfter(jstNow);
              } else if (filterValue === 'overDue') {
                return moment(rowData.reserveDuedate).isBefore(jstNow);
              }
              return true;
            },

予約数列にて予約のあり・なしによるフィルタリングが実装できました。操作時の画面は以下のようになります。(執筆時点の日付は2020/08/16です。)

  • フィルター適用前 スクリーンショット 2020-08-16 4.07.36.png

  • セレクトリストで超過を選択 スクリーンショット 2020-08-16 4.13.33.png

  • セレクトリストで未超過を選択 スクリーンショット 2020-08-16 4.14.03.png

  • セレクトリストですべてを選択 スクリーンショット 2020-08-16 4.14.32.png

おわりに

React + Material-UI + material-tableのアプリで、テーブルの各データの日付や数値の値をもとにセレクトリストによるフィルタリングを実装してみました。

JavaScriptではなくTypeScriptによる実装であるため、Reactやmaterial-table特有の型を明示的に定義する必要があるのがスパルタな感じで大変でしたが、それによりコンポーネントの動作や渡す値の理解が明瞭となり鍛えられますね。

参考

以上