Mastering Vue Component Architecture and Communication Patterns
Interactive Exercise: Rendering and Filtering Student Scores
Start with a table that displays student scores. The first column shows the total score for each student.
// Data structure: each entry has name, math, chinese, english scores
const studentScores = [
{ name: 'Bob', math: 97, chinese: 89, english: 67 },
// ... more students
];
// Compute total and sort descending by total
studentScores.forEach(s => s.total = s.math + s.chinese + s.english);
studentScores.sort((a, b) => b.total - a.total);
<table>
<tr>
<th>Rank</th>
<th>Name</th>
<th>Math</th>
<th>Chinese</th>
<th>English</th>
<th>Total</th>
</tr>
<tr v-for="(student, index) in studentScores" :key="index">
<td>{{ index + 1 }}</td>
<td>{{ student.name }}</td>
<td>{{ student.math }}</td>
<td>{{ student.chinese }}</td>
<td>{{ student.english }}</td>
<td>{{ student.total }}</td>
</tr>
</table>
To display only students who passed all subjects (score ≥ 60):
<tr v-for="(student, index) in studentScores" v-if="student.math >= 60 && student.chinese >= 60 && student.english >= 60">
...
</tr>
Add filtering controls:
- Three buttons: Chinese, Math, English – clicking a button highlights it and sets the filter subject.
- Two input fields for minimum and maximum scores. The table updates to show only rows matching all conditions.
<style>
.active-filter {
background-color: lightblue;
}
</style>
<div id="app">
<button @click="selectedSubject = 'chinese'" :class="{ 'active-filter': selectedSubject === 'chinese' }">Chinese</button>
<button @click="selectedSubject = 'math'" :class="{ 'active-filter': selectedSubject === 'math' }">Math</button>
<button @click="selectedSubject = 'english'" :class="{ 'active-filter': selectedSubject === 'english' }">English</button>
<p>
Score range:
<input type="number" min="0" max="100" v-model.number="minScore">
~
<input type="number" min="0" max="100" v-model.number="maxScore">
</p>
<table border="1" style="margin: auto">
<tr>
<th>Rank</th>
<th v-for="(value, key) in studentScores[0]" v-if="key === 'name' || key === selectedSubject || !selectedSubject">{{ key }}</th>
</tr>
<tr v-for="(student, index) in filteredScores" :key="index">
<td>{{ index + 1 }}</td>
<td v-for="(value, key) in student" v-if="key === 'name' || key === selectedSubject || !selectedSubject">{{ value }}</td>
</tr>
</table>
</div>
<script>
new Vue({
el: '#app',
data: {
studentScores: [...], // original sorted data
selectedSubject: '',
minScore: '',
maxScore: '',
},
computed: {
filteredScores() {
return this.studentScores.filter(s => {
const subject = this.selectedSubject;
const score = s[subject];
if (!subject) return true; // no filter
if (this.minScore !== '' && score < this.minScore) return false;
if (this.maxScore !== '' && score > this.maxScore) return false;
return true;
});
}
}
});
</script>
Component Fundamentals
A Vue component is a reusable bundle of HTML, CSS, and JavaScript logic. There are three types:
- Root component: created via
new Vue(). - Local component: defined as an object and registered inside a parent component.
- Global component: registered with
Vue.component(name, definition).
Every component has a template property that defines its DOM structure.
// Root component with explicit template
new Vue({
el: '#app',
data: { msg: 'Root component data' },
template: '<div>{{ msg }}</div>'
});
// If no template is provided, the el's innerHTML becomes the template.
Child Components
The root component acts as the top parent; local and global components serve as children. Relationships are relative. Each child component has isolated data scope. Local components require registration before use, while global ones are always available (preload into memory). Prefer local components to save memory.
<div id="app">
<div class="gallery">
<local-card></local-card>
<global-card></global-card>
</div>
</div>
<script>
// Local component definition
const LocalCard = {
template: `
<div class="card" @click="handleClick">
<img src="path/to/image.jpg" alt="Profile">
<h2>Beauty</h2>
</div>
`,
methods: {
handleClick() { alert('Clicked!'); }
}
};
// Global component
Vue.component('global-card', {
template: `
<div class="card">
<img src="path/to/product.jpg" alt="Product">
<h2>Milk</h2>
</div>
`
});
new Vue({
el: '#app',
components: {
LocalCard // ES6 shorthand: 'local-card' from 'LocalCard'
}
});
</script>
Component Data Isolasion
Analogous to class instantiation, each component instance gets its own data scope. The data property must be a function returning an object, so every reuse of the component creates a fresh data object.
const ClickCounter = {
template: `
<div class="counter-box" @click="increment">
<img src="path/to/image.jpg" alt="Image">
<p>Clicks: {{ count }}</p>
</div>
`,
data() {
return { count: 0 };
},
methods: {
increment() { this.count++; }
}
};
Parent-to-Child Communication (Props)
The child component defines expected props as an array of strings. The parent binds its data to these custom attributes using the v-bind directive.
<div id="app">
<div class="gallery">
<student-card v-for="person in people" :person-data="person" :key="person.id"></student-card>
</div>
</div>
<script>
const people = [
{ id: 1, name: 'Alice', image: 'path/to/alice.jpg' },
{ id: 2, name: 'Bob', image: 'path/to/bob.jpg' }
];
const StudentCard = {
props: ['personData'],
template: `
<div class="card">
<img :src="personData.image" alt="personData.name">
<h2>{{ personData.name }}</h2>
</div>
`
};
new Vue({
el: '#app',
data: { people },
components: { StudentCard }
});
</script>
Child-to-Parent Communication (Events)
A child component can emit custom events to notify the parent. The parent listens to these events via the v-on directive and defines the handler. The child uses this.$emit('eventName', payload).
<div id="app">
<h1>{{ title }}</h1>
<h2>{{ subtitle }}</h2>
<title-editor @title-changed="updateTitle" @subtitle-changed="updateSubtitle"></title-editor>
</div>
<script>
const TitleEditor = {
template: `
<div>
Main title:
<input type="text" v-model="newTitle" @input="emitTitle">
<br>
Subtitle:
<input type="text" v-model="newSubtitle">
</div>
`,
data() {
return {
newTitle: '',
newSubtitle: ''
};
},
methods: {
emitTitle() {
this.$emit('title-changed', this.newTitle);
}
},
watch: {
newSubtitle(val) {
this.$emit('subtitle-changed', val);
}
}
};
new Vue({
el: '#app',
data: {
title: 'Default Title',
subtitle: 'Default Subtitle'
},
methods: {
updateTitle(value) {
this.title = value || 'Default Title';
},
updateSubtitle(value) {
this.subtitle = value || 'Default Subtitle';
}
},
components: { TitleEditor }
});
</script>
Remember: a component’s template must have exactly one root element. Events belong to the child, but the logic is provided by the parent via the handler.