Vue2 コンポーネントのラッパーコンポーネントの作り方

# この記事でわかること

- Vue2 のスロットの使い方
- Vue2 コンポーネントのラッパーコンポーネントの作り方

# 結論

### ラッパーコンポーネントの作り方

```VueComponentWrapper.vue
<template>
  <vue-component
    v-bind="$attrs"
    v-on="$listeners"
  >
    <template v-slot:default>
      <slot name="default"></slot>
    </template>
    <template
      v-for="(_, name) in $scopedSlots"
      v-slot:[name]="slotProps"
    >
      <slot v-bind:name="name" v-bind="slotProps"></slot>
    </template>
  </vue-component>
</template>
```

または、デフォルトスロット名を省略して

```VueComponentWrapper.vue
<template>
  <vue-component
    v-bind="$attrs"
    v-on="$listeners"
  >
    <template v-slot>
      <slot></slot>
    </template>
    <template
      v-for="(_, name) in $scopedSlots"
      v-slot:[name]="slotProps"
    >
      <slot v-bind:name="name" v-bind="slotProps"></slot>
    </template>
  </vue-component>
</template>
```

または、デフォルトスロットのテンプレートを省略して

```VueComponentWrapper.vue
<template>
  <vue-component
    v-bind="$attrs"
    v-on="$listeners"
  >
    <slot></slot>
    <template
      v-for="(_, name) in $scopedSlots"
      v-slot:[name]="slotProps"
    >
      <slot v-bind:name="name" v-bind="slotProps"></slot>
    </template>
  </vue-component>
</template>
```

### 特定のスロットのデフォルト値をバインドする

```VueComponentWrapperWithBindingSlotDefault.vue
<template>
  <vue-component
    v-bind="$attrs"
    v-on="$listeners"
  >
    <slot></slot>
    <template v-slot:some-binded-slot>
      <slot name="some-binded-slot">BINDED_DEFAULT</slot>
    </template>
    <template
      v-for="(_, name) in $scopedSlots"
      v-slot:[name]="slotProps"
    >
      <slot v-bind:name="name" v-bind="slotProps"></slot>
    </template>
  </vue-component>
</template>
```

### スロットに静的なコンテンツを付加する

```VueComponentWrapperWithStaticContent.vue
<template>
  <vue-component
    v-bind="$attrs"
    v-on="$listeners"
  >
    STATIC_CONTENT_FOR_DEFAULT_SLOT
    <slot></slot>
    <template
      v-for="(_, name) in $scopedSlots"
      v-slot:[name]="slotProps"
    >
      STATIC_CONTENT_FOR_NAMED_SLOT
      <slot v-bind:name="name" v-bind="slotProps"></slot>
    </template>
  </vue-component>
</template>
```

# 詳細

## Vue のスロットの使い方

Vue には、あるコンポーネントの持つスロットに対して、外部からコンポーネントや DOM 要素を流し込む機能がある。例えば、以下のようなコンポーネントがある場合、

```MyComponent.vue
<template>
  <div
    v-bind="$attrs"
    v-on="$listeners"
  >
    DEFAULT:<slot>MY_COMPONENT_DEFAULT</slot>
    NAMED:<slot name="named">MY_COMPONENT_DEFAULT</slot>
    NAMED_WITH_ARGS:<slot name="named-with-args" x="1">MY_COMPONENT_DEFAULT</slot>
  </div>
</template>
```

このコンポーネントの外部から、以下のような実装でコードを流し込むことができる。

```Main.vue
<template>
  <my-component>
  </my-component>

  <my-component>
    <h1>MAIN</h1>
    <template v-slot:named>MAIN</template>
    <template v-slot:named-with-args="{ x }">MAIN({{ x }})</template>
    <h2>MAIN</h2>
  </my-component>
</template>
```

これをコンパイルすると、以下のような HTML になる。

```main.html
<div>
  DEFAULT:MY_COMPONENT_DEFAULT
  NAMED:MY_COMPONENT_DEFAULT
  NAMED_WITH_ARGS:MY_COMPONENT_DEFAULT
</div>

<div>
  DEFAULT:<h1>MAIN</h1>
  <h2>MAIN</h2>
  NAMED:MAIN
  NAMED_WITH_ARGS:MAIN(1)
</div>
```

## ラッパーコンポーネントを作りスロットのデフォルト値をバインドする

このコンポーネントのラッパーコンポーネント `MyComponentWrapper.vue` を作り、`named` スロットのデフォルト値に `MY_COMPONENT_WRAPPER_DEFAULT` をバインドしたい。つまり、

```Main.vue
<template>
  <my-component>
  </my-component>

  <my-component-wrapper>
  </my-component-wrapper>

  <my-component-wrapper>
    <h1>MAIN</h1>
    <template v-slot:named>MAIN</template>
    <template v-slot:named-with-args="{ x }">MAIN({{ x }})</template>
    <h2>MAIN</h2>
  </my-component-wrapper>
</template>
```

これをコンパイルして以下を得られるようにしたい。

```main.html
<div>
  DEFAULT:MY_COMPONENT_DEFAULT
  NAMED:MY_COMPONENT_DEFAULT
  NAMED_WITH_ARGS:MY_COMPONENT_DEFAULT
</div>

<div>
  DEFAULT:MY_COMPONENT_DEFAULT
  NAMED:[[MY_COMPONENT_WRAPPER_DEFAULT]]
  NAMED_WITH_ARGS:MY_COMPONENT_DEFAULT
</div>

<div>
  DEFAULT:<h1>MAIN</h1>
  <h2>MAIN</h2>
  NAMED:MAIN
  NAMED_WITH_ARGS:MAIN(1)
</div>
```

この場合、ラッパーコンポーネントの最も単純な実装は以下のようになる。

```MyComponentWrapper.vue
<template>
  <my-component
    v-bind="$attrs"
    v-on="$listeners"
  >
    <slot></slot>

    <template v-slot:named>
      <slot name="named">[[MY_COMPONENT_WRAPPER_DEFAULT]]</slot>
    </template>

    <template v-slot:named-with-args="args">
      <slot name="named-with-args" v-bind="args"></slot>
    </template>
  </my-component>
</template>
```

しかし、この実装ではラップするコンポーネントのスロットの仕様が変更される度にラッパーコンポーネントの実装もそれに追従する必要があり、変更に弱い。そこで、`$scopedSlots` というコンポーネントインスタンスプロパティを使う。例えば、あるコンポーネントに対して以下のようなスロットテンプレートが記述されている場合、

```vue
<template>
  <some-component>
    <template v-slot:named></template>
    <template v-slot:named-with-args></template>
  </some-component>
</template>
```

コンポーネントインスタンスの `$scopedSlots` は以下のようになる。

```jsx
{
  'named': renderFunction,
  'named-with-args': renderFunction,
}
```

これを利用して、ラッパーコンポーネントの実装を以下のように改善できる。

```MyComponentWrapper2.vue
<template>
  <my-component
    v-bind="$attrs"
    v-on="$listeners"
  >
    <slot></slot>

    <template v-slot:named>
      <slot name="named">[[MY_COMPONENT_WRAPPER_DEFAULT]]</slot>
    </template>

    <template
      v-for="(_, name) in $scopedSlots"
      v-slot:[name]="slotProps"
    >
      <slot v-bind:name="name" v-bind="slotProps"></slot>
    </template>
  </my-component>
</template>
```

## ラッパーコンポーネントを作りスロットに静的なコンテンツを付加する

以下のような実装で、ラッパーコンポーネントのスロットの内容に静的なコンテンツを付与することができる。

```MyComponentWrapperWithStaticContent.vue
<template>
  <my-component
    v-bind="$attrs"
    v-on="$listeners"
  >
    MY_COMPONENT_WRAPPER_STATIC_CONTENT_FOR_DEFAULT_SLOT
    <!-- デフォルトスロットに静的なコンテンツを付加 -->
    <slot></slot>

    <template v-slot:named>
      MY_COMPONENT_WRAPPER_STATIC_CONTENT_FOR_SPECIFIC_SLOT
      <!-- named という名前のスロットに静的なコンテンツを付加 -->
      <slot name="named">[[MY_COMPONENT_WRAPPER_DEFAULT]]</slot>
    </template>

    <template
      v-for="(_, name) in $scopedSlots"
      v-slot:[name]="slotProps"
    >
      MY_COMPONENT_WRAPPER_STATIC_CONTENT_FOR_NAMED_SLOT
      <!-- 名前付きスロットに静的なコンテンツを付加 -->
      <slot v-bind:name="name" v-bind="slotProps"></slot>
    </template>
  </my-component>
</template>
```