Introduction to Vue Templates
Vue's template system is one of its most distinctive features. Templates in Vue are HTML-based but with special enhancements that allow for dynamic content rendering and reactivity. They provide a declarative, readable way to connect the DOM with the Vue instance's data.
Real-world analogy: Vue templates are like smart architectural blueprints. Traditional blueprints (HTML) show the static structure, but Vue's templates add dynamic elements that can automatically adjust based on various conditions - like a blueprint that could show how a building changes with the seasons or number of occupants.
Text Interpolation
The most basic form of data binding in Vue templates is text interpolation using the "Mustache" syntax (double curly braces):
<span>Message: {{ msg }}</span>
Here, msg is a property from the component's data, and the mustache tag will be replaced with
its value. The binding is reactive, so when msg changes, the displayed text updates automatically.
Text vs HTML Interpolation
Text (Safe)
<!-- Renders as escaped text (safe from XSS) -->
<p>{{ htmlContent }}</p>
Renders as: <b>Bold text</b> (literally showing the tags)
HTML (Caution)
<!-- Renders as actual HTML (security risk) -->
<p v-html="htmlContent"></p>
Renders as: Bold text (tags are interpreted)
Security Note: Always use text interpolation (mustaches) by default. Only use v-html
for trusted content, never for user-provided data, to avoid XSS vulnerabilities.
One-Time Interpolation
For content that should only be rendered once and never updated (even if the data changes), use the v-once directive:
<span v-once>This will never change: {{ msg }}</span>
Real-world use case: Displaying static configuration values or initial timestamps that should not update even when the underlying data changes.
Working with JavaScript Expressions
Vue templates support full JavaScript expressions within data bindings:
<!-- Simple property access -->
{{ user.name }}
<!-- Mathematical operations -->
{{ count * 2 }}
<!-- Ternary expressions -->
{{ isActive ? 'Active' : 'Inactive' }}
<!-- Function calls -->
{{ message.split('').reverse().join('') }}
<!-- Template literals (object properties must use bracket notation) -->
{{ `Hello, ${user.name}!` }}
Limitations: Only single expressions are allowed in templates. Statements like if
blocks or variable declarations won't work directly in mustaches.
Best Practices for Template Expressions
While Vue allows complex expressions in templates, it's better to follow these guidelines:
- Keep template expressions simple and focused on presentation logic
- Move complex logic to computed properties or methods
- Avoid side effects in template expressions
<!-- Complex logic in template (avoid this) -->
<div>
{{
items
.filter(item => item.isVisible)
.map(item => item.name)
.join(', ')
}}
</div>
<!-- Better approach with computed property -->
<div>{{ visibleItemNames }}</div>
// In component:
computed: {
visibleItemNames() {
return this.items
.filter(item => item.isVisible)
.map(item => item.name)
.join(', ')
}
}
Attribute Binding
While mustaches work for text content, they cannot be used for HTML attributes. Instead, Vue provides
the v-bind directive (or its shorthand :):
<!-- Full syntax -->
<img v-bind:src="imageUrl" v-bind:alt="imageDesc">
<!-- Shorthand -->
<img :src="imageUrl" :alt="imageDesc">
Boolean Attributes
For boolean attributes (those that are either present or absent), Vue handles them specially:
<!-- The button will be disabled if isDisabled is truthy -->
<button :disabled="isDisabled">Button</button>
If isDisabled is null, undefined, or false, the attribute will be
removed completely.
Multiple Attribute Binding
To bind multiple attributes at once, you can pass an object to v-bind without an argument:
// In your data
data() {
return {
inputAttrs: {
type: 'text',
placeholder: 'Enter your name',
maxlength: 100
}
}
}
// In your template
<input v-bind="inputAttrs">
This expands to:
<input type="text" placeholder="Enter your name" maxlength="100">
Dynamic Class & Style Binding
One of the most common use cases for attribute binding is managing classes and styles dynamically.
Class Binding
Vue has special enhancements for class binding, supporting objects and arrays:
<!-- Object syntax (conditional classes) -->
<div :class="{ active: isActive, 'text-danger': hasError }"></div>
<!-- Array syntax (multiple classes) -->
<div :class="[activeClass, errorClass]"></div>
<!-- Combining array and object syntax -->
<div :class="[baseClass, { active: isActive }]"></div>
<!-- With computed property returning an object -->
<div :class="classObject"></div>
Real-world use case: Applying UI states like active, disabled, loading, or error states to components based on application logic.
Style Binding
Similar to class binding, Vue supports binding to inline styles with objects or arrays:
<!-- Object syntax -->
<div :style="{ color: activeColor, fontSize: fontSize + 'px' }"></div>
<!-- Object variable -->
<div :style="styleObject"></div>
<!-- Array syntax (multiple style objects) -->
<div :style="[baseStyles, overrideStyles]"></div>
Vue automatically handles vendor prefixing for CSS properties that need it. You can also provide multiple values for a property, and Vue will use the first one the browser supports:
<div :style="{ display: ['-webkit-box', '-ms-flexbox', 'flex'] }"></div>
Conditional Rendering in Templates
Vue provides several directives for conditional rendering of content:
v-if / v-else-if / v-else
These directives add or remove elements from the DOM based on conditions:
<div v-if="type === 'A'">Type A</div>
<div v-else-if="type === 'B'">Type B</div>
<div v-else-if="type === 'C'">Type C</div>
<div v-else>Not A/B/C</div>
Element grouping: To apply conditional rendering to a group of elements, use <template>
which acts as an invisible wrapper:
<template v-if="isLoggedIn">
<h1>Welcome back</h1>
<p>We're glad to see you again!</p>
<user-profile></user-profile>
</template>
<template v-else>
<h1>Hello, visitor</h1>
<p>Please sign in to continue</p>
<login-form></login-form>
</template>
v-show
v-show toggles the visibility of an element using CSS display property instead of adding/removing it from the DOM:
<div v-show="isVisible">This toggles visibility</div>
v-if
- Implementation: Actually creates/destroys elements
- Initial cost: Only renders if condition is true initially
- Toggle cost: Higher (adds/removes from DOM)
- Best for: Conditions that don't change often
v-show
- Implementation: Always renders, toggles CSS
display - Initial cost: Always renders regardless of condition
- Toggle cost: Lower (just changes CSS)
- Best for: Elements that toggle frequently
List Rendering
To render lists of items based on arrays or objects, Vue provides the v-for directive:
Basic Array Iteration
<ul>
<li v-for="item in items" :key="item.id">
{{ item.name }}
</li>
</ul>
Always use :key with v-for: Providing a unique key helps Vue track each node's identity, making DOM updates more efficient and avoiding unexpected behavior with component state.
Accessing Array Index
<ul>
<li v-for="(item, index) in items" :key="item.id">
{{ index }}. {{ item.name }}
</li>
</ul>
Object Property Iteration
<ul>
<li v-for="(value, key, index) in object" :key="key">
{{ index }}. {{ key }}: {{ value }}
</li>
</ul>
Range Iteration
<!-- Generate 10 elements (0 to 9) -->
<span v-for="n in 10" :key="n">{{ n }}</span>
Using v-for with Template
Similar to v-if, you can use v-for with <template> to render a group of elements:
<template v-for="item in items" :key="item.id">
<h4>{{ item.title }}</h4>
<p>{{ item.description }}</p>
<hr>
</template>
Array Change Detection
Vue's reactivity system can detect when array methods modify an array and update the view accordingly.
Methods that Trigger Updates
These array mutation methods are wrapped by Vue to trigger view updates:
push()- Add to endpop()- Remove from endshift()- Remove from beginningunshift()- Add to beginningsplice()- Add/remove itemssort()- Sort arrayreverse()- Reverse array
// Example that triggers view update
this.items.push({ id: 4, name: 'New Item' })
Replacing Arrays
Methods that return new arrays (non-mutating methods) won't modify the original array, so you need to replace it:
// These don't modify the original array
// filter(), concat(), slice()
// Replace the array reference to trigger updates
this.items = this.items.filter(item => item.isActive)
Object Change Detection Caveats
Vue cannot detect property additions or deletions for existing objects. Use Vue's methods instead:
// Adding a new property - WON'T be reactive
this.user.address = '123 Main St' // ❌
// Instead, use Vue.set or Object.assign
// Vue 2 approach
Vue.set(this.user, 'address', '123 Main St') // ✅
// Vue 3 approach
this.user = Object.assign({}, this.user, {
address: '123 Main St'
}) // ✅
Understanding Vue's Reactivity System
Vue's reactivity system is what enables data changes to automatically trigger view updates. Let's explore how it works under the hood:
Vue 3's Reactivity: Proxy-based
Vue 3 uses JavaScript Proxies to create a reactive version of your data objects:
// Simple explanation of Vue 3's reactivity
function reactive(obj) {
return new Proxy(obj, {
get(target, key) {
track(target, key) // Remember this property was accessed
return target[key]
},
set(target, key, value) {
target[key] = value
trigger(target, key) // Notify that this property changed
return true
}
})
}
Real-world analogy: Vue's reactivity system is like a smart home automation system. When you enter a room (access a property), motion sensors detect your presence (track dependencies). When you change something like the thermostat (modify a property), the system automatically responds by adjusting the environment (updating the DOM).
Reactivity Limitations
Understanding these limitations helps prevent common reactivity issues:
- Property addition/deletion to existing objects isn't automatically detected
- Directly modifying array elements by index won't trigger updates
- Modifying the length of an array directly won't trigger updates
// These won't trigger reactivity
this.obj.newProp = 'value' // Adding property
delete this.obj.existingProp // Deleting property
this.arr[0] = 'new value' // Replacing by index
this.arr.length = 2 // Truncating array
// Solutions
// For objects:
this.obj = { ...this.obj, newProp: 'value' }
// For arrays:
this.arr.splice(0, 1, 'new value') // Replace element
this.arr = this.arr.slice(0, 2) // Truncate array
Computed Properties
Computed properties are a powerful feature in Vue that let you derive new values from your reactive data. They are cached based on their dependencies and only re-evaluated when needed.
Basic Computed Properties
// In component options
computed: {
// A computed getter
fullName() {
return this.firstName + ' ' + this.lastName
}
}
// Usage in template
<p>{{ fullName }}</p>
Key benefits of computed properties:
- They cache results and only recalculate when dependencies change
- They're declarative rather than imperative
- They keep templates cleaner by moving logic to the component definition
Computed vs Methods
Computed Property
computed: {
fullName() {
return this.firstName + ' ' + this.lastName
}
}
// In template
{{ fullName }}
- Cached based on dependencies
- Only re-evaluates when dependencies change
- Used like a property (no parentheses)
Method
methods: {
getFullName() {
return this.firstName + ' ' + this.lastName
}
}
// In template
{{ getFullName() }}
- Re-evaluates on every render
- No caching mechanism
- Used with function call syntax
When to use which:
- Use computed properties for transforming data or calculating derived values
- Use methods for actions, events, and operations that should run on demand
Writable Computed Properties
You can also create computed properties that are both readable and writable using getter and setter functions:
computed: {
fullName: {
// getter
get() {
return this.firstName + ' ' + this.lastName
},
// setter
set(newValue) {
const names = newValue.split(' ')
this.firstName = names[0]
this.lastName = names[names.length - 1]
}
}
}
// Later, this will update firstName and lastName
this.fullName = 'John Doe'
Real-world use case: Creating a derived property that, when updated, modifies the underlying source data - like a formatted date that can be edited, or a total price that updates individual line items.
Watchers
While computed properties are more declarative, watchers let you perform actions in response to data changes - especially useful for asynchronous or expensive operations.
Basic Watchers
// In component options
data() {
return {
searchQuery: ''
}
},
watch: {
// Watch searchQuery for changes
searchQuery(newValue, oldValue) {
// Do something when searchQuery changes
this.fetchSearchResults(newValue)
}
}
// Equivalent to
this.$watch('searchQuery', function(newValue, oldValue) {
this.fetchSearchResults(newValue)
})
Deep Watchers
By default, watchers won't detect nested property changes. Use the deep option for that:
watch: {
// Watch user object deeply
user: {
handler(newValue, oldValue) {
// Triggered for nested property changes
console.log('User object changed')
},
deep: true
}
}
Immediate Watchers
If you want your watcher to run immediately upon component creation:
watch: {
searchQuery: {
handler(newValue) {
this.fetchSearchResults(newValue)
},
// Run immediately with the current value
immediate: true
}
}
When to Use Watchers vs Computed Properties
Use Computed
- When you need to derive a value from existing data
- When the result should update automatically
- When you need caching based on dependencies
- When the transformation is synchronous
Use Watchers
- When you need to react to data changes with side effects
- When you need to perform asynchronous operations
- When you need to limit how often a computation runs
- When you need access to the previous value
Real-world use cases for watchers:
- Fetching data from an API when a search query changes
- Validating form input after the user stops typing
- Saving changes to localStorage when data changes
- Transitioning between UI states
Practical Example: Reactive Search Application
Let's bring everything together with a practical example that demonstrates Vue's template syntax and reactivity:
<!DOCTYPE html>
<html>
<head>
<title>Vue Product Search</title>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<style>
.product-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 20px;
}
.product-card {
border: 1px solid #ddd;
padding: 15px;
border-radius: 8px;
}
.product-card.sale { border-color: #e44d26; }
.search-controls {
margin-bottom: 20px;
display: flex;
gap: 10px;
}
.search-input {
flex-grow: 1;
padding: 8px;
}
.on-sale {
background-color: #e44d26;
color: white;
padding: 3px 8px;
border-radius: 4px;
display: inline-block;
font-size: 12px;
}
[v-cloak] { display: none; }
</style>
</head>
<body>
<div id="app" v-cloak>
<h1>{{ shopName }}</h1>
<div class="search-controls">
<input
class="search-input"
v-model="searchQuery"
placeholder="Search products..."
@keyup="handleSearch">
<select v-model="sortBy">
<option value="name">Name (A-Z)</option>
<option value="nameDesc">Name (Z-A)</option>
<option value="price">Price (Low to High)</option>
<option value="priceDesc">Price (High to Low)</option>
</select>
<label>
<input type="checkbox" v-model="showOnlyOnSale">
Show only sale items
</label>
</div>
<p v-if="loading">Loading products...</p>
<p v-else-if="filteredProducts.length === 0">No products found matching your criteria.</p>
<div v-else class="product-grid">
<div
v-for="product in filteredProducts"
:key="product.id"
class="product-card"
:class="{ 'sale': product.onSale }">
<h3>{{ product.name }}</h3>
<p>{{ formatPrice(product.price) }}</p>
<span v-if="product.onSale" class="on-sale">ON SALE</span>
<p>{{ product.description }}</p>
<div :style="{ color: product.inStock ? 'green' : 'red' }">
{{ product.inStock ? 'In Stock' : 'Out of Stock' }}
</div>
</div>
</div>
<div class="summary">
<p>Found {{ filteredProducts.length }} of {{ products.length }} products</p>
<p v-if="avgPrice > 0">Average price: {{ formatPrice(avgPrice) }}</p>
</div>
</div>
<script>
const { createApp } = Vue
createApp({
data() {
return {
shopName: 'Vue Product Store',
loading: true,
searchQuery: '',
debouncedSearchQuery: '',
sortBy: 'name',
showOnlyOnSale: false,
products: []
}
},
computed: {
filteredProducts() {
let result = this.products;
// Filter by search query
if (this.debouncedSearchQuery) {
const query = this.debouncedSearchQuery.toLowerCase();
result = result.filter(product =>
product.name.toLowerCase().includes(query) ||
product.description.toLowerCase().includes(query)
);
}
// Filter by sale status
if (this.showOnlyOnSale) {
result = result.filter(product => product.onSale);
}
// Sort products
result = [...result].sort((a, b) => {
switch(this.sortBy) {
case 'name':
return a.name.localeCompare(b.name);
case 'nameDesc':
return b.name.localeCompare(a.name);
case 'price':
return a.price - b.price;
case 'priceDesc':
return b.price - a.price;
default:
return 0;
}
});
return result;
},
avgPrice() {
if (this.filteredProducts.length === 0) return 0;
const total = this.filteredProducts.reduce((sum, product) => sum + product.price, 0);
return total / this.filteredProducts.length;
}
},
methods: {
formatPrice(price) {
return '$' + price.toFixed(2);
},
handleSearch() {
// Clear any existing timeout
if (this._searchTimeout) {
clearTimeout(this._searchTimeout);
}
// Set a new timeout to update the debounced value
this._searchTimeout = setTimeout(() => {
this.debouncedSearchQuery = this.searchQuery;
}, 300); // 300ms debounce
},
fetchProducts() {
this.loading = true;
// Simulating API call
setTimeout(() => {
this.products = [
{
id: 1,
name: 'Laptop Pro',
price: 1299.99,
description: 'High-performance laptop for professionals',
inStock: true,
onSale: false
},
{
id: 2,
name: 'Smartphone X',
price: 799.99,
description: 'Latest model with advanced camera',
inStock: true,
onSale: true
},
{
id: 3,
name: 'Wireless Headphones',
price: 149.99,
description: 'Noise-cancelling with long battery life',
inStock: false,
onSale: false
},
{
id: 4,
name: 'Smart Watch',
price: 249.99,
description: 'Track fitness and stay connected',
inStock: true,
onSale: true
},
{
id: 5,
name: 'Tablet Mini',
price: 399.99,
description: 'Portable and powerful tablet',
inStock: true,
onSale: false
},
{
id: 6,
name: 'Bluetooth Speaker',
price: 99.99,
description: 'Waterproof with rich sound',
inStock: true,
onSale: true
}
];
this.loading = false;
}, 1000);
}
},
// Run when component is created
created() {
this.fetchProducts();
}
}).mount('#app')
</script>
</body>
</html>
This example demonstrates:
- Text interpolation and property binding
- Class and style binding
- Conditional rendering with
v-if/v-else-if/v-else - List rendering with
v-forand:key - Two-way data binding with
v-model - Event handling with modifiers
- Computed properties for derived data (filtered/sorted products, average price)
- Methods for formatting and event handling
- Watchers (implicitly in the debounced search implementation)
- Lifecycle hooks (
created)
Activities for Practice
Exercise 1: Reactive Data Dashboard
Create a small dashboard that displays various statistics based on reactive data. Include:
- A data object with at least 5 numeric values (e.g., sales figures, traffic stats)
- Computed properties that calculate totals, averages, percentages, etc.
- Conditional formatting based on value thresholds (e.g., red for low values, green for high)
- A way to toggle between different visualization modes using
v-if/v-else
Extend the exercise by adding inputs that allow modifying the data and watching the dashboard update in real-time.
Exercise 2: Interactive Form with Validation
Build a registration form with reactive validation feedback:
- Create form fields using
v-model(name, email, password, etc.) - Add computed properties that validate each field (e.g., email format, password strength)
- Use class binding to visually indicate valid/invalid fields
- Implement conditional rendering to show specific error messages
- Add a submit button that's only enabled when all validations pass
Exercise 3: Filtered List with Multiple Controls
Create an interface that displays a list of items (e.g., movies, books, products) with various filtering and sorting options:
- Start with an array of at least 10 items, each with multiple properties (title, category, date, rating, etc.)
- Add a search input with debounced filtering (similar to the example)
- Add filter controls (checkboxes, radio buttons, or dropdowns) for categories
- Add sort controls for different properties
- Create computed properties that apply all filters and sorting
- Display appropriate messages when no results match the filters