material-table のフィルターをテーブルの外側に設置する

はじめに

テントの中から失礼します、CX事業本部のてんとタカハシです!

Material-UI のテーブルを基に作られた material-table のフィルター位置をカスタマイズする方法を紹介していこうと思います。通常テーブルの内側に設置されるフィルターですが、これをテーブルの外側に設置しようと思います。

尚、material-table のフィルターに関する概要については、下記の記事に記載していますので、そちらをご参照して頂ければと思います。

material-table で日付範囲の指定によるフィルタリングがしたい

また、全体のソースコードを下記のリポジトリに置いていますので、併せて参考にして頂ければ幸いです。

GitHub - iam326/material-table-filter-outside-the-table

環境

$ sw_vers
ProductName:    Mac OS X
ProductVersion: 10.15.6
BuildVersion:   19G2021

$ node --version
v12.18.2

$ yarn --version
1.22.4

$ yarn list --depth=0
@material-ui/core@4.11.0
material-table@1.68.0
react@16.13.1
react-dom@16.13.1
typescript@3.7.5

標準のフィルター位置

material-table では、下記の通り、テーブルの内側にフィルターが設置されます。また、フィルター位置を変更するオプションは用意されていません。

ただ、デザインの都合でテーブルの外側にフィルターを設置したいケースがあったりします。例えば、フィルターをページの一番上に並べて表示したいなんて要望があった場合、このままじゃあ困りますね。これを何とかしてカスタマイズしていこうと思います。

フィルター位置のカスタマイズ

カスタマイズ後

下記の通り、テーブルの外側にフィルターが設置されます。

フィルターに「日用品」と入力することで、クーポン名に「日用品」を含むデータだけが表示されるようになります。

実装

const MainContent: React.FC = () => {
  const classes = useStyles();
  const [filterValue, setFilterValue] = useState<string>('');
  return (
    <>
      <Paper className={classes.filters}>
        <form
          className={classes.form}
          noValidate
          autoComplete="off"
          onSubmit={(e) => e.preventDefault()}
        >
          <div>
            <TextField
              id="filter-coupon-name-text-field"
              label="クーポン名"
              value={filterValue}
              InputProps={{
                startAdornment: (
                  <InputAdornment position="start">
                    <SearchIcon />
                  </InputAdornment>
                ),
              }}
              onChange={(e) => setFilterValue(e.target.value)}
            />
          </div>
        </form>
      </Paper>
      <MaterialTable
        options={{
          filtering: false,
          search: false,
          draggable: false,
          toolbar: false,
          showTitle: false,
        }}
        columns={[
          {
            title: 'クーポン ID',
            field: 'couponId',
          },
          { title: '店舗', field: 'storeName' },
          {
            title: 'クーポン名',
            field: 'couponName',
            defaultFilter: [],
            customFilterAndSearch: (
              _: string,
              rowData: { couponName: string }
            ) =>
              filterValue.length === 0 ||
              rowData.couponName.indexOf(filterValue) > -1,
          },
          {
            title: '割引率',
            field: 'discountRate',
            cellStyle: { textAlign: 'right' },
          },
          {
            title: '利用開始日',
            field: 'startDate',
            cellStyle: { textAlign: 'right' },
          },
          {
            title: '利用終了日',
            field: 'endDate',
            cellStyle: { textAlign: 'right' },
          },
        ]}
        data={tableData}
      />
    </>
  );
};

実装のポイント

自前でフィルターを用意する

ここでは、Material-UI の TextField コンポーネントを使用して自前のフィルターを実装しています。値が入力される度に、filterValueが更新されるようにしています。

<TextField
  id="filter-coupon-name-text-field"
  label="クーポン名"
  value={filterValue}
  InputProps={{
    startAdornment: (
      <InputAdornment position="start">
        <SearchIcon />
      </InputAdornment>
    ),
  }}
  onChange={(e) => setFilterValue(e.target.value)} // 入力される度に filterValue を更新する ★
/>

material-table 標準のフィルターを非表示にする

material-table のフィルタリング機能を OFF にして、標準のフィルターを非表示にします。デフォルトで filtering: false となりますが、ここでは明示的に false を指定しています。

<MaterialTable
  options={{
    filtering: false, // 標準のフィルターを非表示にする ★
    search: false,
    draggable: false,
    toolbar: false,
    showTitle: false,
  }}
  ...

フィルタリング処理の実装

フィルタリング処理は、customFilterAndSearch に実装します。customFilterAndSearch は列ごとの設定に組み込むことができる関数です。通常、material-table の標準フィルターに入力があると、customFilterAndSearch の第一引数に入力値が渡され、その値を使用してフィルタリングの条件を組み込みます。今回は、自前で用意したフィルターに入力された値を使用してフィルタリングの条件を組むため、第一引数の値は使用しません。

<MaterialTable
  ...
  columns={[
    ...
    {
      title: 'クーポン名',
      field: 'couponName',
      defaultFilter: [],
      customFilterAndSearch: ( // フィルタリングの実装 ★
        _: string,
        rowData: { couponName: string }
      ) =>
        filterValue.length === 0 ||  // 第一引数は使用せずに、自前のフィルターに入力された後を使用する ★
        rowData.couponName.indexOf(filterValue) > -1,
    },
    ...

フィルターのデフォルト値に、条件式で true となる値を渡す

material-table では、列ごとの設定にフィルターのデフォルト値を渡すことができます。ここで defaultFilter に条件式で true となる値を渡してあげる必要があります。ここの例では、空配列を渡していますが、1でも'A'でも何でも構いません。

<MaterialTable
  ...
  columns={[
    ...
    {
      title: 'クーポン名',
      field: 'couponName',
      defaultFilter: [], // 条件式で true になる値を渡す
      customFilterAndSearch: (
      ...

実際には、material-table のフィルターを使用しないため、ここで渡すデフォルト値が、フィルタリングを実施する際に使用されることはありません。しかし、defaultValue の値に条件式で true になる値を渡してあげないと、customFilterAndSearch が呼ばれなくなってしまいます。

これについては、material-table の実装を見てみると理解ができます。

GitHub - material-table - /src/utils/data-manager.js#L605

filterData = () => {
  this.searched = this.grouped = this.treefied = this.sorted = this.paged = false;

  this.filteredData = [...this.data];

  if (this.applyFilters) {
    this.columns
      .filter((columnDef) => columnDef.tableData.filterValue) // filterValue が true になる列だけ ★
      .forEach((columnDef) => {
        const { lookup, type, tableData } = columnDef;
        if (columnDef.customFilterAndSearch) {
          this.filteredData = this.filteredData.filter(
            (row) =>
              !!columnDef.customFilterAndSearch( // customFilterAndSearch を呼ぶ ★
                tableData.filterValue,
                row,
                columnDef
              )
          );
        } else {
          ...

material-table では、コンポーネントの描画が走る度に、上記の filterData が呼び出されます。実装を見てみると、列の数だけループを回して、フィルター値(columnDef.tableData.filterValue)が true である列のみ、customFilterAndSearch を呼び出しています。ここで、columnDef.tableData.filterValue には、列の設定として渡していた defaultFilter の値が初期値として入っています。

これも material-table の実装を見ると分かります。

GitHub - material-table - /src/utils/data-manager.js#L71

setColumns(columns) {
  const undefinedWidthColumns = columns.filter(
    (c) => c.width === undefined && !c.hidden
  );
  let usedWidth = ["0px"];

  this.columns = columns.map((columnDef, index) => {
    columnDef.tableData = {
      columnOrder: index,
      filterValue: columnDef.defaultFilter, // defaultFilter が初期値になる ★
      ...

defaultValue の値を設定するのは、customFilterAndSearch を呼べるようにするためだけなので、値自体は true にさえなれば何でもOKです。

おわりに

今回は、material-table のフィルターをテーブルの外側に設置する方法について紹介しました。いかがだったでしょうか。

material-table の機能として用意されていないカスタマイズでしたが、既存の機能と、material-table の実装を考慮することで、今回の用件を実現することができました。アプリの機能やデザインが洗練されていくにつれ、こうしたフレームワークに対してカスタマイズを施していく作業は辛いものだったりしますが、実装を丸々入れ替えるのもこれまた辛い話なので、こうしたカスタマイズで事なきを得れると大変嬉しいものですね。他にも実現できたカスタマイズがあれば、どんどん紹介していきたいと思います。

今回は以上になります。最後まで読んで頂きありがとうございました!