Magento2 管理画面のグリッド作成におけるUIComponentファイルの書き方

#はじめに
この記事は [Magento Advent Calendar 2018](https://adventar.org/calendars/3176) の13日目です。 

今回は、Magentoモジュールにおける管理画面のグリッドを作成する方法の一つ、UIComponentの書き方について、備忘録も兼ねて簡単に解説していこうと思います。

また私が初めてグリッドを作成した際、[こちらの質問](https://magento.stackexchange.com/questions/97291/explanation-of-ui-component-grid-in-magento-2/97306)が大いに参考になりました。なので今回の記事もこれに準じていますが、コードは私が実際に書いたものを基にしているので幾分か差があります。

#グリッドについて
まず始めにグリッドがどのような部品でできているのか画像で見てみましょう。 
![図1.png](https://qiita-image-store.s3.amazonaws.com/0/322197/2408ec3a-438c-9ef9-6dc4-0f230c2ca426.png)
これから書くコードをコピペすれば大体こんな感じのグリッドになります。（ちなみに上部の`Grid`というタイトル表示はControllerで変更できます。）
これらの要素をUIComponentで記述していくことになります。 

#定義
今回のグリッドで扱うモジュール名やデータベース名は以下のものとします。 
適宜自分のものと読み替えてください。 

<dl>
<dt>ベンダー名</dt>
<dd>Company</dd> 
<dt>モジュール名</dt>
<dd>Module</dd> 
<dt>テーブル名</dt>
<dd>table</dd> 
<dt>カラム１</dt>
<dd>table_id</dd> 
<dd>→主キー、int型、 auto_increment</dd>
<dt>カラム２</dt>
<dd>table_name </dd>
<dd>→varchar(255) </dd>
</dl>
 
#書き方
layoutファイルでUIComponentファイルを定義します。
このファイルは`app/code/Company/Module/adminhtml/view/layout/`に配置します。

```XML:app/code/Company/Module/adminhtml/view/layout/module_table_index.xml
<?xml version="1.0"?>

<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd">
    <body>
        <referenceContainer name="content">
            <uiComponent name="module_table_list"/>
        </referenceContainer>
    </body>
</page>
```
この場合、uiComponentノードで`"module_table_list"`と指定されているので、UIComponentファイルの名前は`module_table_list.xml`となります。
UIComponentファイルは`app/code/Company/Module/adminhtml/view/ui_component/`に配置します。

UIComponentファイルは一見複雑そうに見えますが、実際には下記のシンプルな構造になっています。

- \<listing>
    - \<argument>
    - \<dataSource>
    - \<container>
    - \<columns>
   
作りたいグリッドによって、これらのノードが増えたり減ったりします。特に\<container>の子ノードは不要なものを省くことができます。実際にコードを書くときは量が多いのでコピペしてリプレースする方が楽でしょう。コード内で出てくる`module`や`table`といった部分は適宜自分のモジュール名やテーブル名に置き換えてください。ではノードごとにコードを分けて解説していきます。

###listingノード
```XML:app/code/Company/Module/adminhtml/view/ui_component/module_table_list.xml
<?xml version="1.0" encoding="UTF-8"?>

<listing xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Ui/etc/ui_configuration.xsd">
<!--この中に以下のノードが入ります。-->
</listing>
```
最上位のノードです。

###argumentノード
```XML:app/code/Company/Module/adminhtml/view/ui_component/module_table_list.xml
    <argument name="data" xsi:type="array">
        <item name="js_config" xsi:type="array">
            <item name="provider" xsi:type="string">module_table_list.module_table_listing_data_source</item>
            <item name="deps" xsi:type="string">module_table_list.module_table_listing_data_source</item>
        </item>
        <item name="spinner" xsi:type="string">module_table_columns</item>
        <item name="buttons" xsi:type="array">
            <item name="add" xsi:type="array">
                <item name="name" xsi:type="string">add</item>
                <item name="label" xsi:type="string" translate="true">Add New Data</item>
                <item name="class" xsi:type="string">primary</item>
                <item name="url" xsi:type="string">*/*/new</item>
            </item>
        </item>
    </argument>
```

- \<argument>にはモジュールに関する基本的なデータを記述します。
- js_config/providerとdepsノードは`"{ファイル名}.{データソース名}"`とします。`{データソース名}`はわかりやすいように`"{モジュール名}_{テーブル名}_listing_data_source"`としています。（実際に`{データソース名}`を定義する箇所は次のノードです。）
- spinnerノードはわかりやすいように`"{モジュール名}_{テーブル名}_columns"`としています。（後から出てきます。）
- これ以降のコードで`module_table_list`等が出てきた場合、それは`"{ファイル名}"`などと同じです。
- グリッド上部のボタンを設定する\<button>には、レコードを追加するボタンを設定しています。buttons/add/urlノードでボタンを押した後のURLを変更できるので、Controllerでデータを追加する処理を記述しています。また`label`はボタン上に書かれる文字なので、役割に合わせて変更しましょう。

###dataSourceノード
```XML:app/code/Company/Module/adminhtml/view/ui_component/module_table_list.xml
    <dataSource name="module_table_listing_data_source">
        <argument name="dataProvider" xsi:type="configurableObject">
            <argument name="class" xsi:type="string">Magento\Framework\View\Element\UiComponent\DataProvider\DataProvider</argument>
            <argument name="name" xsi:type="string">module_table_listing_data_source</argument>
            <argument name="primaryFieldName" xsi:type="string">table_id</argument>
            <argument name="requestFieldName" xsi:type="string">table_id</argument>
            <argument name="data" xsi:type="array">
                <item name="config" xsi:type="array">
                    <item name="update_url" xsi:type="url" path="mui/index/render"/>
                    <item name="storageConfig" xsi:type="array">
                        <item name="indexField" xsi:type="string">table_id</item>
                    </item>
                </item>
            </argument>
        </argument>
        <argument name="data" xsi:type="array">
            <item name="js_config" xsi:type="array">
                <item name="component" xsi:type="string">Magento_Ui/js/grid/provider</item>
            </item>
        </argument>
    </dataSource>
```

- \<dataSource>内の`name`には`{データソース名}`を入れましょう。これ以降のコードでも頻繁に出てきます。
- 一つ目の大きな\<argument>はグリッドに表示されるデータを準備する役割を持つクラスを定義します。
    - `class`は`di.xml`で定義される名前と同様にしなければなりません。
    - `primaryFieldName`はデータベースの主キーと、`requestFieldName`HTTPリクエスト変数と関連していて、両者は同じでよい場合がほとんどです。
- 二つ目の大きな\<argument>はグリッドのAjaxを担うJavascriptファイルを参照します。デフォルトでは`Magento/Ui/view/base/web/js/grid/provider.js`となっています。

###containerノード
```XML:app/code/Company/Module/adminhtml/view/ui_component/module_table_list.xml
    <container name="listing_top">
    <!--この中に以下のノードが入ります。-->
    </container>
```
最上位のノードです。`name`の`listing_top`はこれ以降のコードでたびたび出てきます。機能によって以下の子ノードに分かれます。

- \<argument>
- \<bookmark>
- \<component>
- \<filtersearch>
- \<filters>
- \<massaction>
- \<paging>

一気にすべて書くと長くなるのでノードごとにコードを区切って解説します。
####argumentノード
```XML:app/code/Company/Module/adminhtml/view/ui_component/module_table_list.xml
        <argument name="data" xsi:type="array">
            <item name="config" xsi:type="array">
                <item name="template" xsi:type="string">ui/grid/toolbar</item>
            </item>
        </argument>
```
レイアウトとすべてのアクションを処理するテンプレートファイルを定義するノードです。デフォルトでは`Magento/Ui/view/base/web/templates/grid/toolbar.html`となっています。
####bookmarkノード
```XML:app/code/Company/Module/adminhtml/view/ui_component/module_table_list.xml

        <bookmark name="bookmarks"/>
```
ブックマーク機能を追加するノードです。`name`の`bookmarks`はこれ以降のコードで少しだけ出てきます。
####componentノード
```XML:app/code/Company/Module/adminhtml/view/ui_component/module_table_list.xml
        <component name="columns_controls">
            <argument name="data" xsi:type="array">
                <item name="config" xsi:type="array">
                    <item name="columnsData" xsi:type="array">
                        <item name="provider" xsi:type="string">module_table_list.module_table_list.module_table_columns</item>
                    </item>
                    <item name="component" xsi:type="string">Magento_Ui/js/grid/controls/columns</item>
                    <item name="displayArea" xsi:type="string">dataGridActions</item>
                </item>
            </argument>
        </component>
```

- `component`はグリッドを表示するJavascriptファイルを参照していて、デフォルトでは`Magento/Ui/view/base/web/templates/grid/controls/columns.html`というテンプレートを使っている`Magento/Ui/view/base/web/js/grid/controls/columns.js`を指定しています。
- `displayArea`はcontainer/argument/config/templateノードで定義されたテンプレートファイル内の`getRegion('dataGridActions')`を参照しています。

####filterSearchノード
```XML:app/code/Company/Module/adminhtml/view/ui_component/module_table_list.xml
        <filterSearch name="fulltext">
            <argument name="data" xsi:type="array">
                <item name="config" xsi:type="array">
                    <item name="provider" xsi:type="string">module_table_list.module_table_listing_data_source</item>
                    <item name="chipsProvider" xsi:type="string">module_table_list.module_table_list.listing_top.listing_filters_chips</item>
                    <item name="storageConfig" xsi:type="array">
                        <item name="provider" xsi:type="string">module_table_list.module_table_list.listing_top.bookmarks</item>
                        <item name="namespace" xsi:type="string">current.search</item>
                    </item>
                </item>
            </argument>
        </filterSearch>
```
このノードはデータのテキスト検索機能をページに追加します。わかりやすく言うと"Search by keyword"のやつです。この検索機能はInstallSchema.phpでインデックスを作成しないと機能しないようです。
####filtersノード
```XML:app/code/Company/Module/adminhtml/view/ui_component/module_table_list.xml
        <filters name="listing_filters">
            <argument name="data" xsi:type="array">
                <item name="config" xsi:type="array">
                    <item name="columnsProvider" xsi:type="string">module_table_list.module_table_list.module_table_columns</item>
                    <item name="storageConfig" xsi:type="array">
                        <item name="provider" xsi:type="string">module_table_list.module_table_list.listing_top.bookmarks</item>
                        <item name="namespace" xsi:type="string">current.filters</item>
                    </item>
                    <item name="templates" xsi:type="array">
                        <item name="filters" xsi:type="array">
                            <item name="select" xsi:type="array">
                                <item name="component" xsi:type="string">Magento_Ui/js/form/element/ui-select</item>
                                <item name="template" xsi:type="string">ui/grid/filters/elements/ui-select</item>
                            </item>
                        </item>
                    </item>
                    <item name="childDefaults" xsi:type="array">
                        <item name="provider" xsi:type="string">module_table_list.module_table_list.listing_top.listing_filters</item>
                        <item name="imports" xsi:type="array">
                            <item name="visible" xsi:type="string">module_table_list.module_table_list.module_table_columns.${ $.index }:visible</item>
                        </item>
                    </item>
                </item>
                <item name="observers" xsi:type="array">
                    <item name="column" xsi:type="string">column</item>
                </item>
            </argument>
        </filters>
```

- このノードはフィルターボタンを定義しています。
- `templates`では特定のフィルタオプションをレンダリングするために使用されるファイルを定義することができます。この場合`select`のみが定義され`Magento/Ui/view/base/web/js/form/element/ui-select.js`と` Magento/Ui/view/base/web/templates/grid/filters/elements/ui-select.html`が指定されています。
- 今回は`select`のみがデフォルトをオーバーライドするために定義されていますが、\<filter>ノードや他のノードのデフォルト値は`Magento/Ui/view/base/ui_component/etc/definition.xml`で定義されています。

####massactionノード
```XML:app/code/Company/Module/adminhtml/view/ui_component/module_table_list.xml
        <massaction name="listing_massaction">
            <argument name="data" xsi:type="array">
                <item name="config" xsi:type="array">
                    <item name="selectProvider" xsi:type="string">module_table_list.module_table_list.module_table_columns.ids</item>
                    <item name="component" xsi:type="string">Magento_Ui/js/grid/tree-massactions</item>
                    <item name="indexField" xsi:type="string">table_id</item>
                </item>
            </argument>
            <action name="delete">
                <argument name="data" xsi:type="array">
                    <item name="config" xsi:type="array">
                        <item name="type" xsi:type="string">delete</item>
                        <item name="label" xsi:type="string" translate="true">Delete</item>
                        <item name="url" xsi:type="url" path="*/*/massDelete"/>
                        <item name="confirm" xsi:type="array">
                            <item name="title" xsi:type="string" translate="true">Delete</item>
                            <item name="message" xsi:type="string" translate="true">Do you want to delete selected row record?</item>
                        </item>
                    </item>
                </argument>
            </action>
        </massaction>
```

- 選択したデータに対して一括で削除などを行う機能を追加するノードです。`name`はユニークでなければなりません。
- \<argument>は基本的な情報を記述します。
- \<action>は実際に行う処理を指定しますが、今回は削除機能のみ搭載させています。
- 他の機能は`vender/magento/module-ui/view/base/ui_component/etc/definition.xml`に書いてあるので適宜参照してください。

####pagingノード
```XML:app/code/Company/Module/adminhtml/view/ui_component/module_table_list.xml
        <paging name="listing_paging">
            <argument name="data" xsi:type="array">
                <item name="config" xsi:type="array">
                    <item name="storageConfig" xsi:type="array">
                        <item name="provider" xsi:type="string">module_table_list.module_table_list.listing_top.bookmarks</item>
                        <item name="namespace" xsi:type="string">current.paging</item>
                    </item>
                    <item name="selectProvider" xsi:type="string">module_table_list.module_table_list.module_table_columns.ids</item>
                </item>
            </argument>
        </paging>
```
一ページ当たりの列の表示量選択などの機能を追加するノードです。
以上で長かった\<container>ノードは終了です。
###columnsノード
```XML:app/code/Company/Module/adminhtml/view/ui_component/module_table_list.xml
    <columns name="module_table_columns">
        <selectionsColumn name="ids">
            <argument name="data" xsi:type="array">
                <item name="config" xsi:type="array">
                    <item name="indexField" xsi:type="string">table_id</item>
                    <item name="sortOrder" xsi:type="number">0</item>
                </item>
            </argument>
        </selectionsColumn>
        <column name="table_id">
            <argument name="data" xsi:type="array">
                <item name="config" xsi:type="array">
                    <item name="filter" xsi:type="string">text</item>
                    <item name="label" xsi:type="string" translate="true">Table ID</item>
                    <item name="sortOrder" xsi:type="number">10</item>
                </item>
            </argument>
        </column>
        <column name="table_name">
            <argument name="data" xsi:type="array">
                <item name="config" xsi:type="array">
                    <item name="filter" xsi:type="string">text</item>
                    <item name="label" xsi:type="string" translate="true">Table Name</item>
                    <item name="sortOrder" xsi:type="number">20</item>
                </item>
            </argument>
        </column>
        <actionsColumn name="actions" class="Company\Module\Ui\Component\Listing\Table\Column\Action">
            <argument name="data" xsi:type="array">
                <item name="config" xsi:type="array">
                    <item name="resizeEnabled" xsi:type="boolean">true</item>
                    <item name="resizeDefaultWidth" xsi:type="string">100</item>
                    <item name="indexField" xsi:type="string">table_id</item>
                    <item name="sortOrder" xsi:type="number">30</item>
                </item>
            </argument>
        </actionsColumn>
    </columns>
```

- \<selectionsColumn>は\<massaction>のためにデータを選択するチェックボックスのカラムです。
- \<column>は実際のデータを表示するカラムです。
    - \<name>には表示させたいテーブルのカラム名を入れましょう。（テーブルの指定自体はResourceModelの定義の段階で済んでいるはずです）
    - `filter`は\<filter>で追加したフィルター機能の中で、カラムごとにどのようにフィルタリングさせるか指定できます。`text`の場合はテキスト形式のフィルタリングができるようになります。
    - `label`はグリッドに表示するカラムの名前です。必ずしも実際のテーブルのカラム名と同様にする必要はありません。
- \<actionsColumn>は一つ一つのデータに対して削除や編集機能の処理をするリンクを表示するカラムを追加します。実際にどのような処理を追加するかはUIComponentではなくControllerで記述するためここでは省略します。

#さいごに
このファイルの書き方についてより詳しく解説する開発者ガイドのようなものがなかなか見つけられないので書きました。私にもわかっていない部分が多いし間違いもあるかもしれませんが、これを読んだ方にとって少しでも参考になれば幸いです。