Help us understand the problem. What is going on with this article?

Vue.jsでslotの中身をwrapする方法

More than 1 year has passed since last update.

目的

ReactでCarouselとか作る場合、こんな感じでchildrenをループしてラップする形になっていると汎用性があってやりやすいのですが、それをVueでやる場合の方法について調べました。

サンプルコードはこちらに書きましたので気になる方はご覧になってください。
https://codesandbox.io/s/yp2j1z0xzj

Reactでchildrenをラップする
// Wrapするコンポーネント
const WrapList = (props) => {
  const { children } = props;
  return (
    <ul>
      {children.map((child, index) => (
        <li key={index}>{child}</li>
      ))}
    </ul>
  );
};

// App
const App = () => {
  return (
    {/* WrapListの中に要素を入れて使う */}
    <WrapList>
      <div>太郎</div>
      <div>次郎</div>
    </WrapList>
  );
};

ちなみに以下のようはことはできません。

template内でループ回すことはできない
<template lang="pug">
div
  template(v-for="item in $slots.default")
    //- エラーになる
    div {{ item }}
</template>

JSXでwrapする方法

Vueにはrender関数でjsxを書くことができるので、そちらでReactと同じように実装する方法です。

jsxでwrapするコンポーネント
<script type="text/jsx">
// text/jsxは書かなくても問題はないですが、webstormだとJSXコードを認識してくれます
export default {
  render() {
    return (
      <div>
        <ul class="list">
          {this.$slots.items.map((vnode, index) => (
            {/* class指定はclassNameではなく、普通にclassでいいようです */}
            <li key={index} class="item">{vnode}</li>
          ))}
        </ul>
      </div>
    );
  }
};
</script>

<style lang="scss" scoped>
.list {
  padding: 0;
}

.item {
  display: inline-block;

  & + & {
    padding-left: 20px;
  }
}

// slotから渡されるクラスを設定してしまうとうっかり上書きしてしまう
// .content {
//   font-size: 10px;
// }
</style>
コンポーネントを使用する
<template lang="pug">
.app
  //- 子要素を展開して、中でラップするようにする
  p JSXを使ってアイテムをラップするやり方(直接書く)
  WrapListJSX
    template(slot="items")
      .content 太郎
      .content 次郎
  p JSXを使ってアイテムをラップするやり方(ループで展開する)
  WrapListJSX
    template(slot="items")
      template(v-for="item in list")
        .content(:key="item.id") {{ item.name }}
</template>

<script>
import WrapListJSX from "./components/WrapListJSX";

export default {
  name: "App",
  components: {
    WrapListJSX
  },
  data() {
    return {
      list: [
        { id: 1, name: '太郎' },
        { id: 2, name: '次郎' },
        { id: 3, name: '三郎' },
        { id: 4, name: '花子' }
      ]
    };
  }
};
</script>

<style lang="scss" scoped>
.app {
  .content {
    color: red;
  }
}
</style>

このやり方だとReactと同じことをやっているので目的は理解できそうです。ただ若干構文が違っているのと普段はtemplateで書いているのにそこだけJSXで書かなければいけないという気持ち悪さはあります。(ビルドもjsxも対応する方にモジュールをインストールしないといけなくなりますし)

slot-scopeを使ってitemの中身だけカスタムレンダリングする方法

先に配列データを渡して枠組みだけ作ってもらい、itemの中身の部分だけslot指定で設定する方法です。このときslot-scopeを経由して変数を貰うことができるので、実装自体は可能です。

slot-scopeを使ってwrapするコンポーネント
<template lang="pug">
div
  ul.list
    template(v-for="item in $props.list")
      li.item(:key="item.id")
        //- itemの中身で使用すると思われる変数をslotに渡しておく
        slot(name="item", :item="item")
</template>

<script>
export default {
  props: {
    list: Array
  }
};
</script>

<style lang="scss" scoped>
.list {
  padding: 0;
}

.item {
  display: inline-block;

  & + & {
    padding-left: 20px;
  }
}

// slotから渡されるクラスを設定してしまうとうっかり上書きしてしまう
// .content {
//   font-size: 10px;
// }
</style>
コンポーネントを使用する
<template lang="pug">
.app
  //- slot-scopeを使ってアイテム部分だけ引数を受け取ってrenderする
  p slot-scopeを使ってitemの中身だけカスタムレンダリングするやり方
  WrapListSlotScope(:list="$data.list")
    //- コンポーネント側で渡した変数をslot-scopeで受け取ってitem内の要素を定義する
    template(slot="item", slot-scope="{ item }")
      .content {{ item.name }}
</template>

<script>
import WrapListSlotScope from './components/WrapListSlotScope';

export default {
  name: "App",
  components: {
    WrapListSlotScope
  },
  data() {
    return {
      list: [
        { id: 1, name: '太郎' },
        { id: 2, name: '次郎' },
        { id: 3, name: '三郎' },
        { id: 4, name: '花子' }
      ]
    };
  }
};
</script>

<style lang="scss" scoped>
.app {
  .content {
    color: red;
  }
}
</style>

こちらですといつもと同じようにtemplateで書けるのが魅力ではあります。しかしこのやり方ですと以下のような問題があるのかなと思いました。

  • JSXで書いた時のような要素をそのまま書くということができない
  • 一度リストを渡してアイテムの中身だけslot-scopeで受け取って定義する方法がわかりづらい

一応二番目についてはReactのrederPropと似たことなので、分かってもらえるんじゃないかなと思います。

Vue.jsにおけるRender PropとScoped Slotsについて

まとめ

一番しっくりくるのはJSXで実装することだと思いました。しかしそれだといつもtemplateで書いているやり方ができなくなり、他のコードと一貫性が持てなくなるのが懸念でした。
今回slot-scopeというものを使ってtemplate側のままでも実装することが分かり、難しい感じではありますがVueならではの機能ということで使っていければなと思いました。

ちなみにサンプルコードでSCSS部分のコメントで上書きされちゃう問題が書かれていましたが、あれはどっちで実装しても起きる問題でした。てっきりrender関数で実行した時だけだと思っていたんですけどね・・・。意外とscoped CSSはバッティングを引き起こしてしまうようで、その辺の話は別の記事で書きたいと思います。

Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away