Vue.js course Β· Module 7: Props & Events

Project: NOVA LAB Communication System

5 min read
In this lesson4

The final project of the Communication Station! Time to apply all component communication patterns in one cohesive project. You will build a NOVA LAB station module management system that combines all the techniques you have learned.

Until now you practised each channel separately, on two components. A real station panel is several components at once, and with them come real questions: who owns the list of modules, who may change the generator's status, and how is the status bar supposed to know that the water filter has gone offline? In this project you will answer them by reading a working system.

Module Summary

In this module you learned the complete set of tools for communication between Vue components:

  • Props - one-way data passing from parent to child, with typing, validation, and default values. Defined via defineProps()
  • Events (Emit) - child-to-parent communication by emitting events with optional payload. Defined via defineEmits() with argument validation
  • v-model on components - two-way data binding with a component, internally based on the modelValue prop and the update:modelValue emit, written more briefly with defineModel() since Vue 3.4. Multiple v-model support with named bindings
  • Provide/Inject - passing data through multiple levels of the component tree without "prop drilling". Readonly + actions pattern for safe state modification
  • Fallthrough Attributes - automatic HTML attribute inheritance, controlled via inheritAttrs and $attrs
  • Advanced patterns - combining props, events, provide/inject, and v-model in complex architectures, plus defineExpose as a component's service port

Project Architecture

Before you open the files, look at the connection map. Each entry in parentheses tells you through which channel a given component receives or sends data:

1App (provide: modules, actions)
2  β”œβ”€β”€ StatusBar (inject: modules β†’ computed online count)
3  └── ControlPanel (inject: modules)
4       └── ModuleCard[] (props: module, emit: status-change)

App is the only owner of the state. StatusBar and ControlPanel stand side by side as children of App and both inject the module list, while ModuleCard receives its module through props from ControlPanel.

Task

Analyze the code in the editor and understand how all communication patterns work together:

  1. App.vue - Provider with the central module state in a ref and actions (provide readonly state + action functions)
  2. StatusBar.vue - Inject module state + a computed that counts the online modules against the total
  3. ControlPanel.vue - Inject modules + render cards via v-for, passing data through props
  4. ModuleCard.vue - Receives data through props, emits status changes to parent, uses inject to call actions

Start with the transmitter. App exposes the module list read-only, and next to it the actions that are the only ways to change the state:

1provide('modules', readonly(modules))
2provide('selectedId', selectedId)
3provide('selectModule', (id) => { selectedId.value = id })
4provide('updateStatus', (id, status) => {
5  const m = modules.value.find(m => m.id === id)
6  if (m) m.status = status
7})

The updateStatus action changes the status inside App, so StatusBar recalculates its computed by itself, without any extra signal. Notice one exception to the rule: selectedId reaches the descendants without readonly.

Now the receiver at the other end of the link. ModuleCard combines two channels in a single function:

1const toggle = () => {
2  const newStatus = props.module.status === 'online' ? 'offline' : 'online'
3  updateStatus(props.module.id, newStatus)
4  emit('status-change', props.module.id, newStatus)
5}

The card assigns nothing to props.module - it asks App for a change through the injected action, and sends its own parent, ControlPanel, a notification with an event. In the template the button has the @click.stop modifier so that toggling the status does not also select the whole card.

Key observations:

  • State is provided from App and injected in StatusBar and ControlPanel - neither of them receives the list through props
  • The selectModule and updateStatus actions travel from App straight to ModuleCard - they skip the ControlPanel level
  • ControlPanel passes modules via props to ModuleCard - classic parent-child pattern
  • ModuleCard uses inject for actions and emit for notifications - two different communication channels
  • The readonly + actions pattern protects the module list from direct modification - only App can change data

Extend the system

Three final tasks. First, wrap selectedId in readonly and check that selecting cards still works, since it goes through the selectModule action. Second, the @status-change handler in ControlPanel is empty - add a change log displayed below the cards. Third, add a technician's note field handled with v-model to ModuleCard, or validation for the module prop. Do them one at a time, and at each step ask yourself which component owns the data being changed. At the next location, the Module Storage, you will add slots and dynamic components to this.

Remember: on a well-designed station every piece of data has one owner, and the other modules listen to its signals and ask it for changes.

Code for this lesson: App.vue
1<script setup>
2import { ref, provide, readonly } from 'vue'
3import ControlPanel from './ControlPanel.vue'
4import StatusBar from './StatusBar.vue'
5
6// Central station state
7const modules = ref([
8  { id: 1, name: 'Oxygen Gen', status: 'online', temp: -12 },
9  { id: 2, name: 'Water Recycler', status: 'online', temp: 22 },
10  { id: 3, name: 'Solar Array', status: 'maintenance', temp: 85 }
11])
12const selectedId = ref(null)
13
14// Provide: global state
15provide('modules', readonly(modules))
16provide('selectedId', selectedId)
17provide('selectModule', (id) => { selectedId.value = id })
18provide('updateStatus', (id, status) => {
19  const m = modules.value.find(m => m.id === id)
20  if (m) m.status = status
21})
22</script>
23
24<template>
25  <div class="nova-lab">
26    <h1>NOVA LAB Communication System</h1>
27    <StatusBar />
28    <ControlPanel />
29  </div>
30</template>
31
32<style scoped>
33.nova-lab {
34  background: #0a0e27;
35  color: #00ff88;
36  min-height: 100vh;
37  padding: 20px;
38  font-family: monospace;
39}
40h1 { color: #00ff88; margin-bottom: 20px; }
41</style>

Spotted a mistake in this lesson?

Useful articles