7.2: Template Syntax & Reactivity

Master Vue template syntax to create dynamic, reactive user interfaces with declarative rendering. Learn data binding, directives, event handling, and how to display dynamic content in your Vue components.

1. Template Interpolation

Text Interpolation

The most basic form of data binding uses double curly braces:

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

const message = ref('Hello Vue!')
const user = ref({ name: 'Alice', age: 25 })
const count = ref(42)
</script>

<template>
  <p>{{ message }}</p>
  <p>User: {{ user.name }}, Age: {{ user.age }}</p>
  <p>Count: {{ count }}</p>
</template>

Output:

Hello Vue!
User: Alice, Age: 25
Count: 42

JavaScript Expressions

You can use JavaScript expressions inside interpolations:

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

const count = ref(5)
const price = ref(29.99)
const firstName = ref('Alice')
const lastName = ref('Smith')
</script>

<template>
  <p>{{ count + 1 }}</p>
  <p>{{ count * 10 }}</p>
  <p>{{ price.toFixed(2) }}</p>
  <p>{{ firstName + ' ' + lastName }}</p>
  <p>{{ count > 10 ? 'Many' : 'Few' }}</p>
  <p>{{ message.split('').reverse().join('') }}</p>
</template>

What works:

  • Arithmetic operations: {{ a + b }}
  • Method calls: {{ message.toUpperCase() }}
  • Ternary operators: {{ ok ? 'YES' : 'NO' }}
  • Array/string methods: {{ items.slice(0, 3) }}

What doesn't work:

  • Statements: {{ if (ok) { } }} - Use v-if instead
  • Flow control: {{ for (item in items) }} - Use v-for instead
  • Variable declarations: {{ let x = 1 }}

Raw HTML

Use v-html to output raw HTML (be careful of XSS attacks):

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

const rawHtml = ref('<span style="color: red">Red text</span>')
const userInput = ref('<script>alert("XSS")</script>') // Dangerous!
</script>

<template>
  <!-- Renders as HTML -->
  <div v-html="rawHtml"></div>

  <!-- Never use with user input! -->
  <!-- <div v-html="userInput"></div> -->
</template>

Warning: Never use v-html with user-provided content - it can lead to XSS attacks.


2. Attribute Binding (v-bind)

Basic Attribute Binding

Use v-bind or shorthand : to bind attributes:

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

const imageUrl = ref('https://via.placeholder.com/150')
const linkUrl = ref('https://vuejs.org')
const isDisabled = ref(true)
const itemId = ref('product-123')
</script>

<template>
  <!-- Full syntax -->
  <img v-bind:src="imageUrl" v-bind:alt="'Placeholder image'">

  <!-- Shorthand (preferred) -->
  <img :src="imageUrl" :alt="'Placeholder image'">

  <a :href="linkUrl">Vue Documentation</a>
  <button :disabled="isDisabled">Submit</button>
  <div :id="itemId">Product</div>
</template>

Boolean Attributes

Boolean attributes (disabled, checked, selected) work specially:

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

const isDisabled = ref(true)
const isChecked = ref(false)
</script>

<template>
  <!-- disabled attribute present when isDisabled is true -->
  <button :disabled="isDisabled">Button</button>

  <!-- checked attribute present when isChecked is true -->
  <input type="checkbox" :checked="isChecked">
</template>

Binding Multiple Attributes

Bind an object of attributes at once:

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

const attrs = ref({
  id: 'my-input',
  type: 'text',
  placeholder: 'Enter name',
  disabled: false
})
</script>

<template>
  <input v-bind="attrs">
  <!-- Equivalent to: -->
  <!-- <input
    :id="attrs.id"
    :type="attrs.type"
    :placeholder="attrs.placeholder"
    :disabled="attrs.disabled"
  > -->
</template>

3. Conditional Rendering

v-if, v-else-if, v-else

Conditionally render elements:

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

const score = ref(85)
const isLoggedIn = ref(false)
const userType = ref('admin') // 'admin', 'user', 'guest'
</script>

<template>
  <!-- Simple condition -->
  <p v-if="isLoggedIn">Welcome back!</p>
  <p v-else>Please log in</p>

  <!-- Multiple conditions -->
  <div v-if="score >= 90">Grade: A (Excellent!)</div>
  <div v-else-if="score >= 80">Grade: B (Good!)</div>
  <div v-else-if="score >= 70">Grade: C (Fair)</div>
  <div v-else>Grade: F (Needs improvement)</div>

  <!-- Complex conditions -->
  <div v-if="userType === 'admin'">
    <h2>Admin Dashboard</h2>
    <p>You have full access</p>
  </div>
  <div v-else-if="userType === 'user'">
    <h2>User Dashboard</h2>
    <p>Limited access</p>
  </div>
  <div v-else>
    <h2>Guest View</h2>
    <p>Please register</p>
  </div>
</template>

v-show

v-show toggles visibility with CSS (display: none):

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

const isVisible = ref(true)
</script>

<template>
  <p v-show="isVisible">This can be hidden</p>
  <button @click="isVisible = !isVisible">Toggle</button>
</template>

v-if vs v-show:

Featurev-ifv-show
DOMAdds/removes elementAlways in DOM
Toggle CostHigherLower
Initial CostLowerHigher
Use WhenCondition rarely changesToggling frequently

Example when to use each:

<template>
  <!-- Use v-if for rare changes -->
  <AdminPanel v-if="user.role === 'admin'" />

  <!-- Use v-show for frequent toggles -->
  <Sidebar v-show="sidebarOpen" />
</template>

Conditional Rendering Groups

Use <template> to conditionally render multiple elements:

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

const showDetails = ref(true)
</script>

<template>
  <template v-if="showDetails">
    <h3>Product Details</h3>
    <p>Price: $29.99</p>
    <p>In Stock: Yes</p>
    <button>Add to Cart</button>
  </template>
</template>

Note: <template> won't render as an actual element in the DOM.


4. List Rendering (v-for)

Iterating Arrays

Use v-for to render lists:

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

const items = ref(['Apple', 'Banana', 'Cherry'])
const todos = ref([
  { id: 1, text: 'Learn Vue', done: false },
  { id: 2, text: 'Build project', done: false },
  { id: 3, text: 'Deploy app', done: true }
])
</script>

<template>
  <!-- Simple array -->
  <ul>
    <li v-for="item in items" :key="item">
      {{ item }}
    </li>
  </ul>

  <!-- Array of objects -->
  <ul>
    <li v-for="todo in todos" :key="todo.id">
      <input type="checkbox" :checked="todo.done">
      {{ todo.text }}
    </li>
  </ul>

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

Important: Always use :key with v-for for proper tracking!

Iterating Objects

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

const user = ref({
  name: 'Alice',
  age: 25,
  email: 'alice@example.com'
})
</script>

<template>
  <ul>
    <!-- value only -->
    <li v-for="value in user" :key="value">
      {{ value }}
    </li>

    <!-- value and key -->
    <li v-for="(value, key) in user" :key="key">
      {{ key }}: {{ value }}
    </li>

    <!-- value, key, and index -->
    <li v-for="(value, key, index) in user" :key="key">
      {{ index }}. {{ key }}: {{ value }}
    </li>
  </ul>
</template>

Range v-for

<template>
  <!-- Numbers 1 through 10 -->
  <span v-for="n in 10" :key="n">{{ n }} </span>
</template>

v-for with v-if

Don't use v-if and v-for on the same element:

<!-- Wrong - don't do this -->
<li v-for="todo in todos" v-if="!todo.done" :key="todo.id">
  {{ todo.text }}
</li>

<!-- Correct - use computed property -->
<script setup>
import { ref, computed } from 'vue'

const todos = ref([...])
const activeTodos = computed(() => todos.value.filter(t => !t.done))
</script>

<template>
  <li v-for="todo in activeTodos" :key="todo.id">
    {{ todo.text }}
  </li>
</template>

5. Event Handling (v-on)

Basic Event Handling

Use v-on or shorthand @:

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

const count = ref(0)
const message = ref('')

function increment() {
  count.value++
}

function greet(name) {
  message.value = `Hello, ${name}!`
}

function handleClick(event) {
  console.log('Clicked at:', event.clientX, event.clientY)
}
</script>

<template>
  <!-- Full syntax -->
  <button v-on:click="increment">Count: {{ count }}</button>

  <!-- Shorthand (preferred) -->
  <button @click="increment">Count: {{ count }}</button>

  <!-- Inline expression -->
  <button @click="count++">Increment</button>

  <!-- Method with argument -->
  <button @click="greet('Alice')">Greet Alice</button>

  <!-- Access event object -->
  <button @click="handleClick">Click Me</button>
</template>

Event Modifiers

Vue provides event modifiers for common needs:

<template>
  <!-- Prevent default behavior -->
  <form @submit.prevent="handleSubmit">
    <button>Submit</button>
  </form>

  <!-- Stop propagation -->
  <div @click="parentClick">
    <button @click.stop="childClick">Click Me</button>
  </div>

  <!-- Only trigger once -->
  <button @click.once="showWelcome">Show Welcome</button>

  <!-- Chain modifiers -->
  <form @submit.prevent.stop="handleSubmit">
    <button>Submit</button>
  </form>

  <!-- Capture mode -->
  <div @click.capture="handleClick">...</div>

  <!-- Only trigger if event.target is the element itself -->
  <div @click.self="handleClick">...</div>
</template>

Key Modifiers

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

const searchQuery = ref('')

function search() {
  console.log('Searching for:', searchQuery.value)
}
</script>

<template>
  <!-- Enter key -->
  <input
    v-model="searchQuery"
    @keyup.enter="search"
    placeholder="Press Enter to search"
  >

  <!-- Multiple keys -->
  <input @keyup.ctrl.enter="submit">

  <!-- Common key aliases -->
  <input @keyup.esc="clearInput">
  <input @keyup.tab="nextField">
  <input @keyup.delete="deleteItem">
  <input @keyup.space="pauseVideo">
  <input @keyup.up="moveUp">
  <input @keyup.down="moveDown">
</template>

Mouse Modifiers

<template>
  <!-- Left click only -->
  <button @click.left="handleLeftClick">Left Click</button>

  <!-- Right click only -->
  <button @click.right="handleRightClick">Right Click</button>

  <!-- Middle click only -->
  <button @click.middle="handleMiddleClick">Middle Click</button>
</template>

6. Form Input Bindings (v-model)

Text Input

v-model creates two-way binding:

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

const message = ref('')
const multiline = ref('')
</script>

<template>
  <!-- Text input -->
  <input v-model="message" placeholder="Type something">
  <p>Message: {{ message }}</p>

  <!-- Textarea -->
  <textarea v-model="multiline" rows="4"></textarea>
  <p>{{ multiline }}</p>
</template>

Checkboxes

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

const checked = ref(false)
const checkedNames = ref([])
</script>

<template>
  <!-- Single checkbox -->
  <label>
    <input type="checkbox" v-model="checked">
    Accept terms: {{ checked }}
  </label>

  <!-- Multiple checkboxes -->
  <div>
    <label><input type="checkbox" value="Alice" v-model="checkedNames"> Alice</label>
    <label><input type="checkbox" value="Bob" v-model="checkedNames"> Bob</label>
    <label><input type="checkbox" value="Charlie" v-model="checkedNames"> Charlie</label>
  </div>
  <p>Selected: {{ checkedNames }}</p>
</template>

Radio Buttons

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

const picked = ref('')
</script>

<template>
  <div>
    <label><input type="radio" value="A" v-model="picked"> Option A</label>
    <label><input type="radio" value="B" v-model="picked"> Option B</label>
    <label><input type="radio" value="C" v-model="picked"> Option C</label>
  </div>
  <p>Picked: {{ picked }}</p>
</template>

Select Dropdown

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

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

<template>
  <!-- Single select -->
  <select v-model="selected">
    <option disabled value="">Please select</option>
    <option value="A">Option A</option>
    <option value="B">Option B</option>
    <option value="C">Option C</option>
  </select>
  <p>Selected: {{ selected }}</p>

  <!-- Multiple select -->
  <select v-model="multiSelected" multiple>
    <option value="A">Option A</option>
    <option value="B">Option B</option>
    <option value="C">Option C</option>
  </select>
  <p>Selected: {{ multiSelected }}</p>
</template>

v-model Modifiers

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

const age = ref(0)
const username = ref('')
const bio = ref('')
</script>

<template>
  <!-- .number - convert to number -->
  <input v-model.number="age" type="number">

  <!-- .trim - trim whitespace -->
  <input v-model.trim="username" placeholder="Username">

  <!-- .lazy - sync on change instead of input -->
  <textarea v-model.lazy="bio"></textarea>
</template>

7. Class and Style Bindings

Dynamic Classes

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

const isActive = ref(true)
const hasError = ref(false)
const activeClass = ref('active')
</script>

<template>
  <!-- Object syntax -->
  <div :class="{ active: isActive, error: hasError }">
    Object syntax
  </div>

  <!-- Bind to object -->
  <div :class="classObject">Bound object</div>

  <!-- Array syntax -->
  <div :class="[activeClass, { error: hasError }]">
    Array syntax
  </div>

  <!-- String -->
  <div :class="activeClass">String</div>

  <!-- Combined with static class -->
  <div class="static" :class="{ active: isActive }">
    Static + Dynamic
  </div>
</template>

<style>
.active { background: lightgreen; }
.error { border: 2px solid red; }
</style>

Dynamic Styles

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

const color = ref('red')
const fontSize = ref(20)
const styleObject = ref({
  color: 'blue',
  fontSize: '16px'
})
</script>

<template>
  <!-- Object syntax -->
  <div :style="{ color: color, fontSize: fontSize + 'px' }">
    Styled text
  </div>

  <!-- Bind to object -->
  <div :style="styleObject">Bound styles</div>

  <!-- Array syntax (multiple objects) -->
  <div :style="[styleObject, { fontWeight: 'bold' }]">
    Multiple style objects
  </div>
</template>

8. Computed Properties

Basic Computed

Computed properties are cached and only re-evaluate when dependencies change:

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

const firstName = ref('John')
const lastName = ref('Doe')

// Computed property
const fullName = computed(() => {
  return `${firstName.value} ${lastName.value}`
})

const items = ref([
  { name: 'Apple', price: 1.20 },
  { name: 'Banana', price: 0.80 },
  { name: 'Cherry', price: 2.50 }
])

const totalPrice = computed(() => {
  return items.value.reduce((sum, item) => sum + item.price, 0)
})
</script>

<template>
  <p>First Name: <input v-model="firstName"></p>
  <p>Last Name: <input v-model="lastName"></p>
  <p>Full Name: {{ fullName }}</p>

  <ul>
    <li v-for="item in items" :key="item.name">
      {{ item.name }}: ${{ item.price }}
    </li>
  </ul>
  <p>Total: ${{ totalPrice.toFixed(2) }}</p>
</template>

Computed vs Methods

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

const count = ref(0)

// Computed - cached, only re-runs when count changes
const doubleCount = computed(() => {
  console.log('Computing double...')
  return count.value * 2
})

// Method - runs every time it's called
function getDoubleCount() {
  console.log('Calling method...')
  return count.value * 2
}
</script>

<template>
  <p>Count: {{ count }}</p>
  <button @click="count++">Increment</button>

  <!-- Computed: logs once per count change -->
  <p>{{ doubleCount }}</p>
  <p>{{ doubleCount }}</p>
  <p>{{ doubleCount }}</p>

  <!-- Method: logs three times per render -->
  <p>{{ getDoubleCount() }}</p>
  <p>{{ getDoubleCount() }}</p>
  <p>{{ getDoubleCount() }}</p>
</template>

Use computed when:

  • Value depends on other reactive data
  • Value is used multiple times
  • Value requires expensive calculation

Use methods when:

  • You need to pass arguments
  • You want it to run every time

9. Watchers

Basic Watcher

Watch reactive data and run code when it changes:

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

const question = ref('')
const answer = ref('')

// Watch a single ref
watch(question, (newQuestion, oldQuestion) => {
  if (newQuestion.includes('?')) {
    answer.value = 'Thinking...'
    setTimeout(() => {
      answer.value = 'Yes, probably.'
    }, 1000)
  }
})
</script>

<template>
  <p>
    Ask a yes/no question:
    <input v-model="question">
  </p>
  <p>{{ answer }}</p>
</template>

Watch Multiple Sources

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

const x = ref(0)
const y = ref(0)

// Watch multiple values
watch([x, y], ([newX, newY], [oldX, oldY]) => {
  console.log(`x: ${oldX} → ${newX}`)
  console.log(`y: ${oldY} → ${newY}`)
})
</script>

Deep Watching

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

const user = ref({
  name: 'Alice',
  profile: {
    age: 25,
    city: 'Beijing'
  }
})

// Deep watch - detects nested changes
watch(user, (newUser) => {
  console.log('User changed:', newUser)
}, { deep: true })

function updateCity() {
  user.value.profile.city = 'Shanghai' // Triggers watcher
}
</script>

Immediate Watcher

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

const userId = ref(1)

// Run immediately on creation
watch(userId, async (newId) => {
  const userData = await fetchUser(newId)
  // ...
}, { immediate: true })
</script>

10. Practical Exercises

Exercise 5.2.1: Shopping Cart

Build a shopping cart with:

  1. List of products (name, price)
  2. Add to cart buttons
  3. Cart display with quantities
  4. Total price (computed)
  5. Remove from cart functionality

Exercise 5.2.2: Todo List with Filters

Create a todo app with:

  1. Add todos (input + button)
  2. Toggle completion (checkbox)
  3. Delete todos
  4. Filter buttons: All, Active, Completed
  5. Show count of active todos (computed)

Exercise 5.2.3: Form Validation

Build a registration form with:

  1. Username (required, min 3 chars)
  2. Email (required, valid format)
  3. Password (required, min 8 chars)
  4. Confirm password (must match)
  5. Show validation errors
  6. Disable submit until valid (computed)

Exercise 5.2.4: Search and Filter

Create a searchable list:

  1. List of items (name, category, price)
  2. Search input (filters by name)
  3. Category filter dropdown
  4. Price range filter
  5. Display filtered results (computed)
  6. Show count of results

11. Knowledge Check

Question 1: What's the difference between v-if and v-show?

Show answer `v-if` adds/removes elements from DOM (higher toggle cost), while `v-show` toggles CSS display property (always in DOM). Use `v-if` for rare changes, `v-show` for frequent toggles.

Question 2: Why is :key important in v-for?

Show answer `:key` helps Vue track each element's identity for efficient updates. Without it, Vue may reuse elements incorrectly, causing bugs with state or animations.

Question 3: When should you use computed instead of methods?

Show answer Use computed when the value depends on reactive data and is used multiple times. Computed properties are cached and only recalculate when dependencies change. Methods run every time they're called.

Question 4: What does v-model do?

Show answer `v-model` creates two-way data binding on form inputs. It's syntactic sugar for `:value` binding and `@input` event handling.

Question 5: What are watchers used for?

Show answer Watchers run side effects when reactive data changes, like fetching data from an API, localStorage updates, or triggering animations. Unlike computed, they don't return a value.

12. Key Takeaways

  • {{ }} interpolates data into templates
  • v-bind (:) binds attributes to reactive data
  • v-if/v-else conditionally renders elements (adds/removes from DOM)
  • v-show conditionally displays elements (CSS display toggle)
  • v-for renders lists (always use :key)
  • v-on (@) listens to events
  • v-model creates two-way form binding
  • :class and :style dynamically bind classes and styles
  • computed() creates cached, derived reactive values
  • watch() runs side effects when data changes
  • Event modifiers (.prevent, .stop) simplify common tasks
  • Form modifiers (.number, .trim, .lazy) transform input

13. Further Resources

Official Documentation:

Interactive:


Next Steps

Excellent! You now have a solid grasp of Vue's template syntax and reactivity system.

In Lesson 5.3: Components & Props, you'll learn how to build reusable components and pass data between them.