【Vue.js】Element Plusのtableにおけるスロットの動きをソース読んで超ざっくり理解してみた

### 【概要】

Element Plusの[Expandable row](https://element-plus.org/en-US/component/table.html#expandable-row)のサンプルコードを見てスロットを使用している`<template #default="props">`配下のデータがどのように渡されているのか気になりました。
（そもそもスロット自体の理解もあいまいだったので、スロットの勉強も含めて理解したいと思いました）
そこで実装コードを読んで超ざっくりと理解できたことを、超ざっくりと解説します。

### 【環境】
Element Plus 2.3.14

### 【説明用コード】

[こちらのコード](https://element-plus.run/#eyJzcmMvQXBwLnZ1ZSI6Ijx0ZW1wbGF0ZT5cbiAgPGVsLXRhYmxlIDpkYXRhPVwidGFibGVEYXRhXCIgc3R5bGU9XCJ3aWR0aDogMTAwJVwiPlxuICAgIDxlbC10YWJsZS1jb2x1bW4gdHlwZT1cImV4cGFuZFwiPiAgICBcbiAgICAgIDx0ZW1wbGF0ZSAjZGVmYXVsdD1cInByb3BzXCI+XG4gICAgICAgIDxkaXYgbT1cIjRcIj5cbiAgICAgICAgICA8cCBtPVwidC0wIGItMlwiPuW5tOm9ojoge3sgcHJvcHMucm93LmFnZSB9fTwvcD5cbiAgICAgICAgICA8cCBtPVwidC0wIGItMlwiPuaAp+WIpToge3sgcHJvcHMucm93LmdlbmRlciB9fTwvcD5cbiAgICAgICAgPC9kaXY+XG4gICAgICA8L3RlbXBsYXRlPlxuICAgIDwvZWwtdGFibGUtY29sdW1uPlxuICAgIDxlbC10YWJsZS1jb2x1bW4gbGFiZWw9XCLjg6bjg7zjgrZpZFwiIHByb3A9XCJpZFwiIC8+XG4gICAgPGVsLXRhYmxlLWNvbHVtbiBsYWJlbD1cIuWQjeWJjVwiIHByb3A9XCJuYW1lXCIgLz5cbiAgPC9lbC10YWJsZT5cbjwvdGVtcGxhdGU+XG5cbjxzY3JpcHQgbGFuZz1cInRzXCIgc2V0dXA+XG5jb25zdCB0YWJsZURhdGEgPSBbXG4gIHtcbiAgICBpZDogMSxcbiAgICBuYW1lOiAn5L2Q6JekJyxcbiAgICBhZ2U6IDIwLFxuICAgIGdlbmRlcjogJ+eUtycsXG4gIH0sXG5dXG48L3NjcmlwdD5cbiIsImltcG9ydC1tYXAuanNvbiI6IntcbiAgXCJpbXBvcnRzXCI6IHt9XG59IiwidHNjb25maWcuanNvbiI6IntcbiAgXCJjb21waWxlck9wdGlvbnNcIjoge1xuICAgIFwiYWxsb3dKc1wiOiB0cnVlLFxuICAgIFwiY2hlY2tKc1wiOiB0cnVlLFxuICAgIFwianN4XCI6IFwicHJlc2VydmVcIixcbiAgICBcInRhcmdldFwiOiBcIkVTTmV4dFwiLFxuICAgIFwibW9kdWxlXCI6IFwiRVNOZXh0XCIsXG4gICAgXCJtb2R1bGVSZXNvbHV0aW9uXCI6IFwiQnVuZGxlclwiLFxuICAgIFwiYWxsb3dJbXBvcnRpbmdUc0V4dGVuc2lvbnNcIjogdHJ1ZSxcbiAgICBcInR5cGVzXCI6IFtcImVsZW1lbnQtcGx1cy9nbG9iYWwuZC50c1wiXVxuICB9LFxuICBcInZ1ZUNvbXBpbGVyT3B0aW9uc1wiOiB7XG4gICAgXCJ0YXJnZXRcIjogMy4zXG4gIH1cbn1cbiIsIl9vIjp7fX0=)をもとに説明していきます。

<details>
  <summary>上記リンク先にある同じコード</summary>

```vue
<template>
  <el-table :data="tableData">
    <el-table-column type="expand">    
      <template #default="props">
        <div>
          <p>年齢: {{ props.row.age }}</p>
          <p>性別: {{ props.row.gender }}</p>
        </div>
      </template>
    </el-table-column>
    <el-table-column label="ユーザid" prop="id" />
    <el-table-column label="名前" prop="name" />
  </el-table>
</template>

<script lang="ts" setup>
const tableData = [
  {
    id: 1,
    name: '佐藤',
    age: 20,
    gender: '男',
  },
]
</script>
```
</details>


### 【ローカル開発手順】

https://element-plus.org/en-US/guide/dev-guide.html#bootstrap-project

（VSCodeだと`pnpm`コマンドを使うには、PowerShellではなくGit Bashを利用する必要がありました）

### 【コード解説】

コードは必要箇所を抜粋しています。

#### 1. el-table

まず前提として`el-table`や`el-table-column`というタグはvueのコンポーネントです。
とりあえず一番の親である`el-table`のコードを見てみます。
`<slot />`があるので、ここに`el-table-column`配下の要素が入りそうです。
しかし、レンダリングされた画面ではこの`<slot />`で宣言された箇所には要素が入っていません。
では、どこにレンダリングされるのでしょうか？

```vue:packages\components\table\src\table.vue
      <div ref="hiddenColumns" class="hidden-columns">
        <slot />
      </div>
```

`<slot />`で宣言された箇所には中身のない`div`が3つあるだけです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/121997/5f14b587-c547-3c71-62fc-2fa06af66413.png)

年齢・性別の拡張表示要素は`tbody`配下にレンダリングされているのが確認できます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/121997/19fd2804-381b-0798-5284-5d540ce71cb5.png)


`tbody`を表示しているのは下記箇所です。

```vue:packages\components\table\src\table.vue
            <table-body
              :context="context"
              :highlight="highlightCurrentRow"
              :row-class-name="rowClassName"
              :tooltip-effect="tooltipEffect"
              :tooltip-options="tooltipOptions"
              :row-style="rowStyle"
              :store="store"
              :stripe="stripe"
            />
```

#### 2. table-body

`table-body`の実装を見ると、`render`関数で`wrappedRowRender`の結果を返しています。
では、`wrappedRowRender`の定義を確認してみましょう。

```ts:packages\components\table\src\table-body\index.ts
  render() {
    const { wrappedRowRender, store } = this
    const data = store.states.data.value || []
    // Why do we need tabIndex: -1 ?
    // If you set the tabindex attribute on an element ,
    // then its child content cannot be scrolled with the arrow keys,
    // unless you set tabindex on the content too
    // See https://github.com/facebook/react/issues/25462#issuecomment-1274775248 or https://developer.mozilla.org/zh-CN/docs/Web/HTML/Global_attributes/tabindex
    return h('tbody', { tabIndex: -1 }, [
      data.reduce((acc: VNode[], row) => {
        return acc.concat(wrappedRowRender(row, acc.length))
      }, []),
    ])
  },
```

`useRender`の結果を使っています。
では、`useRender`の定義を確認してみましょう。

```ts:packages\components\table\src\table-body\index.ts
    const { wrappedRowRender, tooltipContent, tooltipTrigger } =
      useRender(props)
```

`useRender`から`wrappedRowRender`の定義を抜粋したものです。
`renderExpanded`での結果を返しています。
では、`renderExpanded`の定義を確認してみましょう。

```ts:packages\components\table\src\table-body\render-helper.ts
  const wrappedRowRender = (row: T, $index: number) => {
    const store = props.store
    const { isRowExpanded, assertRowKey } = store
    const { treeData, lazyTreeNodeMap, childrenColumnName, rowKey } =
      store.states
    const columns = store.states.columns.value
    const hasExpandColumn = columns.some(({ type }) => type === 'expand')
    if (hasExpandColumn) {
      const expanded = isRowExpanded(row)
      const tr = rowRender(row, $index, undefined, expanded)
      const renderExpanded = parent.renderExpanded
      if (expanded) {
        if (!renderExpanded) {
          console.error('[Element Error]renderExpanded is required.')
          return tr
        }
        // 使用二维数组，避免修改 $index
        // Use a matrix to avoid modifying $index
        return [
          [
            tr,
            h(
              'tr',
              {
                key: `expanded-row__${tr.key as string}`,
              },
              [
                h(
                  'td',
                  {
                    colspan: columns.length,
                    class: `${ns.e('cell')} ${ns.e('expanded-cell')}`,
                  },
                  [renderExpanded({ row, $index, store, expanded })]
                ),
              ]
            ),
          ],
        ]
      }
```

#### 3. table-column

`slots.default`がある場合に`slots.default(data)`を返しています。

```ts:packages\components\table\src\table-column\render-helper.ts
    if (column.type === 'expand') {
      // 对于展开行，renderCell 不允许配置的。在上一步中已经设置过，这里需要简单封装一下。
      column.renderCell = (data) =>
        h(
          'div',
          {
            class: 'cell',
          },
          [originRenderCell(data)]
        )
      owner.value.renderExpanded = (data) => {
        return slots.default ? slots.default(data) : slots.default
      }
    } else {
```

`owner.value.renderExpanded = (data) `の`data`について`console.log(data.row)`を使ってデータを確認してみると、このようになりました。
あとは、`slots.default(data)`がこのデータをtemplateタグ内の`props.row.age`とかに紐づけているわけですね。

```json
{
    "id": 1,
    "name": "佐藤",
    "age": 20,
    "gender": "男"
}
```

### 【参考サイト】

https://ja.vuejs.org/guide/components/slots.html