経緯
最近、仕事でVue.jsを触っており(AWSによるサーバーレスSPA)、何とか一週間で他のメンバが開発をできる土台(フレームワーク的なもの)をVue.jsで作成する...程度はできるようになりました。
その中で、コンポーネント(*.vue)の扱いでよく使いそうな、子コンポーネントの扱いについて便利そうな事項をまとめました。
【2021/12/03追記】:最初(2019/7/27)に公開した内容を元に、加筆修正を行いました
参考ページ
- Vue.jsで style scoped な単一ファイルコンポーネントのcssをオーバーライドする
- 【vue.js】コンポーネント間のメソッドの呼び出し|親から子(ref)、子から親(emit)
- スコープ付き CSS
- Vue.jsスタイルガイド
そもそも、子コンポーネントの埋め込み方は?
単に親コンポーネント内に子コンポーネントを埋め込む場合は、下記でOKです。
- 子コンポーネントファイルをimport文で読み込む
- 親コンポーネントのVueコンポーネント定義の「components」に、読み込んだ子コンポーネントを登録する
- 親コンポーネントのテンプレート内に、components内で登録した子コンポーネントの定義名のタグを埋め込む。
/* これが子コンポーネント */ <template> <div id="body_header"> <router-link to="/">page1</router-link> <router-link to="/page2">page2</router-link> </div> </template> <script> export default { name:"body_header", components: {}, data: function () { return {} }, } </script> <style> #body_header { height: 40px; background: white; /* box-shadow: 0px 3px 3px rgba(0,0,0,0.1); */ display: flex; justify-content: center; align-items: center; border-bottom: 1px #000000 solid; } #body_header a { text-decoration: none; color: #2c3e50; margin: 0 10px; padding: 3px 10px; background: #5ccebf; } </style>
/* これは親コンポーネント */ <template> <div id="app"> <!-- ここで埋め込む --> <bodyHeader></bodyHeader> <router-view></router-view> </div> </template> <script> // 上記の子コンポーネントを読み込む import bodyHeader from './components/common/header' export default { name: 'App', components: { // ここで子コンポーネント&HTMLタグ名を登録 bodyHeader: bodyHeader, }, data: function () { return {} } } </script> <style> body { margin: 0; } #app { font-family: 'Avenir', Helvetica, Arial, sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; text-align: center; color: #2c3e50; } </style>
【2021/12/03追記】エントリーポイントに定義する方法
上記は各Vue.jsファイルに直接埋め込みましたが、複数のVue.jsファイルでまたがって使用する場合は、エントリポイント(main.jsなど)に定義することもできます。
※親コンポーネントのHTML内での子コンポーネントの埋め込み方法は同じです。
// main.js // 子コンポーネントの読み込み import bodyHeader from './components/common/header' // 1. グローバルコンポーネントとして登録する方法。 // こうすると、全Vue.jsでbodyHeaderコンポーネントが使えるようになる Vue.component('bodyHeader', bodyHeader); // 2. 個別に設定する場合。 // 個別に設定する場合は、Vue.component()は使用せず、 // new Vue()のcomponentsプロパティに設定する。 // 下の場合、#fugaに対応したVue.jsでのみbodyHeaderが使える。 new Vue({ el: '#hoge', }) new Vue({ el: '#fuga', components: { 'bodyHeader': bodyHeader, } })
子コンポーネントの変数を変えたい
子コンポーネントの使い方でよくあるのが、下記の例だと思います。
その場合、子コンポーネントの変数やスタイルシートなどを変更する必要があると思いますが、そのやり方です。
子コンポーネントの変数の変更方法
子コンポーネントの変数を親コンポーネントから変更する場合、下記でOKです。
- 子コンポーネントの「methods」に、セッター関数を用意する。
- 親コンポーネントのHTML内の子コンポーネント埋め込みタグ内に、「ref」属性と何かの値を付与する。
- 親コンポーネントのスクリプト内で「this.$refs.(ref属性の値).(セッター関数名)」で子コンポーネントのセッター関数を呼び出せるので、引数などに設定したい値を設定する。
/* これが子コンポーネント (無関係な部分は先述のソースと同じなので省略) */ <template> <div id="body_header"> <router-link to="/page1">{{ $myLink }}</router-link> </div> </template> <script> export default { name:"body_header", components: {}, data: function () { return { myLink : '' } }, methods: { setter(linkValue) { this.myLink = linkValue; } } } </script>
/* これは親コンポーネント */ <template> <div id="app"> <bodyHeader ref="myBodyHeader"></bodyHeader> </div> </template> <script> // 上記の子コンポーネントを読み込む import bodyHeader from './components/common/header' export default { mounted: function() { // 子コンポーネントのpage1へのリンクに「page1へ飛びます」と表示される。 this.$refs.myBodyHeader.setter('page1へ飛びます'); } } </script> <style scoped> </style>
(2021/12/03追記) propsを使う方法
上記では子コンポーネントのsetterを直接呼びましたが、propsを使う方法もあります。(てか、むしろこっちの方が一般的かも...)
propsの使い方ですが、子コンポーネントのscriptタグで、こんな感じで定義します。
<script> export default { // propsの定義 props: { isHoge: { type: Boolean, default: false, required: true, validator: function (value) { return typeof value === 'boolean' } } }, data() { return { // 省略 } } } </script>
上のコードで言えば、「isHoge」はpropsの変数名です。
また、各プロパティの意味は以下の通りです。(すべて任意項目です)
key | 意味 | 備考 |
---|---|---|
type | propsの型を表す型クラス(のコンストラクタ) | 型クラスであることに注意。(プリミティブ型でも「string」「boolean」などはダメ) |
default | 値が指定されなかった場合の初期値 | |
required | 値の指定が必須かどうか | |
validator | 指定された値をチェックするバリデーション関数 |
で、親コンポーネントでは、以下のように子コンポーネントのタグ内でpropsの値を指定します。
<template> <div id="app"> <bodyHeader isHoge="true"></bodyHeader> </div> </template>
参考:https://jp.vuejs.org/v2/api/#props
ただし、propsは初回レンダリング時は良いんですが、propsを変更してうんぬん...みたいなことをやろうとすると、途端に厄介になります。(ググってみると色々出てきますし、自分もハマりました)
このあたりは、別途独立してブログに出来ればなあ、と思っています。
参考: https://blog.hatena.ne.jp/Makky12/makky12.hatenablog.com/edit?entry=26006613378542201
子コンポーネントのスタイルの変更方法
子コンポーネントのスタイルを変更する場合は、ちょっと厄介ですが、下記の方法でOKです。
※なお、親コンポーネントのstyleタグにscopeを付けない方法もありますが、本来の使い方ではないですし、Vue.jsのスタイルルールA(必須)に反するので、お勧めしません。
- 子クラスのスタイルを採用したいHTMLタグに、何かクラスを定義する。
- 親クラスで、子クラス埋め込みタグの親要素として、何かクラス付きのタグを埋め込む
- 親クラスのstyle内で、「(子クラス埋め込みタグの親要素クラス) (子クラス埋め込みタグクラス) >>> (子クラスのスタイルを採用したい要素のクラス)」という定義を用意する。
/* これが子コンポーネント (無関係な部分は先述のソースと同じなので省略) */ <template> <div id="body_header"> <div class="my_item">{{ $myItem }}</div> </div> </template>
/* これは親コンポーネント */ <template> <div id="app"> <div class="parent"> <bodyHeader class="bh"></bodyHeader> </div> </div> </template> <script> // 上記の子コンポーネントを読み込む import bodyHeader from './components/common/header' export default { mounted: function() { // 子コンポーネントのpage1へのリンクに「page1へ飛びます」と表示される。 this.$refs.myBodyHeader.setter('page1へ飛びます'); } } </script> <style scoped> .parent .bh >>> .my_item { /* 子コンポーネントのmy_itemクラスの背景色が青色になる */ background-color: #0000FF; } </style>
【2021/12/03追記】
もちろん、子コンポーネントに直接cssを書いてもOKです。
/* 子コンポーネント */ <template> <div id="body_header"> <div class="my_item">{{ $myItem }}</div> </div> </template> <style scoped> .my_item { /* もちろん、直書きでもOK */ background-color: #0000FF; } </style>
(2021/12/03追記) 子コンポーネントのcssを動的に変えたい場合
上記とは少し違いますが、場合によっては「親の状態に応じて、子のスタイルを動的に変えたい」というケースもあると思います。
その場合、方法としては、下記のようなものがあります。
- 親から受け取る変数を「v-if」条件にする
- class名を変更する
- cssクラスのプロパティの値のみ変更する
親から受け取る変数を「v-if」条件にする
これは何となくわかりやすと思います。
ただ、HTML部分の分岐が増えてしまうのがちょっと面倒ですね...
/* これが子コンポーネント (無関係な部分は先述のソースと同じなので省略) */ <template> <div id="body_header"> <div if="bgColor==='red'" class="bg_color_red">{{ $myItem }}</div> <div v-else class="bg_color_blue">{{ $myItem }}</div> </div> </template> <script> // 上記の子コンポーネントを読み込む export default { data() { return { bgColor: '' } }, methods: { setBgColor(color) { this.bgColor =color; } } } </script>
class名を変更する
親から取得した変数の値に応じて、class名を変更する方法です。(classはv-bindしておく)
この場合、computed関数「getBgClass」に定義したように、
- 親から受け取ったクラス名をそのまま返す
- 親からは状態(state)を受け取り、それに応じたクラス名を返す
などの方法があります。
/* これが子コンポーネント (無関係な部分は先述のソースと同じなので省略) */ <template> <div id="body_header"> <div :class="getBgClass">abc</div> </div> </template> <script> // 上記の子コンポーネントを読み込む export default { data(): { return { bgClass: '' } }, computed: { getBgClass() { // this.bgClassに直接クラス名を埋め込む場合 return this.bgClass; // this.bgClassにフラグ(0, 1やred, blueなどの値)を埋め込む場合 if (this.bgClass==='red') { return 'bg_color_red'; } else if (this.bgClass==='blue') { return 'bg_color_blue'; } else { // その他処理 } }, }, methods: { setBgClass(value) { this.bgClass = value; } } } </script>
cssクラスのプロパティの値のみ変更する
<template>タグ内に<component is="style">タグを記載することで、<template>タグ内にcssを直接記述することが可能になります。
またVue.js 2.xでは、styleタグ内のcssの値は、data()変数やcomputed関数などの値をバインディングすることはできない(はず)ですが、上記のcssならば、それを行うことが可能になりますので、タグ内のclass名を直接変更せずとも、スタイルを動的に変更することができます。
この方法だと、上2つの方法と違い、
- 条件が増えても、(<template>内の)HTMLタグが増えない
- class名を直接変更する必要がない(=class定義を分岐数分増やさなくていい)
というメリットがあります。
※Vue.js 3.2から、styleタグ内のcssの値にdata()変数の値をバインディングできるみたいです
/* これが子コンポーネント (無関係な部分は先述のソースと同じなので省略) */ <template> <div> <component is="style"> .bg_class { background-color: {{ getBgColor }} }; </component> <div id="body_header"> <div class="bg_class">abc</div> </div> </div> </template> <script> // 上記の子コンポーネントを読み込む export default { data(): { return { bgColor: '' } }, computed: { getBgColor() { if (this.bgColor==='red') { return '#FF0000'; } else if (this.bgColor==='blue') { return '#0000FF'; } else { // その他処理 } }, }, methods: { setBgColor(color) { this.bgColor = color; } } } </script>
ちなみに
もちろん、1つの親コンポーネント内で同じ子コンポーネントを使いまわすことも可能です。
/* これが子コンポーネント (無関係な部分は先述のソースと同じなので省略) */ <template> <div id="body_header"> <div class="my_item">{{ $myItem }}</div> </div> </template> <script> export default { name:"body_header", components: {}, data: function () { return { myItem : '' } }, methods: { setter(itemValue) { this.myItem = itemValue; } } } </script> <style scoped> </style>
/* これは親コンポーネント */ <template> <div id="app"> <div class="parent"> <bodyHeader ref="myBodyHeader" class="bh1"></bodyHeader> </div> <div class="parent"> <bodyHeader2 ref="myBodyHeader2" class="bh2"></bodyHeader> </div> </div> </template> <script> // 上記の子コンポーネントを読み込む import bodyHeader from './components/common/header' import bodyHeader2 from './components/common/header' export default { components: { bodyHeader : bodyHeader , bodyHeader2: bodyHeader2 }, mounted: function() { this.$refs.myBodyHeader.setter('bodyHeader1'); this.$refs.myBodyHeader2.setter('bodyHeader2') } } </script> <style scoped> .parent .bh1 >>> .my_item { background-color: #0000FF; } .parent .bh2 >>> .my_item { background-color: #FF0000; } </style>
ちょっと急ぎ足&文字数が多くなりましたが、今回はここまで。
...てかはてなブログ、Vue.jsに対応してくれないかなあ。(HTML/CSS/javascriptうまく表示してくれるとか)