初めてのVue⑦flag,slotの使い方

初めてのVue⑦flag,slotの使い方

前回までにコンポーネントを別々のファイルで管理する方法を紹介してきました。コンポーネントを使い回す際に、片方にだけ表示させたい要素があるとしたらどうしたら良いでしょうか?

この記事の対象者
  • コンポーネントを使い回すときの工夫を知りたい
  • フラグやスロットについて知りたい
  • 効率の良いコードの書き方が知りたい

flagの設定方法

紹介するコードは前回までのコードを元にしています。

import Assingment from "./Assingment.js";
import AssingmentTags from "./AssingmentTags.js";

export default{
  components:{ Assingment, AssingmentTags},

  template: `
//ポイント①:v-showにshowを追加
    <section v-show="show && assingments.length">
      <div>
        <h2>
          {{ title }}
          <span>({{ assingments.length }})</span>
        </h2>
//ポイント②:buttonタブを追加、v-showと@clickを設定(ボタンをクリックするとsectionが閉じる)
        <button v-show="canHide" @click="show = false">×</button>
      </div>

      <assingment-tags
        v-model:currentTag="currentTag"
        :initial-tags="assingments.map(a=> a.tag)"
        
      />
      <ul>
        <assingment
          v-for="assingment in filteredAssingments"
          :key="assingment.id"
          :assingment="assingment"
        >
        </assingment>
        
      </ul>
    </section>
  `,

  props: {
    assingments: Array,
    title: String,
//追加
    canHide:{
      type: Boolean, default: false
    }
  },

  data(){
    return{
      currentTag:'all',
//追加
      show: true
    };
  },

  computed: {
    filteredAssingments(){
      if(this.currentTag == 'all'){
        return this.assingments;
      }
      return this.assingments.filter(a => a.tag === this.currentTag);
    },
  }
}
import AssingmentList from "./AssingmentList.js";
import AssingmentCreate from "./AssingmentCreate.js";

export default{
  components: { AssingmentList, AssingmentCreate},
  template: `
    <assingment-list :assingments="filters.inProgress" title="In Progress"></assingment-list>
//ポイント①:canHide追加してボタンタグを見えるようにしてます
    <assingment-list :assingments="filters.completed" title="Completed" canHide></assingment-list>

    <assingment-create @add="add"></assingment-create>
    ` ,

    data(){
        return{
          assingments: [],
        }
      },

      computed: {
        filters(){
          return{
            inProgress: this.assingments.filter(assingment => ! assingment.complete),
            completed: this.assingments.filter(assingment => assingment.complete)
          };
        }
      },

      created(){
        fetch('http://localhost:3001/assingments')
          .then(response => response.json())
          .then(assingments => {
            this.assingments = assingments;
          });
      },

      methods: {
        add(name){
          this.assingments.push({
            name: name,
            complete: false,
            id: this.assingments.length +1
          });
        }
      }

}

ブラウザで確認します。

completedにだけ❌が出ていますね。

❌を押すとリストが消せます。こうした設定をflagと言います。

CSSが気になりますが、今回は触りません。

もう一つのflagの設定方法

他にも設定方法があるので見ていきましょう。

import Assingment from "./Assingment.js";
import AssingmentTags from "./AssingmentTags.js";

export default{
  components:{ Assingment, AssingmentTags},

  template: `
    <section v-show="assingments.length">
      <div>
        <h2>
          {{ title }}
          <span>({{ assingments.length }})</span>
        </h2>
//ポイント①:emitに変更
        <button v-show="canHide" @click="$emit('toggle')">×</button>
      </div>

      <assingment-tags
        v-model:currentTag="currentTag"
        :initial-tags="assingments.map(a=> a.tag)"
        
      />
      <ul>
        <assingment
          v-for="assingment in filteredAssingments"
          :key="assingment.id"
          :assingment="assingment"
        >
        </assingment>
        
      </ul>
    </section>
  `,

  props: {
    assingments: Array,
    title: String,
    canHide:{
      type: Boolean, default: false
    }
  },

  data(){
    return{
      currentTag:'all',
      // show: true
    };
  },
}
import AssingmentList from "./AssingmentList.js";
import AssingmentCreate from "./AssingmentCreate.js";

export default{
  components: { AssingmentList, AssingmentCreate},
  template: `
    <assingment-list :assingments="filters.inProgress" title="In Progress">
      <assingment-create @add="add"></assingment-create>
    </assingment-list>
    <assingment-list 
//ポイント①:v-showと@toggleを追加
      v-show="showCompleted"
      :assingments="filters.completed" 
      title="Completed" 
      canHide
      @toggle="showCompleted = !showCompleted"
    ></assingment-list>
    ` ,

    data(){
        return{
          assingments: [],
//showCompletedを追加
          showCompleted: true
        }
      },
}

ブラウザでは同じ画面になります。

子要素でemitして親で@toggleを設定してやり、v-showで非表示にしています。

さっきは子要素の中で完結してましたが、今回は親要素で非表示にしているので子要素の中になかった要素も一緒に非表示にすることができます。

slotを使ってみよう

次に似たような設定でslotというのがあるので見ていきましょう。

<assingment-list :assingments="filters.inProgress" title="In Progress">
      <assingment-create @add="add"></assingment-create>
</assingment-list>
<assingment-list :assingments="filters.completed" title="Completed" canHide></assingment-list>

//ポイント:inProgressのassingment-listの中にassingment-createを移動
    <section v-show="show && assingments.length">
      <div>
        <h2>
          {{ title }}
          <span>({{ assingments.length }})</span>
        </h2>

        <button v-show="canHide" @click="show = false">×</button>
      </div>

      <assingment-tags
        v-model:currentTag="currentTag"
        :initial-tags="assingments.map(a=> a.tag)"
        
      />
      <ul>
        <assingment
          v-for="assingment in filteredAssingments"
          :key="assingment.id"
          :assingment="assingment"
        >
        </assingment>
        
      </ul>
//ポイント:slotを設定
      <slot></slot>
    </section>

ではブラウザで確認していきます。

In Progressの下にタスクを追加するためのコンポーネントが来ているのが分かります。

親要素でタグの中に別の子要素を入れることで、slotを設定した場所に子要素が挿入されます。

めちゃ簡単ですね。

slotが複数ある場合

1つならこれでいいのですが、2つ以上ある場合はどうしたら良いでしょうか?

もう少し詳しく見ていきます。

export default{
  template:`
    <div>
      <h2>{{heading}}</h2>
      <h2 v-text="heading"></h2>
    </div>
  `,

  props:{
    heading: String
  },

}
import Assingments from "./Assingments.js";
import panel from "./panel.js";

export default {
    components: {Assingments, panel},

    template: `
      <assingments></assingments>
      <panel heading="hello"></panel>
    `,
}

ブラウザで確認します。

今までの復習ですね。

では、slotを使っていきます。

export default{
  template:`
    <div>
      //<h2>{{heading}}</h2>
      //<h2 v-text="heading"></h2>

      //ポイント①:子要素ではslotタグにnameを設定してやります。
      <h2 v-if="$slots.heading">
     <slot name="heading"/>
    </h2>

      <slot name="default"/>
    </div>
  `,

  props:{
    heading: String
  },
}
import Assingments from "./Assingments.js";
import panel from "./panel.js";

export default {
    components: {Assingments, panel},

    template: `
      <assingments></assingments>
      
   //<panel heading="hello"></panel>

     //ポイント①:親ではv-slotを使うことでslotを特定して使用しています
      <panel>
        <template v-slot:heading>heading slot</template>
        <template v-slot:default>default slot</template>
      </panel>
      <panel>
        <template v-slot:default>default slot</template>
      </panel>
      <panel>
        <template #default>default slot</template>
      </panel>
    `,

    

}

ブラウザで確認します。

うまくいってます。

slotに何も情報が入ってないままに使用すると空のh2タグが入ってしまいますが、v-ifを使うことで情報が入っている時にだけh2タグが使われるようにしています。

ちなみに、v-slot:defaultは#defaultと同じ意味になります。

おまけ

今回はflagとslotについて紹介しました。

slotがどんな時に役立つかよく分からないかも知れませんが、テンプレートを作るときに役立ちます。

例えば、下記のようなテンプレートを作ったとします。

<div :class="{
     'text-red' : true,
     'text-white' : theme == 'white',
     'text-black' : theme == 'black',
    }">

   <h2 v-if="$slots.heading">
    <slot name="heading"/>
   </h2>

   <slot/>

   <footer v-if="$slots.footer">
    <slot name="footer"/>
   </footer>

</div>

これを下記のように呼び出せばテンプレートに沿って作成できて便利ですよね。

<panel theme="white">
 <template v-slot:heading>
  Hello
 </template>
</panel>

関連記事