第5章: データバインディング & 条件分岐

第5章: データバインディング & 条件分岐

概要

本章では、Vue.jsのデータバインディングと条件分岐について学びます。データバインディングを使用してJavaScriptの状態とDOMを連携させる方法、条件分岐を使用して要素の表示・非表示を制御する方法について詳しく理解します。

1. データバインディングの基本

データバインディングとは、JavaScriptの状態(データ)とDOM(HTML要素)を連携させるメカニズムです。Vue.jsでは、さまざまな方法でデータバインディングを実装できます。

テキスト補間(マスタッシュ構文)

最も基本的なデータバインディングは、「Mustache構文」(二重中括弧 {{ }})を使用したテキスト補間です。

<script setup>
import { ref } from 'vue'

const message = ref('こんにちは、Vue!')
const userName = ref('ゲスト')
</script>

<template>
  <p>メッセージ: {{ message }}</p>
  <p>ようこそ、{{ userName }}さん</p>
</template>

二重中括弧内では、シンプルなJavaScript式も使用できます:

<template>
  <p>{{ message.split('').reverse().join('') }}</p>
  <p>{{ userName.toUpperCase() }}</p>
  <p>1 + 1 = {{ 1 + 1 }}</p>
  <p>三項演算子: {{ userName === 'ゲスト' ? 'ログインしていません' : 'ログイン済み' }}</p>
</template>

注意点:

v-textディレクティブ

v-textディレクティブを使用すると、要素のテキストコンテンツを設定できます。これは{{ }}構文の代替手段として使用できます。

<span v-text="message"></span>

<!-- 上記は以下と同等 -->
<span>{{ message }}</span>

v-htmlディレクティブ

v-htmlディレクティブを使用すると、要素のinnerHTMLを設定できます。これにより、HTML文字列をレンダリングできます。

<script setup>
import { ref } from 'vue'

const rawHtml = ref('<span style="color: red">これは赤色のテキストです</span>')
</script>

<template>
  <p>Using mustaches: {{ rawHtml }}</p>
  <p>Using v-html: <span v-html="rawHtml"></span></p>
</template>

重要な注意点: v-htmlを使用する際は、XSS(クロスサイトスクリプティング)攻撃のリスクに注意してください。信頼できないコンテンツには絶対に使用しないでください。ユーザー入力から生成されたHTMLは常にサニタイズしてください。

2. 属性バインディング

v-bindディレクティブを使用すると、HTML属性をJavaScriptの式にバインドできます。

<script setup>
import { ref } from 'vue'

const imageUrl = ref('/images/logo.png')
const buttonId = ref('submit-button')
const isButtonDisabled = ref(false)
</script>

<template>
  <!-- 基本的な属性バインディング -->
  <img v-bind:src="imageUrl" alt="ロゴ">
  
  <!-- 短縮構文 -->
  <img :src="imageUrl" alt="ロゴ">
  
  <!-- 複数の属性をバインド -->
  <button :id="buttonId" :disabled="isButtonDisabled">送信</button>
</template>

v-bindは非常によく使われるため、短縮構文(:)を使用することが一般的です。

動的属性名

Vue 3では、動的な属性名のバインディングもサポートされています。

<script setup>
import { ref } from 'vue'

const attributeName = ref('title')
const attributeValue = ref('これはツールチップです')
</script>

<template>
  <button :[attributeName]="attributeValue">ホバーしてみてください</button>
</template>

クラスとスタイルのバインディング

クラスとスタイルのバインディングは、Vue.jsで特別に拡張されています。

クラスバインディング
<script setup>
import { ref } from 'vue'

// オブジェクト構文
const isActive = ref(true)
const hasError = ref(false)

// 配列構文
const activeClass = ref('active')
const errorClass = ref('text-danger')
</script>

<template>
  <!-- オブジェクト構文: キーがクラス名、値が真偽値 -->
  <div :class="{ active: isActive, 'text-danger': hasError }">
    クラスバインディング(オブジェクト構文)
  </div>
  
  <!-- 配列構文 -->
  <div :class="[activeClass, errorClass]">
    クラスバインディング(配列構文)
  </div>
  
  <!-- 条件付きクラス(配列内の三項演算子) -->
  <div :class="[isActive ? activeClass : '', hasError ? errorClass : '']">
    条件付きクラスバインディング
  </div>
  
  <!-- 配列内のオブジェクト -->
  <div :class="[{ active: isActive }, errorClass]">
    複合クラスバインディング
  </div>
</template>
スタイルバインディング
<script setup>
import { ref, reactive } from 'vue'

// オブジェクト構文
const activeColor = ref('red')
const fontSize = ref(30)

// オブジェクト参照
const styleObject = reactive({
  color: 'blue',
  fontSize: '20px',
  backgroundColor: '#f0f0f0'
})
</script>

<template>
  <!-- オブジェクト構文(キャメルケース) -->
  <div :style="{ color: activeColor, fontSize: fontSize + 'px' }">
    スタイルバインディング(オブジェクト構文)
  </div>
  
  <!-- オブジェクト構文(ケバブケース、引用符が必要) -->
  <div :style="{ 'font-size': fontSize + 'px' }">
    スタイルバインディング(ケバブケース)
  </div>
  
  <!-- オブジェクト参照 -->
  <div :style="styleObject">
    スタイルオブジェクト参照
  </div>
  
  <!-- 配列構文(複数のスタイルオブジェクトを適用) -->
  <div :style="[{ color: activeColor }, styleObject]">
    スタイルバインディング(配列構文)
  </div>
</template>

Vue.jsは自動的にベンダープレフィックスを必要とするCSSプロパティを検出し、適切なプレフィックスを追加します。

3. 条件付きレンダリング

条件付きレンダリングを使用すると、条件に基づいて要素の表示・非表示を制御できます。

v-if、v-else-if、v-else

v-ifディレクティブを使用すると、条件に基づいて要素を条件付きでレンダリングできます。

<script setup>
import { ref } from 'vue'

const type = ref('A')
const isAuthenticated = ref(false)
const userRole = ref('user')
</script>

<template>
  <div>
    <h2>v-if の例</h2>
    
    <div v-if="type === 'A'">
      タイプ A
    </div>
    <div v-else-if="type === 'B'">
      タイプ B
    </div>
    <div v-else-if="type === 'C'">
      タイプ C
    </div>
    <div v-else>
      タイプ A/B/C ではありません
    </div>
    
    <h2>認証状態に基づく表示</h2>
    
    <div v-if="isAuthenticated">
      <p>ログイン済みです</p>
      
      <div v-if="userRole === 'admin'">
        管理者向けコンテンツ
      </div>
      <div v-else-if="userRole === 'editor'">
        編集者向けコンテンツ
      </div>
      <div v-else>
        一般ユーザー向けコンテンツ
      </div>
    </div>
    <div v-else>
      <p>ログインしていません</p>
      <button @click="isAuthenticated = true">ログイン</button>
    </div>
  </div>
</template>

v-ifは「真の条件付きレンダリング」です。条件がfalseの場合、要素は完全にDOMから削除されます。

テンプレートでのv-if

複数の要素を条件付きでレンダリングする場合は、<template>要素を使用できます。

<template>
  <template v-if="isAuthenticated">
    <h2>プロフィール</h2>
    <p>ユーザー名: {{ userName }}</p>
    <p>メール: {{ email }}</p>
  </template>
</template>

<template>要素自体はレンダリングされず、その中の要素のみがレンダリングされます。

v-show

v-showディレクティブも条件付きレンダリングに使用できますが、v-ifとは動作が異なります。

<template>
  <h2>v-show の例</h2>
  <div v-show="isAuthenticated">
    認証済みユーザーのみに表示されるコンテンツ
  </div>
</template>

v-showは、要素が常にDOMに存在し、CSSのdisplayプロパティを切り替えるだけです。

v-ifとv-showの違い

ディレクティブ 初期レンダリングコスト 切り替えコスト 使用ケース
v-if 条件がfalseの場合、レンダリングされない(低コスト) 要素の作成/破棄が必要(高コスト) 条件が実行時に変更されない場合や、頻繁に切り替えない場合
v-show 常にレンダリングされる(高コスト) CSSの切り替えのみ(低コスト) 頻繁に切り替える場合(トグルスイッチなど)

4. リストレンダリングの基礎

v-forディレクティブを使用すると、配列やオブジェクトに基づいて要素のリストをレンダリングできます。この詳細は次の章で学びますが、ここでは簡単な例を紹介します。

<script setup>
import { ref } from 'vue'

const items = ref(['りんご', 'バナナ', 'オレンジ'])
</script>

<template>
  <ul>
    <li v-for="(item, index) in items" :key="index">
      {{ index }}: {{ item }}
    </li>
  </ul>
</template>

5. フォームの入力バインディング

v-modelディレクティブを使用すると、フォーム入力要素とデータを双方向バインディングできます。

<script setup>
import { ref } from 'vue'

const message = ref('')
const checked = ref(false)
const selected = ref('')
const multiSelected = ref([])
</script>

<template>
  <div>
    <h2>テキスト入力</h2>
    <input v-model="message" placeholder="メッセージを入力">
    <p>メッセージ: {{ message }}</p>
    
    <h2>複数行テキスト</h2>
    <textarea v-model="message" placeholder="複数行メッセージ"></textarea>
    <p style="white-space: pre-line">{{ message }}</p>
    
    <h2>チェックボックス</h2>
    <input type="checkbox" id="checkbox" v-model="checked">
    <label for="checkbox">{{ checked ? '選択済み' : '未選択' }}</label>
    
    <h2>セレクトボックス(単一選択)</h2>
    <select v-model="selected">
      <option disabled value="">選択してください</option>
      <option>りんご</option>
      <option>バナナ</option>
      <option>オレンジ</option>
    </select>
    <p>選択: {{ selected }}</p>
    
    <h2>セレクトボックス(複数選択)</h2>
    <select v-model="multiSelected" multiple>
      <option>りんご</option>
      <option>バナナ</option>
      <option>オレンジ</option>
    </select>
    <p>選択: {{ multiSelected }}</p>
  </div>
</template>

v-modelの修飾子

v-modelには、入力の振る舞いを調整するための修飾子があります。

<script setup>
import { ref } from 'vue'

const message = ref('')
const age = ref(null)
const trimmed = ref('')
</script>

<template>
  <div>
    <h2>.lazy修飾子</h2>
    <!-- 入力イベントではなく、changeイベント後に同期 -->
    <input v-model.lazy="message">
    <p>{{ message }}</p>
    
    <h2>.number修飾子</h2>
    <!-- 入力を数値として自動的に型変換 -->
    <input v-model.number="age" type="number">
    <p>年齢: {{ age }}, 型: {{ typeof age }}</p>
    
    <h2>.trim修飾子</h2>
    <!-- 入力の前後の空白を自動的に削除 -->
    <input v-model.trim="trimmed">
    <p>トリミング後: "{{ trimmed }}"</p>
  </div>
</template>

6. 計算プロパティと条件分岐の組み合わせ

計算プロパティを使用して、複雑な条件分岐ロジックをテンプレートから分離できます。

<script setup>
import { ref, computed } from 'vue'

const score = ref(75)

// 計算プロパティを使用して成績を決定
const grade = computed(() => {
  if (score.value >= 90) return 'A'
  if (score.value >= 80) return 'B'
  if (score.value >= 70) return 'C'
  if (score.value >= 60) return 'D'
  return 'F'
})

// 計算プロパティを使用して色を決定
const gradeColor = computed(() => {
  switch (grade.value) {
    case 'A': return 'green'
    case 'B': return 'blue'
    case 'C': return 'orange'
    case 'D': return 'red'
    default: return 'gray'
  }
})
</script>

<template>
  <div>
    <h2>成績計算</h2>
    <p>
      <label for="score">点数: </label>
      <input id="score" v-model.number="score" type="number" min="0" max="100">
    </p>
    
    <p :style="{ color: gradeColor }">
      成績: {{ grade }}
    </p>
    
    <p v-if="grade === 'A'">
      素晴らしい!よく頑張りました!
    </p>
    <p v-else-if="grade === 'B' || grade === 'C'">
      良い成績です。もう少し頑張りましょう!
    </p>
    <p v-else>
      次回はもっと勉強しましょう。
    </p>
  </div>
</template>

7. 実践的な例: シンプルなログインフォーム

データバインディングと条件分岐を組み合わせて、シンプルなログインフォームを作成してみましょう。

<script setup>
import { ref, computed } from 'vue'

// リアクティブな状態
const username = ref('')
const password = ref('')
const rememberMe = ref(false)
const isSubmitting = ref(false)
const loginError = ref(null)
const isLoggedIn = ref(false)
const currentUser = ref(null)

// バリデーション
const isFormValid = computed(() => {
  return username.value.trim() !== '' && password.value.length >= 6
})

// ログイン処理(ダミー)
function login() {
  if (!isFormValid.value) return
  
  // ログイン処理中フラグをセット
  isSubmitting.value = true
  loginError.value = null
  
  // 非同期処理をシミュレート
  setTimeout(() => {
    // ダミーのバリデーション(実際の実装ではAPIリクエストなどを行う)
    if (username.value === 'admin' && password.value === 'password') {
      // ログイン成功
      isLoggedIn.value = true
      currentUser.value = {
        username: username.value,
        role: 'admin'
      }
      loginError.value = null
    } else {
      // ログイン失敗
      loginError.value = 'ユーザー名またはパスワードが正しくありません'
      isLoggedIn.value = false
      currentUser.value = null
    }
    
    isSubmitting.value = false
  }, 1000)
}

// ログアウト処理
function logout() {
  isLoggedIn.value = false
  currentUser.value = null
  username.value = ''
  password.value = ''
  rememberMe.value = false
}
</script>

<template>
  <div class="login-container">
    <h2>{{ isLoggedIn ? 'ようこそ!' : 'ログイン' }}</h2>
    
    <!-- ログインフォーム:ログインしていない場合のみ表示 -->
    <form v-if="!isLoggedIn" @submit.prevent="login">
      <!-- エラーメッセージ -->
      <div v-if="loginError" class="error-message">
        {{ loginError }}
      </div>
      
      <!-- ユーザー名フィールド -->
      <div class="form-group">
        <label for="username">ユーザー名</label>
        <input
          id="username"
          v-model="username"
          type="text"
          :disabled="isSubmitting"
          placeholder="ユーザー名を入力"
          required
        >
      </div>
      
      <!-- パスワードフィールド -->
      <div class="form-group">
        <label for="password">パスワード</label>
        <input
          id="password"
          v-model="password"
          type="password"
          :disabled="isSubmitting"
          placeholder="パスワードを入力"
          required
        >
        <small v-show="password.length > 0 && password.length < 6" class="error-message">
          パスワードは6文字以上必要です
        </small>
      </div>
      
      <!-- リメンバーミーチェックボックス -->
      <div class="form-group">
        <label class="checkbox-label">
          <input
            type="checkbox"
            v-model="rememberMe"
            :disabled="isSubmitting"
          >
          ログイン情報を記憶する
        </label>
      </div>
      
      <!-- 送信ボタン -->
      <div class="form-group">
        <button
          type="submit"
          :disabled="!isFormValid || isSubmitting"
          class="login-button"
        >
          {{ isSubmitting ? 'ログイン中...' : 'ログイン' }}
        </button>
      </div>
      
      <!-- ヒント -->
      <div class="hint">
        <small>ヒント: username = "admin", password = "password"</small>
      </div>
    </form>
    
    <!-- ログイン後の表示 -->
    <div v-else class="welcome-container">
      <p>ようこそ、<strong>{{ currentUser.username }}</strong> さん!</p>
      <p v-if="currentUser.role === 'admin'">管理者権限があります。</p>
      <button @click="logout" class="logout-button">ログアウト</button>
    </div>
  </div>
</template>

<style scoped>
.login-container {
  max-width: 400px;
  margin: 0 auto;
  padding: 20px;
  border: 1px solid #ddd;
  border-radius: 8px;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}

.form-group {
  margin-bottom: 15px;
}

label {
  display: block;
  margin-bottom: 5px;
  font-weight: bold;
}

.checkbox-label {
  display: flex;
  align-items: center;
  font-weight: normal;
}

.checkbox-label input {
  margin-right: 8px;
}

input[type="text"],
input[type="password"] {
  width: 100%;
  padding: 8px;
  border: 1px solid #ddd;
  border-radius: 4px;
  font-size: 16px;
}

.login-button,
.logout-button {
  background-color: #42b983;
  color: white;
  border: none;
  border-radius: 4px;
  padding: 10px 15px;
  font-size: 16px;
  cursor: pointer;
  width: 100%;
}

.login-button:hover,
.logout-button:hover {
  background-color: #36a073;
}

.login-button:disabled {
  background-color: #ccc;
  cursor: not-allowed;
}

.error-message {
  color: #ff4c4c;
  margin-top: 5px;
  font-size: 14px;
}

.hint {
  margin-top: 15px;
  text-align: center;
  color: #666;
}

.welcome-container {
  text-align: center;
}
</style>

8. ディレクティブの修飾子

Vue.jsのディレクティブには、特別な接尾辞(修飾子)を付けることで、その挙動をカスタマイズできます。修飾子は . で繋げて指定します。

イベント修飾子

イベントハンドリング(v-on または @)では、以下のような修飾子が使用できます:

<!-- クリックイベントのデフォルト動作を防止 -->
<a @click.prevent="handleClick">リンク</a>

<!-- イベントの伝播を停止 -->
<div @click="outerClick">
  <button @click.stop="innerClick">ボタン</button>
</div>

<!-- キーイベント修飾子 -->
<input @keyup.enter="submit">

<!-- マウスボタン修飾子 -->
<div @click.right="showContextMenu">右クリックメニュー</div>

v-bind修飾子

バインディング(v-bind または :)には以下のような修飾子があります:


                
<!-- プロパティとして束縛(innerHTML などの DOM プロパティに束縛)-->
<div :text-content.prop="message"></div>

<!-- 両方向バインディング(v-model の代替) -->
<input :value.sync="message">

9. Vue.jsディレクティブの省略記法一覧

Vue.jsでは、頻繁に使用されるディレクティブには省略記法が用意されています。以下の表は、完全な構文と省略記法の対比です。

機能 完全な構文 省略記法
属性バインディング v-bind:属性名 :属性名 v-bind:href="url"
:href="url"
イベントバインディング v-on:イベント名 @イベント名 v-on:click="handleClick"
@click="handleClick"
スロット v-slot:スロット名 #スロット名 v-slot:header
#header
動的な属性バインディング v-bind:[動的属性名] :[動的属性名] v-bind:[attrName]="value"
:[attrName]="value"
動的なイベントバインディング v-on:[動的イベント名] @[動的イベント名] v-on:[eventName]="handler"
@[eventName]="handler"

省略記法を使用すると、テンプレートはより簡潔になり、読みやすくなります。特にv-bindv-onは非常によく使われるため、それらの省略記法を覚えておくと効率的にコーディングできます。

10. まとめ

本章では、Vue.jsのデータバインディングと条件分岐について学びました。マスタッシュ構文({{ }})を使用したテキスト補間、v-bindを使用した属性バインディング、v-if/v-else/v-showを使用した条件付きレンダリング、v-modelを使用したフォーム入力バインディングなど、Vue.jsの基本的なディレクティブについて理解しました。

また、計算プロパティと条件分岐を組み合わせた実践的な例や、リアルなログインフォームの実装例を通じて、これらの概念がどのように連携するかも学びました。さらに、よく使用されるディレクティブの省略記法についても確認し、効率的なコーディングのためのヒントを得ました。

次の章では、リストレンダリングとイベントハンドリングについて詳しく学びます。

目次に戻る