【Vue.js】v-bind, v-model, v-for, v-if

2025-07-29

はじめに

Vue.jsのディレクティブは、HTML要素に特別な振る舞いを追加するための命令です。この記事では、Vue.jsで最もよく使用される4つのディレクティブ(v-bind, v-model, v-for, v-if)について、詳細な解説と実践的なデモを通じて学びます。

v-bindディレクティブ

基本構文

v-bindは、HTML属性をVueインスタンスのデータにバインドするために使用します。省略記法として:が使用できます。

<!-- 完全な記法 -->
<a v-bind:href="url">リンク</a>

<!-- 省略記法 -->
<a :href="url">リンク</a>

実践的な例

<div id="app">
  <img :src="imageSrc" :alt="imageAlt" :width="imageWidth">
  <button :disabled="isButtonDisabled">送信</button>
</div>

<script>
  const { createApp } = Vue;

  createApp({
    data() {
      return {
        imageSrc: 'https://vuejs.org/images/logo.png',
        imageAlt: 'Vue.jsロゴ',
        imageWidth: 100,
        isButtonDisabled: true
      }
    }
  }).mount('#app');
</script>

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

v-bindはクラスやスタイルのバインディングにも特に便利です。

<div id="app">
  <div 
    :class="{ active: isActive, 'text-danger': hasError }" 
    :style="{ color: textColor, fontSize: fontSize + 'px' }">
    動的なクラスとスタイル
  </div>
</div>

<script>
  const { createApp } = Vue;

  createApp({
    data() {
      return {
        isActive: true,
        hasError: false,
        textColor: 'blue',
        fontSize: 20
      }
    }
  }).mount('#app');
</script>

v-modelディレクティブ

基本構文

v-modelは、フォーム入力とアプリケーションデータの双方向バインディングを実現します。

<input v-model="message" placeholder="編集してみてください">
<p>入力されたメッセージ: {{ message }}</p>

様々な入力タイプでの使用

<div id="app">
  <h2>フォーム入力デモ</h2>

  <div>
    <label>テキスト:</label>
    <input v-model="textValue" type="text">
  </div>

  <div>
    <label>チェックボックス:</label>
    <input v-model="checked" type="checkbox">
    {{ checked ? 'ON' : 'OFF' }}
  </div>

  <div>
    <label>複数チェックボックス:</label>
    <div v-for="option in options" :key="option.id">
      <input 
        type="checkbox" 
        v-model="selectedOptions" 
        :value="option.id" 
        :id="'option-' + option.id">
      <label :for="'option-' + option.id">{{ option.label }}</label>
    </div>
    選択済み: {{ selectedOptions }}
  </div>

  <div>
    <label>ラジオボタン:</label>
    <div v-for="item in radioItems" :key="item.value">
      <input 
        type="radio" 
        v-model="radioValue" 
        :value="item.value" 
        :id="'radio-' + item.value">
      <label :for="'radio-' + item.value">{{ item.label }}</label>
    </div>
    選択済み: {{ radioValue }}
  </div>

  <div>
    <label>セレクトボックス:</label>
    <select v-model="selectedItem">
      <option disabled value="">選択してください</option>
      <option v-for="item in selectItems" :key="item.value" :value="item.value">
        {{ item.label }}
      </option>
    </select>
    選択済み: {{ selectedItem }}
  </div>
</div>

<script>
  const { createApp } = Vue;

  createApp({
    data() {
      return {
        textValue: '',
        checked: false,
        options: [
          { id: 1, label: 'オプション1' },
          { id: 2, label: 'オプション2' },
          { id: 3, label: 'オプション3' }
        ],
        selectedOptions: [],
        radioItems: [
          { value: 'A', label: '選択肢A' },
          { value: 'B', label: '選択肢B' },
          { value: 'C', label: '選択肢C' }
        ],
        radioValue: 'A',
        selectItems: [
          { value: 'apple', label: 'りんご' },
          { value: 'banana', label: 'バナナ' },
          { value: 'orange', label: 'オレンジ' }
        ],
        selectedItem: ''
      }
    }
  }).mount('#app');
</script>

v-forディレクティブ

基本構文

v-forは、配列やオブジェクトのアイテムを基に要素を繰り返しレンダリングします。

<!-- 配列のレンダリング -->
<ul>
  <li v-for="(item, index) in items" :key="item.id">
    {{ index + 1 }}. {{ item.text }}
  </li>
</ul>

<!-- オブジェクトのレンダリング -->
<ul>
  <li v-for="(value, key, index) in object">
    {{ index }}. {{ key }}: {{ value }}
  </li>
</ul>

実践的な例

<div id="app">
  <h2>商品一覧</h2>

  <div class="product-filter">
    <input v-model="filterText" placeholder="商品名でフィルタリング">
    <button @click="sortByPrice">価格でソート</button>
  </div>

  <ul class="product-list">
    <li v-for="product in filteredProducts" :key="product.id" class="product-item">
      <h3>{{ product.name }}</h3>
      <p>価格: {{ product.price | currency }}</p>
      <p>在庫: {{ product.stock }}個</p>
      <button @click="addToCart(product)">カートに追加</button>
    </li>
  </ul>

  <h3>カート ({{ cart.length }}点)</h3>
  <ul>
    <li v-for="(item, index) in cart" :key="index">
      {{ item.name }} - {{ item.price | currency }}
      <button @click="removeFromCart(index)">削除</button>
    </li>
  </ul>
</div>

<script>
  const { createApp } = Vue;

  createApp({
    data() {
      return {
        filterText: '',
        products: [
          { id: 1, name: 'ノートパソコン', price: 120000, stock: 10 },
          { id: 2, name: 'スマートフォン', price: 80000, stock: 15 },
          { id: 3, name: 'タブレット', price: 50000, stock: 8 },
          { id: 4, name: 'ワイヤレスイヤホン', price: 15000, stock: 20 },
          { id: 5, name: 'スマートウォッチ', price: 25000, stock: 12 }
        ],
        cart: [],
        sortAscending: true
      }
    },
    computed: {
      filteredProducts() {
        return this.products
          .filter(product => 
            product.name.toLowerCase().includes(this.filterText.toLowerCase())
          )
          .sort((a, b) => this.sortAscending ? a.price - b.price : b.price - a.price);
      }
    },
    methods: {
      addToCart(product) {
        if (product.stock > 0) {
          this.cart.push({...product});
          product.stock--;
        }
      },
      removeFromCart(index) {
        const item = this.cart[index];
        const product = this.products.find(p => p.id === item.id);
        if (product) {
          product.stock++;
        }
        this.cart.splice(index, 1);
      },
      sortByPrice() {
        this.sortAscending = !this.sortAscending;
      }
    },
    filters: {
      currency(value) {
        return '¥' + value.toLocaleString();
      }
    }
  }).mount('#app');
</script>

<style>
  .product-list {
    list-style: none;
    padding: 0;
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
    gap: 20px;
  }
  .product-item {
    border: 1px solid #ddd;
    padding: 15px;
    border-radius: 5px;
  }
  .product-filter {
    margin-bottom: 20px;
  }
</style>

v-ifディレクティブ

基本構文

v-ifは条件に基づいて要素の表示/非表示を制御します。v-else-ifv-elseと組み合わせて使用できます。

<div v-if="type === 'A'">Type A</div>
<div v-else-if="type === 'B'">Type B</div>
<div v-else>Type C</div>

v-if vs v-show

v-ifv-showの違いを理解することが重要です。

特徴v-ifv-show
DOMへの追加/削除条件がfalseならDOMから削除display: noneで非表示にする
初期レンダリング条件がfalseならレンダリングしない常にレンダリングされる
切り替えコスト高い(DOM操作が必要)低い(スタイル変更のみ)
使用例初期表示が不要な大きなコンポーネント頻繁に切り替わる要素

実践的な例

<div id="app">
  <h2>ユーザープロファイル</h2>

  <div v-if="isLoading" class="loading">
    読み込み中...
  </div>

  <div v-else-if="error" class="error">
    エラーが発生しました: {{ error }}
  </div>

  <div v-else>
    <div class="profile">
      <img :src="user.avatar" alt="プロフィール画像" class="avatar">
      <h3>{{ user.name }}</h3>
      <p>{{ user.bio }}</p>

      <div v-if="user.isAdmin" class="admin-badge">
        管理者
      </div>

      <div v-show="user.hasPosts">
        <h4>最近の投稿</h4>
        <ul>
          <li v-for="post in user.posts" :key="post.id">
            {{ post.title }}
          </li>
        </ul>
      </div>

      <div v-show="!user.hasPosts">
        投稿がありません
      </div>
    </div>
  </div>

  <button @click="fetchUser">再読み込み</button>
</div>

<script>
  const { createApp } = Vue;

  createApp({
    data() {
      return {
        isLoading: false,
        error: null,
        user: null
      }
    },
    created() {
      this.fetchUser();
    },
    methods: {
      fetchUser() {
        this.isLoading = true;
        this.error = null;

        // 模擬API呼び出し
        setTimeout(() => {
          try {
            // ランダムに成功または失敗
            if (Math.random() > 0.2) {
              this.user = {
                name: '山田太郎',
                avatar: 'https://i.pravatar.cc/150?img=3',
                bio: 'フロントエンド開発者。Vue.jsが大好きです。',
                isAdmin: true,
                hasPosts: true,
                posts: [
                  { id: 1, title: 'Vue.js入門' },
                  { id: 2, title: 'Vuexの使い方' },
                  { id: 3, title: 'Vue Router徹底解説' }
                ]
              };
            } else {
              throw new Error('ユーザーデータの取得に失敗しました');
            }
          } catch (err) {
            this.error = err.message;
          } finally {
            this.isLoading = false;
          }
        }, 1000);
      }
    }
  }).mount('#app');
</script>

<style>
  .profile {
    max-width: 500px;
    margin: 0 auto;
    padding: 20px;
    border: 1px solid #ddd;
    border-radius: 8px;
  }
  .avatar {
    width: 100px;
    height: 100px;
    border-radius: 50%;
    display: block;
    margin: 0 auto 15px;
  }
  .loading, .error {
    padding: 20px;
    text-align: center;
    background-color: #f5f5f5;
    border-radius: 8px;
  }
  .error {
    background-color: #ffebee;
    color: #c62828;
  }
  .admin-badge {
    display: inline-block;
    padding: 3px 8px;
    background-color: #ff5722;
    color: white;
    border-radius: 4px;
    font-size: 0.8em;
    margin-bottom: 15px;
  }
</style>

まとめ

この記事では、Vue.jsの主要な4つのディレクティブについて詳しく解説しました:

  1. v-bind: HTML属性を動的にバインド
  2. v-model: フォーム入力とデータの双方向バインディング
  3. v-for: リストやオブジェクトのアイテムを繰り返しレンダリング
  4. v-if: 条件に基づいて要素を表示/非表示

これらのディレクティブを組み合わせることで、動的でインタラクティブなWebアプリケーションを効率的に構築できます。実際のプロジェクトでは、これらの基本を理解した上で、より複雑なコンポーネントやアプリケーション構造へと進んでいくことになります。