Building Modular UIs with Vue Component Architecture
Organizing user interfaces as a tree of isolated, reusable components is central to Vue development. This approach simplifies code management and improves scalability by breaking a page into small, self-contained units.
Creating and Registering Components
A component can be defined using an options object and registered globally or locally.
<div id="app">
<custom-card></custom-card>
</div>
<script>
// Global registration
Vue.component("custom-card", {
template: `
<div class="card">
<h3>Default Card</h3>
</div>`
});
new Vue({
el: "#app"
});
</script>
Global vs. Local Registration
A globally registered component is available everywhere. Local registration confines the component to a specific parent instance:
new Vue({
el: "#app",
components: {
"custom-card": {
template: `<div class="card"><h3>Local Card</h3></div>`
}
}
});
Parent-Child Relatoinships
Components can nest, forming parent-child trees.
<div id="app">
<parent-box></parent-box>
</div>
<script>
const ChildBox = {
template: `<p>Child content</p>`
};
const ParentBox = {
template: `
<div>
<h2>Parent section</h2>
<child-box></child-box>
</div>`,
components: {
"child-box": ChildBox
}
};
new Vue({
el: "#app",
components: {
"parent-box": ParentBox
}
});
</script>
Isolating Template Content
Templates can be extracted using <template> or <script type="text/x-template"> with a matching id.
<template id="card-template">
<div class="card">
<h4>{{ title }}</h4>
</div>
</template>
<script>
Vue.component("info-card", {
template: "#card-template",
data() {
return { title: "Information" };
}
});
</script>
Component Data Must Be a Function
Each component instance requires its own copy of data. A factory function returns a fresh object, preventing shared state across instances.
<div id="app">
<counter-button></counter-button>
<counter-button></counter-button>
</div>
<template id="counter-btn">
<div>
<span>Count: {{ count }}</span>
<button @click="count++">+</button>
<button @click="count--">-</button>
</div>
</template>
<script>
Vue.component("counter-button", {
template: "#counter-btn",
data() {
return { count: 0 };
}
});
new Vue({ el: "#app" });
</script>
Avoid sharing external objects directly to data. Each component needs its own count value, otherwise envolved state causes all instances to change together.
const sharedState = { count: 0 };
Vue.component("shared-counter", {
template: "#counter-btn",
data() {
return sharedState; // wrong – every instance mutates the same object
}
});
Parent-to-Child Communication via Props
Props flow from parent to child. They can be defined as an array or a detailed object with type checks, default values, and validators.
<div id="app">
<movie-list :items="movies"></movie-list>
</div>
<template id="movie-template">
<ul>
<li v-for="item in items">{{ item }}</li>
</ul>
</template>
<script>
Vue.component("movie-list", {
template: "#movie-template",
props: {
items: {
type: Array,
default() {
return [];
}
}
}
});
new Vue({
el: "#app",
data: {
movies: ["Inception", "Interstellar", "Tenet"]
}
});
</script>
When using camelCase prop names, use their kebab-case equivalents in templates:
<user-profile :user-name="name"></user-profile>
props: {
userName: String
}
Child-to-Parent Communication via Custom Events
Children emit events that parents listen to using $emit.
<div id="app">
<selection-list @pick="onPick"></selection-list>
<p>Selected: {{ selected }}</p>
</div>
<template id="list-template">
<div>
<button
v-for="option in options"
@click="choose(option)"
:key="option"
>
{{ option }}
</button>
</div>
</template>
<script>
Vue.component("selection-list", {
template: "#list-template",
data() {
return {
options: ["Alpha", "Beta", "Gamma"]
};
},
methods: {
choose(val) {
this.$emit("pick", val);
}
}
});
new Vue({
el: "#app",
data: { selected: "" },
methods: {
onPick(value) {
this.selected = value;
}
}
});
</script>
Achieving Two-Way Binding Between Parent and Child
For a parent-child model synchronization, a child can accept a prop and emit an update event. A watcher ensures05子どもux stays sync when the prop changes.
<div id="app">
<p>Parent quantity: {{ qty }}</p>
<input type="number" v-model.number="qty" />
<quantity-editor
:value="qty"
@update="qty = $event"
></quantity-editor>
</div>
<template id="editor-template">
<div>
<p>Child editor: {{ localValue }}</p>
<input
type="number"
:value="localValue"
@input="updateLocal"
/>
</div>
</template>
<script>
Vue.component("quantity-editor", {
template: "#editor-template",
props: {
value: Number
},
data() {
return {
localValue: this.value
};
},
watch: {
value(newVal) {
this.localValue = newVal;
}
},
methods: {
updateLocal(event) {
const num = Number(event.target.value);
this.localValue = num;
this.$emit("update", num);
}
}
});
new Vue({
el: "#app",
data: {
qty: 1
}
});
</script>
Direct Component Access
) para collectedos parent can reach children using $refs, while a child can access its parent through $parent`.
<div id="app">
<child-widget ref="widgetA"></child-widget>
<button @click="showInfo">Log child info</button>
</div>
<script>
Vue.component("child-widget", {
template: `<p>Widget component</p>`,
data() {
return { secret: 42 };
}
});
new Vue({
el: "#app",
methods: {
showInfo() {
console.log(this.$refs.widgetA.secret);
}
}
});
</script>
Slots for Content Distribution
Slots allow parent components to inject content into predefined places inside a child.
Default Slot
<div id="app">
<panel-box>
<strong>Dynamic content</strong>
</panel-box>
</div>
<template id="panel">
<div class="panel">
<h3>Panel Header</h3>
<slot>Fallback text</slot>
</div>
</template>
<script>
Vue.component("panel-box", {
template: "#panel"
});
new Vue({ el: "#app" });
</script>
Named Slots
指定name属性占据不同出口
<div id="app">
<layout-widget>
<template v-slot:title>
<h2>Custom Title</h2>
</template>
<template v-slot:body>
<p>body area</p>
</template>
</layout-widget>
</div>
<template id="layout">
<div>
<slot name="title">Default Title</slot>
<slot name="body">Default Body</slot>
</div>
</template>
<script>
Vue.component("layout-widget", {
template: "#layout"
});
new Vue({ el: "#app" });
</script>
Scoped Slots
Scoped slots allow0 a parent to receive data from the child while overriding the child’s slot content. Use v-slot (or shorthand #) to access the exposed props.
<div id="app">
<todo-list>
<template #default="{ tasks }">
<ul>
<li v-for="task in tasks" :key="task.id">
{{ task.label }}
</li>
</ul>
</template>
</todo-list>
</div>
<template id="todo-template">
<div>
<h4>Tasks</h4>
<slot :tasks="todoList">
<p v-for="task in todoList">{{ task.label }}</p>
</slot>
</div>
</template>
<script>
Vue.component("todo-list", {
template: "#todo-template",
data() {
return {
todoList: [
{ id: 1, label: "Design UI" },
{ id: 2, label: "Implement logic" }
]
};
}
});
new Vue({ el: "#app" });
</script>
v-slot unification
The v-slot directive replaces slot and slot-scope (Vue 2.6+). Note that v-slot is placed on <template>. For default slots you can use v-slot or #default. When using destructuring, you can also rename properties:
<template v-slot="{ tasks: projectTasks }">
<span>{{ projectTasks.length }} tasks</span>
</template>