Vue.js course Β· Module 2: Template Syntax

Project: Interactive NOVA LAB Hologram Panel

5 min read
In this lesson3

Time for the final project! The operator of the Hologram Workshop looks after a dozen projections: maps, reactor schematics, atmospheric data. Some of them are switched off, and new ones arrive during the shift. The operator needs a panel that shows every hologram, filters them by state and reacts to every click, while keeping the data in one place, so the list, the filter and the counter never drift apart.

In this project you'll combine all the Vue template techniques you've learned in this module: from displaying data, through filtering and styling, to event handling and direct DOM access.

Module Summary

In this module you learned:

  • Interpolation {{ }} - displaying reactive data and JavaScript expressions in templates. Remember the limitations: only expressions, not statements.
  • v-bind (:) - dynamic HTML attribute binding: :src, :class, :style, :disabled. The shorthand syntax : is the standard.
  • v-on (@) - handling user events: @click, @submit, @keyup. Modifiers like .prevent, .stop, .once simplify common operations.
  • Class and style binding - object syntax (:class="{ active: isActive }"), array syntax (:class="[classA, classB]") and inline styles with :style.
  • v-if / v-else-if / v-else / v-show - conditional rendering. v-if adds/removes from DOM, v-show only toggles display.
  • v-for - list rendering with :key, iteration over arrays and objects, avoiding v-for with v-if on the same element.
  • Template Refs - direct DOM element access through ref, function refs, refs on components with defineExpose.
  • v-html - rendering raw HTML (beware of XSS!).

How to Approach the Project

Start with the data and add the template only afterwards. The holograms are an array of objects with the fields id, name, type and active, kept in ref([...]). You display the list with v-for and :key="h.id". The index is not a good key: after filtering, the same number goes to a different hologram and Vue mixes up the cards. It is the same trap that sorting the sensors showed in the v-for lesson.

The filter is a separate ref, for example filter with the values 'all', 'active' and 'inactive'. A computed returns the filtered list by reading filter and the hologram array, and v-for loops over exactly that computed. Don't add v-if to the element with v-for: v-if has a higher priority and can't see the loop variable. In the starter, filteredHolograms is a plain ref holding a copy of the array, so replace it with a computed.

A card's look depends on its state, so use :class="{ 'is-active': h.active, 'is-inactive': !h.active }" and describe the border colors and the neon glow in CSS. Keep :style for values really computed from data, such as the width of a power bar.

Events connect the panel to the operator. @click on a card toggles its active field, Enter in the name field (@keyup.enter) and the button add a new hologram, and "Remove last" calls pop() on the array. The "X active out of Y total" counter is another computed, so it recalculates itself after every change.

Finally, scrolling: a new hologram lands at the end of the list, often outside the visible area. This is where the template ref from the previous lesson and the nextTick function, described in point 5 below, come in.

Project Requirements

Build a Hologram Control Panel that includes:

1. Hologram list with v-for

Display a list of station holograms. Each hologram has a name, type (e.g., "navigation", "diagnostic", "communication") and status (active/inactive). Use :key with a unique ID.

2. Filtering with computed

Add three filter buttons: "All", "Active", "Inactive". Based on the selected filter, show the appropriate holograms. Use a computed property to filter the list.

3. Dynamic classes and styles

Use :class so that active holograms have a green border (border-color: #00ff88), and inactive ones have a red border (border-color: #ff4444). Add a neon glow effect (box-shadow) for active elements.

4. Event handling

  • Clicking a hologram toggles its status (active/inactive)
  • An "Add hologram" button adds a hologram with the name typed in the field to the list
  • A "Remove last" button removes the last element from the list
  • Display a counter: "X active out of Y total"

5. Template Ref

Mark the list container with ref (for example ref="listRef", and const listRef = ref(null) in the script). After a hologram is added, Vue updates the DOM a moment later, so scroll inside nextTick (imported from vue): nextTick(() => { listRef.value.scrollTop = listRef.value.scrollHeight }). Without nextTick you scroll to the old bottom and the new element stays below the edge.

In the function that adds a hologram, it looks like this:

1function addHologram() {
2  if (!newName.value.trim()) return
3  holograms.value.push({ id: Date.now(), name: newName.value, type: 'custom', active: true })
4  newName.value = ''
5  nextTick(() => {
6    listRef.value.scrollTop = listRef.value.scrollHeight
7  })
8}

The holograms array changes immediately and only the page update is delayed, which is why you read scrollHeight inside the function passed to nextTick. To make the container scrollable at all, give it a limited height in CSS, for example max-height: 360px and overflow-y: auto.

Expand the starter below with all the required features!

Remember: a good panel keeps the data in one place, and the template only displays it and passes on the operator's commands.

Code for this lesson: App.vue
1<script setup>
2import { ref } from 'vue'
3
4// NOVA LAB Hologram Panel - Module 2 final project
5const filter = ref('all')
6const newName = ref('')
7
8const holograms = ref([
9  { id: 1, name: 'Mars Map', type: 'navigation', active: true },
10  { id: 2, name: 'Reactor Schematic', type: 'engineering', active: true },
11  { id: 3, name: 'Atmospheric Data', type: 'science', active: false },
12  { id: 4, name: 'Habitat Plan', type: 'architecture', active: true }
13])
14
15// TODO: Add hologram filtering based on status
16// TODO: Add hologram status toggling
17// TODO: Add adding a new hologram
18// TODO: Use template ref for scrolling the list
19
20const filteredHolograms = ref(holograms.value)
21
22function toggleStatus(id) {
23  const h = holograms.value.find(h => h.id === id)
24  if (h) h.active = !h.active
25}
26
27function addHologram() {
28  if (!newName.value.trim()) return
29  holograms.value.push({
30    id: Date.now(),
31    name: newName.value,
32    type: 'custom',
33    active: true
34  })
35  newName.value = ''
36}
37</script>
38
39<template>
40  <div class="hologram-panel">
41    <h1>NOVA LAB Hologram Panel</h1>
42
43    <div class="filters">
44      <button @click="filter = 'all'" :class="{ active: filter === 'all' }">All</button>
45      <button @click="filter = 'active'" :class="{ active: filter === 'active' }">Active</button>
46      <button @click="filter = 'inactive'" :class="{ active: filter === 'inactive' }">Inactive</button>
47    </div>
48
49    <div class="add-form">
50      <input v-model="newName" placeholder="New hologram name" @keyup.enter="addHologram" />
51      <button @click="addHologram">Add</button>
52    </div>
53
54    <div class="hologram-list">
55      <!-- TODO: Use v-for with filtered holograms -->
56      <!-- TODO: Add dynamic :class classes -->
57      <div
58        v-for="h in holograms"
59        :key="h.id"
60        class="hologram-card"
61        :class="{ 'is-active': h.active, 'is-inactive': !h.active }"
62        @click="toggleStatus(h.id)"
63      >
64        <h3>{{ h.name }}</h3>
65        <span class="type">{{ h.type }}</span>
66        <span class="status">{{ h.active ? 'ACTIVE' : 'INACTIVE' }}</span>
67      </div>
68    </div>
69  </div>
70</template>
71
72<style scoped>
73.hologram-panel { background: #0d0d2b; color: #e0e0ff; padding: 2rem; min-height: 100vh; font-family: monospace; }
74h1 { color: #7b68ee; text-align: center; }
75.filters { display: flex; gap: 0.5rem; margin: 1rem 0; justify-content: center; }
76.filters button { background: #1a1a4e; color: #aaa; border: 1px solid #333; padding: 0.5rem 1rem; cursor: pointer; border-radius: 4px; }
77.filters button.active { background: #7b68ee; color: white; }
78.add-form { display: flex; gap: 0.5rem; margin: 1rem 0; }
79.add-form input { flex: 1; background: #1a1a4e; border: 1px solid #333; color: white; padding: 0.5rem; border-radius: 4px; }
80.add-form button { background: #7b68ee; color: white; border: none; padding: 0.5rem 1rem; cursor: pointer; border-radius: 4px; }
81.hologram-list { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 1rem; }
82.hologram-card { padding: 1rem; border-radius: 8px; cursor: pointer; background: #1a1a4e; transition: all 0.3s; }
83.hologram-card.is-active { border: 2px solid #00ff88; }
84.hologram-card.is-inactive { border: 2px solid #ff4444; opacity: 0.7; }
85.type { color: #888; font-size: 0.8rem; display: block; }
86.status { font-weight: bold; margin-top: 0.5rem; display: block; }
87</style>

Spotted a mistake in this lesson?

Useful articles