😺

Vue コンポーネントについて②

2024/12/03に公開

前回に引き続きコンポーネントの解説です。

今回はコンポーネントタグにidやclassなどの属性を指定した時の動きについて説明していきます。
 これはコンポーネントの形によって動きが変わるのですが、
仮にBaseButtonの新規コンポーネントを準備します。
BaseButton.vue

<template>
 <div>
   <button>BaseButton</button>
 </div>
</template>

App.vueにインポートします。

<script setup>
import CountUp from '@/components/CountUp.vue'
import BaseButton from '@/components/BaseButton.vue'
</script>

<template>
  <h1>App</h1>
  <BaseIcon />
  <CountUp />
  <BaseButton />
</template>

として <BaseButton />にidを付与します。

 <BaseButton id="base-button" />

この際、id="base-button"はBaseButton.vueコンポーネントの一番外側の<div>に継承されます。
実際に検証ツールを見てみると、

このようにidが付与されています。
もちろん、子コンポーネントの<div>タグにも別の属性やclassをつけることができます。

<template>
  <div class="pink bg-green">
    <button>BaseButton</button>
  </div>
</template>

こうすると

このようにidとclass両方適用されています。
 この状態で子コンポーネントにidを付けると親の方が優先されるので子コンポーネントに付けたidは適用されませんが、classとstyleの場合は親コンポーネントと子コンポーネント両方合体したものが適用されます。
 親コンポーネントに class="border"をつけてみます。

styleも同じように作用します。
 あと、v-onディレクティブも同じように継承されるようになっています。

 <BaseButton id="base-button" class="border" @click="console.log('App.vue')" />

こうすることで子コンポーネントの一番外側の<div>に付けたのと同じになりますし、これも子コンポーネントの一番外側の<div>に付けて合体させることができます。
ボタンをクリックすると

 このように両方実行されます。
あと、親>子>子のような状態でも継承されます。

これが外側のタグが一つだけのコンポーネントに普通の属性などを付けた時の動きになります。
 で、CountUpのようにタグが複数あったりBaseIconのように<div>タグがない場合は一切適用されませんし、エラーも出ます。

<template>
  <h1>App</h1>
  <BaseIcon id="base-button" class="border" @click="console.log('App.vue')" />
  <CountUp id="base-button" class="border" @click="console.log('App.vue')" />
  <BaseButton id="base-button" class="border" @click="console.log('App.vue')" />
</template>


 複数のタグがある時に継承させる方法はあります。

<template>
  <div>
    <h2 v-bind="$attrs">CountUp++</h2>
    <BaseIcon />
    <p>count:{{ count }}</p>
    <button @click="count++">+1</button>
  </div>
</template>


このようにv-bindアトリビュートと書くことで親で指定した全ての普通の属性をそのまま特定の要素に継承させることができ、エラーも消えます。

ちなみにディレクティブに関してはv-bindやv-onは先ほどのような感じで、v-ifの場合中身に関係なくfalseにしたら全て消えるし、trueにしたら全部表示されます。

<template>
 <h1>App</h1>
 <BaseIcon v-if="false" />
 <CountUp v-if="false" id="base-button" class="border" @click="console.log('App.vue')" />
 <BaseButton v-if="false" id="base-button" class="border" @click="console.log('App.vue')" />
</template>

v-forも中身の形に関わらず問題なく動きますが、v-showに関してはタグにdisplay-noneのstyleを適用させる必要があるので一番外側のタグが一つだけの場合は動きますが、タグがないものや複数のものに関しては動きません。
なので必ず一番外側のタグが一つだけの場合の時のみ使用しましょう。

次にstyleですが、<script setup>で定義されたデータは他のコンポーネントからはアクセスすることができませんでしたが<style>タグで定義されたCSSは他のコンポーネントで使用することができます。
例えば、App.vueで<style>設定します。


<template>
 <h1>App</h1>
 <BaseIcon />
 <CountUp id="base-button" class="border" @click="console.log('App.vue')" />
 <BaseButton id="base-button" class="border" @click="console.log('App.vue')" />
</template>

<style>
.red {
 color: red;
}
</style>

で、CountUp.vueの<p>タグにclassのセットをします。

<template>
  <div>
    <h2 v-bind="$attrs">CountUp++</h2>
    <BaseIcon />
    <p class="red">count:{{ count }}</p>
    <button @click="count++">+1</button>
  </div>
</template>

こうするとclassが適用されています。

なので<style>タグで書かれているものはどこのコンポーネントでもimportがしてあれば自由に使えることになります。

そこで、一つのコンポーネントの中だけでしか使えないCSSを書く方法は
<style scoped></style>この<style>タグにscopedを書くとこのコンポーネントに限定され、内部だけで使用できます。

<template>
 <h1>App</h1>
 <BaseIcon />
 <CountUp id="base-button" class="border" @click="console.log('App.vue')" />
 <BaseButton id="base-button" class="border" @click="console.log('App.vue')" />
</template>

<style scoped>
.red {
 color: red;
}
</style>

先程のCountUp.vueの<p>タグは黒くなりました。

基本的に<style>タグにscopedを書く<style scoped></style>この書き方が一般的なのでこの形を使っていきましょう。

ちなみに、Vueがどのような動きをしているか検証ツールで見てみます。
<div>内に識別子が付いていますが、scopedが付いた<style>タグによってコンポーネントごとにユニークな識別子の属性が割り振られます。

 その上で実際に書いたCSSはHEADタグの中に書かれていて、scopedを付けた場合はその識別子が各CSSセレクタの隣に[]で付くようになっています。

これはどういう意味かというと、redクラスを持っていて、かつこの属性を持っている要素にcolor:red;を適用させるという意味になります。
こういう仕組みになっているのでscopedを付けたらそのコンポーネント内の要素にだけスタイルが適用されるということです。
注意点ですが、属性の継承と似ていて子コンポーネントが一つのタグだけで囲われている場合は子コンポーネントの外側のタグにも識別子の属性が付くようになります。
 なので今回であればBaseButtonの<div>タグにもこのdata-識別子が付いています。

なのでBaseButtonにもclass=redが適用されているので、ここに文字を加えるとredが適用されます。

<template>
 <div class="red bg-green" @click="console.log('BaseButton.vue')">
   Click!
   <button>BaseButton</button>
 </div>
</template>

BaseButton.vueのコンポーネントに<style>を書かなくても一番外側の<div>タグにclassが書いてあれば親コンポーネントから適用されるということです。(内側のbuttonタグについては反映されません)
もしこのBaseButton.vueのところにも<style scoped>で<style>設定していたらこの時は識別子の属性が二つ付きます。
そしてCountUp.vueのような外側のタグが一つでないもの(複数ある場合)は親コンポーネントの識別子は一切継承されませんので二つ付かないようになっています。

あと、<style>タグはscopedが付かないのも同じコンポーネント内で付けることができます。

<template>
  <div>
    <h2 v-bind="$attrs">CountUp++</h2>
    <BaseIcon />
    <p class="red">count:{{ count }}</p>
    <button @click="count++">+1</button>
  </div>
</template>

<style>
.red {
  color: red;
}
</style>

<style scoped>
.red {
  color: red;
}
</style>

この場合はもちろん、scopedが付いたものはこのコンポーネント内だけ適用されて、scoped付かない<style>タグはグローバルに適用されます。

Viteを使ってもグローバルに適用させることが簡単にできます。
まずグローバル適用させたいCSSファイルを用意し(今回はassetsフォルダを作りmain.cssファイルを作ります)、このCSSファイルをViteの起点になっているmain.jsにimportします。

import { createApp } from 'vue'
import App from './App.vue'
import BaseIcon from './components/BaseIcon.vue'
import './assets/main.css'

const app = createApp(App)
app.component('BaseIcon', BaseIcon)

app.mount('#app')

こうするだけでmain.cssは自動的にHTMLのheadタグに挿入されてグローバル適用されます。

試しにCSSを書いてみると

 このように適用されているのがわかるかと思います。

グローバルにCSSを使用する方法が2つあるというのを知っておくといいと思います。

では次回から親子間のコンポーネントでのデータの受け渡しについて解説していきます。

Discussion