Template Syntax and Reactivity

Module 14: JavaScript Frontend Frameworks - Vue & Angular

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.

flowchart TD A[Vue Template] --> B[Template Compiler] B --> C[Render Function] C --> D[Virtual DOM] D --> E[Actual DOM] F[Data Changes] --> G[Reactivity System] G --> D style A fill:#42b883,color:white style B fill:#42b883,color:white style C fill:#42b883,color:white style D fill:#42b883,color:white style E fill:#dddddd,color:black style F fill:#35495e,color:white style G fill:#35495e,color:white

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:

<!-- 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:

// 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:

graph TD A[Data Object] -->|1. Converted to reactive| B[Reactive Proxy] B -->|2. Access property| C[Track dependency] B -->|3. Set property| D[Trigger update] C --> E[Dependency Collection] D --> E E -->|4. Notify| F[Re-render affected DOM] style A fill:#35495e,color:white style B fill:#42b883,color:white style C fill:#42b883,color:white style D fill:#42b883,color:white style E fill:#35495e,color:white style F fill:#42b883,color:white

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:

// 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:

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:

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:

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:

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

Additional Resources