Master Vue.js component architecture, props and emit patterns, component lifecycle, data flow between components, and building reusable component systems.
Vue.js components are the building blocks of scalable applications. They encapsulate HTML, CSS, and JavaScript into reusable, self-contained units that can be composed together to build complex user interfaces.
Understanding component architecture is crucial for building maintainable Vue applications. Components promote reusability, testability, and separation of concerns. They allow you to break down complex UIs into manageable pieces that can be developed, tested, and debugged independently.
Vue's component system is both powerful and intuitive. It provides clear patterns for data flow, event communication, and lifecycle management. Whether you're building a simple widget or a complex single-page application, mastering Vue components is essential for modern web development.
In this comprehensive lesson, you'll learn to create reusable components, manage data flow between parent and child components, understand component lifecycle, and build sophisticated component hierarchies that scale with your application needs.
Vue components encapsulate functionality into reusable units. They follow the single-file component (SFC) structure that keeps template, script, and style together for better organization and maintainability.
Basic Component Structure:
1<!-- UserCard.vue -->
2<template>
3 <div class="user-card">
4 <div class="avatar">
5 <img :src="user.avatar" :alt="user.name + ' avatar'">
6 <div class="status-indicator" :class="user.status"></div>
7 </div>
8 <div class="user-info">
9 <h3>{{user.name}}</h3>
10 <p class="title">{{user.title}}</p>
11 <p class="email">{{user.email}}</p>
12 <div class="skills">
13 <span v-for="skill in user.skills" :key="skill" class="skill-tag">
14 {{skill}}
15 </span>
16 </div>
17 </div>
18 <div class="actions">
19 <button @click="handleMessage" class="btn-primary">Message</button>
20 <button @click="handleConnect" class="btn-secondary">Connect</button>
21 </div>
22 </div>
23</template>
24
25<script>
26export default {
27 name: 'UserCard',
28 props: {
29 user: {
30 type: Object,
31 required: true,
32 validator(value) {
33 return value.name && value.email;
34 }
35 }
36 },
37 methods: {
38 handleMessage() {
39 this.$emit('message', this.user.id);
40 },
41 handleConnect() {
42 this.$emit('connect', this.user.id);
43 }
44 }
45};
46</script>
47
48<style scoped>
49.user-card {
50 border: 1px solid #e2e8f0;
51 border-radius: 12px;
52 padding: 24px;
53 background: white;
54 box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05);
55 transition: transform 0.2s ease;
56}
57
58.user-card:hover {
59 transform: translateY(-2px);
60 box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15);
61}
62
63.avatar {
64 position: relative;
65 width: 64px;
66 height: 64px;
67 margin-bottom: 16px;
68}
69
70.avatar img {
71 width: 100%;
72 height: 100%;
73 border-radius: 50%;
74 object-fit: cover;
75}
76
77.status-indicator {
78 position: absolute;
79 bottom: 4px;
80 right: 4px;
81 width: 16px;
82 height: 16px;
83 border-radius: 50%;
84 border: 2px solid white;
85}
86
87.status-indicator.online { background: #48bb78; }
88.status-indicator.away { background: #ed8936; }
89.status-indicator.offline { background: #a0aec0; }
90
91.user-info h3 {
92 margin: 0 0 4px 0;
93 color: #2d3748;
94}
95
96.title {
97 color: #4a5568;
98 font-size: 0.9rem;
99 margin: 0 0 8px 0;
100}
101
102.email {
103 color: #718096;
104 font-size: 0.8rem;
105 margin: 0 0 12px 0;
106}
107
108.skills {
109 display: flex;
110 flex-wrap: wrap;
111 gap: 6px;
112 margin-bottom: 16px;
113}
114
115.skill-tag {
116 background: #edf2f7;
117 color: #4a5568;
118 padding: 4px 8px;
119 border-radius: 12px;
120 font-size: 0.75rem;
121}
122
123.actions {
124 display: flex;
125 gap: 8px;
126}
127
128.btn-primary, .btn-secondary {
129 padding: 8px 16px;
130 border: none;
131 border-radius: 6px;
132 cursor: pointer;
133 font-size: 0.875rem;
134 transition: background-color 0.2s ease;
135}
136
137.btn-primary {
138 background: #4299e1;
139 color: white;
140}
141
142.btn-primary:hover {
143 background: #3182ce;
144}
145
146.btn-secondary {
147 background: #e2e8f0;
148 color: #4a5568;
149}
150
151.btn-secondary:hover {
152 background: #cbd5e0;
153}
154</style>Component Registration and Usage:
1<!-- Parent component using UserCard -->
2<template>
3 <div class="user-directory">
4 <h1>Team Directory</h1>
5 <div class="search-bar">
6 <input
7 v-model="searchQuery"
8 placeholder="Search team members..."
9 class="search-input"
10 >
11 </div>
12 <div class="user-grid">
13 <UserCard
14 v-for="user in filteredUsers"
15 :key="user.id"
16 :user="user"
17 @message="handleMessage"
18 @connect="handleConnect"
19 />
20 </div>
21 <div v-if="filteredUsers.length === 0" class="no-results">
22 No users found matching "{{searchQuery}}"
23 </div>
24 </div>
25</template>
26
27<script>
28import UserCard from './components/UserCard.vue';
29
30export default {
31 name: 'UserDirectory',
32 components: {
33 UserCard
34 },
35 data() {
36 return {
37 searchQuery: '',
38 users: [
39 {
40 id: 1,
41 name: 'Alice Johnson',
42 title: 'Senior Frontend Developer',
43 email: '[email protected]',
44 avatar: '/avatars/alice.jpg',
45 status: 'online',
46 skills: ['Vue.js', 'React', 'TypeScript']
47 },
48 {
49 id: 2,
50 name: 'Bob Smith',
51 title: 'Backend Engineer',
52 email: '[email protected]',
53 avatar: '/avatars/bob.jpg',
54 status: 'away',
55 skills: ['Node.js', 'Python', 'PostgreSQL']
56 },
57 {
58 id: 3,
59 name: 'Carol Davis',
60 title: 'UX Designer',
61 email: '[email protected]',
62 avatar: '/avatars/carol.jpg',
63 status: 'offline',
64 skills: ['Figma', 'Sketch', 'User Research']
65 }
66 ]
67 };
68 },
69 computed: {
70 filteredUsers() {
71 if (!this.searchQuery) return this.users;
72
73 const query = this.searchQuery.toLowerCase();
74 return this.users.filter(user =>
75 user.name.toLowerCase().includes(query) ||
76 user.title.toLowerCase().includes(query) ||
77 user.skills.some(skill => skill.toLowerCase().includes(query))
78 );
79 }
80 },
81 methods: {
82 handleMessage(userId) {
83 const user = this.users.find(u => u.id === userId);
84 alert(`Starting conversation with ${user.name}`);
85 },
86 handleConnect(userId) {
87 const user = this.users.find(u => u.id === userId);
88 alert(`Sending connection request to ${user.name}`);
89 }
90 }
91};
92</script>
93
94<style scoped>
95.user-directory {
96 max-width: 1200px;
97 margin: 0 auto;
98 padding: 24px;
99}
100
101.search-bar {
102 margin-bottom: 32px;
103}
104
105.search-input {
106 width: 100%;
107 max-width: 400px;
108 padding: 12px 16px;
109 border: 2px solid #e2e8f0;
110 border-radius: 8px;
111 font-size: 1rem;
112}
113
114.search-input:focus {
115 outline: none;
116 border-color: #4299e1;
117 box-shadow: 0 0 0 3px rgba(66, 153, 225, 0.1);
118}
119
120.user-grid {
121 display: grid;
122 grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
123 gap: 24px;
124}
125
126.no-results {
127 text-align: center;
128 color: #718096;
129 font-style: italic;
130 padding: 48px 24px;
131}
132</style>Global vs Local Component Registration:
1// main.js - Global registration
2import { createApp } from 'vue';
3import App from './App.vue';
4import UserCard from './components/UserCard.vue';
5
6const app = createApp(App);
7
8// Global registration - available in all components
9app.component('UserCard', UserCard);
10app.component('BaseButton', BaseButton);
11app.component('BaseInput', BaseInput);
12
13app.mount('#app');
14
15// Local registration (preferred for most cases)
16export default {
17 components: {
18 UserCard, // ES6 shorthand
19 BaseButton: BaseButton, // Full syntax
20 'custom-input': CustomInput // Custom name
21 }
22};Component Composition Patterns:
1<!-- Compound components working together -->
2<template>
3 <div class="dashboard">
4 <!-- Header component -->
5 <DashboardHeader
6 :user="currentUser"
7 :notifications="notifications"
8 @logout="handleLogout"
9 />
10
11 <!-- Sidebar navigation -->
12 <DashboardSidebar
13 :navigation="navigationItems"
14 :active-section="activeSection"
15 @navigate="handleNavigation"
16 />
17
18 <!-- Main content area -->
19 <DashboardContent>
20 <component
21 :is="currentPageComponent"
22 :data="currentPageData"
23 @action="handlePageAction"
24 />
25 </DashboardContent>
26
27 <!-- Modal system -->
28 <ModalContainer>
29 <UserSettingsModal
30 v-if="showSettingsModal"
31 :user="currentUser"
32 @save="handleSaveSettings"
33 @close="showSettingsModal = false"
34 />
35 </ModalContainer>
36 </div>
37</template>Component Best Practices:
Creating reusable components is the foundation of scalable Vue applications.
Props are the primary way to pass data from parent components to child components in Vue. They create a unidirectional data flow that makes applications predictable and easier to debug.
Basic Props Declaration:
1<!-- Child Component: ProductCard.vue -->
2<template>
3 <div class="product-card">
4 <img :src="product.image" :alt="product.name">
5 <div class="product-info">
6 <h3>{{product.name}}</h3>
7 <p class="price">${{formattedPrice}}</p>
8 <p class="description">{{product.description}}</p>
9 <div class="rating">
10 <StarRating :rating="product.rating" />
11 <span>({{product.reviews}} reviews)</span>
12 </div>
13 <button
14 :disabled="!product.inStock"
15 @click="handleAddToCart"
16 class="add-to-cart-btn"
17 >
18 {{product.inStock ? 'Add to Cart' : 'Out of Stock'}}
19 </button>
20 </div>
21 </div>
22</template>
23
24<script>
25import StarRating from './StarRating.vue';
26
27export default {
28 name: 'ProductCard',
29 components: {
30 StarRating
31 },
32 props: {
33 // Object syntax with validation
34 product: {
35 type: Object,
36 required: true,
37 validator(value) {
38 // Custom validation logic
39 return value.id && value.name && value.price >= 0;
40 }
41 },
42 // Array syntax for simple props
43 showDiscount: Boolean,
44 currency: {
45 type: String,
46 default: 'USD'
47 }
48 },
49 computed: {
50 formattedPrice() {
51 return new Intl.NumberFormat('en-US', {
52 style: 'currency',
53 currency: this.currency
54 }).format(this.product.price);
55 }
56 },
57 methods: {
58 handleAddToCart() {
59 this.$emit('add-to-cart', {
60 productId: this.product.id,
61 quantity: 1
62 });
63 }
64 }
65};
66</script>Prop Types and Validation:
1<script>
2export default {
3 props: {
4 // Basic types
5 title: String,
6 count: Number,
7 isActive: Boolean,
8 tags: Array,
9 config: Object,
10 callback: Function,
11
12 // Multiple possible types
13 id: [String, Number],
14
15 // Required props
16 userId: {
17 type: [String, Number],
18 required: true
19 },
20
21 // Default values
22 theme: {
23 type: String,
24 default: 'light'
25 },
26
27 // Default factory for objects/arrays
28 items: {
29 type: Array,
30 default: () => []
31 },
32 settings: {
33 type: Object,
34 default: () => ({
35 notifications: true,
36 theme: 'auto'
37 })
38 },
39
40 // Custom validation
41 email: {
42 type: String,
43 validator(value) {
44 return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value);
45 }
46 },
47
48 // Enum-like validation
49 size: {
50 type: String,
51 default: 'medium',
52 validator(value) {
53 return ['small', 'medium', 'large', 'extra-large'].includes(value);
54 }
55 }
56 }
57};
58</script>Dynamic Props and Binding:
1<!-- Parent Component -->
2<template>
3 <div class="product-catalog">
4 <!-- Static props -->
5 <ProductCard
6 :product="selectedProduct"
7 :show-discount="true"
8 currency="USD"
9 />
10
11 <!-- Dynamic props -->
12 <ProductCard
13 v-for="product in products"
14 :key="product.id"
15 :product="product"
16 :show-discount="product.discount > 0"
17 :currency="userCurrency"
18 @add-to-cart="handleAddToCart"
19 />
20
21 <!-- Passing all properties of an object as props -->
22 <ProductCard v-bind="productWithProps" />
23
24 <!-- Conditional props -->
25 <ProductCard
26 :product="product"
27 :show-reviews="product.reviews > 0"
28 :highlight="product.id === featuredProductId"
29 />
30
31 <!-- Props with computed values -->
32 <ProductCard
33 :product="product"
34 :discounted-price="calculateDiscountedPrice(product)"
35 :is-favorite="favoriteProductIds.includes(product.id)"
36 />
37 </div>
38</template>
39
40<script>
41export default {
42 data() {
43 return {
44 selectedProduct: null,
45 products: [],
46 userCurrency: 'USD',
47 featuredProductId: null,
48 favoriteProductIds: [1, 3, 7],
49 productWithProps: {
50 product: { /* product data */ },
51 showDiscount: true,
52 currency: 'EUR'
53 }
54 };
55 },
56 computed: {
57 calculateDiscountedPrice() {
58 return (product) => {
59 if (product.discount) {
60 return product.price * (1 - product.discount / 100);
61 }
62 return product.price;
63 };
64 }
65 },
66 methods: {
67 handleAddToCart(cartItem) {
68 console.log('Adding to cart:', cartItem);
69 // Add to cart logic
70 }
71 }
72};
73</script>Advanced Prop Patterns:
1<!-- Render Props Pattern -->
2<template>
3 <DataProvider :fetch-fn="fetchUserData">
4 <template v-slot="{ data, loading, error }">
5 <div v-if="loading">Loading user data...</div>
6 <div v-else-if="error">Error: {{error.message}}</div>
7 <UserProfile v-else :user="data" />
8 </template>
9 </DataProvider>
10</template>
11
12<!-- Configuration Object Props -->
13<template>
14 <DataTable
15 :columns="tableColumns"
16 :data="tableData"
17 :options="{
18 pagination: true,
19 sorting: true,
20 filtering: true,
21 pageSize: 25,
22 theme: 'modern'
23 }"
24 />
25</template>
26
27<!-- Function Props for Customization -->
28<template>
29 <InfiniteScroll
30 :items="items"
31 :render-item="renderItem"
32 :load-more="loadMoreItems"
33 :has-more="hasMoreItems"
34 />
35</template>
36
37<script>
38export default {
39 methods: {
40 renderItem(item) {
41 return {
42 component: 'ProductCard',
43 props: { product: item }
44 };
45 },
46 loadMoreItems() {
47 // Load more items logic
48 return this.fetchMoreProducts();
49 }
50 }
51};
52</script>Prop Drilling and Solutions:
1<!-- Problem: Prop drilling through multiple levels -->
2<template>
3 <!-- GrandParent -->
4 <Parent :user="user" :theme="theme" />
5</template>
6
7<!-- Parent.vue -->
8<template>
9 <!-- Just passing props through -->
10 <Child :user="user" :theme="theme" />
11</template>
12
13<!-- Child.vue -->
14<template>
15 <!-- Finally using the props -->
16 <div :class="theme">{{user.name}}</div>
17</template>
18
19<!-- Solution 1: Provide/Inject -->
20<template>
21 <!-- GrandParent -->
22 <Parent /> <!-- No props needed -->
23</template>
24
25<script>
26// GrandParent
27export default {
28 provide() {
29 return {
30 user: this.user,
31 theme: this.theme
32 };
33 }
34};
35
36// Child
37export default {
38 inject: ['user', 'theme'],
39 template: '<div :class="theme">{{user.name}}</div>'
40};
41</script>
42
43<!-- Solution 2: Store/State Management -->
44<template>
45 <!-- Child -->
46 <div :class="$store.state.theme">
47 {{$store.state.user.name}}
48 </div>
49</template>Props Performance Considerations:
1<script>
2export default {
3 props: {
4 // Good: Primitive values are efficient
5 count: Number,
6 title: String,
7
8 // Caution: Large objects can impact performance
9 largeDataset: {
10 type: Array,
11 // Use function for default to avoid shared references
12 default: () => []
13 }
14 },
15 computed: {
16 // Good: Computed props for expensive operations
17 processedData() {
18 return this.largeDataset.map(item => ({
19 ...item,
20 formatted: this.formatItem(item)
21 }));
22 }
23 },
24 watch: {
25 // Watch for prop changes efficiently
26 'largeDataset.length'(newLength) {
27 console.log(`Dataset size changed to: ${newLength}`);
28 }
29 }
30};
31</script>Prop Best Practices:
Props create a clear contract between parent and child components, making your Vue applications predictable and maintainable.
The emit system in Vue enables child components to communicate with their parents by sending events upward. This maintains unidirectional data flow while allowing interactive communication between components.
Basic Emit Pattern:
1<!-- Child Component: CommentForm.vue -->
2<template>
3 <form @submit.prevent="submitComment" class="comment-form">
4 <h3>Add a Comment</h3>
5
6 <div class="form-group">
7 <label for="author">Name:</label>
8 <input
9 id="author"
10 v-model.trim="formData.author"
11 type="text"
12 required
13 placeholder="Your name"
14 >
15 </div>
16
17 <div class="form-group">
18 <label for="content">Comment:</label>
19 <textarea
20 id="content"
21 v-model.trim="formData.content"
22 required
23 placeholder="Share your thoughts..."
24 rows="4"
25 ></textarea>
26 </div>
27
28 <div class="form-group">
29 <label>
30 <input
31 v-model="formData.isPrivate"
32 type="checkbox"
33 >
34 Private comment
35 </label>
36 </div>
37
38 <div class="form-actions">
39 <button type="button" @click="handleCancel">Cancel</button>
40 <button type="submit" :disabled="!isFormValid">
41 Post Comment
42 </button>
43 </div>
44
45 <div v-if="errors.length" class="errors">
46 <p v-for="error in errors" :key="error">{{error}}</p>
47 </div>
48 </form>
49</template>
50
51<script>
52export default {
53 name: 'CommentForm',
54 emits: {
55 // Define emitted events with validation
56 'comment-submitted': (comment) => {
57 return comment && comment.author && comment.content;
58 },
59 'form-cancelled': null, // No validation needed
60 'form-error': (errors) => Array.isArray(errors)
61 },
62 data() {
63 return {
64 formData: {
65 author: '',
66 content: '',
67 isPrivate: false
68 },
69 errors: []
70 };
71 },
72 computed: {
73 isFormValid() {
74 return this.formData.author.trim() &&
75 this.formData.content.trim() &&
76 this.formData.content.length >= 10;
77 }
78 },
79 methods: {
80 submitComment() {
81 this.errors = [];
82
83 // Validation
84 if (!this.isFormValid) {
85 this.errors.push('Please fill in all required fields');
86 this.$emit('form-error', this.errors);
87 return;
88 }
89
90 if (this.formData.content.length > 500) {
91 this.errors.push('Comment must be less than 500 characters');
92 this.$emit('form-error', this.errors);
93 return;
94 }
95
96 // Create comment object
97 const comment = {
98 id: Date.now(),
99 author: this.formData.author,
100 content: this.formData.content,
101 isPrivate: this.formData.isPrivate,
102 timestamp: new Date().toISOString()
103 };
104
105 // Emit the event with data
106 this.$emit('comment-submitted', comment);
107
108 // Reset form
109 this.resetForm();
110 },
111
112 handleCancel() {
113 this.resetForm();
114 this.$emit('form-cancelled');
115 },
116
117 resetForm() {
118 this.formData = {
119 author: '',
120 content: '',
121 isPrivate: false
122 };
123 this.errors = [];
124 }
125 }
126};
127</script>
128
129<style scoped>
130.comment-form {
131 background: #f7fafc;
132 border: 1px solid #e2e8f0;
133 border-radius: 8px;
134 padding: 24px;
135 margin-bottom: 24px;
136}
137
138.form-group {
139 margin-bottom: 16px;
140}
141
142.form-group label {
143 display: block;
144 margin-bottom: 4px;
145 font-weight: 600;
146 color: #2d3748;
147}
148
149.form-group input,
150.form-group textarea {
151 width: 100%;
152 padding: 8px 12px;
153 border: 1px solid #cbd5e0;
154 border-radius: 4px;
155}
156
157.form-group input:focus,
158.form-group textarea:focus {
159 outline: none;
160 border-color: #4299e1;
161 box-shadow: 0 0 0 3px rgba(66, 153, 225, 0.1);
162}
163
164.form-actions {
165 display: flex;
166 gap: 12px;
167 justify-content: flex-end;
168}
169
170.form-actions button {
171 padding: 8px 16px;
172 border: 1px solid #cbd5e0;
173 border-radius: 4px;
174 cursor: pointer;
175}
176
177.form-actions button[type="submit"] {
178 background: #4299e1;
179 color: white;
180 border-color: #4299e1;
181}
182
183.form-actions button[type="submit"]:disabled {
184 background: #a0aec0;
185 cursor: not-allowed;
186}
187
188.errors {
189 background: #fed7d7;
190 border: 1px solid #fc8181;
191 border-radius: 4px;
192 padding: 12px;
193 margin-top: 16px;
194}
195
196.errors p {
197 margin: 0;
198 color: #c53030;
199}
200</style>Parent Component Handling Events:
1<!-- Parent Component: BlogPost.vue -->
2<template>
3 <article class="blog-post">
4 <header class="post-header">
5 <h1>{{post.title}}</h1>
6 <div class="post-meta">
7 By {{post.author}} on {{formatDate(post.date)}}
8 </div>
9 </header>
10
11 <div class="post-content">
12 {{post.content}}
13 </div>
14
15 <section class="comments-section">
16 <h2>Comments ({{comments.length}})</h2>
17
18 <!-- Comment Form -->
19 <CommentForm
20 @comment-submitted="handleNewComment"
21 @form-cancelled="handleFormCancel"
22 @form-error="handleFormError"
23 />
24
25 <!-- Comments List -->
26 <div class="comments-list">
27 <CommentItem
28 v-for="comment in displayedComments"
29 :key="comment.id"
30 :comment="comment"
31 @comment-edited="handleCommentEdit"
32 @comment-deleted="handleCommentDelete"
33 @reply-submitted="handleReplySubmitted"
34 />
35 </div>
36
37 <!-- Load More Comments -->
38 <button
39 v-if="hasMoreComments"
40 @click="loadMoreComments"
41 class="load-more-btn"
42 >
43 Load More Comments
44 </button>
45 </section>
46
47 <!-- Success/Error Messages -->
48 <div v-if="message.text" :class="['message', message.type]">
49 {{message.text}}
50 </div>
51 </article>
52</template>
53
54<script>
55import CommentForm from './components/CommentForm.vue';
56import CommentItem from './components/CommentItem.vue';
57
58export default {
59 name: 'BlogPost',
60 components: {
61 CommentForm,
62 CommentItem
63 },
64 data() {
65 return {
66 post: {
67 title: 'Getting Started with Vue.js',
68 author: 'Jane Developer',
69 date: '2024-01-15',
70 content: 'Vue.js is an amazing framework for building user interfaces...'
71 },
72 comments: [],
73 displayedComments: [],
74 commentsPerPage: 5,
75 message: {
76 text: '',
77 type: 'success' // 'success' or 'error'
78 }
79 };
80 },
81 computed: {
82 hasMoreComments() {
83 return this.displayedComments.length < this.comments.length;
84 }
85 },
86 methods: {
87 handleNewComment(comment) {
88 // Add comment to list
89 this.comments.unshift(comment);
90 this.displayedComments.unshift(comment);
91
92 // Show success message
93 this.showMessage('Comment posted successfully!', 'success');
94
95 // Optional: Save to server
96 this.saveCommentToServer(comment);
97 },
98
99 handleFormCancel() {
100 this.showMessage('Comment cancelled', 'info');
101 },
102
103 handleFormError(errors) {
104 this.showMessage(errors.join('. '), 'error');
105 },
106
107 handleCommentEdit(editedComment) {
108 const index = this.comments.findIndex(c => c.id === editedComment.id);
109 if (index !== -1) {
110 this.comments.splice(index, 1, editedComment);
111 this.showMessage('Comment updated successfully!', 'success');
112 }
113 },
114
115 handleCommentDelete(commentId) {
116 this.comments = this.comments.filter(c => c.id !== commentId);
117 this.displayedComments = this.displayedComments.filter(c => c.id !== commentId);
118 this.showMessage('Comment deleted', 'success');
119 },
120
121 handleReplySubmitted(reply) {
122 // Handle nested reply logic
123 const parentComment = this.comments.find(c => c.id === reply.parentId);
124 if (parentComment) {
125 if (!parentComment.replies) {
126 parentComment.replies = [];
127 }
128 parentComment.replies.push(reply);
129 this.showMessage('Reply posted!', 'success');
130 }
131 },
132
133 loadMoreComments() {
134 const currentLength = this.displayedComments.length;
135 const newComments = this.comments.slice(
136 currentLength,
137 currentLength + this.commentsPerPage
138 );
139 this.displayedComments.push(...newComments);
140 },
141
142 showMessage(text, type = 'success') {
143 this.message = { text, type };
144 setTimeout(() => {
145 this.message.text = '';
146 }, 3000);
147 },
148
149 formatDate(dateString) {
150 return new Date(dateString).toLocaleDateString('en-US', {
151 year: 'numeric',
152 month: 'long',
153 day: 'numeric'
154 });
155 },
156
157 async saveCommentToServer(comment) {
158 try {
159 await fetch('/api/comments', {
160 method: 'POST',
161 headers: {
162 'Content-Type': 'application/json'
163 },
164 body: JSON.stringify(comment)
165 });
166 } catch (error) {
167 console.error('Failed to save comment:', error);
168 this.showMessage('Failed to save comment to server', 'error');
169 }
170 }
171 },
172
173 async mounted() {
174 // Load existing comments
175 try {
176 const response = await fetch('/api/comments');
177 const allComments = await response.json();
178 this.comments = allComments;
179 this.displayedComments = allComments.slice(0, this.commentsPerPage);
180 } catch (error) {
181 console.error('Failed to load comments:', error);
182 }
183 }
184};
185</script>Advanced Emit Patterns:
1<!-- Custom Input Component with v-model support -->
2<template>
3 <div class="custom-input">
4 <label :for="inputId">{{label}}</label>
5 <input
6 :id="inputId"
7 :value="modelValue"
8 :type="type"
9 :placeholder="placeholder"
10 @input="handleInput"
11 @blur="handleBlur"
12 @focus="handleFocus"
13 >
14 <div v-if="error" class="error-message">{{error}}</div>
15 </div>
16</template>
17
18<script>
19export default {
20 name: 'CustomInput',
21 props: {
22 modelValue: String,
23 label: String,
24 type: { type: String, default: 'text' },
25 placeholder: String,
26 error: String
27 },
28 emits: [
29 'update:modelValue', // Required for v-model
30 'input-blur',
31 'input-focus',
32 'input-change'
33 ],
34 data() {
35 return {
36 inputId: `input-${Math.random().toString(36).substr(2, 9)}`
37 };
38 },
39 methods: {
40 handleInput(event) {
41 // Emit for v-model support
42 this.$emit('update:modelValue', event.target.value);
43
44 // Emit additional events
45 this.$emit('input-change', {
46 value: event.target.value,
47 inputId: this.inputId
48 });
49 },
50
51 handleBlur(event) {
52 this.$emit('input-blur', {
53 value: event.target.value,
54 inputId: this.inputId
55 });
56 },
57
58 handleFocus(event) {
59 this.$emit('input-focus', {
60 value: event.target.value,
61 inputId: this.inputId
62 });
63 }
64 }
65};
66</script>Event Bus Pattern (Alternative Communication):
1// eventBus.js
2import { createApp } from 'vue';
3export const eventBus = createApp({}).config.globalProperties;
4
5// Or using mitt library for better event handling
6import mitt from 'mitt';
7export const eventBus = mitt();
8
9// Component A - Emitting events
10export default {
11 methods: {
12 notifyOtherComponents() {
13 eventBus.emit('data-updated', {
14 timestamp: Date.now(),
15 data: this.someData
16 });
17 }
18 }
19};
20
21// Component B - Listening to events
22export default {
23 mounted() {
24 eventBus.on('data-updated', this.handleDataUpdate);
25 },
26 beforeUnmount() {
27 eventBus.off('data-updated', this.handleDataUpdate);
28 },
29 methods: {
30 handleDataUpdate(payload) {
31 console.log('Data updated:', payload);
32 // Handle the event
33 }
34 }
35};Emit Best Practices:
The emit system provides a clean, predictable way for components to communicate upward while maintaining Vue's reactive data flow patterns.
Vue component lifecycle hooks allow you to run code at specific moments in a component's existence, from creation to destruction. Understanding lifecycle is crucial for proper resource management and optimal performance.
Vue 3 Composition API Lifecycle:
1<template>
2 <div class="user-dashboard">
3 <div v-if="loading" class="loading">Loading user data...</div>
4 <div v-else-if="error" class="error">{{error}}</div>
5 <div v-else class="dashboard-content">
6 <h1>Welcome, {{user.name}}!</h1>
7 <div class="stats">
8 <div class="stat-item">
9 <span class="label">Last Login:</span>
10 <span class="value">{{formatDate(user.lastLogin)}}</span>
11 </div>
12 <div class="stat-item">
13 <span class="label">Session Time:</span>
14 <span class="value">{{formatTime(sessionTime)}}</span>
15 </div>
16 </div>
17 <div class="activity-feed">
18 <h2>Recent Activity</h2>
19 <div v-for="activity in activities" :key="activity.id" class="activity-item">
20 {{activity.description}}
21 </div>
22 </div>
23 </div>
24 </div>
25</template>
26
27<script>
28import {
29 ref,
30 reactive,
31 computed,
32 onBeforeMount,
33 onMounted,
34 onBeforeUpdate,
35 onUpdated,
36 onBeforeUnmount,
37 onUnmounted,
38 onActivated,
39 onDeactivated,
40 watch
41} from 'vue';
42
43export default {
44 name: 'UserDashboard',
45 props: {
46 userId: {
47 type: [String, Number],
48 required: true
49 }
50 },
51 setup(props, { emit }) {
52 // Reactive state
53 const user = ref(null);
54 const activities = ref([]);
55 const loading = ref(true);
56 const error = ref(null);
57 const sessionTime = ref(0);
58
59 // Internal state
60 let sessionTimer = null;
61 let activityPoller = null;
62
63 // Computed properties
64 const formatDate = computed(() => {
65 return (date) => new Date(date).toLocaleDateString();
66 });
67
68 const formatTime = computed(() => {
69 return (seconds) => {
70 const hours = Math.floor(seconds / 3600);
71 const minutes = Math.floor((seconds % 3600) / 60);
72 const secs = seconds % 60;
73 return `${hours}h ${minutes}m ${secs}s`;
74 };
75 });
76
77 // Lifecycle Hooks
78
79 onBeforeMount(() => {
80 console.log('Component is about to mount');
81 // Setup that doesn't require DOM
82 sessionTime.value = 0;
83 });
84
85 onMounted(async () => {
86 console.log('Component has been mounted');
87
88 // DOM is available, can access refs
89 await loadUserData();
90 startSessionTimer();
91 startActivityPolling();
92
93 // Set up event listeners
94 window.addEventListener('beforeunload', handleBeforeUnload);
95 document.addEventListener('visibilitychange', handleVisibilityChange);
96 });
97
98 onBeforeUpdate(() => {
99 console.log('Component is about to update');
100 // Runs before any reactive changes cause re-render
101 });
102
103 onUpdated(() => {
104 console.log('Component has been updated');
105 // Runs after reactive changes cause re-render
106 // Use sparingly to avoid infinite loops
107 });
108
109 onBeforeUnmount(() => {
110 console.log('Component is about to unmount');
111
112 // Cleanup before component is destroyed
113 if (sessionTimer) {
114 clearInterval(sessionTimer);
115 }
116
117 if (activityPoller) {
118 clearInterval(activityPoller);
119 }
120
121 // Remove event listeners
122 window.removeEventListener('beforeunload', handleBeforeUnload);
123 document.removeEventListener('visibilitychange', handleVisibilityChange);
124
125 // Save session data
126 saveSessionData();
127 });
128
129 onUnmounted(() => {
130 console.log('Component has been unmounted');
131 // Final cleanup after component is destroyed
132 // Rarely needed as onBeforeUnmount handles most cases
133 });
134
135 // Keep-alive hooks (if component is wrapped in <keep-alive>)
136 onActivated(() => {
137 console.log('Component activated (keep-alive)');
138 // Resume timers, refresh data
139 if (sessionTimer) {
140 startSessionTimer();
141 }
142 });
143
144 onDeactivated(() => {
145 console.log('Component deactivated (keep-alive)');
146 // Pause timers, stop polling
147 if (sessionTimer) {
148 clearInterval(sessionTimer);
149 }
150 });
151
152 // Watchers (can be considered lifecycle-related)
153 watch(() => props.userId, async (newUserId, oldUserId) => {
154 console.log(`User changed from ${oldUserId} to ${newUserId}`);
155 loading.value = true;
156 await loadUserData();
157 });
158
159 watch(sessionTime, (newTime) => {
160 // Auto-save session every 5 minutes
161 if (newTime > 0 && newTime % 300 === 0) {
162 saveSessionData();
163 }
164 });
165
166 // Methods
167 async function loadUserData() {
168 try {
169 loading.value = true;
170 error.value = null;
171
172 // Simulate API call
173 const response = await fetch(`/api/users/${props.userId}`);
174 if (!response.ok) throw new Error('Failed to load user data');
175
176 user.value = await response.json();
177
178 // Load activities
179 await loadActivities();
180
181 } catch (err) {
182 error.value = err.message;
183 console.error('Error loading user data:', err);
184 } finally {
185 loading.value = false;
186 }
187 }
188
189 async function loadActivities() {
190 try {
191 const response = await fetch(`/api/users/${props.userId}/activities`);
192 if (response.ok) {
193 activities.value = await response.json();
194 }
195 } catch (err) {
196 console.error('Error loading activities:', err);
197 }
198 }
199
200 function startSessionTimer() {
201 sessionTimer = setInterval(() => {
202 sessionTime.value++;
203 }, 1000);
204 }
205
206 function startActivityPolling() {
207 // Poll for new activities every 30 seconds
208 activityPoller = setInterval(loadActivities, 30000);
209 }
210
211 function handleBeforeUnload(event) {
212 // Save data before page unload
213 saveSessionData();
214
215 // Show confirmation dialog
216 event.preventDefault();
217 event.returnValue = '';
218 }
219
220 function handleVisibilityChange() {
221 if (document.hidden) {
222 // Page became hidden - pause timers
223 if (sessionTimer) {
224 clearInterval(sessionTimer);
225 }
226 } else {
227 // Page became visible - resume timers
228 startSessionTimer();
229 }
230 }
231
232 function saveSessionData() {
233 const sessionData = {
234 userId: props.userId,
235 sessionTime: sessionTime.value,
236 timestamp: Date.now()
237 };
238
239 localStorage.setItem('userSession', JSON.stringify(sessionData));
240 emit('session-saved', sessionData);
241 }
242
243 // Return reactive refs and methods for template
244 return {
245 user,
246 activities,
247 loading,
248 error,
249 sessionTime,
250 formatDate,
251 formatTime
252 };
253 }
254};
255</script>Options API Lifecycle (Vue 2 Style):
1<script>
2export default {
3 name: 'LegacyComponent',
4 data() {
5 return {
6 message: 'Hello World',
7 counter: 0,
8 timer: null
9 };
10 },
11
12 // Creation phase
13 beforeCreate() {
14 console.log('beforeCreate - component instance created');
15 // Data and methods not yet available
16 // this.message would be undefined
17 },
18
19 created() {
20 console.log('created - component initialized');
21 // Data and methods available
22 // DOM not yet available
23 console.log(this.message); // "Hello World"
24
25 // Good place for API calls
26 this.loadData();
27 },
28
29 // Mounting phase
30 beforeMount() {
31 console.log('beforeMount - about to mount to DOM');
32 // Template compiled but not yet rendered
33 },
34
35 mounted() {
36 console.log('mounted - component mounted to DOM');
37 // DOM available, can access $refs
38 this.startTimer();
39
40 // Set up event listeners
41 window.addEventListener('resize', this.handleResize);
42 },
43
44 // Update phase
45 beforeUpdate() {
46 console.log('beforeUpdate - before DOM update');
47 // Called when reactive data changes
48 console.log('Counter before update:', this.counter);
49 },
50
51 updated() {
52 console.log('updated - after DOM update');
53 // DOM has been updated
54 console.log('Counter after update:', this.counter);
55
56 // Be careful of infinite loops here
57 // Don't modify reactive data directly
58 },
59
60 // Destruction phase
61 beforeUnmount() { // beforeDestroy in Vue 2
62 console.log('beforeUnmount - about to unmount');
63
64 // Cleanup
65 if (this.timer) {
66 clearInterval(this.timer);
67 }
68
69 window.removeEventListener('resize', this.handleResize);
70 },
71
72 unmounted() { // destroyed in Vue 2
73 console.log('unmounted - component destroyed');
74 // Final cleanup if needed
75 },
76
77 // Error handling
78 errorCaptured(error, instance, info) {
79 console.error('Error captured:', error);
80 console.log('Component instance:', instance);
81 console.log('Error info:', info);
82
83 // Return false to prevent error from propagating
84 return false;
85 },
86
87 methods: {
88 loadData() {
89 // API call logic
90 console.log('Loading data...');
91 },
92
93 startTimer() {
94 this.timer = setInterval(() => {
95 this.counter++;
96 }, 1000);
97 },
98
99 handleResize() {
100 console.log('Window resized');
101 }
102 }
103};
104</script>Lifecycle Hooks Use Cases:
1<script>
2export default {
3 setup() {
4 // Different hooks for different purposes
5
6 onBeforeMount(() => {
7 // Initialize non-DOM dependent state
8 // Set up reactive refs
9 // Prepare data structures
10 });
11
12 onMounted(() => {
13 // DOM manipulation
14 // Event listeners setup
15 // Third-party library initialization
16 // API calls that need DOM
17 // Focus management
18 // Scroll position setup
19 });
20
21 onBeforeUpdate(() => {
22 // Capture DOM state before update
23 // Prepare for expensive operations
24 // Cache calculations
25 });
26
27 onUpdated(() => {
28 // DOM operations after update
29 // Third-party library updates
30 // Scroll position adjustments
31 // Measurement operations
32 });
33
34 onBeforeUnmount(() => {
35 // Clear timers and intervals
36 // Remove event listeners
37 // Cancel network requests
38 // Save state to localStorage
39 // Cleanup third-party libraries
40 });
41
42 onUnmounted(() => {
43 // Final cleanup
44 // Memory leak prevention
45 // Analytics tracking
46 });
47
48 // Keep-alive specific
49 onActivated(() => {
50 // Resume operations
51 // Refresh data
52 // Restart timers
53 });
54
55 onDeactivated(() => {
56 // Pause operations
57 // Stop timers
58 // Save current state
59 });
60 }
61};
62</script>Common Lifecycle Patterns:
1<script>
2import { ref, onMounted, onBeforeUnmount } from 'vue';
3
4export default {
5 setup() {
6 // Pattern 1: API Data Loading
7 const data = ref(null);
8 const loading = ref(true);
9
10 onMounted(async () => {
11 try {
12 const response = await fetch('/api/data');
13 data.value = await response.json();
14 } finally {
15 loading.value = false;
16 }
17 });
18
19 // Pattern 2: Timer/Interval Management
20 const seconds = ref(0);
21 let timer = null;
22
23 onMounted(() => {
24 timer = setInterval(() => {
25 seconds.value++;
26 }, 1000);
27 });
28
29 onBeforeUnmount(() => {
30 if (timer) {
31 clearInterval(timer);
32 }
33 });
34
35 // Pattern 3: Event Listener Management
36 const handleKeydown = (event) => {
37 if (event.key === 'Escape') {
38 // Handle escape key
39 }
40 };
41
42 onMounted(() => {
43 document.addEventListener('keydown', handleKeydown);
44 });
45
46 onBeforeUnmount(() => {
47 document.removeEventListener('keydown', handleKeydown);
48 });
49
50 return {
51 data,
52 loading,
53 seconds
54 };
55 }
56};
57</script>Lifecycle Best Practices:
Understanding component lifecycle is essential for building robust, performant Vue applications that properly manage resources and provide great user experiences.
Building complex applications requires managing data flow between multiple levels of components. Vue provides several patterns and tools for effective communication in deep component hierarchies.
Multi-Level Component Communication:
1<!-- App.vue - Root Component -->
2<template>
3 <div class="app">
4 <AppHeader
5 :user="currentUser"
6 :notifications="notifications"
7 @search="handleGlobalSearch"
8 @user-menu-action="handleUserMenuAction"
9 />
10
11 <div class="app-layout">
12 <Sidebar
13 :navigation="navigationItems"
14 :active-section="activeSection"
15 @navigate="handleNavigation"
16 />
17
18 <MainContent
19 :loading="contentLoading"
20 :error="contentError"
21 >
22 <component
23 :is="currentPageComponent"
24 :data="currentPageData"
25 @data-change="handleDataChange"
26 @action="handlePageAction"
27 />
28 </MainContent>
29 </div>
30
31 <!-- Global Modals -->
32 <ModalContainer>
33 <UserSettingsModal
34 v-if="modals.userSettings"
35 :user="currentUser"
36 @save="handleUserSettingsSave"
37 @close="modals.userSettings = false"
38 />
39
40 <ConfirmDialog
41 v-if="modals.confirm"
42 :message="confirmMessage"
43 @confirm="handleConfirm"
44 @cancel="modals.confirm = false"
45 />
46 </ModalContainer>
47
48 <!-- Global Toast Notifications -->
49 <ToastContainer
50 :toasts="toasts"
51 @dismiss="dismissToast"
52 />
53 </div>
54</template>
55
56<script>
57import { provide, reactive, ref } from 'vue';
58import AppHeader from './components/AppHeader.vue';
59import Sidebar from './components/Sidebar.vue';
60import MainContent from './components/MainContent.vue';
61
62export default {
63 name: 'App',
64 components: {
65 AppHeader,
66 Sidebar,
67 MainContent,
68 ModalContainer: () => import('./components/ModalContainer.vue'),
69 UserSettingsModal: () => import('./components/UserSettingsModal.vue'),
70 ConfirmDialog: () => import('./components/ConfirmDialog.vue'),
71 ToastContainer: () => import('./components/ToastContainer.vue')
72 },
73
74 setup() {
75 // Global application state
76 const appState = reactive({
77 currentUser: null,
78 notifications: [],
79 navigationItems: [],
80 activeSection: 'dashboard',
81 contentLoading: false,
82 contentError: null,
83 modals: {
84 userSettings: false,
85 confirm: false
86 },
87 toasts: []
88 });
89
90 // Provide global state to all child components
91 provide('appState', appState);
92 provide('userService', userService);
93 provide('notificationService', notificationService);
94
95 // Global methods
96 const showToast = (message, type = 'info') => {
97 const toast = {
98 id: Date.now(),
99 message,
100 type,
101 timestamp: new Date()
102 };
103 appState.toasts.push(toast);
104
105 // Auto-dismiss after 5 seconds
106 setTimeout(() => {
107 dismissToast(toast.id);
108 }, 5000);
109 };
110
111 const dismissToast = (toastId) => {
112 const index = appState.toasts.findIndex(t => t.id === toastId);
113 if (index !== -1) {
114 appState.toasts.splice(index, 1);
115 }
116 };
117
118 provide('showToast', showToast);
119
120 // Event handlers
121 const handleGlobalSearch = (searchQuery) => {
122 console.log('Global search:', searchQuery);
123 // Implement global search logic
124 };
125
126 const handleUserMenuAction = (action) => {
127 switch (action) {
128 case 'settings':
129 appState.modals.userSettings = true;
130 break;
131 case 'logout':
132 handleLogout();
133 break;
134 }
135 };
136
137 const handleNavigation = (section) => {
138 appState.activeSection = section;
139 // Update current page component
140 updateCurrentPage(section);
141 };
142
143 return {
144 ...appState,
145 handleGlobalSearch,
146 handleUserMenuAction,
147 handleNavigation,
148 dismissToast
149 };
150 }
151};
152</script>Provide/Inject Pattern for Deep Communication:
1<!-- UserService for dependency injection -->
2<script>
3// services/userService.js
4export class UserService {
5 constructor() {
6 this.currentUser = null;
7 this.permissions = [];
8 }
9
10 async loadUser(userId) {
11 try {
12 const response = await fetch(`/api/users/${userId}`);
13 this.currentUser = await response.json();
14 this.permissions = await this.loadUserPermissions(userId);
15 return this.currentUser;
16 } catch (error) {
17 console.error('Failed to load user:', error);
18 throw error;
19 }
20 }
21
22 hasPermission(permission) {
23 return this.permissions.includes(permission);
24 }
25
26 async updateUser(userData) {
27 try {
28 const response = await fetch(`/api/users/${this.currentUser.id}`, {
29 method: 'PUT',
30 headers: { 'Content-Type': 'application/json' },
31 body: JSON.stringify(userData)
32 });
33
34 if (response.ok) {
35 this.currentUser = { ...this.currentUser, ...userData };
36 return this.currentUser;
37 }
38 } catch (error) {
39 console.error('Failed to update user:', error);
40 throw error;
41 }
42 }
43}
44
45// Root component providing services
46export default {
47 setup() {
48 const userService = new UserService();
49 const themeService = new ThemeService();
50
51 provide('userService', userService);
52 provide('themeService', themeService);
53
54 return {};
55 }
56};
57</script>
58
59<!-- Deep child component using injected services -->
60<script>
61import { inject } from 'vue';
62
63export default {
64 setup() {
65 const userService = inject('userService');
66 const themeService = inject('themeService');
67 const showToast = inject('showToast');
68
69 const updateProfile = async (profileData) => {
70 try {
71 await userService.updateUser(profileData);
72 showToast('Profile updated successfully!', 'success');
73 } catch (error) {
74 showToast('Failed to update profile', 'error');
75 }
76 };
77
78 const toggleTheme = () => {
79 themeService.toggleTheme();
80 showToast('Theme changed', 'info');
81 };
82
83 return {
84 updateProfile,
85 toggleTheme,
86 hasPermission: userService.hasPermission
87 };
88 }
89};
90</script>Event Bus Pattern for Sibling Communication:
1// eventBus.js
2import mitt from 'mitt';
3export const eventBus = mitt();
4
5// Alternative: Simple event bus
6export const simpleEventBus = {
7 events: {},
8
9 on(event, callback) {
10 if (!this.events[event]) {
11 this.events[event] = [];
12 }
13 this.events[event].push(callback);
14 },
15
16 off(event, callback) {
17 if (this.events[event]) {
18 this.events[event] = this.events[event].filter(cb => cb !== callback);
19 }
20 },
21
22 emit(event, data) {
23 if (this.events[event]) {
24 this.events[event].forEach(callback => callback(data));
25 }
26 }
27};1<!-- Component A - Shopping Cart -->
2<script>
3import { eventBus } from '@/services/eventBus';
4
5export default {
6 setup() {
7 const addToCart = (product) => {
8 // Add to cart logic
9 cart.value.push(product);
10
11 // Notify other components
12 eventBus.emit('cart:item-added', {
13 product,
14 cartTotal: cart.value.length
15 });
16 };
17
18 return { addToCart };
19 }
20};
21</script>
22
23<!-- Component B - Header Cart Counter -->
24<script>
25import { ref, onMounted, onBeforeUnmount } from 'vue';
26import { eventBus } from '@/services/eventBus';
27
28export default {
29 setup() {
30 const cartCount = ref(0);
31
32 const handleCartUpdate = (data) => {
33 cartCount.value = data.cartTotal;
34 // Show notification
35 showCartNotification(data.product);
36 };
37
38 onMounted(() => {
39 eventBus.on('cart:item-added', handleCartUpdate);
40 });
41
42 onBeforeUnmount(() => {
43 eventBus.off('cart:item-added', handleCartUpdate);
44 });
45
46 return { cartCount };
47 }
48};
49</script>
50
51<!-- Component C - Product List -->
52<script>
53import { eventBus } from '@/services/eventBus';
54
55export default {
56 setup() {
57 const handleProductPurchased = (data) => {
58 // Update product availability
59 updateProductStock(data.product.id);
60 };
61
62 onMounted(() => {
63 eventBus.on('cart:item-added', handleProductPurchased);
64 });
65
66 onBeforeUnmount(() => {
67 eventBus.off('cart:item-added', handleProductPurchased);
68 });
69
70 return {};
71 }
72};
73</script>State Management with Composables:
1// composables/useGlobalState.js
2import { reactive, readonly } from 'vue';
3
4const globalState = reactive({
5 user: null,
6 cart: [],
7 notifications: [],
8 theme: 'light',
9 language: 'en'
10});
11
12// Actions
13const actions = {
14 setUser(user) {
15 globalState.user = user;
16 },
17
18 addToCart(product) {
19 const existingItem = globalState.cart.find(item => item.id === product.id);
20 if (existingItem) {
21 existingItem.quantity += 1;
22 } else {
23 globalState.cart.push({ ...product, quantity: 1 });
24 }
25 },
26
27 removeFromCart(productId) {
28 const index = globalState.cart.findIndex(item => item.id === productId);
29 if (index !== -1) {
30 globalState.cart.splice(index, 1);
31 }
32 },
33
34 addNotification(notification) {
35 globalState.notifications.push({
36 id: Date.now(),
37 ...notification,
38 timestamp: new Date()
39 });
40 },
41
42 removeNotification(notificationId) {
43 const index = globalState.notifications.findIndex(n => n.id === notificationId);
44 if (index !== -1) {
45 globalState.notifications.splice(index, 1);
46 }
47 },
48
49 setTheme(theme) {
50 globalState.theme = theme;
51 document.documentElement.setAttribute('data-theme', theme);
52 },
53
54 setLanguage(language) {
55 globalState.language = language;
56 }
57};
58
59// Getters
60const getters = {
61 cartTotal: () => globalState.cart.reduce((total, item) => total + item.quantity, 0),
62 cartValue: () => globalState.cart.reduce((total, item) => total + (item.price * item.quantity), 0),
63 unreadNotifications: () => globalState.notifications.filter(n => !n.read).length,
64 isLoggedIn: () => !!globalState.user,
65 userPermissions: () => globalState.user?.permissions || []
66};
67
68export function useGlobalState() {
69 return {
70 state: readonly(globalState),
71 actions,
72 getters
73 };
74}Component Communication Patterns Summary:
1<script>
2export default {
3 setup() {
4 // 1. Props (Parent → Child)
5 // - Direct data passing
6 // - Type-safe and validated
7 // - Unidirectional flow
8
9 // 2. Emit (Child → Parent)
10 // - Event-based communication
11 // - Maintains data flow integrity
12 // - Type-safe with emit definitions
13
14 // 3. Provide/Inject (Ancestor ↔ Descendant)
15 // - Avoid prop drilling
16 // - Dependency injection
17 // - Services and utilities
18
19 // 4. Event Bus (Any ↔ Any)
20 // - Sibling communication
21 // - Cross-component events
22 // - Requires careful cleanup
23
24 // 5. Global State/Store (Any ↔ Any)
25 // - Shared reactive state
26 // - Predictable updates
27 // - Centralized logic
28
29 // 6. URL/Router (Any ↔ Any)
30 // - Route-based communication
31 // - Persistent state
32 // - Browser history integration
33
34 return {};
35 }
36};
37</script>Best Practices for Complex Hierarchies:
Managing complex component hierarchies is about choosing the right communication pattern for each situation and maintaining clear, predictable data flow throughout your application.
Build reusable Vue components with proper props, emit patterns, lifecycle management, and complex data flow. Create a complete component hierarchy with real-world patterns.
1<!DOCTYPE html>
2<html lang="en">
3<head>
4 <meta charset="UTF-8">
5 <meta name="viewport" content="width=device-width, initial-scale=1.0">
6 <title>Vue.js Components & Data Binding - Interactive Editor</title>
7 <script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
8</head>
9<body>
10 <div id="app"></div>
11
12 <script type="text/babel">
13 const { createApp, ref, reactive, computed, onMounted, onBeforeUnmount, provide, inject } = Vue;
14
15 // Product Card Component
16 const ProductCard = {
17 props: {
18 product: {
19 type: Object,
20 required: true,
21 validator(value) {
22 return value.id && value.name && typeof value.price === 'number';
23 }
24 },
25 showActions: {
26 type: Boolean,
27 default: true
28 }
29 },
30 emits: ['add-to-cart', 'add-to-wishlist', 'view-details'],
31 template: `
32 <div class="product-card">
33 <div class="product-image">
34 <img :src="product.image" :alt="product.name">
35 <div v-if="product.discount" class="discount-badge">
36 -{{product.discount}}%
37 </div>
38 </div>
39
40 <div class="product-info">
41 <h3>{{product.name}}</h3>
42 <p class="product-category">{{product.category}}</p>
43
44 <div class="product-rating">
45 <star-rating :rating="product.rating" />
46 <span class="rating-text">({{product.reviews}})</span>
47 </div>
48
49 <div class="product-price">
50 <span v-if="product.originalPrice" class="original-price">
51 ${{product.originalPrice}}
52 </span>
53 <span class="current-price">${{product.price}}</span>
54 </div>
55
56 <div v-if="showActions" class="product-actions">
57 <button
58 @click="$emit('add-to-cart', product)"
59 :disabled="!product.inStock"
60 class="btn-primary"
61 >
62 {{product.inStock ? 'Add to Cart' : 'Out of Stock'}}
63 </button>
64 <button
65 @click="$emit('add-to-wishlist', product)"
66 class="btn-secondary"
67 >
68 ♡ Wishlist
69 </button>
70 <button
71 @click="$emit('view-details', product.id)"
72 class="btn-link"
73 >
74 View Details
75 </button>
76 </div>
77 </div>
78 </div>
79 `
80 };
81
82 // Star Rating Component
83 const StarRating = {
84 props: {
85 rating: {
86 type: Number,
87 required: true,
88 validator: (value) => value >= 0 && value <= 5
89 },
90 maxStars: {
91 type: Number,
92 default: 5
93 }
94 },
95 computed: {
96 stars() {
97 const fullStars = Math.floor(this.rating);
98 const hasHalfStar = this.rating % 1 !== 0;
99 const emptyStars = this.maxStars - fullStars - (hasHalfStar ? 1 : 0);
100
101 return {
102 full: fullStars,
103 half: hasHalfStar,
104 empty: emptyStars
105 };
106 }
107 },
108 template: `
109 <div class="star-rating">
110 <span v-for="star in stars.full" :key="'full-' + star" class="star full">★</span>
111 <span v-if="stars.half" class="star half">★</span>
112 <span v-for="star in stars.empty" :key="'empty-' + star" class="star empty">☆</span>
113 </div>
114 `
115 };
116
117 // Shopping Cart Component
118 const ShoppingCart = {
119 props: {
120 isOpen: Boolean,
121 items: Array
122 },
123 emits: ['close', 'update-quantity', 'remove-item', 'checkout'],
124 computed: {
125 totalItems() {
126 return this.items.reduce((total, item) => total + item.quantity, 0);
127 },
128 totalPrice() {
129 return this.items.reduce((total, item) => total + (item.price * item.quantity), 0);
130 }
131 },
132 template: `
133 <div v-if="isOpen" class="cart-overlay" @click="$emit('close')">
134 <div class="cart-panel" @click.stop>
135 <div class="cart-header">
136 <h2>Shopping Cart ({{totalItems}} items)</h2>
137 <button @click="$emit('close')" class="close-btn">×</button>
138 </div>
139
140 <div class="cart-content">
141 <div v-if="items.length === 0" class="empty-cart">
142 <p>Your cart is empty</p>
143 </div>
144
145 <div v-else class="cart-items">
146 <div v-for="item in items" :key="item.id" class="cart-item">
147 <img :src="item.image" :alt="item.name" class="item-image">
148 <div class="item-details">
149 <h4>{{item.name}}</h4>
150 <p class="item-price">${{item.price}}</p>
151 </div>
152 <div class="item-quantity">
153 <button @click="$emit('update-quantity', item.id, item.quantity - 1)">-</button>
154 <span>{{item.quantity}}</span>
155 <button @click="$emit('update-quantity', item.id, item.quantity + 1)">+</button>
156 </div>
157 <div class="item-total">
158 ${{(item.price * item.quantity).toFixed(2)}}
159 </div>
160 <button @click="$emit('remove-item', item.id)" class="remove-btn">×</button>
161 </div>
162 </div>
163 </div>
164
165 <div v-if="items.length > 0" class="cart-footer">
166 <div class="cart-total">
167 <strong>Total: ${{totalPrice.toFixed(2)}}</strong>
168 </div>
169 <button @click="$emit('checkout')" class="checkout-btn">
170 Proceed to Checkout
171 </button>
172 </div>
173 </div>
174 </div>
175 `
176 };
177
178 // Header Component
179 const AppHeader = {
180 props: {
181 cartItemCount: Number
182 },
183 emits: ['toggle-cart', 'search'],
184 data() {
185 return {
186 searchQuery: ''
187 };
188 },
189 methods: {
190 handleSearch() {
191 if (this.searchQuery.trim()) {
192 this.$emit('search', this.searchQuery);
193 }
194 }
195 },
196 template: `
197 <header class="app-header">
198 <div class="header-content">
199 <div class="logo">
200 <h1>🛍️ Vue Shop</h1>
201 </div>
202
203 <div class="search-bar">
204 <input
205 v-model="searchQuery"
206 @keyup.enter="handleSearch"
207 type="text"
208 placeholder="Search products..."
209 class="search-input"
210 >
211 <button @click="handleSearch" class="search-btn">Search</button>
212 </div>
213
214 <div class="header-actions">
215 <button @click="$emit('toggle-cart')" class="cart-btn">
216 🛒 Cart
217 <span v-if="cartItemCount > 0" class="cart-badge">
218 {{cartItemCount}}
219 </span>
220 </button>
221 </div>
222 </div>
223 </header>
224 `
225 };
226
227 // Product Filter Component
228 const ProductFilter = {
229 props: {
230 categories: Array,
231 selectedCategory: String,
232 priceRange: Object,
233 selectedPriceRange: Object
234 },
235 emits: ['category-changed', 'price-range-changed', 'reset-filters'],
236 template: `
237 <div class="product-filter">
238 <h3>Filter Products</h3>
239
240 <div class="filter-section">
241 <h4>Category</h4>
242 <select
243 :value="selectedCategory"
244 @change="$emit('category-changed', $event.target.value)"
245 class="filter-select"
246 >
247 <option value="">All Categories</option>
248 <option v-for="category in categories" :key="category" :value="category">
249 {{category}}
250 </option>
251 </select>
252 </div>
253
254 <div class="filter-section">
255 <h4>Price Range</h4>
256 <div class="price-range">
257 <input
258 type="range"
259 :min="priceRange.min"
260 :max="priceRange.max"
261 :value="selectedPriceRange.min"
262 @input="updatePriceRange('min', $event.target.value)"
263 class="range-slider"
264 >
265 <input
266 type="range"
267 :min="priceRange.min"
268 :max="priceRange.max"
269 :value="selectedPriceRange.max"
270 @input="updatePriceRange('max', $event.target.value)"
271 class="range-slider"
272 >
273 <div class="price-display">
274 ${{selectedPriceRange.min}} - ${{selectedPriceRange.max}}
275 </div>
276 </div>
277 </div>
278
279 <button @click="$emit('reset-filters')" class="reset-btn">
280 Reset Filters
281 </button>
282 </div>
283 `,
284 methods: {
285 updatePriceRange(type, value) {
286 const newRange = { ...this.selectedPriceRange };
287 newRange[type] = parseInt(value);
288 this.$emit('price-range-changed', newRange);
289 }
290 }
291 };
292
293 // Main App Component
294 const App = {
295 components: {
296 ProductCard,
297 StarRating,
298 ShoppingCart,
299 AppHeader,
300 ProductFilter
301 },
302 setup() {
303 // Application state
304 const state = reactive({
305 products: [
306 {
307 id: 1,
308 name: 'Wireless Headphones',
309 category: 'Electronics',
310 price: 99.99,
311 originalPrice: 149.99,
312 discount: 33,
313 rating: 4.5,
314 reviews: 128,
315 image: 'data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200"><rect width="200" height="200" fill="%23f0f0f0"/><text x="100" y="100" font-family="Arial" font-size="16" text-anchor="middle" dy=".3em">Headphones</text></svg>',
316 inStock: true
317 },
318 {
319 id: 2,
320 name: 'Smartphone Case',
321 category: 'Accessories',
322 price: 24.99,
323 rating: 4.2,
324 reviews: 89,
325 image: 'data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200"><rect width="200" height="200" fill="%23e0e0e0"/><text x="100" y="100" font-family="Arial" font-size="16" text-anchor="middle" dy=".3em">Phone Case</text></svg>',
326 inStock: true
327 },
328 {
329 id: 3,
330 name: 'Bluetooth Speaker',
331 category: 'Electronics',
332 price: 79.99,
333 rating: 4.7,
334 reviews: 203,
335 image: 'data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200"><rect width="200" height="200" fill="%23d0d0d0"/><text x="100" y="100" font-family="Arial" font-size="16" text-anchor="middle" dy=".3em">Speaker</text></svg>',
336 inStock: false
337 },
338 {
339 id: 4,
340 name: 'Laptop Stand',
341 category: 'Office',
342 price: 45.99,
343 rating: 4.1,
344 reviews: 67,
345 image: 'data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200"><rect width="200" height="200" fill="%23c0c0c0"/><text x="100" y="100" font-family="Arial" font-size="16" text-anchor="middle" dy=".3em">Laptop Stand</text></svg>',
346 inStock: true
347 }
348 ],
349 cart: [],
350 wishlist: [],
351 isCartOpen: false,
352 searchQuery: '',
353 selectedCategory: '',
354 selectedPriceRange: { min: 0, max: 200 }
355 });
356
357 // Computed properties
358 const categories = computed(() => {
359 return [...new Set(state.products.map(p => p.category))];
360 });
361
362 const priceRange = computed(() => {
363 const prices = state.products.map(p => p.price);
364 return {
365 min: Math.floor(Math.min(...prices)),
366 max: Math.ceil(Math.max(...prices))
367 };
368 });
369
370 const filteredProducts = computed(() => {
371 return state.products.filter(product => {
372 const matchesSearch = !state.searchQuery ||
373 product.name.toLowerCase().includes(state.searchQuery.toLowerCase());
374 const matchesCategory = !state.selectedCategory ||
375 product.category === state.selectedCategory;
376 const matchesPrice = product.price >= state.selectedPriceRange.min &&
377 product.price <= state.selectedPriceRange.max;
378
379 return matchesSearch && matchesCategory && matchesPrice;
380 });
381 });
382
383 const cartItemCount = computed(() => {
384 return state.cart.reduce((total, item) => total + item.quantity, 0);
385 });
386
387 // Methods
388 const addToCart = (product) => {
389 const existingItem = state.cart.find(item => item.id === product.id);
390 if (existingItem) {
391 existingItem.quantity += 1;
392 } else {
393 state.cart.push({ ...product, quantity: 1 });
394 }
395 showNotification(`Added ${product.name} to cart!`);
396 };
397
398 const addToWishlist = (product) => {
399 if (!state.wishlist.find(item => item.id === product.id)) {
400 state.wishlist.push(product);
401 showNotification(`Added ${product.name} to wishlist!`);
402 }
403 };
404
405 const viewDetails = (productId) => {
406 showNotification(`Viewing details for product #${productId}`);
407 };
408
409 const updateCartQuantity = (productId, newQuantity) => {
410 if (newQuantity <= 0) {
411 removeFromCart(productId);
412 return;
413 }
414 const item = state.cart.find(item => item.id === productId);
415 if (item) {
416 item.quantity = newQuantity;
417 }
418 };
419
420 const removeFromCart = (productId) => {
421 const index = state.cart.findIndex(item => item.id === productId);
422 if (index !== -1) {
423 state.cart.splice(index, 1);
424 }
425 };
426
427 const toggleCart = () => {
428 state.isCartOpen = !state.isCartOpen;
429 };
430
431 const handleSearch = (query) => {
432 state.searchQuery = query;
433 };
434
435 const handleCategoryChange = (category) => {
436 state.selectedCategory = category;
437 };
438
439 const handlePriceRangeChange = (range) => {
440 state.selectedPriceRange = range;
441 };
442
443 const resetFilters = () => {
444 state.selectedCategory = '';
445 state.selectedPriceRange = { ...priceRange.value };
446 state.searchQuery = '';
447 };
448
449 const checkout = () => {
450 showNotification(`Checkout initiated for ${cartItemCount.value} items!`);
451 };
452
453 const showNotification = (message) => {
454 console.log('Notification:', message);
455 // In a real app, this would show a toast notification
456 };
457
458 return {
459 state,
460 categories,
461 priceRange,
462 filteredProducts,
463 cartItemCount,
464 addToCart,
465 addToWishlist,
466 viewDetails,
467 updateCartQuantity,
468 removeFromCart,
469 toggleCart,
470 handleSearch,
471 handleCategoryChange,
472 handlePriceRangeChange,
473 resetFilters,
474 checkout
475 };
476 },
477 template: `
478 <div class="app">
479 <AppHeader
480 :cart-item-count="cartItemCount"
481 @toggle-cart="toggleCart"
482 @search="handleSearch"
483 />
484
485 <div class="app-layout">
486 <aside class="sidebar">
487 <ProductFilter
488 :categories="categories"
489 :selected-category="state.selectedCategory"
490 :price-range="priceRange"
491 :selected-price-range="state.selectedPriceRange"
492 @category-changed="handleCategoryChange"
493 @price-range-changed="handlePriceRangeChange"
494 @reset-filters="resetFilters"
495 />
496 </aside>
497
498 <main class="main-content">
499 <div class="products-header">
500 <h2>Products ({{filteredProducts.length}})</h2>
501 <div v-if="state.searchQuery" class="search-info">
502 Showing results for: "{{state.searchQuery}}"
503 </div>
504 </div>
505
506 <div class="products-grid">
507 <ProductCard
508 v-for="product in filteredProducts"
509 :key="product.id"
510 :product="product"
511 @add-to-cart="addToCart"
512 @add-to-wishlist="addToWishlist"
513 @view-details="viewDetails"
514 />
515 </div>
516
517 <div v-if="filteredProducts.length === 0" class="no-products">
518 <p>No products found matching your criteria.</p>
519 <button @click="resetFilters" class="btn-primary">Reset Filters</button>
520 </div>
521 </main>
522 </div>
523
524 <ShoppingCart
525 :is-open="state.isCartOpen"
526 :items="state.cart"
527 @close="toggleCart"
528 @update-quantity="updateCartQuantity"
529 @remove-item="removeFromCart"
530 @checkout="checkout"
531 />
532 </div>
533 `
534 };
535
536 // Mount the application
537 createApp(App).mount('#app');
538
539 console.log('Vue.js Components & Data Binding Workshop loaded! 🟢');
540 console.log('This demo showcases:');
541 console.log('1. Reusable Components - ProductCard, StarRating, ShoppingCart');
542 console.log('2. Props Validation - Type checking and custom validators');
543 console.log('3. Emit Events - Child-to-parent communication patterns');
544 console.log('4. Component Composition - Complex component hierarchies');
545 console.log('5. Data Flow - Props down, events up pattern');
546 console.log('6. Computed Properties - Reactive derived state');
547 </script>
548
549 <style>
550 * {
551 margin: 0;
552 padding: 0;
553 box-sizing: border-box;
554 }
555
556 body {
557 font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
558 background: #f5f5f5;
559 color: #333;
560 }
561
562 .app {
563 min-height: 100vh;
564 }
565
566 /* Header Styles */
567 .app-header {
568 background: #2d3748;
569 color: white;
570 padding: 1rem 0;
571 }
572
573 .header-content {
574 max-width: 1200px;
575 margin: 0 auto;
576 padding: 0 20px;
577 display: flex;
578 align-items: center;
579 justify-content: space-between;
580 gap: 2rem;
581 }
582
583 .logo h1 {
584 font-size: 1.5rem;
585 font-weight: bold;
586 }
587
588 .search-bar {
589 display: flex;
590 flex-grow: 1;
591 max-width: 400px;
592 }
593
594 .search-input {
595 flex: 1;
596 padding: 0.5rem;
597 border: none;
598 border-radius: 4px 0 0 4px;
599 font-size: 1rem;
600 }
601
602 .search-btn {
603 background: #4299e1;
604 color: white;
605 border: none;
606 padding: 0.5rem 1rem;
607 border-radius: 0 4px 4px 0;
608 cursor: pointer;
609 }
610
611 .cart-btn {
612 position: relative;
613 background: #48bb78;
614 color: white;
615 border: none;
616 padding: 0.5rem 1rem;
617 border-radius: 4px;
618 cursor: pointer;
619 font-size: 1rem;
620 }
621
622 .cart-badge {
623 position: absolute;
624 top: -8px;
625 right: -8px;
626 background: #f56565;
627 color: white;
628 border-radius: 50%;
629 width: 20px;
630 height: 20px;
631 font-size: 0.75rem;
632 display: flex;
633 align-items: center;
634 justify-content: center;
635 }
636
637 /* Layout */
638 .app-layout {
639 max-width: 1200px;
640 margin: 0 auto;
641 padding: 2rem 20px;
642 display: grid;
643 grid-template-columns: 250px 1fr;
644 gap: 2rem;
645 }
646
647 /* Sidebar */
648 .sidebar {
649 background: white;
650 border-radius: 8px;
651 padding: 1.5rem;
652 height: fit-content;
653 box-shadow: 0 2px 4px rgba(0,0,0,0.1);
654 }
655
656 .product-filter h3 {
657 margin-bottom: 1rem;
658 color: #2d3748;
659 }
660
661 .filter-section {
662 margin-bottom: 1.5rem;
663 }
664
665 .filter-section h4 {
666 margin-bottom: 0.5rem;
667 color: #4a5568;
668 font-size: 0.9rem;
669 }
670
671 .filter-select {
672 width: 100%;
673 padding: 0.5rem;
674 border: 1px solid #e2e8f0;
675 border-radius: 4px;
676 }
677
678 .price-range {
679 display: flex;
680 flex-direction: column;
681 gap: 0.5rem;
682 }
683
684 .range-slider {
685 width: 100%;
686 }
687
688 .price-display {
689 text-align: center;
690 font-weight: bold;
691 color: #4299e1;
692 }
693
694 .reset-btn {
695 background: #e2e8f0;
696 color: #4a5568;
697 border: none;
698 padding: 0.5rem 1rem;
699 border-radius: 4px;
700 cursor: pointer;
701 width: 100%;
702 }
703
704 /* Main Content */
705 .main-content {
706 background: white;
707 border-radius: 8px;
708 padding: 1.5rem;
709 box-shadow: 0 2px 4px rgba(0,0,0,0.1);
710 }
711
712 .products-header {
713 margin-bottom: 1.5rem;
714 }
715
716 .products-header h2 {
717 color: #2d3748;
718 margin-bottom: 0.5rem;
719 }
720
721 .search-info {
722 color: #718096;
723 font-style: italic;
724 }
725
726 .products-grid {
727 display: grid;
728 grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
729 gap: 1.5rem;
730 margin-bottom: 2rem;
731 }
732
733 /* Product Card */
734 .product-card {
735 border: 1px solid #e2e8f0;
736 border-radius: 8px;
737 overflow: hidden;
738 transition: transform 0.2s ease, box-shadow 0.2s ease;
739 background: white;
740 }
741
742 .product-card:hover {
743 transform: translateY(-4px);
744 box-shadow: 0 8px 25px rgba(0,0,0,0.15);
745 }
746
747 .product-image {
748 position: relative;
749 height: 200px;
750 overflow: hidden;
751 }
752
753 .product-image img {
754 width: 100%;
755 height: 100%;
756 object-fit: cover;
757 }
758
759 .discount-badge {
760 position: absolute;
761 top: 10px;
762 right: 10px;
763 background: #f56565;
764 color: white;
765 padding: 4px 8px;
766 border-radius: 4px;
767 font-size: 0.8rem;
768 font-weight: bold;
769 }
770
771 .product-info {
772 padding: 1rem;
773 }
774
775 .product-info h3 {
776 margin-bottom: 0.5rem;
777 color: #2d3748;
778 }
779
780 .product-category {
781 color: #718096;
782 font-size: 0.9rem;
783 margin-bottom: 0.5rem;
784 }
785
786 .product-rating {
787 display: flex;
788 align-items: center;
789 gap: 0.5rem;
790 margin-bottom: 0.5rem;
791 }
792
793 .rating-text {
794 color: #718096;
795 font-size: 0.8rem;
796 }
797
798 .product-price {
799 margin-bottom: 1rem;
800 }
801
802 .original-price {
803 color: #a0aec0;
804 text-decoration: line-through;
805 font-size: 0.9rem;
806 margin-right: 0.5rem;
807 }
808
809 .current-price {
810 color: #2d3748;
811 font-weight: bold;
812 font-size: 1.1rem;
813 }
814
815 .product-actions {
816 display: flex;
817 flex-direction: column;
818 gap: 0.5rem;
819 }
820
821 /* Star Rating */
822 .star-rating {
823 display: flex;
824 gap: 2px;
825 }
826
827 .star {
828 font-size: 1rem;
829 }
830
831 .star.full {
832 color: #ffd700;
833 }
834
835 .star.half {
836 color: #ffd700;
837 }
838
839 .star.empty {
840 color: #e2e8f0;
841 }
842
843 /* Buttons */
844 .btn-primary {
845 background: #4299e1;
846 color: white;
847 border: none;
848 padding: 0.5rem 1rem;
849 border-radius: 4px;
850 cursor: pointer;
851 font-size: 0.9rem;
852 }
853
854 .btn-primary:hover {
855 background: #3182ce;
856 }
857
858 .btn-primary:disabled {
859 background: #a0aec0;
860 cursor: not-allowed;
861 }
862
863 .btn-secondary {
864 background: #e2e8f0;
865 color: #4a5568;
866 border: none;
867 padding: 0.5rem 1rem;
868 border-radius: 4px;
869 cursor: pointer;
870 font-size: 0.9rem;
871 }
872
873 .btn-secondary:hover {
874 background: #cbd5e0;
875 }
876
877 .btn-link {
878 background: transparent;
879 color: #4299e1;
880 border: none;
881 padding: 0.5rem;
882 cursor: pointer;
883 font-size: 0.9rem;
884 text-decoration: underline;
885 }
886
887 /* Shopping Cart */
888 .cart-overlay {
889 position: fixed;
890 top: 0;
891 left: 0;
892 right: 0;
893 bottom: 0;
894 background: rgba(0, 0, 0, 0.5);
895 display: flex;
896 align-items: center;
897 justify-content: center;
898 z-index: 1000;
899 }
900
901 .cart-panel {
902 background: white;
903 border-radius: 8px;
904 max-width: 600px;
905 width: 90%;
906 max-height: 80vh;
907 display: flex;
908 flex-direction: column;
909 }
910
911 .cart-header {
912 display: flex;
913 justify-content: space-between;
914 align-items: center;
915 padding: 1rem;
916 border-bottom: 1px solid #e2e8f0;
917 }
918
919 .close-btn {
920 background: transparent;
921 border: none;
922 font-size: 1.5rem;
923 cursor: pointer;
924 color: #718096;
925 }
926
927 .cart-content {
928 flex: 1;
929 overflow-y: auto;
930 padding: 1rem;
931 }
932
933 .empty-cart {
934 text-align: center;
935 padding: 2rem;
936 color: #718096;
937 }
938
939 .cart-item {
940 display: flex;
941 align-items: center;
942 gap: 1rem;
943 padding: 1rem;
944 border-bottom: 1px solid #e2e8f0;
945 }
946
947 .item-image {
948 width: 60px;
949 height: 60px;
950 object-fit: cover;
951 border-radius: 4px;
952 }
953
954 .item-details {
955 flex: 1;
956 }
957
958 .item-details h4 {
959 margin-bottom: 0.25rem;
960 }
961
962 .item-price {
963 color: #718096;
964 font-size: 0.9rem;
965 }
966
967 .item-quantity {
968 display: flex;
969 align-items: center;
970 gap: 0.5rem;
971 }
972
973 .item-quantity button {
974 background: #e2e8f0;
975 border: none;
976 padding: 0.25rem 0.5rem;
977 border-radius: 4px;
978 cursor: pointer;
979 }
980
981 .item-total {
982 font-weight: bold;
983 color: #2d3748;
984 }
985
986 .remove-btn {
987 background: transparent;
988 border: none;
989 color: #f56565;
990 font-size: 1.2rem;
991 cursor: pointer;
992 }
993
994 .cart-footer {
995 padding: 1rem;
996 border-top: 1px solid #e2e8f0;
997 }
998
999 .cart-total {
1000 margin-bottom: 1rem;
1001 text-align: right;
1002 font-size: 1.2rem;
1003 }
1004
1005 .checkout-btn {
1006 background: #48bb78;
1007 color: white;
1008 border: none;
1009 padding: 0.75rem 1.5rem;
1010 border-radius: 4px;
1011 cursor: pointer;
1012 font-size: 1rem;
1013 width: 100%;
1014 }
1015
1016 .checkout-btn:hover {
1017 background: #38a169;
1018 }
1019
1020 /* No Products */
1021 .no-products {
1022 text-align: center;
1023 padding: 2rem;
1024 color: #718096;
1025 }
1026
1027 .no-products p {
1028 margin-bottom: 1rem;
1029 }
1030
1031 /* Responsive */
1032 @media (max-width: 768px) {
1033 .header-content {
1034 flex-direction: column;
1035 gap: 1rem;
1036 }
1037
1038 .app-layout {
1039 grid-template-columns: 1fr;
1040 padding: 1rem 10px;
1041 }
1042
1043 .products-grid {
1044 grid-template-columns: 1fr;
1045 }
1046
1047 .product-actions {
1048 flex-direction: column;
1049 }
1050 }
1051 </style>
1052</body>
1053</html>This comprehensive Vue.js Components & Data Binding workshop demonstrates professional component architecture patterns:
🎯 Advanced Component Patterns:
1. Reusable Component Design:
2. Props Validation & Best Practices:
3. Emit System & Communication:
4. Component Composition Patterns:
5. Data Flow Management:
🔧 Real-World Application Features:
Shopping Cart System:
Product Catalog:
User Interface:
Performance Optimizations:
Component Architecture Benefits:
Vue-Specific Patterns:
This workshop provides hands-on experience with building production-ready Vue applications using modern component architecture patterns!
Outstanding work mastering Vue.js components and data binding! You now have solid experience with both React and Vue component patterns. In our next lesson, we'll explore Component Communication patterns that work across different frameworks, including advanced state management, event systems, and architectural patterns for building scalable applications. Get ready to compare and contrast framework approaches!
Your progress is saved in this browser, on this device only.