Week 6 · Advanced Vue: components, props, events and Vue Router
The one-minute story
Components are Lego bricks, the router is the signboard, and props and events are the parent and child talking. A big page (a dashboard) is not one huge block. You build small bricks: a navigation bar, a card, a button. Each brick has its own look and its own little notebook of data. You can use the same brick many times.
Bricks talk in only two ways, like a parent and a child at home. The parent gives the child things (props: "here is the name to show"). The child cannot open the parent's cupboard; if it wants something changed, it calls out (events: "I was clicked!") and the parent decides. Props down, events up.
The router is the signboard at a railway station. It does not build new pages from the server; it just decides which brick sits in the big empty frame (router-view) for the address in the URL. You press a link, the address changes, and a different brick takes the frame, with no full page reload.
By the end of this week you can:
- create components (global and local), pass props with validation, and send events back with
$emit; - use slots (default, named, scoped), dynamic components and
keep-alive; - read and write a single file component (
.vue) and set up a Vue CLI project; - configure Vue Router 3: routes,
router-link,router-view, dynamic segments, query, named routes, nested routes, guards, lazy loading; - predict what a component tree or a router configuration renders for a given URL;
- state the lifecycle order for a parent and its child.
Words you need
- Component - a reusable piece of UI with its own template, data and logic.
- Global / local registration - global:
Vue.component("name", {...})works everywhere; local: listed in a component'scomponentsoption, works only inside that one. - Prop - data a parent passes to a child (
:title="x"); read-only in the child. - Custom event - a message from child to parent:
this.$emit("name", payload), heard with@name="handler". - Slot - a hole in a child's template that the parent fills with its own HTML.
- Single file component (SFC) - a
.vuefile withtemplate,scriptandstyleparts, compiled by a build tool. - Vue CLI - the command-line tool that creates and builds Vue projects (
vue create). - Vue Router - the official router library; version 3 goes with Vue 2.
- Route - a mapping from a URL path to a component.
- Dynamic segment - a path part such as
:idthat matches any value; read it as$route.params.id. - Navigation guard - a function that runs before a route change and can allow, redirect or cancel it.
- Mixin - an object of options merged into a component to share code.
- Plugin - a library installed with
Vue.use()(Vue Router and Vuex are plugins). - provide / inject - an ancestor offers a value and any descendant picks it up, skipping the levels between.
Idea 1: Components: global and local
A component is almost a small Vue instance, with two differences: it has no el, and data must be a function that returns an object (so each copy gets its own data, and a plain object triggers a warning I reproduced).
<div id="app">
<hello-card></hello-card>
<hello-card></hello-card>
</div>
<script>
// Global registration: available in every template
Vue.component("hello-card", {
data() { return { likes: 0 }; },
template: `<div><button @click="likes++">Like</button> {{ likes }}</div>`,
});
new Vue({ el: "#app" });
</script>
The page shows two independent buttons, because data() runs once per copy. If data were an object { likes: 0 } all copies would share it.
// Local registration: only the registering component can use it
const NavBar = { template: `<nav>Home | About</nav>` };
const Page = {
components: { NavBar },
template: `<div><nav-bar></nav-bar><p>Content</p></div>`,
};
new Vue({ el: "#app", components: { Page }, template: `<page></page>` });
Global versus local
Global Vue.component(...) | Local components: {...} | |
|---|---|---|
| Where usable | any instance and component template | only in the component that lists it |
| Must be done | before the root instance is created | in that component's options |
| Downside | always bundled, even if unused | none; clearer and preferred |
Naming: register as NavBar or "nav-bar"; in an in-DOM HTML template always write kebab-case tags (<nav-bar>), because HTML is case-insensitive. In string templates both work.
Vue.component("NavBar", { template: "<p>This is navbar</p>" });
const Model = { template: "<p>This is model</p>" };
new Vue({ el: "#app", template: "<nav-bar></nav-bar>" });
Answer: This is navbar. NavBar is global and used. Model is just a JavaScript object that is never registered or used, so it never renders. A related real question: Batsman has components: { NavBar } and its template contains <nav-bar>; the root registers only Batsman. The root can use batsman, and Batsman can use nav-bar, but the root cannot use nav-bar directly.
- A local component is not inherited by its children: if
AregistersCandAusesB, thenBcannot useCunlessBregisters it too. - A component that is defined but not registered (or registered but not used in a template) renders nothing.
datamust bedata() { return {...} }in a component. Two copies must not share the same object.
Idea 2: Props down
Props are the input of a component.
Vue.component("player-card", {
props: {
playerName: { type: String, default: "Unknown Player" },
age: { type: Number, required: true },
tags: { type: Array, default: () => [] }, // object/array defaults are functions
level: { validator: (v) => ["a", "b"].includes(v) },
},
template: `<div><h2>{{ playerName }}</h2><p>Age {{ age }}</p></div>`,
});
<player-card player-name="Rahul" :age="21"></player-card>
<player-card :age="30"></player-card>
- A camelCase prop (
playerName) is passed with kebab-case in HTML:player-name="...". age="21"passes the string "21";:age="21"passes the number 21. Usev-bind(:) for anything that is not a string.- If the parent does not pass the prop, the
defaultis used: the second card above showsUnknown Player. I ran a card like this: with noplayer-nameit printedUnknown Player. - Short form:
props: ["id", "name"](no checks). Type checks only warn in the console; they do not stop the app. I reproducedInvalid prop: type check failedfor a string passed to aNumberprop.
One-way data flow. Data flows down from parent to child. A child must not change a prop: Vue warns "Avoid mutating a prop directly" (I reproduced this by doing n++ on a prop) because the parent's next re-render would overwrite it. To change something:
- copy it to local state:
data() { return { count: this.initial }; }, or - use a computed property for a transformed version, or
- emit an event so the parent changes its own data (Idea 3).
If a prop is an object or array, the child can mutate its inside (it shares the reference), but that is bad style.
<div id="app">
<player-card>
<p>Rahul is a versatile player, skilled in defense and attack.</p>
<p>Position: Captain</p>
</player-card>
</div>
<script>
Vue.component("player-card", {
props: { playerName: { type: String, default: "Unknown Player" } },
template: `<div>
<h2>{{ playerName }}</h2>
<slot>This player is an important part of the team.</slot>
</div>`,
});
new Vue({ el: "#app" });
</script>
The parent never passes player-name, so the heading is the default Unknown Player (not "Rahul"). The content between the tags replaces the slot's fallback text, so the fallback sentence disappears and the two paragraphs show. Rendered: Unknown Player, Rahul is a versatile player..., Position: Captain. (I ran this and got exactly that.) Both Position lines appear because all unnamed content goes to the default slot.
A star-rating component receives value (1 to 5) from the parent and has a button that adds one. What is wrong with methods: { more() { this.value++; } } and how do you fix it?
Need a hint?
Who owns value?
Show the full solution
It mutates a prop. Either emit an event, this.$emit("input", this.value + 1), so the parent updates its own data, or keep a local copy data() { return { current: this.value }; } and increment current.
Idea 3: Events up and v-model on components
The child calls this.$emit("eventName", payload). The parent listens with @eventName="handler".
<div id="app">
<p>{{ message }}</p>
<child-comp @message="updateMessage"></child-comp>
</div>
<script>
Vue.component("child-comp", {
template: `<button @click="send">Click</button>`,
methods: {
send() { this.$emit("message", "Hello"); },
},
});
new Vue({
el: "#app",
data: { message: "Waiting..." },
methods: { updateMessage(msg) { this.message = msg; } },
});
</script>
Clicking the button changes the text from Waiting... to Hello (tested). The payload is passed as the handler's first argument. Both of these are correct ways (a real MSQ):
@message="updateMessage"withthis.$emit("message", "Hello")(handler receives the payload);@message="updateMessage('Hello')"withthis.$emit("message")(the parent supplies the argument in the inline call).
Wrong: this.emit(...) (it is $emit), :message="..." (that is a prop, not a listener), or an event name that differs from the @ name (names must match exactly, including case).
v-model on a component
In Vue 2, v-model on a component is shorthand for a prop named value and an event named input:
Vue.component("my-input", {
props: ["value"],
template: `<input :value="value" @input="$emit('input', $event.target.value)">`,
});
<my-input v-model="name"></my-input>
Typing in the box changes the parent's name (tested). The correct way to do "two-way binding in a custom component" is v-model with a prop and an emit; not mutating the prop, not a plain v-bind. There is also :title.sync="x", which pairs the prop title with an event update:title.
In newer Vue 3: v-model uses the prop modelValue and the event update:modelValue; components can declare emits: [...].
Parent and child talk: props down, events up.
| Direction | Mechanism | Example |
|---|---|---|
| parent to child | props | <child :name="user.name"> |
| child to parent | events | this.$emit("saved", item) |
| ancestor to deep descendant | provide / inject (Idea 7) | provide: { theme } |
| unrelated components | shared state: Vuex (week 7) | $store |
| parent peeks at child | ref and this.$refs.child | use sparingly |
Write a counter-button child that shows count from a prop and, when clicked, asks the parent to increase it by 1. Show the parent.
Need a hint?
Child: no mutation, emit instead.
Show the full solution
Vue.component("counter-button", {
props: ["count"],
template: `<button @click="$emit('increment')">Count: {{ count }}</button>`,
});
new Vue({
el: "#app",
data: { total: 0 },
template: `<counter-button :count="total" @increment="total++"></counter-button>`,
});
The child only displays and announces; the parent owns total.
Idea 4: Slots
A slot lets the parent decide part of the child's markup. Content in the parent's template is compiled with the parent's data.
Vue.component("data-panel", {
template: `
<div>
<header><slot name="header">Default title</slot></header>
<main><slot>Default body</slot></main>
<footer><slot name="footer"></slot></footer>
</div>`,
});
<data-panel>
<template v-slot:header><h1>Navigation Menu</h1></template>
<p>Main body text</p>
<template v-slot:footer>Last updated: Today</template>
</data-panel>
- Default slot (
<slot>): receives all content that is not in a named template. Text inside<slot>...</slot>is fallback shown only if the parent provides nothing. - Named slot (
<slot name="header">): filled by<template v-slot:header>(shorthand#header). Old syntax:slot="header"attribute (Vue 2.5 and before; still works). - Scoped slot: the child passes data up into the slot so the parent decides how to show each item:
Vue.component("item-list", {
props: ["items"],
template: `<ul><li v-for="i in items"><slot :item="i">{{ i }}</slot></li></ul>`,
});
<item-list :items="[1, 2]">
<template v-slot:default="sp">#{{ sp.item }}</template>
</item-list>
renders <ul><li>#1</li><li>#2</li></ul> (tested). Without a slot template, the fallback {{ i }} would print 1 and 2.
What disappears. I ran these cases:
- A child without a
<slot>throws away everything placed between its tags.<main-comp><nest-comp></nest-comp></main-comp>wheremain-comp's template is just<p>This is main Component</p>renders onlyThis is main Component: the nested component never appears (a real exam question). - Content for a named slot that the child does not define is silently dropped. If the child has
header, the default slot andactionsbut nofooter, thenLast updated: Todayplaced inv-slot:footeris not rendered. A real paper's output isNavigation Menu,Main Dashboard...,Extra content outside templates,Save Changes Cancel, without the "Last updated" line, for this reason. - Unnamed content outside any
templategoes to the default slot.
Idea 5: Dynamic components and keep-alive
<component :is="name"> renders whichever component name currently refers to: a tab view without a router.
new Vue({
el: "#app",
data: { current: "tab-a" },
components: {
"tab-a": { template: "<p>A <input></p>" },
"tab-b": { template: "<p>B</p>" },
},
template: `<div>
<button @click="current = 'tab-a'">A</button>
<button @click="current = 'tab-b'">B</button>
<keep-alive><component :is="current"></component></keep-alive>
</div>`,
});
- Switching destroys the old component and creates the new one, so typed text in
A's input is lost. - Wrapping in
<keep-alive>keeps the inactive component alive in memory with its state. I tested:createdruns once, switching away runsdeactivated, switching back runsactivated, andcreateddoes not run again. keep-alivealso works around<router-view>to keep a page's state while navigating away.
Idea 6: Single file components and the Vue CLI
In a real project, components live in .vue files:
<!-- src/components/TaskItem.vue -->
<template>
<li :class="{ done: task.done }" @click="$emit('toggle', task.id)">
{{ task.title }}
</li>
</template>
<script>
export default {
name: "TaskItem",
props: { task: { type: Object, required: true } },
};
</script>
<style scoped>
.done { text-decoration: line-through; }
</style>
template: exactly one root element in Vue 2.script:export default { ... }, the options object (name, props, data, methods...).style: CSS.scopedmakes the rules apply only to this component's elements (Vue adds a uniquedata-v-xxxxattribute to them).- A browser cannot run a
.vuefile. It must be compiled by a tool (vue-loader in Vue CLI, or Vite). You cannot load a.vuefile in a script tag. Real MSQ: ".vue files typically contain template, script and style" is true; "must be compiled before they run in the browser" is true; "can be imported directly without preprocessing" is false.
Using it:
<script>
import TaskItem from "./components/TaskItem.vue";
export default {
components: { TaskItem },
data() { return { tasks: [{ id: 1, title: "Learn", done: false }] }; },
methods: { toggle(id) { const t = this.tasks.find((x) => x.id === id); t.done = !t.done; } },
};
</script>
<!-- in the template: -->
<!-- <ul><task-item v-for="t in tasks" :key="t.id" :task="t" @toggle="toggle"></task-item></ul> -->
Vue CLI
npm install -g @vue/cli # once
vue create my-app # choose Vue 2 (and Router if you want it)
cd my-app
npm run serve # dev server with hot reload, http://localhost:8080
npm run build # production files in dist/
Project layout: public/index.html (the page with div id="app"), src/main.js (creates the root instance), src/App.vue (root component), src/components/, src/views/ (page-level components), src/router/index.js, package.json (scripts and dependencies), node_modules/ (never edit), vue.config.js (optional settings, such as the proxy from week 5).
// src/main.js (Vue 2)
import Vue from "vue";
import App from "./App.vue";
import router from "./router";
new Vue({
router,
render: (h) => h(App),
}).$mount("#app");
render: h => h(App) is used because the CLI build has no template compiler at run time. Vite is the newer, faster tool (Vue 3 projects usually use it) and Vue CLI is in maintenance mode, but this course uses Vue CLI and Vue 2.
Idea 7: Mixins, plugins and provide / inject at a glance
Mixin. An object of options merged into a component, to share code.
const logMixin = {
created() { console.log("mixin created"); },
methods: { hello() { return "hi"; } },
};
export default { mixins: [logMixin], created() { console.log("component created"); } };
Both hooks run; the mixin's hook first, then the component's. If a name clashes (a method with the same name), the component's own wins.
Plugin. A library installed once with Vue.use(plugin, options). A plugin is an object with an install(Vue, options) function that can add global components, directives or Vue.prototype.$something. Vue.use(VueRouter) and Vue.use(Vuex) are exactly that. (With the CDN scripts of Vue and Vue Router 3, the router installs itself.)
provide / inject. A far ancestor offers; any descendant takes, without passing props through every level.
// ancestor
provide: { theme: "dark" },
// any descendant
inject: ["theme"], // then use this.theme (shows "dark", tested)
It is not reactive by default for plain values; use it for configuration. For shared changing state use Vuex.
Idea 8: Vue Router 3
Vue Router maps URLs to components in a single-page app. Install npm install vue-router@3 (for Vue 2) or use the CDN script, which gives VueRouter.
const Home = { template: "<p>Home</p>" };
const User = { template: "<p>User {{ $route.params.id }}</p>" };
const router = new VueRouter({
routes: [
{ path: "/", component: Home },
{ path: "/user/:id", name: "user", component: User },
],
});
new Vue({ el: "#app", router });
<div id="app">
<router-link to="/">Home</router-link>
<router-link :to="{ name: 'user', params: { id: 7 } }">User 7</router-link>
<router-view></router-view>
</div>
router-linkrenders anatag (href="/user/7") and navigates without reloading. The active link gets the classesrouter-link-activeandrouter-link-exact-active.router-viewis the frame where the matched component is shown.- Routes are tried in the order written; the first match wins. Put the catch-all
{ path: "*", component: NotFound }last. - Inside any component:
this.$routeis the current route (read it);this.$routeris the router (use it to navigate). Mixing them up is a common wrong option (this.$router.paramsis wrong;this.$route.params.usernameis right).
Params, query and named routes
| Part of the URL | Route config | Read it |
|---|---|---|
dynamic segment /user/7 | path: "/user/:id" | this.$route.params.id (a string, "7") |
query string /search?q=vue&page=2 | none needed | this.$route.query.q |
hash #top | none | this.$route.hash |
A real question: message() { return this.$route.query.update ? "This is create page" : "This is update page" } at /#/?update=true. The query value is the string "true", truthy, so it gives This is create page. Even ?update=false is the string "false", which is also truthy (I checked: it prints the "create" text too); only a missing query gives "update".
Props from the route. { path: "/dashboard/:id", component: Dashboard, props: true } passes :id to the component as a prop id (still a string). With id "8" and a lookup that falls back to the last user when none matches, /dashboard/8 shows This is dashboard for User3 (a real question; the compare uses == between a number and a string, so "1" matches 1).
Named routes. name: "settings" on the route lets you navigate by name: { name: "settings" }. In a real question, the answer was { "name": "Settings" }: the route's name (not the component's name, and path: "Settings" would be missing the slash and capital mismatch).
Programmatic navigation
this.$router.push("/user/7");
this.$router.push({ path: "/search", query: { q: "vue" } }); // /search?q=vue
this.$router.push({ name: "user", params: { id: 7 } });
this.$router.replace("/login"); // like push, but no new history entry
this.$router.go(-1); // one step back in history (like the Back button)
push adds a history entry, replace does not. In Vue Router 3.1+ push returns a promise that rejects with NavigationDuplicated if you push the route you are already on (I reproduced it); add .catch(() => {}) if that noise matters.
Nested routes
A route can have children. The parent component must contain its own router-view where the child appears. Child paths without a leading slash are relative to the parent.
const user = { template: "<div>Welcome {{ $route.params.name }}<router-view></router-view></div>" };
const home = { template: "<p>This is home page</p>" };
const profile = { template: "<p>This is profile page of {{ $route.params.name }}</p>" };
const routes = [
{ path: "/", component: home },
{
path: "/user/:name",
component: user,
children: [
{ path: "", component: home },
{ path: "home", component: home },
{ path: "profile", component: profile },
],
},
];
I rendered these for different URLs (abstract mode, same logic):
| URL | Rendered |
|---|---|
/user/mohan | Welcome mohan then This is home page (the child with path "" is the default) |
/user/mohan/profile | Welcome mohan then This is profile page of mohan |
const routes = [
{ path: "/", component: Home },
{ path: "/dashboard", component: Dashboard, // template: "Dashboard" + router-view
children: [{ path: "stats", component: Stats }, { path: "settings", component: Settings }] },
{ path: "*", component: NotFound },
];
/dashboard/statsrendersDashboardfollowed byStats Component: parent text first, child in the parent'srouter-view. (The real paper uses"**"; in my test it also acted as a catch-all, but write*.)/dashboardalone renders onlyDashboard: there is no child with path"", so the innerrouter-viewis empty./zzzmatches*and renders404 Not Found.
Similarly: a route / with children profile and *, and a parent template This is header plus router-view: URL / renders This is header404 Page not found (the child * also matches /, tested), and /profile renders This is headerWelcome to profile page. The parent always shows, the child follows it.
A further real question: a root template with top-bar, nav-bar and router-view, and a route /video/:id with child comments/newest, at /video/5/comments/newest: Topbar and Navbar stay visible (they are outside the router-view), and Newest Comments appears inside Video Details.
Hash mode and history mode
| hash (default) | history | |
|---|---|---|
| URL | http://host/#/records/2 | http://host/records/2 |
| Config | nothing | mode: "history" |
| Server | needs nothing (the server only sees /) | must return index.html for every path, or a refresh gives 404 |
| Looks | has # | clean URLs |
Consequences I verified with a /records/:id route and a * fallback:
| Mode | URL | Result |
|---|---|---|
| hash | /#/records/2 | Pianobind has 13 songs... |
| hash | /records/1 (no #) | not the record: the router sees path /, so it matches * and shows Unknown record! |
| hash | /#/records (no id) | * matches: Unknown record! |
| hash | /#/records/3 | route matches, but records[3] is undefined; record.albums throws; the component shows nothing and the console has an error. Not "Unknown record!" |
| history | /inventory/1 | the product |
| history | /#/inventory/1 | the hash is ignored; path is /, so * shows Item not found |
| history | /inventory/3 | route matches, item undefined: error and blank, not the not-found page |
The lesson: * only catches URLs that no route matches. An id that matches the pattern but has no data must be handled in your component (v-if="record").
Same component, changing parameter
Going from /u/1 to /u/2 reuses the same component instance, so created and mounted do not run again. I logged: created 1, then on navigating beforeRouteUpdate 2 and a watcher 1->2. React with a watcher or the in-component guard:
data() { return { item: inventory[this.$route.params.productId] }; },
watch: {
$route(to, from) { this.item = inventory[to.params.productId]; }, // as in the paper
// or just the param: "$route.params.productId"(id) { ... }
},
Another real question watches '$route.path', sets a heading and shows links with v-if. At first load none of the watch runs (a watch is not immediate), so the heading shows the initial data value (Vue Router Demo). It then changes on each route change. A computed property that reads $route.params also updates by itself, because $route is reactive.
Navigation guards
A guard runs before the route changes: global router.beforeEach((to, from, next) => {...}), per-route beforeEnter, or in-component beforeRouteEnter, beforeRouteUpdate, beforeRouteLeave. You must call next() (allow), next(false) (cancel) or next("/login") (redirect), exactly once in Vue Router 3.
router.beforeEach((to, from, next) => {
const loggedIn = !!localStorage.getItem("token");
if (to.meta.requiresAuth && !loggedIn) next("/login");
else next();
});
// route: { path: "/profile", component: Profile, meta: { requiresAuth: true } }
I tested a guard with meta: { auth: true }: asking for /settings/7 ended at /login and the guard also ran again for the redirect target. meta is any extra data you attach to a route.
Lazy loading and other options
{ path: "/about", component: () => import("./views/About.vue") } // loaded only when visited
{ path: "/old", redirect: "/new" } // redirect
{ path: "/search", component: Search, props: (route) => ({ q: route.query.q }) }
Lazy loading splits the build into chunks, so the first page loads faster.
In newer Vue 3: Vue Router 4 uses createRouter({ history: createWebHistory(), routes }), useRoute(), useRouter(), and a catch-all is /:pathMatch(.*)*.
Idea 9: Lifecycle of a parent and child
When a parent contains a child, hooks interleave. I logged both with a mixin:
create: parent beforeCreate, parent created, parent beforeMount,
child beforeCreate, child created, child beforeMount,
child mounted, parent mounted
update: parent beforeUpdate, child beforeUpdate, child updated, parent updated
destroy: parent beforeDestroy, child beforeDestroy, child destroyed, parent destroyed
- Creation goes down (parent first), but
mountedgoes up: the child'smountedruns before the parent's. So in the parent'smountedthe children are in the DOM. - A component's hooks are independent of the parent's: it has its own lifecycle (a real MSQ).
- Typical use: load initial data in
created(), start a timer or DOM work inmounted(), clear the timer inbeforeDestroy();destroyed()is after teardown, not for declaring data. - With
keep-alive,activatedanddeactivatedreplace the destroy and create on switching.
Routes: { path: "/player/:name", component: Player, props: true, children: [{ path: "", component: OneDay }, { path: "test", component: Test }] } and { path: "*", component: Test }. Player shows Name: {{ name }} and a router-view; OneDay shows Oneday Runs: 10000; Test shows Test Runs: 5000. What do /#/player/Rohit, /#/player/Rohit/test and /#/player render?
Need a hint?
For the last one, does :name match nothing?
Show the full solution
/player/Rohit renders Name: Rohit then Oneday Runs: 10000 (empty child path). /player/Rohit/test renders Name: Rohit then Test Runs: 5000. /player has no name, so the /player/:name pattern does not match; the catch-all * shows Test Runs: 5000 only. (A real question had exactly this last case as its answer.)
In the viva, be ready to draw your component tree: which component is the view (one per route), which are reusable (card, navbar, form), what props each receives, and what events it emits. Show a navigation guard that sends a logged-out user to the login page, and be ready to explain why you used hash mode (no server setup) or history mode (Flask catch-all route).
Exam radar
The evidence file counts questions on this week's topics (components, props, events, router, lifecycle) as about 26 of 848 Quiz 1, 95 of 608 Quiz 2 and 125 of 1471 End Term questions (a keyword match, approximate), with about 75 distinct questions read. The router questions are by far the most repeated; the same nested-route problems come back across several exams. Typical shapes:
- "What will the browser render for this URL" for nested routes (
children, empty child path, parent text then child text), hash versus history URLs,*fallback, dynamic:id, routeprops: true,$route.query, and a route whose data lookup isundefined. - Components and slots: global versus local registration, unregistered components, default prop values, content inside a component without a slot, missing named slots.
- Events: choose the correct
@eventand$emitpair;v-modelon a component with a prop and an emit; "props pass data from child to parent" is false. - API facts:
$routeversus$router, named route navigation,$router.go(-1),*as a fallback,childrenfor nested routes. - Lifecycle hooks in components: which hook for loading data (
created), timers (mounted) and cleanup (beforeDestroy);beforeDestroyruns before teardown,destroyedafter; data must be a function. - True or false:
.vuefiles and compilation; advantages of components (reuse, maintainability, easier UI management; not "automatic SQL optimization"); a few Vuex questions (week 7).
- Child
router-viewneeded inside the parent component, or the child never shows. - A child route path with a leading slash (
/profile) is treated as absolute, not nested under the parent URL. this.$route(read) versusthis.$router(act).*catches only URLs that no route matches; a matching URL with missing data fails in the component.- In hash mode the part after
#is the route; in history mode the#part is ignored. - Props down, events up; props are never mutated by the child.
- A route
props: truedelivers params as strings.
Quick quiz
A child component's template is <p>This is main Component</p> (no slot). The parent writes <main-comp><nest-comp></nest-comp></main-comp> and both are registered. What is rendered?
- This is main Component and This is nested Component
- This is nested Component
- This is main Component
- nothing
slot in the child, content between its tags is discarded.A route { path: "/", component: Header, children: [{ path: "profile", component: Profile }, { path: "*", component: Err }] } and Header renders This is header and a router-view. The URL is http://127.0.0.1:8080/ (hash mode). What shows?
- This is header only
- Welcome to profile page
- This is header followed by 404 Page not found
- 404 Page not found followed by This is header
* also matches the root path; the parent text comes first, then the child inside its router-view.In a Vue 2 component, which is the right way to read username from the URL /user/:username?
this.$router.params.usernamethis.$route.params.usernamethis.$router.query.usernamethis.$route.query.username
$route holds the current route; dynamic segments are in params, the query string in query.A child must update the parent's message to "Hello" when a button is clicked. Select correct pairs. (select all that apply)
- parent
@message="updateMessage", childthis.$emit("message", "Hello") - parent
@message="updateMessage('Hello')", childthis.$emit("message") - parent
:message="updateMessage", childthis.emit("message", "Hello") - parent
@updateMessage="message", childthis.$emit("updateMessage")
$emit; : is a prop, not a listener.The component is props: ["id"] with a lookup that returns the third user when no user matches, the route is /dashboard/:id with props: true, and the URL is /#/dashboard/8. What shows?
- This is dashboard for User1
- This is dashboard for User2
- This is dashboard for User3
- nothing
id is "8", no user has that id, and the fallback returns the third user.Which statements about .vue files are true? (select all that apply)
- They usually contain template, script and style sections.
- They must be compiled (Vue CLI or Vite) before running in a browser.
- JavaScript can import and run them with no preprocessing.
- They can be loaded with a plain script tag.
.vue; a build tool turns it into JavaScript and CSS.For parent P and child C (child inside the parent's template), in what order do the mounted hooks run?
- parent then child
- child then parent
- only the parent
- they run in parallel
The URL http://localhost:8080/#/records/3 matches /records/:id, but the data lookup records[3] is undefined and the template reads record.albums. What does the page show?
- Unknown record! from the
*route - nothing, and an error appears in the console
- the first record
- a 404 from the server
* is not used; reading a property of undefined throws during rendering.Viva: say it out loud
Why do we split an app into components?
Each component is a reusable unit with its own template, data and logic. It makes the code easier to read, test and change, and I can use the same card or navbar in several places. My page becomes a tree of small components.
How do components communicate?
Props go from parent to child, and events go from child to parent with $emit. For distant components I use provide and inject, or Vuex for shared state. A child never changes a prop; it emits an event and the parent updates its own data.
What is the difference between global and local registration?
A global component is registered with Vue.component and can be used in every template. A local one is listed in a component's components option and only that component can use it. Local is preferred because it is explicit and keeps the bundle clean.
What are slots and when do you use them?
A slot is a placeholder in the child that the parent fills with its own markup. I use named slots for layouts like a card with header and footer, and scoped slots when the child has data, like a list item, and the parent decides how to display it.
What is a single file component and what does scoped do?
A .vue file has template, script and style parts. A build tool (Vue CLI or Vite) compiles it. scoped on the style makes its CSS apply only to that component's own elements by adding a unique data attribute.
How does Vue Router work? Explain $route and $router.
The router maps URL paths to components, and router-view is where the matched component shows. router-link navigates without reloading. $route is the current route (params, query, path), which I read. $router is the router object I use to navigate, like this.$router.push({ name: "user", params: { id: 7 } }).
How do you protect a page so only logged-in users see it?
I mark the route with meta: { requiresAuth: true } and write a global router.beforeEach guard. If the route needs auth and there is no token, it calls next("/login"), otherwise next(). The backend still checks the token because front-end checks can be bypassed.
What is the difference between hash mode and history mode?
Hash mode puts the route after #, so the server only ever sees / and needs no setup. History mode gives clean URLs, but the server must return index.html for all paths, otherwise a page refresh on /about gives a 404.
Recap
- Components:
Vue.component(global) orcomponents: {}(local).datamust be a function. Registered children are not inherited by grandchildren. - Props down (kebab-case in HTML,
:for non-strings, validation with type, default, required; object and array defaults are functions); never mutate a prop. Events up with$emit.v-modelon a component isvalueplusinput. - Slots: default, named (
v-slot:name), scoped. A child without a slot drops the content; content for an undefined named slot is dropped. - Dynamic components with
:is;keep-alivekeeps state and usesactivated/deactivated. - SFC: template, script, style (scoped); needs Vue CLI or Vite.
npm run serveandnpm run build. - Router:
routes,router-link,router-view,:idin$route.params, query in$route.query, named routes,push,replace,go(-1),childrenneed arouter-viewin the parent,*last, guards withnext(), lazy() => import(...), hash versus history. - Same component with a changed param is reused: use a watcher on
$route. - Lifecycle: parent create, child create, child mounted, parent mounted; destroy is parent before, child, child destroyed, parent destroyed.
- Global:
Vue.component("my-comp", { props: [...], data() { return {...}; }, template: "..." }) - Local:
components: { MyComp }(use<my-comp>) - Prop:
props: { n: { type: Number, required: true, default: 0 } }; pass:n="5" - Event: child
this.$emit("name", payload); parent@name="handler" - v-model on component: prop
value, eventinput - Slots:
<slot name="x">fallback</slot>;<template v-slot:x>(or#x); scoped<slot :item="i">withv-slot:default="sp" - Dynamic:
<component :is="name">,<keep-alive> - Router:
new VueRouter({ mode: "history", routes: [...] }),{ path: "/u/:id", name: "user", component: U, props: true, children: [...] } - Read:
this.$route.params.id,this.$route.query.q; go:this.$router.push({ name: "user", params: { id: 1 } }),.replace(),.go(-1) - Guard:
router.beforeEach((to, from, next) => { ... next() }); catch-all{ path: "*", component: NotFound } - Lazy:
component: () => import("./views/About.vue") - Vue CLI:
vue create app,npm run serve,npm run build; Vue 3 router:createRouter,createWebHistory
- MAD1 week 3 - Views: Jinja2 templates, Bootstrap and forms: Jinja2 template inheritance and macros become Vue components, props and slots.
Practise with real exam questions
These come straight from past papers (with the real figures and the official answer key). Open 60 real questions on this week All Quiz 2 papers