Vue 3 テンプレート構文:補間、ディレクティブ、および省略形のクイックリファレンス
Vueのテンプレート構文は、HTMLの宣言型拡張です。標準のHTMLに「ディレクティブ」と「補間」を追加することで、データをDOMにレンダリングできるようになります。テンプレート構文の90%はHTMLと類似しているため、HTMLに慣れている人なら誰でも30分で使い始めることができます。
Vueのテンプレートの核心となるのは、補間(データの表示)+ディレクティブ(プロパティやイベントのバインディング)+式(JavaScriptの実行)という3つの重要な要素です。このレッスンでは、これら3つの要素を標準的な方法で使用する方法について解説します。
1. 学習内容
- マスタッシュ補間の4つの活用法
{{ }} - 3つのテキストディレクティブ「v-text」、「v-html」、「v-once」の違い
- v-bind:属性バインディング
:およびオブジェクト構文の略語 - v-on:イベントバインディング
@および修飾子の略語 - テンプレート内で使用できるJS式に関する制限(ステートメントは使用不可)
- 2つのボーダーディレクティブ「v-pre」と「v-cloak」の目的
2. Eコマースの商品カードの表示要件
(1) 課題:30種類の商品――30回もコピー&ペーストしなければならないのか?
アリスは、ECサイトの管理ダッシュボードを構築しています。彼女は30枚の商品カードを表示する必要があります。彼女の当初のアプローチは次のとおりです:
<!-- Copy and paste 30 times, nightmare... -->
<div class="product-card">
<h3>iPhone 15 Pro</h3>
<p>Price: $999</p>
<button>Edit</button>
</div>
<div class="product-card">
<h3>MacBook Pro</h3>
<p>Price: $2499</p>
<button>Edit</button>
</div>
<!-- ... And also 28 more ... -->
プロダクトマネージャーのチャーリーが「通貨記号が表示されるように価格の表示形式を変更してください」と言ったとき:
「アリス、30個のファイルを手作業で編集するのは無理だよ。それに、来月商品数が5,000個になったらどうするの? 動的なテンプレートが必要だ。」
(2) Vueテンプレートの解決策:コンポーネント1つ、データポイント30個
<template>
<div v-for="product in products" :key="product.id" class="product-card">
<h3>{{ product.name }}</h3>
<p>Price: ${{ product.price }}</p>
<button @click="editProduct(product.id)">Edit</button>
</div>
</template>
<script setup>
const products = ref([
{ id: 1, name: 'iPhone 15 Pro', price: 999 },
{ id: 2, name: 'MacBook Pro', price: 2499 },
// ... 30 items, each only 1 line 1 ...
])
function editProduct(id) {
console.log('Editing product', id)
}
</script>
テンプレートコード5行 × 30商品 = コピー&ペーストは一切不要。商品名や価格が変更されると、Vueが自動的にDOMを更新します。
(3) 収益
Vueテンプレートを採用した後:
- コード量:30 × 15行 = 450行 → テンプレート1つ + データ30件 = 60行(-87%)
- メンテナンスコスト:テンプレート1つのスタイルを変更 → 30点の商品が同時に更新される
- 新しい商品を追加:データを1行追加 → テンプレートが自動的に表示されます
3. 補間:Mustache {{ }}
(1) 補間を実装する4つの方法
| 構文 | 結果 | 例 |
|---|---|---|
{{ var }} |
変数の値を表示 | <p>{{ username }}</p> → アリス |
{{ expr }} |
JS式の結果を表示 | <p>{{ count + 1 }}</p> → 6 |
{{ fn() }} |
関数呼び出しの戻り値 | <p>{{ formatDate(date) }}</p> → 2026年7月2日 |
{{ obj.prop }} |
オブジェクトのプロパティへのアクセス | <p>{{ user.name }}</p> → アリス |
(2) 補間の5つの主な特徴
<template>
<!-- 1. Automatically Responsive:Automatically updates when variables change -->
<p>{{ message }}</p>
<!-- 2. Support JS Expression(Cannot write statements)-->
<p>{{ count * 2 + 1 }}</p>
<p>{{ isVip ? 'VIP' : 'Regular' }}</p>
<p>{{ items.length > 0 ? items[0].name : 'Empty' }}</p>
<!-- 3. Supports ternary expressions、Arithmetic、Function Call -->
<p>{{ new Date().getFullYear() }}</p>
<!-- 4. Not supported: if/for/Variable Declaration (Use v-if/v-for instead) -->
<!-- ❌ {{ if (x) { y } }} // Wrong -->
<!-- ✅ <p v-if=\"x\">y</p> // Correct -->
<!-- 5. Support for template strings -->
<p>{{ `Hello, ${name}!` }}</p>
</template>
(3) 補間とv-textの違い
<template>
<!-- Interpolation:You can add other content here -->
<p>Welcome, {{ name }}!</p>
<!-- v-text:Replace the entire element's content -->
<p v-text="`Welcome, ${name}!`"></p>
<!-- The above is equivalent to <p>Welcome, {{ name }}!</p> -->
</template>
4. テキストディレクティブ:v-text / v-html / v-once
(1) 3つのコマンドの比較
| コマンド | 機能 | 用途 | セキュリティ |
|---|---|---|---|
{{ }} 補間 |
テキスト補間 | デフォルト設定 | ✅ セキュリティ |
v-text |
要素の textContent を設定する | 補間の代替構文 | ✅ 安全 |
v-html |
要素の innerHTML を設定する | リッチテキストをレンダリングする(使用には注意が必要です) | ⚠️ XSS のリスク |
v-once |
一度レンダリングすれば、その後は更新されない | 静的最適化 | ✅ セキュリティ |
(2) v-html に関する警告
<template>
<!-- ✅ Safety:Content You Control -->
<div v-html="markdownHtml"></div>
<!-- ❌ Danger:User-entered HTML -->
<div v-html="userComment"></div>
<!-- User Input <script>alert('hacked')</script> Will be executed! -->
<!-- ✅ Recommended: Use DOMPurify Cleanup -->
<div v-html="DOMPurify.sanitize(userComment)"></div>
</template>
(3) v-once のパフォーマンス最適化
<template>
<!-- This productId won't change → Use v-once to Reduce Reactive Overhead -->
<span v-once>{{ productId }}</span>
<!-- But this currentPrice Will change → Do not use v-once -->
<span>{{ currentPrice }}</span>
</template>
5. プロパティのバインディング:v-bind と : 短縮表記
(1) v-bind の基礎
<template>
<!-- Complete Syntax -->
<img v-bind:src="imageUrl" v-bind:alt="imageAlt">
<!-- Abbreviation(Recommendations):Remove v-bind,Only : -->
<img :src="imageUrl" :alt="imageAlt">
<!-- Dynamic property names(v-bind Special Syntax)-->
<img :[dynamicAttr]="value">
</template>
(2) クラスとスタイルのバインディング(要点)
<template>
<!-- 1. Object Syntax:Switch Based on Conditions class -->
<div :class="{ active: isActive, 'text-danger': hasError }">
Conditional classes
</div>
<!-- 2. Array Syntax:Apply multiple at the same time class -->
<div :class="[activeClass, errorClass]">
Multiple classes
</div>
<!-- 3. Trinomial Expression(Array + Object)-->
<div :class="[isActive ? 'active' : '', { 'has-error': hasError }]">
Mixed
</div>
<!-- 4. Nested Objects in an Array -->
<div :class="[{ active: isActive }, errorClass]">
Nested
</div>
</template>
(3) スタイルのバインディング
<template>
<!-- 1. Object Syntax -->
<div :style="{ color: activeColor, fontSize: fontSize + 'px' }">
Styled
</div>
<!-- 2. Array Syntax(Merge multiple style Object)-->
<div :style="[baseStyles, overridingStyles]">
Multi styles
</div>
<!-- 3. CSS Variable(Vue 3 Recommendations)-->
<div :style="{ '--main-color': mainColor }">
With CSS variables
</div>
</template>
6. イベントバインディング:v-on と @ の省略形
(1) v-on の基本
<template>
<!-- Complete Syntax -->
<button v-on:click="handleClick">Click</button>
<!-- Abbreviation(Recommendations)-->
<button @click="handleClick">Click</button>
<!-- Inline Processor(Write the simple logic directly)-->
<button @click="count++">Increment</button>
<!-- Passing Parameters -->
<button @click="handleClick('arg1', $event)">Click</button>
</template>
(2) 一般的なイベント修飾子 (9)
<template>
<!-- 1. .stop:Prevent Bubbling(e.stopPropagation())-->
<button @click.stop="handleClick">Stop propagation</button>
<!-- 2. .prevent:Prevent the default behavior(e.preventDefault())-->
<form @submit.prevent="handleSubmit">Submit</form>
<!-- 3. .once:Triggered only once -->
<button @click.once="handleClick">Click once</button>
<!-- 4. .self:Only event.target This is triggered only if it is the current element -->
<div @click.self="handleClick">Self only</div>
<!-- 5. .capture:Using the Capture Phase -->
<div @click.capture="handleClick">Capture</div>
<!-- 6. .passive:Do not prevent the default behavior(Performance Optimization)-->
<div @scroll.passive="onScroll">Passive scroll</div>
<!-- 7. Keyboard Modifiers -->
<input @keyup.enter="submit"> <!-- Enter Key -->
<input @keyup.esc="cancel"> <!-- Esc Key -->
<input @keyup.tab="nextField"> <!-- Tab Key -->
<input @keyup.delete="deleteItem"> <!-- Delete Key -->
<!-- 8. Mouse Modifiers -->
<button @click.left="leftClick">Left</button>
<button @click.right.prevent="rightClick">No right click</button>
<button @click.middle="middleClick">Middle</button>
<!-- 9. System Modifiers(Key combinations)-->
<button @click.ctrl="save">Ctrl + Click</button>
<button @click.ctrl.exact="save">Accurate Ctrl(You cannot add other keys)</button>
</template>
(3) 修飾子の連鎖
<template>
<!-- Combinations of Multiple Modifiers(Execute in order)-->
<button @click.stop.prevent="handle">Stop + Prevent</button>
<!-- is effectively equivalent to -->
<button @click="(e) => { e.stopPropagation(); e.preventDefault(); handle(); }">
Manual
</button>
</template>
7. 式の制限(特定の文は使用不可)
(1) テンプレート式とJavaScript文
<template>
<!-- ✅ Sure.(Expression,Has a return value)-->
<p>{{ count + 1 }}</p>
<p>{{ isVip ? 'VIP' : 'Regular' }}</p>
<p>{{ items.filter(i => i.active).length }}</p>
<!-- ❌ That won't work.(Statement,No return value)-->
<!-- <p>{{ const x = 1 }}</p> // Wrong: Variable Declaration -->
<!-- <p>{{ if (x) { y } }}</p> // Wrong: if Statement -->
<!-- <p>{{ for (i of arr) {} }}</p> // Wrong: for Loop -->
<!-- ✅ Switch to v-if / v-for Instructions -->
<p v-if="x">y</p>
<div v-for="i in arr" :key="i.id">{{ i.name }}</div>
</template>
(2) 式と計算プロパティ
| シナリオ | 式の使用 | 計算プロパティの使用 |
|---|---|---|
| 簡単な計算(演算子1~2個) | ✅ | ❌ |
| 複雑なロジック(多段階、ループ) | ❌ | ✅ |
| キャッシュが必要 | ❌ | ✅ |
| 必須パラメータ | ✅ | ❌ (代わりにメソッドを使用してください) |
<template>
<!-- Simple:Expression -->
<p>{{ price * quantity }}</p>
<!-- Complex:Computed Properties(computed)-->
<p>{{ totalWithTax }}</p>
</template>
8. Edgeディレクティブ:v-pre / v-cloak
(1) v-pre: コンパイルをスキップ
<template>
<!-- Skip compiling this element,{{ }} Display as is -->
<span v-pre>{{ this is literal, not interpolated }}</span>
</template>
シナリオ:Vueのテンプレート構文そのものを示す場合(例:チュートリアル、ドキュメントサイトなど)。
(2) v-cloak:コンパイル前に非表示にする
<template>
<!-- v-cloak Make elements invisible until compilation is complete -->
<p v-cloak>{{ message }}</p>
</template>
<style>
[v-cloak] { display: none; }
</style>
シナリオ:コンパイル前にユーザーが {{ message }} が点滅しているのを目にするのを防ぐ。
9. 完全な例:動的な商品カード
(1) ▶ サンプル:. 基本商品カード
<template>
<div class="product-card">
<h3>{{ product.name }}</h3>
<p class="price">${{ product.price }}</p>
<span :class="['badge', stockStatusClass]">
{{ stockStatusText }}
</span>
<button :disabled="product.stock === 0" @click="addToCart">
Add to Cart
</button>
</div>
</template>
<script setup>
import { computed } from 'vue'
const props = defineProps({
product: { type: Object, required: true }
})
const stockStatusClass = computed(() => {
if (props.product.stock === 0) return 'out-of-stock'
if (props.product.stock < 10) return 'low-stock'
return 'in-stock'
})
const stockStatusText = computed(() => {
if (props.product.stock === 0) return 'Out of Stock'
if (props.product.stock < 10) return `Only ${props.product.stock} left`
return 'In Stock'
})
function addToCart() {
console.log('Added', props.product.name)
}
</script>
(2) ▶ サンプル:. 補間の記述方法のバリエーション
<template>
<!-- Variable Interpolation -->
<p>{{ username }}</p>
<!-- Expression Interpolation -->
<p>{{ count * 2 + 1 }}</p>
<!-- Trinomial Expression -->
<p v-if="score >= 60">Passed</p>
<p v-else>Failed</p>
<!-- Function Call -->
<p>{{ formatDate(new Date()) }}</p>
<!-- Object Access -->
<p>{{ user.profile.name }}</p>
</template>
(3) ▶ サンプル:. v-bind の 5 つの活用法
<template>
<!-- 1. Basic Binding -->
<img :src="imageUrl">
<!-- 2. Dynamic property names -->
<img :[attrName]="value">
<!-- 3. Object Syntax(class)-->
<div :class="{ active: isActive }">A</div>
<!-- 4. Array Syntax(class)-->
<div :class="[activeClass, errorClass]">B</div>
<!-- 5. String Concatenation(href)-->
<a :href="`/users/${userId}`">Profile</a>
</template>
(4) ▶ サンプル:. v-on の記述方法 5 通り
<template>
<!-- 1. Just a click -->
<button @click="handleClick">Click</button>
<!-- 2. Inline Statements -->
<button @click="count++">+</button>
<!-- 3. Passing Parameters + event Object -->
<button @click="handleDelete(item.id, $event)">Delete</button>
<!-- 4. Modifier Chain -->
<button @click.stop.prevent="handle">Save</button>
<!-- 5. Keyboard shortcuts -->
<input @keyup.ctrl.enter="submit">
</template>
(5) ▶ サンプル:. テンプレートとHTMLのクイックリファレンス
| 要素 | HTML構文 | Vue構文 | 説明 |
|---|---|---|---|
| 静的テキスト | <p>Hello</p> |
<p>{{ msg }}</p> |
動的にする |
| 静的プロパティ | <img src="x.jpg"> |
<img :src="url"> |
動的プロパティに変更 |
| 静的クラス | <div class="box"> |
<div :class="cls"> |
動的に変更 |
| 静的スタイル | <div style="color: red"> |
<div :style="sty"> |
動的スタイルに変更 |
| クリックイベント | <button onclick="fn()"> |
<button @click="fn"> |
Vueを編集 |
| 条件付きレンダリング | なし | <div v-if="ok"> |
Vue固有 |
| リストのレンダリング | なし | <div v-for="i in list"> |
Vue固有 |
(6) ▶ サンプル:. よくある5つの間違い
| エラー | 症状 | 解決策 |
|---|---|---|
.value を削除 |
テンプレートには {{ count }} を、JS には count.value を記述 |
テンプレートでは自動的に展開されるが、JS では .value が必要 |
{{ }} での if/for の記述 |
コンパイルエラー | v-if / v-for ディレクティブの使用 |
| v-html XSS | ユーザー入力 <script> 実行 |
DOMPurify による対策 |
| インライン関数が $event を受け取らなかった | イベントオブジェクトにアクセスできない | @click="fn($event)" 明示的に渡す |
| 修飾子の順序が間違っている | バブリングの防止に失敗した | .stop が .prevent の前にある(必要に応じて) |
❓ よくある質問
{{ }} と v-text の違いは何ですか?textContent を設定します。違いは、{{ }} は静的テキスト (<p>Hi, {{ name }}</p>) と併用できるのに対し、v-text は要素全体を占めなければならない (<p v-text="msg"></p>) という点です。柔軟性を高めるため、{{ }}の使用をお勧めします。<script> を注入してサイトを攻撃する可能性があります。データソース(自分で作成したMarkdownレンダラーなど)を100%信頼できる場合を除き、補間には{{ }}を使用するか、事前にDOMPurify.sanitize()でデータをサニタイズしてください。@click.stop.preventはまずstopPropagation()を呼び出し、次にpreventDefault()を呼び出します。v-bind は略せますか?v-on も略せますか?v-bind の略記は :(v-bind は含まない)であり、v-on の略記は @(推奨)です。どちらの略記も Vue 2 以降でサポートされている公式の表記であり、IDE のシンタックスハイライトや自動補完機能でもサポートされています。:class="{ active: isActive, 'text-danger': hasError }" を使用します。あるいは、配列構文 :class="[activeClass, errorClass]" を使用します。この 2 つを組み合わせて使用することも可能です::class="[isActive && 'active', { 'error': hasError }]"。@keyup.ctrl.s="save" または @keydown.ctrl.s.prevent="save" を使用してください(ブラウザのデフォルトの保存ダイアログが表示されないようにするには、.prevent を追加してください)。Vue 3 では、正確な修飾子 .exact もサポートされています。たとえば、@click.ctrl.exact では Ctrl キーのみが押される必要があり、他のキーは許可されません。📖 まとめ
- Vueのテンプレート構文は、補間、ディレクティブ、および式を中核とする、HTMLの宣言型拡張です。
- Mustache Interpolation
{{ }}は、変数、式、関数呼び出し、および三項演算子をサポートしています - テンプレート式には、if ステートメントや for ステートメント、変数宣言を含めることはできません(代わりに v-if および v-for ディレクティブを使用してください)
- v-text は要素のテキスト全体を置き換えます。v-html はリッチテキストをレンダリングします(XSS のリスクあり)。
- v-once によるパフォーマンスの最適化(静的コンテンツは一度だけレンダリングされる)、v-pre によるコンパイルの省略
- v-bind(略称:
:)、v-on(略称:@)、および3つの一般的なクラスバインディング手法 - 9つのイベント修飾子:.stop / .prevent / .once / .self / .capture / .passive + keyboard/mouse/system
- テンプレート式と計算プロパティのどちらを選ぶか:単純な場合は式を、複雑な場合は計算プロパティを使用する
📝 練習問題
(1) 基礎問題(難易度:⭐)
以下の内容を含むユーザーカードを表示する Vue 3 コンポーネントを作成してください:
- ユーザー名(補間)
- プロフィール画像(v-bind)
- 「フォロー」ボタン(v-on)
- オンライン/オフラインの状態(動的切り替え)
使用データ:ユーザーオブジェクト { name: 'Alice', avatar: 'alice.jpg', online: true }
(2) 上級問題(難易度:⭐⭐)
簡単なToDoリストを実装する:
- 入力フィールド(v-model は現時点では使用されていません。v-bind:value + @input を使用してシミュレートしています)
- 「追加」ボタン (@click + 式)
- リストのレンダリング(v-for;これについては次のレッスンで解説します)
- 各項目には削除ボタンがあります(@click + パラメータの受け渡し)
データ:const todos = ref(['Learn Vue', 'Build app', 'Deploy'])
(3) チャレンジ問題(難易度:⭐⭐⭐)
以下の機能をサポートする、完全な商品詳細カードコンポーネントを作成してください:
- 5種類の商品ステータスバッジ(在庫あり/在庫切れ/予約受付中/セール中/販売終了)
- 5つの状態はそれぞれ異なるクラスの色に対応しています(オブジェクト構文を使用して一括でバインドされます)。
- 数量の増減ボタン(クリックで増減)
- 数量が在庫数以上の場合、「カートに追加」ボタンを無効にする(動的に無効化)
- 少なくとも3つのイベント修飾子(@click.stop、@submit.preventなど)



