とある師走のWeb制作会社
社長「お〜い、やめ太郎くん」
ワイ「なんでっか」
社長「ちょっと作ってほしい画面があんねん」
社長「↑こんなんを作ってほしいねん」
ワイ「おお、SNSの設定画面とかによくあるやつでんな」
ワイ「簡単ですわ、任しといてください」
社長「おお、ありがとう」
社長「フレームワークはVue.jsで頼むわ」
社長「ほな、よろしく〜」
作り始めてみる
ワイ「ええと、このトグルスイッチみたいなんはいくつ必要なんや」
ワイ「3つやな」
ワイ「ほな、このトグルスイッチはコンポーネントとして作っとこか」
ワイ「そしたら3回使い回せるからな」
ワイ「イメージとしては、ToggleSwitchっていう名前のコンポーネントを作って・・・」
<ul class="switch-list">
<li>通知 <ToggleSwitch /></li>
<li>検索フィルター <ToggleSwitch /></li>
<li>ダークモード <ToggleSwitch /></li>
</ul>
ワイ「↑こんな感じで使うんや」
ワイ「ほな、そのToggleSwitchコンポーネントを作っていこか」
コンポーネント作り開始
ワイ「ええと」
ワイ「↑これを作るんやから」
ワイ「構成要素は、枠の部分と、中の丸いやつ」
ワイ「その2つやな」
ワイ「ほな、div2つで作れそうやな」
<template>
<div class="frame">
<div class="button" />
</div>
</template>
ワイ「↑こうやな」
ワイ「ほんで、ON/OFFの状態は、このコンポーネントのdataの中で管理するんや」
ワイ「ほな、dataの中にvalueってプロパティを作って管理していこか」
ワイ「初期値はfalse
や」
data() {
return {
value: false
}
}
ワイ「↑こうやな」
ワイ「ほんで、スイッチがONの時だけ・・・つまりvalueがtrue
の時だけ」
ワイ「div要素にframe--on
っていうクラスを付けたいから」
<div :class="{ 'frame--on': value }" class="frame">
<div class="button" />
</div>
ワイ「↑こうやな」
ワイ「ほんで、frame--on
っていうクラスが付いてる場合は」
ワイ「背景が緑色になって、スイッチは右側に行くんや」
ワイ「そういうCSSを書くで」
.frame--on {
text-align: right;
background-color: #42ff42;
border-color: transparent;
}
ワイ「↑こんな感じや」
ワイ「試しにvalueをtrue
にしてみよか」
data() {
return {
value: true
}
}
ワイ「どれどれ・・・」
ワイ「おお、ちゃんとONになったで」
ワイ「これでCSSはいい感じやな」
次は、イベントに応じた動きを実装
ワイ「ほんで、クリックしたらON/OFFが切り替わるようにしたいんや」
ワイ「せやから、クリックしたらtoggle
っちゅう関数を実行することにしよか」
<div @click="toggle" :class="{ 'frame--on': value }" class="frame">
<div class="button" />
</div>
ワイ「↑こうやな」
ワイ「ほな、toggle
っていう関数を作っていくで」
ワイ「関数はmethodsの中に書くんや」
methods: {
toggle() {
this.value = !this.value
}
}
ワイ「これでええな」
ワイ「トグルボタンをクリックすると、toggle
関数が実行されて」
ワイ「valueの値、つまりtrue
とfalse
が切り替わる、と」
何かが足りない
ワイ「ありゃ、そういえば・・・」
スイッチがONになったよ!
ワイ「っていうことを親コンポーネントにも伝えんといかんよな」
ワイ「最終的には親コンポーネントで値をまとめて、SubmitなりAPIに送信なりするはずやもんな」
ワイ「ほな$emit
を使って、親コンポーネントにも変更後の値を伝えるで」
methods: {
toggle() {
this.value = !this.value
this.$emit('input', this.value)
}
}
ワイ「↑こうやな」
ワイ「親コンポーネントにinput
っていうイベントを送ってやるんや」
ワイ「よっしゃ、ToggleSwitchコンポーネント完成や!」
ハスケル子「なんか、微妙な気がします・・・」
ワイ「何がやねん」
ワイ「ハスケル子ちゃんは、Vue.jsよく知らんやろ」
ワイ「大丈夫やから、見といて!」
実際にコンポーネントを使ってみる
ワイ「ほな、今作ったToggleSwitchコンポーネントを使って・・・」
ワイ「↑この画面を作っていくで!」
<ul class="switch-list">
<li>
<span>通知</span>
<ToggleSwitch />
</li>
<li>
<span>検索フィルター</span>
<ToggleSwitch />
</li>
<li>
<span>ダークモード</span>
<ToggleSwitch />
</li>
</ul>
ワイ「テンプレート部分はこんな感じやな」
ハスケル子「んー、やっぱり何か」
ハスケル子「ToggleSwitchコンポーネントが微妙な気がします・・・」
ワイ「ええ・・・何で・・・?」
ハスケル子「子コンポーネントに状態を持たせない方がいいです」
ハスケル子「今回のケースで言うと、ToggleSwitchコンポーネントのdata内でtrue/falseを管理しない方がいいと思います」
ハスケル子「状態はページ側で保持するようにして」
ハスケル子「子コンポーネントには、それをpropsで渡すようにしないと」
ハスケル子「あとで、初期値に戻すボタンが実装しにくくなると思います・・・」
ワイ「いやいや、大丈夫やろ」
ワイ「初期値に戻すボタンを押したら、全部OFFにすんねやろ」
ワイ「今のやり方で実現できるって」
ワイ「ワイには全て見えとる!!!」
見ていろ、ハスケル子ちゃん
ワイ「まず、コンポーネントを呼び出しとるページの方にもdataを用意するやろ?」
data() {
return {
notice: false,
searchFilter: false,
darkMode: false
}
},
ワイ「↑こうや」
ワイ「ほんで、ToggleSwitchコンポーネントをクリックしたら」
ワイ「つまりinput
っていうイベントが伝わってきたら・・・」
ワイ「ページの方のdataも更新されるようにする」
<ul class="switch-list">
<li>
<span>通知</span>
<ToggleSwitch @input="notice = $event" />
</li>
<li>
<span>検索フィルター</span>
<ToggleSwitch @input="searchFilter = $event" />
</li>
<li>
<span>ダークモード</span>
<ToggleSwitch @input="darkMode = $event" />
</li>
</ul>
ワイ「よっしゃ」
ワイ「これで、トグルスイッチをクリックしたら・・・」
ワイ「それと連動して、ページのdataも変わるようになったで!」
ワイ「大丈夫や〜ん」
ハスケル子「じゃあ、初期値に戻すボタン作ってみてください・・・」
ワイ「おう、任しとき!」
<button @click="reset">初期値に戻す</button>
ワイ「↑こうやな」
ワイ「クリックしたら**reset
っていうメソッドが実行されるんや」
ワイ「ほな、次はそのreset
**メソッドを作るで」
methods: {
reset() {
this.notice = false
this.searchFilter = false
this.darkMode = false
}
}
ワイ「↑こうや」
ワイ「dataの中のプロパティ達を全部false
にしたるんや」
できた・・・!?
ワイ「ほな、ハスケル子ちゃん」
ワイ「初期値に戻すボタン、押すで・・・!?」
ワイ「ホンマに押すで・・・!?」
ハスケル子「いいから押せばいいじゃないですか」
・・・ポチッ!
結果
ワイ「戻らへんのかい!!!」
ハスケル子「そりゃそうですよ」
ハスケル子「だって、このコンポーネント、propsが1つもないんですもん」
ハスケル子「コンポーネントっていうのは、propsの渡し方で操作するものなんですから」
ワイ「Oh...」
ワイ「じゃ、じゃあハスケル子ちゃん」
ワイ「ちょっとインターンの課題の一環として」
ワイ「これを直してみせてや・・・」
ハスケル子「はぁ・・・」
親からpropsを受け取るようにする
ハスケル子「まずToggleSwitchコンポーネントを修正して」
ハスケル子「親からpropsを受け取れるようにしましょう」
props: {
value: {
type: Boolean,
default: false
}
},
ハスケル子「↑こうですね」
ハスケル子「このコンポーネントはvalueっていう真偽値を親から受け取りますよ、ってことです」
ハスケル子「代わりにdataは消しました」
ハスケル子「そして、methodsの中のtoggle
関数も変えます」
methods: {
toggle() {
this.$emit('input', !this.value)
}
}
ハスケル子「↑こうですね」
ハスケル子「dataをいじる部分がなくなりました」
ハスケル子「親コンポーネントにinput
というイベントと真偽値を受け渡す」
ハスケル子「それだけになりました」
ページの方も修正していく
ハスケル子「じゃあ、コンポーネントを呼び出しているページ側も修正していきます」
ハスケル子「ToggleSwitchコンポーネントにvalueというpropsを渡してあげます」
<ul class="switch-list">
<li>
<span>通知</span>
<ToggleSwitch :value="notice" @input="notice = $event" />
</li>
<li>
<span>検索フィルター</span>
<ToggleSwitch :value="searchFilter" @input="searchFilter = $event" />
</li>
<li>
<span>ダークモード</span>
<ToggleSwitch :value="darkMode" @input="darkMode = $event" />
</li>
</ul>
<button @click="reset">初期値に戻す</button>
ハスケル子「↑こうですね」
ハスケル子「でも:value
と@input
は」
ハスケル子「まとめてv-model
にしちゃいましょう」1
<ul class="switch-list">
<li>
<span>通知</span>
<ToggleSwitch v-model="notice" />
</li>
<li>
<span>検索フィルター</span>
<ToggleSwitch v-model="searchFilter" />
</li>
<li>
<span>ダークモード</span>
<ToggleSwitch v-model="darkMode" />
</li>
</ul>
<button @click="reset">初期値に戻す</button>
ハスケル子「このほうがスッキリしますね」
修正完了
ハスケル子「じゃあ、初期値に戻すボタン押しますよ」
・・・ポチッ!
ワイ「おお、ちゃんと戻った・・・」
ハスケル子による解説
ハスケル子「子コンポーネントは、親から受け取った状態を映し出す単なるディスプレイとして作りましょう」
ハスケル子「下手に記憶装置を持たせると、親コンポーネントからリセットしたい時とかに」
ハスケル子「わざわざ$refsとかで、子コンポーネントの状態を変えて回らないといけないですし」
ワイ「なるほどなぁ・・・」
ハスケル子「親コンポーネントのdataを変えたら子コンポーネントも自動で反応!」
ハスケル子「ってできるのがVueのメリットなのに」
ハスケル子「いちいち$refs
で状態を変えて回ってたら、全然リアクティブじゃないですよね」
ワイ「むむぅ・・・」
ワイ「でも、その**$refs
**いうのを使えば、できることはできるんやろ?」
ハスケル子「できなくはないですけど、コンポーネントの階層が深くなったときとかに面倒ですよ・・・」
ハスケル子「this.$refs.xxx
の$refs.yyy
の$refs.zzz
のreset
メソッドを実行・・・みたいな」
ワイ「そうかぁ・・・」
ワイはただ、直感的に考えたかっただけなんや
ワイ「でもな、例えば現実世界ではそれぞれの生き物が脳を持ってて」
ワイ「それぞれ自分の脳に情報を溜めていくやん?」
ワイ「だから、それぞれのコンポーネントが状態を保持してた方が」
ワイ「現実世界っぽくて想像しやすいんや」
ワイ「直感的なんや」
ハスケル子「現実世界っぽくて理解しやすい・・・」
ハスケル子「それはそうなんですが、あまり現実世界に似せることにこだわると」
ハスケル子「現実世界のデメリットまで模倣してしまうことになるんです」
ハスケル子「たとえば、人間は一人一人が意思や意見を持っているから」
ハスケル子「みんなに関係があるような重大な決定をするときには」
ハスケル子「わざわざ選挙をして、みんなの意見を集計しなければいけませんよね?」
ハスケル子「でも、もしみんなの意思があらかじめ一括管理されていれば、集計なんて一瞬のはずです」
ハスケル子「そんなSF映画みたいな話、現実にはあり得ませんけどね」
ハスケル子「でも、そんな現実世界にはあり得ないような便利なものを作りたいから、プログラミングをするんですよね?」
ワイ「た、確かに・・・」
話を戻すと
ハスケル子「初期値に戻すボタンを作るときの話でしたね」
ハスケル子「ページ側から、全部スイッチOFF!ってしたいときは」
ハスケル子「やっぱりpropsで渡したいじゃないですか」
ワイ「せやな・・・」
ワイ「$refs
を使って」
スイッチA君、値をリセットしてね。
スイッチB君も値をリセットしてね。
スイッチC君も値をリセットしてね。
ワイ「ってやるよりも」
ワイ「ページ側のdataを変えたら、それと連動して自動的に変わってほしいよな・・・」
ハスケル子「はい」
ハスケル子「コンポーネントって使い回すために作るわけですから」
ハスケル子「親からの使いやすさを意識したいですもんね」
ワイ「ほんまやな・・・」
だがしかし
ワイ「せやけどハスケル子ちゃん!」
ワイ「子コンポーネントでpropsを受け取らんくても」
ワイ「Vuexを使えば出来るんちゃうか!」
ワイ「ToggleSwitchコンポーネントから、Vuexストアの状態を書き換えて」
ワイ「そしたらページの方も、Vuexストアの情報と連動して状態が変わる・・・」
ワイ「確か、そんなことできたよな!」
ハスケル子「はい」
ハスケル子「でも、ToggleSwitchコンポーネントは1つなのに」
ハスケル子「ストアの中のどの状態を変えるかを、どうやってコントロールするんですか?」
ハスケル子「今回の例で言えば、通知の状態を変えるのか」
ハスケル子「検索フィルターの状態を変えるのか、それともダークモードを変えるのか」
ハスケル子「それをどうコントロールするんですか・・・?」
ワイ「そ、それは・・・」
ワイ「ToggleSwitchコンポーネントを呼び出すときにpropsを渡して・・・」
ワイ「あ・・・結局propsが必要になってもうた・・・」
ハスケル子「ですよね」
ハスケル子「しかも、特定のVuexストアを意識したコンポーネントにしてしまうと」
ハスケル子「今回のプロジェクトと密結合というか」
ハスケル子「そのVuexストアとセットじゃないと成り立たないコンポーネントになってしまって」
ハスケル子「他の案件で再利用しづらいコンポーネントになっちゃいます」
ワイ「ほんまや。。。」
まとめ
- ページ側から操作したいような状態を、子コンポーネントのdataに持たせない。
- 親コンポーネントからpropsを通じて操作できるように子コンポーネントを作る。
- 子コンポーネントは、親から受け取った情報を映し出す、UI付きの単なるディスプレイ。
- 子コンポーネントに記憶装置(data)は不要なケースも多い。
ハスケル子「って感じですね」
ハスケル子「でも、間違ってたらすみません・・・」
ハスケル子「何しろ、今日初めてVue.jsを触ったので・・・」
ワイ「・・・」
ワイ「すご過ぎか!!!」
〜おしまい〜
明日の記事
ゆめみアドベントカレンダー2日目は@su_mi1228さんが登場や。
ためになる記事やから要チェックやで!
フロントエンドエンジニア1年目はコードレビューでどんな指摘を受けるのか
-
参考URL:コンポーネントで v-model を使う ↩