Vue.js
The Progressive JavaScript Framework. Incrementally adoptable with low learning curve, providing two-way data binding and component system.
GitHub Overview
vuejs/core
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
Topics
Star History
Framework
Vue.js
Overview
Vue.js is a progressive JavaScript framework designed for building user interfaces on the web. It is lightweight, easy to learn, and can be incrementally adopted.
Details
Vue.js (Vue) is a progressive JavaScript framework developed by Evan You in 2014. "Progressive" means it can be incrementally adopted into existing projects, supporting everything from small applications to large-scale SPAs. Vue 3 introduces the Composition API, which significantly improves component logic organization and reusability. Key features include reactive data binding (Proxy-based reactivity system), component-oriented architecture, virtual DOM, Single File Components (SFC), choice between Options API and Composition API, Vue Router (official router), and Vuex/Pinia (state management). Vue.js has a low learning curve with intuitive template syntax and excellent TypeScript support. It has a rich ecosystem including high-quality tools like Nuxt.js, Vite, VuePress, and VitePress, and is widely adopted by companies like GitLab, Adobe, Nintendo, and BMW.
Pros and Cons
Pros
- Easy to Learn: Intuitive template syntax and gradual adoption approach
- High Flexibility: Progressive framework supporting partial usage to full SPA development
- Excellent Performance: Lightweight virtual DOM and compile-time optimizations
- Reactive System: Vue 3's Proxy-based reactivity for efficient data binding
- Rich Ecosystem: High-quality tools like Vue Router, Pinia, Nuxt.js, and Vite
- TypeScript Support: Excellent type inference and development experience with Volar
- Component-Oriented: Reusable component architecture
- Great Documentation: Detailed and clear official documentation
Cons
- Ecosystem Compared to React/Angular: Relatively fewer third-party library options
- Enterprise Adoption: Fewer large-scale enterprise adoption cases compared to others
- Developer Availability: Fewer Vue.js specialists compared to React/Angular developers
- Version Migration Impact: Some breaking changes from Vue 2 to Vue 3 migration
- Performance Tuning: Optimization can become complex in large applications
- Internationalization: Smaller global community compared to React and others
Key Links
- Vue.js Official Website
- Vue.js Official Documentation
- Vue.js GitHub Repository
- Vue Router
- Pinia (State Management)
- Vue.js Community
Code Examples
Hello World
<!-- App.vue -->
<template>
<div class="app">
<h1>{{ greeting }}</h1>
<p>Count: {{ count }}</p>
<button @click="increment">+1</button>
<button @click="decrement">-1</button>
<button @click="reset">Reset</button>
<!-- Conditional rendering -->
<p v-if="count > 10" class="warning">
Count is getting high!
</p>
<p v-else-if="count < 0" class="error">
Count is negative!
</p>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
// Reactive state
const count = ref(0)
const name = ref('Vue.js')
// Computed property
const greeting = computed(() => {
return `Welcome to ${name.value}!`
})
// Methods
function increment() {
count.value++
}
function decrement() {
count.value--
}
function reset() {
count.value = 0
}
</script>
<style scoped>
.app {
text-align: center;
padding: 2rem;
font-family: Arial, sans-serif;
}
h1 {
color: #42b883;
margin-bottom: 1rem;
}
button {
margin: 0 0.5rem;
padding: 0.5rem 1rem;
background-color: #42b883;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
button:hover {
background-color: #369870;
}
.warning {
color: #f56565;
}
.error {
color: #e53e3e;
}
</style>
Component Creation and Props
<!-- UserCard.vue -->
<template>
<div class="user-card">
<img :src="user.avatar" :alt="user.name" class="avatar" />
<div class="user-info">
<h3>{{ user.name }}</h3>
<p>{{ user.email }}</p>
<span class="role" :class="roleClass">{{ user.role }}</span>
</div>
<div class="actions">
<button @click="$emit('edit', user)" class="btn btn-primary">
Edit
</button>
<button @click="$emit('delete', user.id)" class="btn btn-danger">
Delete
</button>
</div>
</div>
</template>
<script setup>
import { computed } from 'vue'
// Props definition
const props = defineProps({
user: {
type: Object,
required: true,
validator: (user) => {
return user && user.id && user.name && user.email
}
}
})
// Emits definition
const emit = defineEmits(['edit', 'delete'])
// Computed property
const roleClass = computed(() => {
const roleMap = {
admin: 'role-admin',
user: 'role-user',
guest: 'role-guest'
}
return roleMap[props.user.role] || 'role-default'
})
</script>
<style scoped>
.user-card {
display: flex;
align-items: center;
padding: 1rem;
border: 1px solid #e2e8f0;
border-radius: 8px;
gap: 1rem;
margin-bottom: 1rem;
}
.avatar {
width: 60px;
height: 60px;
border-radius: 50%;
object-fit: cover;
}
.user-info {
flex: 1;
}
.user-info h3 {
margin: 0 0 0.5rem 0;
color: #2d3748;
}
.user-info p {
margin: 0;
color: #718096;
}
.role {
display: inline-block;
padding: 0.25rem 0.5rem;
border-radius: 4px;
font-size: 0.875rem;
font-weight: 500;
}
.role-admin {
background-color: #fed7d7;
color: #c53030;
}
.role-user {
background-color: #bee3f8;
color: #2b6cb0;
}
.role-guest {
background-color: #f7fafc;
color: #4a5568;
}
.actions {
display: flex;
gap: 0.5rem;
}
.btn {
padding: 0.5rem 1rem;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 0.875rem;
}
.btn-primary {
background-color: #4299e1;
color: white;
}
.btn-danger {
background-color: #f56565;
color: white;
}
</style>
Reactive State and Watchers
<!-- UserList.vue -->
<template>
<div class="user-list">
<div class="filters">
<input
v-model="searchQuery"
type="text"
placeholder="Search users..."
class="search-input"
/>
<select v-model="selectedRole" class="role-filter">
<option value="">All roles</option>
<option value="admin">Admin</option>
<option value="user">User</option>
<option value="guest">Guest</option>
</select>
</div>
<div class="stats">
<p>Showing {{ filteredUsers.length }} of {{ users.length }} users</p>
</div>
<div v-if="loading" class="loading">
Loading...
</div>
<div v-else class="users">
<UserCard
v-for="user in filteredUsers"
:key="user.id"
:user="user"
@edit="editUser"
@delete="deleteUser"
/>
</div>
</div>
</template>
<script setup>
import { ref, computed, watch, onMounted } from 'vue'
import UserCard from './UserCard.vue'
// Reactive state
const users = ref([])
const searchQuery = ref('')
const selectedRole = ref('')
const loading = ref(false)
// Computed property
const filteredUsers = computed(() => {
let filtered = users.value
// Filter by search query
if (searchQuery.value) {
const query = searchQuery.value.toLowerCase()
filtered = filtered.filter(user =>
user.name.toLowerCase().includes(query) ||
user.email.toLowerCase().includes(query)
)
}
// Filter by role
if (selectedRole.value) {
filtered = filtered.filter(user => user.role === selectedRole.value)
}
return filtered
})
// Watchers
watch(searchQuery, (newQuery) => {
console.log('Search query changed:', newQuery)
// Can implement debounce logic here
})
watch(selectedRole, (newRole) => {
console.log('Selected role changed:', newRole)
})
// Watch multiple values
watch([searchQuery, selectedRole], ([newQuery, newRole]) => {
console.log('Filters updated:', { newQuery, newRole })
})
// Lifecycle hook
onMounted(() => {
fetchUsers()
})
// Methods
async function fetchUsers() {
loading.value = true
try {
// Fetch user data from API (mock data)
await new Promise(resolve => setTimeout(resolve, 1000))
users.value = [
{ id: 1, name: 'John Doe', email: '[email protected]', role: 'admin', avatar: 'https://i.pravatar.cc/150?img=1' },
{ id: 2, name: 'Jane Smith', email: '[email protected]', role: 'user', avatar: 'https://i.pravatar.cc/150?img=2' },
{ id: 3, name: 'Bob Johnson', email: '[email protected]', role: 'user', avatar: 'https://i.pravatar.cc/150?img=3' },
{ id: 4, name: 'Alice Brown', email: '[email protected]', role: 'guest', avatar: 'https://i.pravatar.cc/150?img=4' }
]
} catch (error) {
console.error('Failed to fetch user data:', error)
} finally {
loading.value = false
}
}
function editUser(user) {
console.log('Edit user:', user)
// Implement edit logic
}
function deleteUser(userId) {
if (confirm('Are you sure you want to delete this user?')) {
users.value = users.value.filter(user => user.id !== userId)
console.log('User deleted:', userId)
}
}
</script>
<style scoped>
.user-list {
max-width: 800px;
margin: 0 auto;
padding: 2rem;
}
.filters {
display: flex;
gap: 1rem;
margin-bottom: 1rem;
}
.search-input,
.role-filter {
padding: 0.5rem;
border: 1px solid #d2d6dc;
border-radius: 4px;
font-size: 1rem;
}
.search-input {
flex: 1;
}
.stats {
margin-bottom: 1rem;
color: #718096;
}
.loading {
text-align: center;
padding: 2rem;
color: #4a5568;
}
.users {
display: grid;
gap: 1rem;
}
</style>
Form Handling and Validation
<!-- ContactForm.vue -->
<template>
<form @submit.prevent="submitForm" class="contact-form">
<h2>Contact Form</h2>
<div class="form-group">
<label for="name">Name *</label>
<input
id="name"
v-model="form.name"
type="text"
required
:class="{ error: errors.name }"
@blur="validateField('name')"
/>
<span v-if="errors.name" class="error-message">{{ errors.name }}</span>
</div>
<div class="form-group">
<label for="email">Email Address *</label>
<input
id="email"
v-model="form.email"
type="email"
required
:class="{ error: errors.email }"
@blur="validateField('email')"
/>
<span v-if="errors.email" class="error-message">{{ errors.email }}</span>
</div>
<div class="form-group">
<label for="category">Category</label>
<select id="category" v-model="form.category">
<option value="">Please select</option>
<option value="general">General Inquiry</option>
<option value="support">Support</option>
<option value="business">Business</option>
<option value="bug">Bug Report</option>
</select>
</div>
<div class="form-group">
<label for="message">Message *</label>
<textarea
id="message"
v-model="form.message"
rows="5"
required
:class="{ error: errors.message }"
@blur="validateField('message')"
></textarea>
<span v-if="errors.message" class="error-message">{{ errors.message }}</span>
</div>
<div class="form-group">
<label class="checkbox-label">
<input
v-model="form.agreeToTerms"
type="checkbox"
required
/>
I agree to the terms and conditions *
</label>
<span v-if="errors.agreeToTerms" class="error-message">{{ errors.agreeToTerms }}</span>
</div>
<button type="submit" :disabled="!isFormValid || isSubmitting" class="submit-btn">
{{ isSubmitting ? 'Submitting...' : 'Submit' }}
</button>
<div v-if="submitStatus === 'success'" class="success-message">
Thank you for your message! We'll get back to you soon.
</div>
<div v-if="submitStatus === 'error'" class="error-message">
Submission failed. Please try again later.
</div>
</form>
</template>
<script setup>
import { ref, reactive, computed } from 'vue'
// Form data
const form = reactive({
name: '',
email: '',
category: '',
message: '',
agreeToTerms: false
})
// Error state
const errors = reactive({
name: '',
email: '',
message: '',
agreeToTerms: ''
})
// UI state
const isSubmitting = ref(false)
const submitStatus = ref('')
// Validation functions
const validators = {
name: (value) => {
if (!value) return 'Name is required'
if (value.length < 2) return 'Name must be at least 2 characters'
return ''
},
email: (value) => {
if (!value) return 'Email is required'
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
if (!emailRegex.test(value)) return 'Please enter a valid email address'
return ''
},
message: (value) => {
if (!value) return 'Message is required'
if (value.length < 10) return 'Message must be at least 10 characters'
return ''
},
agreeToTerms: (value) => {
if (!value) return 'You must agree to the terms and conditions'
return ''
}
}
// Field validation
function validateField(field) {
if (validators[field]) {
errors[field] = validators[field](form[field])
}
}
// Form validation
function validateForm() {
Object.keys(validators).forEach(field => {
validateField(field)
})
}
// Form validity check
const isFormValid = computed(() => {
return Object.values(errors).every(error => !error) &&
form.name && form.email && form.message && form.agreeToTerms
})
// Form submission
async function submitForm() {
validateForm()
if (!isFormValid.value) {
return
}
isSubmitting.value = true
submitStatus.value = ''
try {
// Submit data to API endpoint (mock)
await new Promise(resolve => setTimeout(resolve, 2000))
// Success handling
submitStatus.value = 'success'
// Reset form
Object.keys(form).forEach(key => {
form[key] = typeof form[key] === 'boolean' ? false : ''
})
Object.keys(errors).forEach(key => {
errors[key] = ''
})
} catch (error) {
console.error('Submission error:', error)
submitStatus.value = 'error'
} finally {
isSubmitting.value = false
}
}
</script>
<style scoped>
.contact-form {
max-width: 600px;
margin: 0 auto;
padding: 2rem;
background-color: #f7fafc;
border-radius: 8px;
}
.form-group {
margin-bottom: 1rem;
}
label {
display: block;
margin-bottom: 0.5rem;
font-weight: 500;
color: #2d3748;
}
input,
select,
textarea {
width: 100%;
padding: 0.75rem;
border: 1px solid #d2d6dc;
border-radius: 4px;
font-size: 1rem;
}
input.error,
textarea.error {
border-color: #f56565;
}
.checkbox-label {
display: flex;
align-items: center;
gap: 0.5rem;
}
.checkbox-label input {
width: auto;
}
.error-message {
color: #f56565;
font-size: 0.875rem;
margin-top: 0.25rem;
display: block;
}
.submit-btn {
background-color: #4299e1;
color: white;
padding: 0.75rem 2rem;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 1rem;
width: 100%;
}
.submit-btn:disabled {
background-color: #a0aec0;
cursor: not-allowed;
}
.success-message {
background-color: #c6f6d5;
color: #22543d;
padding: 1rem;
border-radius: 4px;
margin-top: 1rem;
}
</style>
Lifecycle and Composables
<!-- Dashboard.vue -->
<template>
<div class="dashboard">
<h1>Dashboard</h1>
<div class="stats-grid">
<div class="stat-card">
<h3>Total Users</h3>
<p class="stat-number">{{ stats.totalUsers }}</p>
</div>
<div class="stat-card">
<h3>Active Users</h3>
<p class="stat-number">{{ stats.activeUsers }}</p>
</div>
<div class="stat-card">
<h3>Today's Revenue</h3>
<p class="stat-number">${{ stats.todayRevenue.toLocaleString() }}</p>
</div>
</div>
<div class="current-time">
<p>Current Time: {{ currentTime }}</p>
<p>Online Time: {{ onlineTime }}</p>
</div>
<div class="window-size">
<p>Window Size: {{ windowSize.width }} × {{ windowSize.height }}</p>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
import { useStats, useCurrentTime, useWindowSize } from '@/composables'
// Using composables
const { stats, fetchStats } = useStats()
const { currentTime, onlineTime, startTimer, stopTimer } = useCurrentTime()
const { windowSize } = useWindowSize()
// Component-specific state
const refreshInterval = ref(null)
// Lifecycle hooks
onMounted(async () => {
console.log('Dashboard mounted')
// Fetch initial data
await fetchStats()
// Start timer
startTimer()
// Set up periodic data refresh (every 30 seconds)
refreshInterval.value = setInterval(() => {
fetchStats()
}, 30000)
})
onUnmounted(() => {
console.log('Dashboard unmounted')
// Stop timer
stopTimer()
// Clear interval
if (refreshInterval.value) {
clearInterval(refreshInterval.value)
}
})
</script>
<style scoped>
.dashboard {
padding: 2rem;
max-width: 1200px;
margin: 0 auto;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 1rem;
margin-bottom: 2rem;
}
.stat-card {
background-color: white;
padding: 1.5rem;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.stat-card h3 {
margin: 0 0 0.5rem 0;
color: #4a5568;
font-size: 0.875rem;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.stat-number {
margin: 0;
font-size: 2rem;
font-weight: bold;
color: #2d3748;
}
.current-time,
.window-size {
background-color: #f7fafc;
padding: 1rem;
border-radius: 4px;
margin-bottom: 1rem;
}
</style>
// composables/useStats.js
import { ref } from 'vue'
export function useStats() {
const stats = ref({
totalUsers: 0,
activeUsers: 0,
todayRevenue: 0
})
const isLoading = ref(false)
const error = ref(null)
async function fetchStats() {
isLoading.value = true
error.value = null
try {
// Fetch data from API (mock)
await new Promise(resolve => setTimeout(resolve, 1000))
stats.value = {
totalUsers: Math.floor(Math.random() * 10000) + 1000,
activeUsers: Math.floor(Math.random() * 1000) + 100,
todayRevenue: Math.floor(Math.random() * 1000000) + 100000
}
} catch (err) {
error.value = err.message
} finally {
isLoading.value = false
}
}
return {
stats,
isLoading,
error,
fetchStats
}
}
// composables/useCurrentTime.js
import { ref, onUnmounted } from 'vue'
export function useCurrentTime() {
const currentTime = ref('')
const onlineTime = ref('')
const startTime = ref(null)
let timeInterval = null
function updateTime() {
const now = new Date()
currentTime.value = now.toLocaleString('en-US')
if (startTime.value) {
const diff = now - startTime.value
const hours = Math.floor(diff / 3600000)
const minutes = Math.floor((diff % 3600000) / 60000)
const seconds = Math.floor((diff % 60000) / 1000)
onlineTime.value = `${hours}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`
}
}
function startTimer() {
startTime.value = new Date()
updateTime()
timeInterval = setInterval(updateTime, 1000)
}
function stopTimer() {
if (timeInterval) {
clearInterval(timeInterval)
timeInterval = null
}
}
onUnmounted(() => {
stopTimer()
})
return {
currentTime,
onlineTime,
startTimer,
stopTimer
}
}
// composables/useWindowSize.js
import { ref, onMounted, onUnmounted } from 'vue'
export function useWindowSize() {
const windowSize = ref({
width: 0,
height: 0
})
function updateSize() {
windowSize.value.width = window.innerWidth
windowSize.value.height = window.innerHeight
}
onMounted(() => {
updateSize()
window.addEventListener('resize', updateSize)
})
onUnmounted(() => {
window.removeEventListener('resize', updateSize)
})
return {
windowSize
}
}
Vue Router Integration
// router/index.js
import { createRouter, createWebHistory } from 'vue-router'
import Home from '@/views/Home.vue'
import Dashboard from '@/views/Dashboard.vue'
import Users from '@/views/Users.vue'
import UserDetail from '@/views/UserDetail.vue'
import Contact from '@/views/Contact.vue'
import NotFound from '@/views/NotFound.vue'
const routes = [
{
path: '/',
name: 'Home',
component: Home
},
{
path: '/dashboard',
name: 'Dashboard',
component: Dashboard,
meta: { requiresAuth: true }
},
{
path: '/users',
name: 'Users',
component: Users
},
{
path: '/users/:id',
name: 'UserDetail',
component: UserDetail,
props: true
},
{
path: '/contact',
name: 'Contact',
component: Contact
},
{
path: '/about',
name: 'About',
// Lazy loading
component: () => import('@/views/About.vue')
},
{
path: '/:pathMatch(.*)*',
name: 'NotFound',
component: NotFound
}
]
const router = createRouter({
history: createWebHistory(),
routes
})
// Navigation guards
router.beforeEach((to, from, next) => {
// Check authentication for protected pages
if (to.meta.requiresAuth) {
const isAuthenticated = localStorage.getItem('auth-token')
if (!isAuthenticated) {
next('/login')
return
}
}
next()
})
export default router
// main.js
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
const app = createApp(App)
app.use(router)
app.mount('#app')