Chapter 06

条件付きレンダリング

OJK
OJK
2021.11.08に更新

今回と次回はこれまでとは経路が異なり、HTML 側にプログラミングの制御構文を持ち込む方法について学びます。今回は条件分岐(if 文)です。

雛形コード
index.html
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>petite-vue入門</title>
</head>
<body>

  <p>Petite Vue!!</p>

  <script src="https://unpkg.com/petite-vue"></script>
  <script src="script.js"></script>
</body>
</html>
script.js
'use strict';

PetiteVue.createApp({

}).mount();

HTML 要素の表示/非表示を切り替える

ウェブアプリケーションで画面の表示を切り替えるときは、ウェブサイトのように複数の HTML ファイルをリンクで切り替えるのではなく、HTML 要素単位で表示/非表示を切り替えるのが一般的です。

これは CSS の display プロパティを変更することによって実現可能です。v-bind と v-on を使って書いてみましょう。

HTML
<button @click="isShow = !isShow">切替</button>
<p>↓↓↓</p>
<p :style="isShow ? {display: 'block'} : {display: 'none'}">
  表示/非表示
</p>
<p>↑↑↑</p>
JS
PetiteVue.createApp({
  isShow: true
}).mount();

この例では、isShow というデータプロパティの true/false によって CSS の display プロパティの値を切り替えています。v-bind のところで三項演算子を使ってみましたが読み取れるでしょうか。

また、HTML 要素を DOM ツリーへ追加/削除することで表示を切り替える方法もあります。

HTML
<button @click="show">切替</button>
<p>↓↓↓</p>
<p>表示/非表示</p>
<p>↑↑↑</p>
JS
PetiteVue.createApp({
  el: document.querySelectorAll('p')[1], // 2番目のp要素
  rf: document.querySelectorAll('p')[2], // 3番目のp要素
  isShow: true,
  show() {
    if (this.isShow) {
      document.body.removeChild(this.el);
    } else {
      document.body.insertBefore(this.el, this.rf);
    }
    this.isShow = !this.isShow;
  },
}).mount();

こちらはちょっと難しいですね。JavaScript の復習がてら読み解いてみてください。

もちろん、こんな長いコードを書くのは面倒だから petite-vue でサクッと書きたい…というのが今回の話です。

v-if ディレクティブ

petite-vue では v-if ディレクティブを使って HTML 要素の表示/非表示の切り替えが簡単に行なえます。ディレクティブという名のとおり、HTML 要素の中に記述する制御命令です。

v-ifディレクティブ
<HTML要素名 v-if="ブール値">コンテント</HTML要素名>

v-if に指定する値はブール値(true/false)です。もちろんブール値を直接記述しても意味がないので、Boolean 型のデータプロパティを指定してその値を制御するか、ブール値を返す JavaScript の式(三項演算子やメソッド、ゲッター)を指定します。

次のサンプルコードでは Boolean 型のデータプロパティ isShow を v-if に指定し、click イベントで isShow の値を切り替えています。

HTML
<button @click="isShow = !isShow">切替</button>
<p>↓↓↓</p>
<p v-if="isShow">表示/非表示</p>
<p>↑↑↑</p>
JS
PetiteVue.createApp({
  isShow: true
}).mount();

Chrome の開発者モードの Elements タブ(Firebox ならインスペクタータブ)を見ながら表示/非表示を切り替えてみてください。v-if の付けられた p 要素が HTML から消えたり現れたりすることがわかります。これは、CSS の display プロパティを切り替えているのではなく、DOM ツリーから削除/追加されているためです。

v-if の追加された要素に子孫要素がある場合、子孫要素もまとめて表示/非表示が切り替わります。

HTML
<button @click="isShow = !isShow">切替</button>
<ul v-if="isShow">
  <li></li>
  <li></li>
  <li></li>
</ul>

template 要素と v-if

複数の HTML 要素を一括して表示/非表示させたいときに適当な親要素がない場合、v-if を付けるためだけに仮の親要素(div 要素)を用意する必要はありません。template 要素 を使います。

HTML
<button @click="isShow = !isShow">切替</button>
<template v-if="isShow">
  <p>ぼくたちには</p>
  <p>共通する親といふものが</p>
  <p>いないのである</p>
</template>

Chrome の Elements タブ(Firebox ならインスペクタータブ)を確認してください。template 要素のタグは表示されず、p 要素が 3 つ並んでいるだけであることがわかります。


template 要素で囲んだ場合

template 要素の代わりに div 要素を仮の親要素とすると、petite-vue の処理が終わったあと、HTML コードに不要な div タグが残ってしまいます。


div 要素で囲んだ場合

template 要素についての説明は割愛しますが、このように中身だけを JavaScript から利用するために使われます。詳しく知りたい人は こちらのブログ が丁寧に解説されています。

なお、template 要素に v-if を使用した場合、その子要素に更に v-if を使うとエラーになるようです(孫要素以降なら大丈夫です)。本家 Vue ではこの問題は生じないので、petite-vue 独自の仕様、あるいは単純にバグかもしれません。

v-else ディレクティブ

if があるなら else もあるでしょう…ということで、v-else ディレクティブも用意されています。素の JavaScript の else 文と同じく、v-else には値は指定しません。

HTML
<button @click="isShow = !isShow">切替</button>
<p v-if="isShow">こちらが表示されているときは</p>
<p v-else>こちらは表示されない</p>
JS
PetiteVue.createApp({
  isShow: true
}).mount();

v-else は、v-if の付けられた要素の直後の兄弟要素に付けなければ機能しません。以下の例は「直後」ではないため、v-else は無視されます。

HTML
<p v-if="isShow">こちらが表示されているときは</p>
<p>あいだに他の兄弟要素が入ると</p>
<p v-else>ちゃんと機能しない</p>

兄弟関係と親子関係の区別をきちんとつけましょう。以下の例は兄弟関係ではなく親子関係なので v-else は機能しません。

HTML
<div v-if="isShow">
  ここはdiv要素の中です
  <p v-else>
    私は兄弟ではなく子です
  </p>
</div>

どれだけ離れていても、連続した兄弟要素ならちゃんと動作します。

HTML
<div v-if="isShow">
  <p>こういうのは</p>
  <!-- 長いコード -->
</div>
<div v-else>
  <p>ちゃんと兄弟</p>
  <!-- 長いコード -->
</div>

v-else-if ディレクティブ

もちろん v-else-if ディレクティブ もあります。

HTML
<input type="radio" name="sisters" v-model="sister" value="1">長女
<input type="radio" name="sisters" v-model="sister" value="2">次女
<input type="radio" name="sisters" v-model="sister" value="3">三女

<p v-if="sister == 1">長女です</p>
<p v-else-if="sister == 2">次女です</p>
<p v-else>三女です</p>
JS
PetiteVue.createApp({
  sister: 2
}).mount();

素の JavaScript の else-if 文とルールは同じで、v-else-if はいくつでも書けます。また、v-else は必須ではありません。

HTML
<p v-if="sister == 1">長女です</p>
<p v-else-if="sister == 2">次女です</p>
<p v-else-if="sister == 3">三女です</p>
<p v-else-if="sister == 4">四女です</p>

v-show ディレクティブ

v-if とよく似た機能を持つ v-show ディレクティブ もあります。

v-ifディレクティブ
<HTML要素名 v-show="ブール値">コンテント</要素名>

v-if が v-show に変わっただけで、使い方もほぼ同じです。表面的な使用上の違いは次の 2 点です。

  • v-else、v-else-if に当たるものがない
  • template 要素には付けられない(忘れがちなので注意)

ただし、裏側の動作は全く異なります。すでに述べたように、v-if は DOM ツリーに追加/削除することで HTML 要素の表示/非表示を切り替えます[1]。それに対して、v-show は CSS の display プロパティを変更することで表示を切り替えます。CSS の操作は DOM の操作よりも軽いので、頻繁に表示/非表示を繰り返すような用途では v-if よりも v-show を使用したほうが効率的です。

一方、v-if の強みは、初期状態が非表示(v-if=false)の場合、その HTML 要素に関する処理が一切実行されないことです。v-show は、値が false の HTML 要素もひとまず全て描画され、あとから CSS で非表示にされます。つまり、v-if のほうが v-show よりもアプリケーション起動時の読み込みが速いのです。

したがって v-if の使いどころは、アプリケーション起動時に非表示で、かつ、頻繁に切り替えない HTML 要素です。そんなものあるかな?と思うかもしれませんが、タブやメニューで画面を切り替えるアプリケーションは一般的ですし、ボタンを押して正解を表示するクイズアプリも初心者には人気の習作課題です。

@mounted と @unmounted

v-if は DOM ツリーへの追加/削除であるという話をしたところで、@mounted と @unmonted というディレクティブを紹介しておきます。これは、HTML 要素が DOM ツリーに追加(マウント)されたとき、DOM ツリーから削除(アンマウント)されたときに呼び出すメソッド(もしくは式)を登録するディレクティブです。HTLM 要素を表示する/削除すると同時に何かしらの処理をさせたいときに使います。

@mounted と @unmouned
<HTML要素名 @mounted="DOMツリーへの追加時に実行">
<HTML要素名 @mounted="DOMツリーから削除時に実行">

頭に @ が付いていることから察するに、これらは v-on イベントの一種という扱いだと思います。v-on と使い方も同じです。

以下のサンプルコードで動作が確認できます。

HTML
<button @click="isShow=!isShow">{{command}}</button>
<p v-if="isShow"
   @mounted="command = 'アンマウントする'"
   @unmounted="command = 'マウントする'">
  表示/非表示
</p>
JS
PetiteVue.createApp({
  command: '',
  isShow: true,
}).mount();

なお、command の初期値が空文字なのに、起動時からボタンに「アンマウントする」と表示されているのは、isShow の初期値が true なので、ボタンを押す以前に(ブラウザが画面を描画した時点で)DOM ツリーに p 要素がマウントされるからです。

言い換えれば、@mounted は、v-if の使用の有無に関係なく全ての HTML 要素に適用でき、ブラウザが最初に画面を描画したときに実行するメソッドを指定できます。したがって、petite-vue を適用している HTML 要素(本講座ではほぼ body 要素)に @mounted を付けておけば、アプリケーションの初期化処理が行えるということです。

HTML
<body @mounted="init">
<!-- 関数登録でも関数呼び出しでも可 -->
JS
PetiteVue.createApp({
  init() {
    // アプリケーション起動時に行いたい処理
  }
}).mount();

以前、チャプター 4 で「オブジェクト内のプロパティ定義で他のプロパティは使用できない」と説明しましたが、body 要素の @mounted を使えば以下のように読み込み時にメソッド内で値を設定することが可能です。

JS
PetiteVue.createApp({
  init() {
    this.styleList = [this.styleObj1, this.styleObj2]; // これは可
  },
  // styleList: [styleObj1, styleObj2];  // これは不可
  styleList: [],
  styleObj1: { border: 'solid 2px royalblue' },
  styleObj2: { color: 'firebrick' }
}).mount();
脚注
  1. 実際の v-if の実装は removeChild と insertBefore を使ったような単純なものではないと思います。 ↩︎