MUIのDataGridにPHPでデータを渡して使ってみる


普段はjQueryのDataTablesなどを使っていて、Reactはまだ慣れないのですが、とりあえず何とか動くところまで行ったのでまとめてみます。

DataGridに渡す要素とロード処理をカスタムフックに(useGrid)

  • DataGridに渡すプロパティのuseStateなどをここでまとめて処理
  • リモートサーバからデータを取ってくる機能(fnLoadDataGrid)もここに実装
  • fnLoadDataGrid関数にpage/pageSize/sortModel/filterModelを渡す
import { useState, useCallback } from "react";
import { GridRowsProp, GridSortModel, GridFilterModel } from "@mui/x-data-grid";
import dataLoader from "./dataLoader";

//  grid用のカスタムフック
type TypeProps = {
  urlGrid: string;
  defaultPageSize?: number;
};
const useGrid = ({ urlGrid, defaultPageSize = 10 }: TypeProps) => {
  //  DataGrid関係のuseState群
  const [page, onPageChange] = useState<number>(0);
  const [pageSize, onPageSizeChange] = useState<number>(defaultPageSize);
  const [sortModel, setSortModel] = useState<GridSortModel>([
    { field: "id", sort: "asc" }
  ]);
  const [filterModel, setFilterModel] = useState<GridFilterModel | undefined>();
  const [rows, setRows] = useState<GridRowsProp>([]);
  const [rowCount, setRowCount] = useState<number>(0);
  const [loading, setLoading] = useState<boolean>(false);

  //  フィルター変更機能
  const onFilterModelChange = useCallback(
    (filterModel: GridFilterModel) => {
      setFilterModel(filterModel);
    },
    [setFilterModel]
  );

  //  ソート変更機能
  const onSortModelChange = useCallback((newSortModel: GridSortModel) => {
    setSortModel(newSortModel);
  }, []);

  //  データローディング関数
  const fnLoadDataGrid = useCallback(async () => {
    setLoading(true);
    const res = await dataLoader(
      urlGrid,
      page,
      pageSize,
      sortModel,
      filterModel
    );
    setLoading(false);

    if (res.success) {
      setRows(res.data);
      setRowCount(Number(res.total));
    } else {
      //  エラー処理
    }
  }, [
    urlGrid,
    page,
    pageSize,
    sortModel,
    setRows,
    setRowCount,
    filterModel,
    setLoading
  ]);

  return {
    fnLoadDataGrid,
    page,
    onPageChange,
    pageSize,
    onPageSizeChange,
    sortModel,
    setSortModel,
    filterModel,
    setFilterModel,
    rows,
    setRows,
    rowCount,
    setRowCount,
    loading,
    setLoading,
    onSortModelChange,
    onFilterModelChange
  };
};
export default useGrid;

axiosでリモートサーバからデータ取得(dataLoader)

  • sortModelとfilterModelはそのまま投げる
  • PHP側で$_POSTとかで受ける場合は URLSearchParams() を使って
  • 戻り値の処理はリモートサーバの実装次第
import axios from "axios";
import { AxiosResponse } from "axios";
import { GridSortModel, GridFilterModel } from "@mui/x-data-grid";

const dataLoader = async (
  urlGrid: string,
  page: number,
  pageSize: number,
  sortModel: GridSortModel,
  filterModel: GridFilterModel | undefined
) => {
  //console.log("load data");
  //  awaitでaxiosでリモートからデータ呼び出して戻す
  //  本当はここでpostでsortModelとかfilterModelとかを渡す
  return await axios
    .get(urlGrid, {
        page: page,
        pageSize: pageSize,
        sortModel: sortModel,
        filterModel: filterModel
    })
    .then((res: AxiosResponse) => {
      const d = res.data;
      return d;
    })
    .catch((err: any) => {
      //  エラー処理
      console.log("error");
      console.log(err);
    });
};
export default dataLoader;

サーバ側(PHP)の実装

  • なんかMySQLのitemテーブルみたいなのがある前提で
  • page/pageSizeでLIMIT句、sortModelでORDER句、filterModelでWHERE句を作成
  • npm startなどでローカルサーバから試す場合CORSエラーにならないように設定が必要

    // axiosからjson投げたリクエストを取得
    $json = file_get_contents('php://input');
    $p = json_decode($json);


    $paging = "";
    $ordering = "";
    $where = array();

    //  paging処理
    if (isset($p->page) && $p->pageSize) {
        $start = $p->page * $p->pageSize;
        $paging = " LIMIT {$start},{$p->pageSize}";
    }
    //  sortModel処理
    if (is_array($p->sortModel) && count($p->sortModel)) {
        $sorts = array();
        foreach ($p->sortModel as $sm) {
            $sorts[] = "{$sm->field} {$sm->sort}";
        }
        $ordering = " ORDER BY " . implode(",", $sorts);
    }
    //  filter処理
    if (isset($p->filterModel)) {
        //  AND/OR・・・通常使わないかな・・・
        $linkOp = $p->filterModel->linkOperator;

        //  filterModel.itemsをループして検索条件配列作成
        foreach ($p->filterModel->items as $f) {
            if ($f->operatorValue == 'equals' && $f->value) {
                $where[] = "{$f->columnField} = '{$f->value}'";
            } elseif ($f->operatorValue == 'startsWith' && $f->value) {
                $where[] = "{$f->columnField} like '{$f->value}%'";
            } elseif ($f->operatorValue == 'endsWith' && $f->value) {
                $where[] = "{$f->columnField} like '%{$f->value}'";
            } elseif ($f->operatorValue == 'isEmpty') {
                $where[] = "{$f->columnField} = ''";
            } elseif ($f->operatorValue == 'isNotEmpty') {
                $where[] = "{$f->columnField} != ''";
            } elseif ($f->value) {
                $where[] = "{$f->columnField} like '%{$f->value}%'";
            }
        }
    }
    
    if (count($where)) {
        $where = " WHERE " . implode(" AND ", $where);
    }else{
        $where = "";
    }
    
    // 件数
    $sql = "SELECT COUNT(*) AS cnt FROM item {$where}";
    $total = $pdo->query($sql)->fetchColumn();
    // データ取得
    $sql = "SELECT id, item_name, item_size, item_price
	    FROM item {$where} {$ordering} {$paging}";
    $data = $pdo->query($sql)->fetchAll();
    
  print(json_encode(array("success"=>true, "data"=>$data, "total"=>$total)));

DataGridをカスタム(DataGridWrapper)

  • DataGridをカスタムコンポーネントにしておく(xxxMode系は全部server)
  • 最初こっちにDataLoaderを埋め込むようにしてuseImperativehandleで親コンポーネントに戻すようにしてたけど、後々で使いづらかったのでカスタムフック側に移動した。
  • {...gridApi}のところは何をセットしたのか忘れがちなので1行ずつ書いたほうが良いかも
  • 本当はCustomToolbarに「新規登録ボタン」とかを設定する
import {
  DataGrid,
  GridColDef,
  GridToolbarColumnsButton,
  GridToolbarContainer,
  GridToolbarFilterButton
} from "@mui/x-data-grid";
import { Button } from "@mui/material";
import RefreshIcon from "@mui/icons-material/Refresh";

//  GridToolbarにReloadボタンを追加
const CustomToolbar = ({ fnLoadDataGrid }: { fnLoadDataGrid: any }) => {
  return (
    <GridToolbarContainer>
      <Button onClick={fnLoadDataGrid}>
        <RefreshIcon fontSize="small" sx={{ ml: "-2px", mr: 1 }} />
        Reload
      </Button>
      <GridToolbarColumnsButton />
      <GridToolbarFilterButton />
    </GridToolbarContainer>
  );
};

/* DataGridのPropsのtype定義 */
type TypeDataGridWrapper = {
  columns: GridColDef[];
  fnLoadDataGrid: any;
  gridApi: any;
  [key: string]: any;
};

/* DataGridの拡張コンポーネント */
const DataGridWrapper = ({
  columns,
  fnLoadDataGrid,
  gridApi,
  ...rest
}: TypeDataGridWrapper) => {
  return (
    <>
      <DataGrid
        components={{
          Toolbar: CustomToolbar
        }}
        componentsProps={{
          toolbar: {
            fnLoadDataGrid: fnLoadDataGrid
          }
        }}
        columns={columns}
        pagination
        rowsPerPageOptions={[5, 10, 20]}
        paginationMode="server"
        sortingMode="server"
        filterMode="server"
        /*
        rows={gridApi.rows}
        pageSize={gridApi.pageSize}
        loading={gridApi.loading}
        sortModel={gridApi.sortModel}
        onSortModelChange={gridApi.onSortModelChange}
        rowCount={gridApi.rowCount}
        onCellEditCommit={gridApi.handleCellEditCommit}
        onPageChange={gridApi.onPageChange}
        onPageSizeChange={gridApi.onPageSizeChange}
        onFilterModelChange={gridApi.onFilterModelChange}
        */
        {...gridApi}
        {...rest}
      />
    </>
  );
};
export default DataGridWrapper;

App

  • 上で作っったuseGridとDataGridWrapperを使ってページ作成
  • ココでcolumnsの設定とリモートサーバのURLを設定
  • 実際はgetActionsの辺りに編集フォームなどを埋め込みます
import { useMemo, useEffect } from "react";
import { IconButton, Box, CssBaseline } from "@mui/material";
import { GridColDef } from "@mui/x-data-grid";
import EditIcon from "@mui/icons-material/Edit";
import DeleteIcon from "@mui/icons-material/Delete";
import useGrid from "./useGrid";
import DataGridWrapper from "./DataGridWrapper";

//  メインのコンポーネント
export default function App() {
  //  Grid用のカスタムフック-取得先のURLはここでセット
  const urlGrid = 'grid_items.php';
  const { fnLoadDataGrid, ...gridApi } = useGrid({ urlGrid: urlGrid });

  //  初期データローディング
  useEffect(() => {
    fnLoadDataGrid();
  }, [fnLoadDataGrid]);

  //  DataGridに渡すグリッドカラムの定義
  const columns: GridColDef[] = useMemo(
    () => [
      { field: "id", headerName: "id" },
      { field: "item_name", headerName: "商品名", flex: 1 },
      { field: "item_size", headerName: "サイズ", width: 150 },
      { field: "item_price", headerName: "単価" },
      {
        field: "actions",
        type: "actions",
        getActions: (params: any) => {
          return [
            <IconButton>
              <EditIcon fontSize="small" />
            </IconButton>,
            <IconButton>
              <DeleteIcon fontSize="small" />
            </IconButton>
          ];
        }
      }
    ],
    []
  );

  //  *** RETURN ***
  return (
    <>
      <CssBaseline />
      <Box sx={{ height: "100vh" }}>
        <DataGridWrapper
          sx={{ flexGrow: 1 }}
          columns={columns}
          fnLoadDataGrid={fnLoadDataGrid}
          gridApi={gridApi}
          initialState={{
            columns: {
              columnVisibilityModel: {
                id: false
              }
            }
          }}
        />
      </Box>
    </>
  );
}

ハマった所

  • id列を隠すためにcolumns定義でhide: trueと設定していて・・・initialStateで設定しなきゃダメだったんですね(ちゃんと公式に書いてました

サンプル

  • 肝心なデータ渡すところ、typicodeに置き換えてるのでソートとか出来ませんが