7.4: Events & Form Handling

Understand Vue reactivity system and manage component state effectively using data, computed properties, and watchers. Learn how Vue automatically updates the UI when data changes and implement reactive data patterns.

1. Advanced Event Handling

Event Handler Methods

Different ways to handle events:

<script setup>
import { ref } from 'vue'

const count = ref(0)
const message = ref('')

// Method handler
function increment() {
  count.value++
}

// Inline handler
function greet(name) {
  message.value = `Hello, ${name}!`
}

// Event object access
function handleClick(event) {
  console.log('Button clicked at:', event.clientX, event.clientY)
  console.log('Event target:', event.target)
}

// Prevent default and handle
function handleSubmit(event) {
  event.preventDefault()
  console.log('Form submitted')
}
</script>

<template>
  <!-- Method reference -->
  <button @click="increment">Count: {{ count }}</button>

  <!-- Inline expression -->
  <button @click="count++">Increment</button>

  <!-- Inline method call with argument -->
  <button @click="greet('Alice')">Greet Alice</button>

  <!-- Access event in inline -->
  <button @click="greet('Bob'), $event.target.blur()">
    Greet and blur
  </button>

  <!-- Method with event -->
  <button @click="handleClick">Click Me</button>

  <!-- Form submission -->
  <form @submit="handleSubmit">
    <button type="submit">Submit</button>
  </form>
</template>

Event Modifiers Mastery

<script setup>
import { ref } from 'vue'

const items = ref(['Item 1', 'Item 2', 'Item 3'])
const clickCount = ref(0)

function handleParentClick() {
  console.log('Parent clicked')
}

function handleChildClick() {
  console.log('Child clicked')
  clickCount.value++
}

function handleLinkClick() {
  console.log('Link clicked, but not navigating')
}

function handleOnce() {
  console.log('This will only log once')
}
</script>

<template>
  <!-- Prevent default (form submission, link navigation) -->
  <a href="https://vuejs.org" @click.prevent="handleLinkClick">
    Vue.js (won't navigate)
  </a>

  <!-- Stop propagation (event won't bubble to parent) -->
  <div @click="handleParentClick">
    Parent
    <button @click.stop="handleChildClick">
      Child (won't trigger parent)
    </button>
  </div>

  <!-- Only trigger once -->
  <button @click.once="handleOnce">
    Click me (only works once)
  </button>

  <!-- Capture mode (event flows from parent to child) -->
  <div @click.capture="handleParentClick">
    Parent (captures first)
    <button @click="handleChildClick">Child</button>
  </div>

  <!-- Self (only trigger if event.target is element itself) -->
  <div @click.self="handleParentClick">
    Parent (only when clicking background)
    <button @click="handleChildClick">Child</button>
  </div>

  <!-- Passive (improves scroll performance) -->
  <div @scroll.passive="handleScroll">
    Scrollable content
  </div>

  <!-- Combine modifiers -->
  <form @submit.prevent.stop="handleSubmit">
    <button type="submit">Submit</button>
  </form>
</template>

Keyboard Event Handling

<script setup>
import { ref } from 'vue'

const searchQuery = ref('')
const todos = ref([])
const currentInput = ref('')

function search() {
  console.log('Searching for:', searchQuery.value)
}

function addTodo() {
  if (currentInput.value.trim()) {
    todos.value.push(currentInput.value)
    currentInput.value = ''
  }
}

function clearSearch() {
  searchQuery.value = ''
}

function handleCtrlS(event) {
  event.preventDefault()
  console.log('Save shortcut triggered')
}
</script>

<template>
  <div>
    <!-- Enter key -->
    <input
      v-model="searchQuery"
      @keyup.enter="search"
      placeholder="Press Enter to search"
    >

    <!-- Escape key -->
    <input
      v-model="searchQuery"
      @keyup.esc="clearSearch"
      placeholder="Press Esc to clear"
    >

    <!-- Ctrl/Cmd + Enter -->
    <textarea
      v-model="currentInput"
      @keyup.ctrl.enter="addTodo"
      @keyup.meta.enter="addTodo"
      placeholder="Ctrl+Enter to add todo"
    ></textarea>

    <!-- Ctrl + S (save) -->
    <div @keydown.ctrl.s="handleCtrlS">
      Press Ctrl+S to save
    </div>

    <!-- Arrow keys -->
    <div
      @keydown.up.prevent="movePrevious"
      @keydown.down.prevent="moveNext"
      @keydown.left="moveLeft"
      @keydown.right="moveRight"
    >
      Use arrow keys to navigate
    </div>

    <!-- Tab key -->
    <input @keydown.tab.prevent="handleTab">
  </div>
</template>

Mouse Event Handling

<script setup>
import { ref } from 'vue'

const mouseX = ref(0)
const mouseY = ref(0)
const isDragging = ref(false)

function handleMouseMove(event) {
  mouseX.value = event.clientX
  mouseY.value = event.clientY
}

function handleLeftClick() {
  console.log('Left clicked')
}

function handleRightClick(event) {
  event.preventDefault() // Prevent context menu
  console.log('Right clicked')
}

function handleDoubleClick() {
  console.log('Double clicked')
}

function startDrag() {
  isDragging.value = true
}

function stopDrag() {
  isDragging.value = false
}
</script>

<template>
  <div>
    <!-- Mouse position tracking -->
    <div @mousemove="handleMouseMove" class="tracking-area">
      Mouse position: {{ mouseX }}, {{ mouseY }}
    </div>

    <!-- Specific mouse buttons -->
    <button @click.left="handleLeftClick">Left click only</button>
    <button @click.right="handleRightClick">Right click only</button>
    <button @click.middle="handleMiddleClick">Middle click only</button>

    <!-- Double click -->
    <div @dblclick="handleDoubleClick">Double click me</div>

    <!-- Drag and drop -->
    <div
      @mousedown="startDrag"
      @mouseup="stopDrag"
      @mouseleave="stopDrag"
      :class="{ dragging: isDragging }"
    >
      Drag me
    </div>
  </div>
</template>

2. Form Validation

Basic Form Validation

<script setup>
import { ref, computed } from 'vue'

const formData = ref({
  username: '',
  email: '',
  password: '',
  confirmPassword: ''
})

const errors = ref({
  username: '',
  email: '',
  password: '',
  confirmPassword: ''
})

const isFormValid = computed(() => {
  return Object.values(errors.value).every(error => error === '') &&
         formData.value.username &&
         formData.value.email &&
         formData.value.password &&
         formData.value.confirmPassword
})

function validateUsername() {
  if (!formData.value.username) {
    errors.value.username = 'Username is required'
  } else if (formData.value.username.length < 3) {
    errors.value.username = 'Username must be at least 3 characters'
  } else if (!/^[a-zA-Z0-9_]+$/.test(formData.value.username)) {
    errors.value.username = 'Username can only contain letters, numbers, and underscores'
  } else {
    errors.value.username = ''
  }
}

function validateEmail() {
  const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
  if (!formData.value.email) {
    errors.value.email = 'Email is required'
  } else if (!emailRegex.test(formData.value.email)) {
    errors.value.email = 'Please enter a valid email address'
  } else {
    errors.value.email = ''
  }
}

function validatePassword() {
  if (!formData.value.password) {
    errors.value.password = 'Password is required'
  } else if (formData.value.password.length < 8) {
    errors.value.password = 'Password must be at least 8 characters'
  } else if (!/(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/.test(formData.value.password)) {
    errors.value.password = 'Password must contain uppercase, lowercase, and number'
  } else {
    errors.value.password = ''
  }
}

function validateConfirmPassword() {
  if (!formData.value.confirmPassword) {
    errors.value.confirmPassword = 'Please confirm your password'
  } else if (formData.value.password !== formData.value.confirmPassword) {
    errors.value.confirmPassword = 'Passwords do not match'
  } else {
    errors.value.confirmPassword = ''
  }
}

function handleSubmit() {
  // Validate all fields
  validateUsername()
  validateEmail()
  validatePassword()
  validateConfirmPassword()

  if (isFormValid.value) {
    console.log('Form submitted:', formData.value)
    // Submit to API
  }
}
</script>

<template>
  <form @submit.prevent="handleSubmit" class="registration-form">
    <h2>Create Account</h2>

    <!-- Username -->
    <div class="form-group">
      <label for="username">Username</label>
      <input
        id="username"
        v-model="formData.username"
        @blur="validateUsername"
        @input="validateUsername"
        type="text"
        :class="{ invalid: errors.username }"
      >
      <span v-if="errors.username" class="error">
        {{ errors.username }}
      </span>
    </div>

    <!-- Email -->
    <div class="form-group">
      <label for="email">Email</label>
      <input
        id="email"
        v-model="formData.email"
        @blur="validateEmail"
        @input="validateEmail"
        type="email"
        :class="{ invalid: errors.email }"
      >
      <span v-if="errors.email" class="error">
        {{ errors.email }}
      </span>
    </div>

    <!-- Password -->
    <div class="form-group">
      <label for="password">Password</label>
      <input
        id="password"
        v-model="formData.password"
        @blur="validatePassword"
        @input="validatePassword"
        type="password"
        :class="{ invalid: errors.password }"
      >
      <span v-if="errors.password" class="error">
        {{ errors.password }}
      </span>
    </div>

    <!-- Confirm Password -->
    <div class="form-group">
      <label for="confirmPassword">Confirm Password</label>
      <input
        id="confirmPassword"
        v-model="formData.confirmPassword"
        @blur="validateConfirmPassword"
        @input="validateConfirmPassword"
        type="password"
        :class="{ invalid: errors.confirmPassword }"
      >
      <span v-if="errors.confirmPassword" class="error">
        {{ errors.confirmPassword }}
      </span>
    </div>

    <!-- Submit -->
    <button type="submit" :disabled="!isFormValid">
      Create Account
    </button>
  </form>
</template>

<style scoped>
.registration-form {
  max-width: 400px;
  margin: 2rem auto;
  padding: 2rem;
  border: 1px solid #ddd;
  border-radius: 8px;
}

.form-group {
  margin-bottom: 1.5rem;
}

label {
  display: block;
  margin-bottom: 0.5rem;
  font-weight: 500;
}

input {
  width: 100%;
  padding: 0.5rem;
  border: 2px solid #ddd;
  border-radius: 4px;
  font-size: 1rem;
}

input:focus {
  outline: none;
  border-color: #42b983;
}

input.invalid {
  border-color: #ff4444;
}

.error {
  display: block;
  margin-top: 0.25rem;
  color: #ff4444;
  font-size: 0.875rem;
}

button {
  width: 100%;
  padding: 0.75rem;
  background: #42b983;
  color: white;
  border: none;
  border-radius: 4px;
  font-size: 1rem;
  font-weight: 500;
  cursor: pointer;
}

button:hover:not(:disabled) {
  background: #359268;
}

button:disabled {
  background: #ccc;
  cursor: not-allowed;
}
</style>

Real-Time Validation with Debounce

<script setup>
import { ref, watch } from 'vue'

const username = ref('')
const isCheckingUsername = ref(false)
const usernameAvailable = ref(null)
let usernameCheckTimeout = null

// Debounced username availability check
watch(username, (newUsername) => {
  // Clear previous timeout
  clearTimeout(usernameCheckTimeout)

  if (!newUsername || newUsername.length < 3) {
    usernameAvailable.value = null
    return
  }

  isCheckingUsername.value = true

  // Wait 500ms after user stops typing
  usernameCheckTimeout = setTimeout(async () => {
    try {
      // Simulate API call
      await new Promise(resolve => setTimeout(resolve, 500))

      // Simulate checking if username is taken
      const taken = ['admin', 'user', 'test'].includes(newUsername.toLowerCase())
      usernameAvailable.value = !taken

    } catch (error) {
      console.error('Error checking username:', error)
    } finally {
      isCheckingUsername.value = false
    }
  }, 500)
})
</script>

<template>
  <div class="form-group">
    <label for="username">Username</label>
    <div class="input-wrapper">
      <input
        id="username"
        v-model="username"
        type="text"
        placeholder="Enter username"
      >
      <span v-if="isCheckingUsername" class="status">
        Checking...
      </span>
      <span v-else-if="usernameAvailable === true" class="status success">
        ✓ Available
      </span>
      <span v-else-if="usernameAvailable === false" class="status error">
        ✗ Taken
      </span>
    </div>
  </div>
</template>

<style scoped>
.input-wrapper {
  position: relative;
}

.status {
  position: absolute;
  right: 10px;
  top: 50%;
  transform: translateY(-50%);
  font-size: 0.875rem;
}

.status.success {
  color: #42b983;
}

.status.error {
  color: #ff4444;
}
</style>

3. Custom Form Components

Reusable Text Input Component

<!-- components/TextInput.vue -->
<script setup>
import { computed } from 'vue'

const props = defineProps({
  modelValue: String,
  label: String,
  type: {
    type: String,
    default: 'text'
  },
  placeholder: String,
  error: String,
  required: Boolean,
  disabled: Boolean
})

const emit = defineEmits(['update:modelValue'])

const inputValue = computed({
  get: () => props.modelValue,
  set: (value) => emit('update:modelValue', value)
})
</script>

<template>
  <div class="text-input">
    <label v-if="label">
      {{ label }}
      <span v-if="required" class="required">*</span>
    </label>
    <input
      v-model="inputValue"
      :type="type"
      :placeholder="placeholder"
      :disabled="disabled"
      :class="{ invalid: error }"
    >
    <span v-if="error" class="error">{{ error }}</span>
  </div>
</template>

<style scoped>
.text-input {
  margin-bottom: 1rem;
}

label {
  display: block;
  margin-bottom: 0.5rem;
  font-weight: 500;
}

.required {
  color: #ff4444;
}

input {
  width: 100%;
  padding: 0.5rem;
  border: 2px solid #ddd;
  border-radius: 4px;
  font-size: 1rem;
}

input:focus {
  outline: none;
  border-color: #42b983;
}

input.invalid {
  border-color: #ff4444;
}

.error {
  display: block;
  margin-top: 0.25rem;
  color: #ff4444;
  font-size: 0.875rem;
}
</style>

Using the component:

<script setup>
import { ref } from 'vue'
import TextInput from './components/TextInput.vue'

const email = ref('')
const emailError = ref('')

function validateEmail() {
  const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
  if (!email.value) {
    emailError.value = 'Email is required'
  } else if (!emailRegex.test(email.value)) {
    emailError.value = 'Invalid email format'
  } else {
    emailError.value = ''
  }
}
</script>

<template>
  <TextInput
    v-model="email"
    label="Email Address"
    type="email"
    placeholder="your@email.com"
    :error="emailError"
    :required="true"
    @blur="validateEmail"
  />
</template>

Select/Dropdown Component

<!-- components/SelectInput.vue -->
<script setup>
import { computed } from 'vue'

const props = defineProps({
  modelValue: [String, Number],
  label: String,
  options: {
    type: Array,
    required: true
  },
  placeholder: {
    type: String,
    default: 'Select an option'
  },
  error: String,
  required: Boolean
})

const emit = defineEmits(['update:modelValue'])

const selectedValue = computed({
  get: () => props.modelValue,
  set: (value) => emit('update:modelValue', value)
})
</script>

<template>
  <div class="select-input">
    <label v-if="label">
      {{ label }}
      <span v-if="required" class="required">*</span>
    </label>
    <select
      v-model="selectedValue"
      :class="{ invalid: error }"
    >
      <option value="" disabled>{{ placeholder }}</option>
      <option
        v-for="option in options"
        :key="option.value"
        :value="option.value"
      >
        {{ option.label }}
      </option>
    </select>
    <span v-if="error" class="error">{{ error }}</span>
  </div>
</template>

<style scoped>
.select-input {
  margin-bottom: 1rem;
}

label {
  display: block;
  margin-bottom: 0.5rem;
  font-weight: 500;
}

select {
  width: 100%;
  padding: 0.5rem;
  border: 2px solid #ddd;
  border-radius: 4px;
  font-size: 1rem;
  background: white;
}

select:focus {
  outline: none;
  border-color: #42b983;
}

select.invalid {
  border-color: #ff4444;
}

.error {
  display: block;
  margin-top: 0.25rem;
  color: #ff4444;
  font-size: 0.875rem;
}
</style>

4. File Upload Handling

<script setup>
import { ref } from 'vue'

const selectedFile = ref(null)
const previewUrl = ref('')
const uploadProgress = ref(0)
const isUploading = ref(false)

function handleFileSelect(event) {
  const file = event.target.files[0]

  if (!file) return

  // Validate file type
  const allowedTypes = ['image/jpeg', 'image/png', 'image/gif']
  if (!allowedTypes.includes(file.type)) {
    alert('Please select a valid image file (JPEG, PNG, or GIF)')
    return
  }

  // Validate file size (max 5MB)
  const maxSize = 5 * 1024 * 1024
  if (file.size > maxSize) {
    alert('File size must be less than 5MB')
    return
  }

  selectedFile.value = file

  // Create preview
  const reader = new FileReader()
  reader.onload = (e) => {
    previewUrl.value = e.target.result
  }
  reader.readAsDataURL(file)
}

async function uploadFile() {
  if (!selectedFile.value) return

  isUploading.value = true
  uploadProgress.value = 0

  const formData = new FormData()
  formData.append('file', selectedFile.value)

  try {
    // Simulate upload with progress
    const response = await fetch('/api/upload', {
      method: 'POST',
      body: formData,
      onUploadProgress: (progressEvent) => {
        uploadProgress.value = Math.round(
          (progressEvent.loaded * 100) / progressEvent.total
        )
      }
    })

    if (response.ok) {
      console.log('Upload successful')
    }
  } catch (error) {
    console.error('Upload failed:', error)
  } finally {
    isUploading.value = false
  }
}

function removeFile() {
  selectedFile.value = null
  previewUrl.value = ''
  uploadProgress.value = 0
}
</script>

<template>
  <div class="file-upload">
    <div v-if="!selectedFile" class="upload-area">
      <label for="file-input" class="upload-label">
        <span>Click to select file</span>
        <input
          id="file-input"
          type="file"
          accept="image/*"
          @change="handleFileSelect"
          hidden
        >
      </label>
    </div>

    <div v-else class="preview-area">
      <img :src="previewUrl" alt="Preview" class="preview-image">

      <div class="file-info">
        <p>{{ selectedFile.name }}</p>
        <p>{{ (selectedFile.size / 1024).toFixed(2) }} KB</p>
      </div>

      <div v-if="isUploading" class="progress-bar">
        <div
          class="progress-fill"
          :style="{ width: uploadProgress + '%' }"
        ></div>
      </div>

      <div class="actions">
        <button @click="uploadFile" :disabled="isUploading">
          {{ isUploading ? 'Uploading...' : 'Upload' }}
        </button>
        <button @click="removeFile" :disabled="isUploading">
          Remove
        </button>
      </div>
    </div>
  </div>
</template>

<style scoped>
.upload-area {
  border: 2px dashed #ddd;
  border-radius: 8px;
  padding: 2rem;
  text-align: center;
}

.upload-label {
  cursor: pointer;
  color: #42b983;
  font-weight: 500;
}

.preview-area {
  border: 1px solid #ddd;
  border-radius: 8px;
  padding: 1rem;
}

.preview-image {
  max-width: 100%;
  max-height: 300px;
  display: block;
  margin: 0 auto 1rem;
}

.progress-bar {
  height: 8px;
  background: #f0f0f0;
  border-radius: 4px;
  overflow: hidden;
  margin: 1rem 0;
}

.progress-fill {
  height: 100%;
  background: #42b983;
  transition: width 0.3s;
}

.actions {
  display: flex;
  gap: 0.5rem;
}

button {
  flex: 1;
  padding: 0.5rem 1rem;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}
</style>

5. Multi-Step Form

<script setup>
import { ref, computed } from 'vue'

const currentStep = ref(1)
const formData = ref({
  // Step 1
  name: '',
  email: '',

  // Step 2
  address: '',
  city: '',
  country: '',

  // Step 3
  cardNumber: '',
  expiryDate: '',
  cvv: ''
})

const totalSteps = 3

const canProceed = computed(() => {
  if (currentStep.value === 1) {
    return formData.value.name && formData.value.email
  } else if (currentStep.value === 2) {
    return formData.value.address && formData.value.city && formData.value.country
  } else if (currentStep.value === 3) {
    return formData.value.cardNumber && formData.value.expiryDate && formData.value.cvv
  }
  return false
})

function nextStep() {
  if (currentStep.value < totalSteps) {
    currentStep.value++
  }
}

function previousStep() {
  if (currentStep.value > 1) {
    currentStep.value--
  }
}

function submitForm() {
  console.log('Form submitted:', formData.value)
  // Submit to API
}
</script>

<template>
  <div class="multi-step-form">
    <!-- Progress indicator -->
    <div class="progress-steps">
      <div
        v-for="step in totalSteps"
        :key="step"
        class="step"
        :class="{ active: step === currentStep, completed: step < currentStep }"
      >
        {{ step }}
      </div>
    </div>

    <!-- Step 1: Personal Info -->
    <div v-show="currentStep === 1" class="step-content">
      <h2>Personal Information</h2>
      <TextInput
        v-model="formData.name"
        label="Full Name"
        required
      />
      <TextInput
        v-model="formData.email"
        label="Email"
        type="email"
        required
      />
    </div>

    <!-- Step 2: Address -->
    <div v-show="currentStep === 2" class="step-content">
      <h2>Address</h2>
      <TextInput
        v-model="formData.address"
        label="Street Address"
        required
      />
      <TextInput
        v-model="formData.city"
        label="City"
        required
      />
      <TextInput
        v-model="formData.country"
        label="Country"
        required
      />
    </div>

    <!-- Step 3: Payment -->
    <div v-show="currentStep === 3" class="step-content">
      <h2>Payment Information</h2>
      <TextInput
        v-model="formData.cardNumber"
        label="Card Number"
        placeholder="1234 5678 9012 3456"
        required
      />
      <TextInput
        v-model="formData.expiryDate"
        label="Expiry Date"
        placeholder="MM/YY"
        required
      />
      <TextInput
        v-model="formData.cvv"
        label="CVV"
        type="password"
        placeholder="123"
        required
      />
    </div>

    <!-- Navigation buttons -->
    <div class="form-actions">
      <button
        v-if="currentStep > 1"
        @click="previousStep"
        type="button"
      >
        Previous
      </button>

      <button
        v-if="currentStep < totalSteps"
        @click="nextStep"
        :disabled="!canProceed"
        type="button"
      >
        Next
      </button>

      <button
        v-if="currentStep === totalSteps"
        @click="submitForm"
        :disabled="!canProceed"
        type="submit"
      >
        Submit
      </button>
    </div>
  </div>
</template>

<style scoped>
.progress-steps {
  display: flex;
  justify-content: center;
  gap: 1rem;
  margin-bottom: 2rem;
}

.step {
  width: 40px;
  height: 40px;
  border-radius: 50%;
  border: 2px solid #ddd;
  display: flex;
  align-items: center;
  justify-content: center;
  font-weight: bold;
}

.step.active {
  border-color: #42b983;
  color: #42b983;
}

.step.completed {
  background: #42b983;
  border-color: #42b983;
  color: white;
}

.form-actions {
  display: flex;
  gap: 1rem;
  margin-top: 2rem;
}
</style>

6. Practical Exercises

Exercise 5.4.1: Contact Form

Build a contact form with:

  1. Name, email, subject, message fields
  2. Real-time validation
  3. Character count for message (max 500)
  4. Submit button disabled until valid
  5. Success/error messages

Exercise 5.4.2: Survey Form

Create a survey with:

  1. Multiple choice (radio buttons)
  2. Multi-select (checkboxes)
  3. Rating scale (1-5 stars)
  4. Text feedback
  5. Save progress to localStorage

Build an image uploader:

  1. Multiple file selection
  2. Preview all selected images
  3. Remove individual images
  4. Validate file types and sizes
  5. Upload progress for each file

Exercise 5.4.4: Wizard Form

Create a 4-step wizard:

  1. Account details
  2. Profile information
  3. Preferences
  4. Review and confirm
  5. Progress saving between steps

7. Knowledge Check

Question 1: What's the difference between @blur and @input for validation?

Show answer `@input` validates on every keystroke (real-time), while `@blur` validates when the user leaves the field. Use `@input` for immediate feedback, `@blur` to avoid annoying users while typing.

Question 2: Why should you debounce API calls in forms?

Show answer Debouncing waits for the user to stop typing before making API calls, preventing excessive requests on every keystroke and improving performance.

Question 3: How do you make a custom form component work with v-model?

Show answer Accept a `modelValue` prop and emit `update:modelValue` events. Use a computed property with getter/setter for two-way binding.

Question 4: What's the best way to validate file uploads?

Show answer Check file type (MIME type), file size, and optionally file name/extension before uploading. Show clear error messages if validation fails.

8. Key Takeaways

  • Event modifiers simplify common event handling patterns
  • Keyboard events can be filtered by key with modifiers
  • Form validation should provide clear, immediate feedback
  • Debouncing prevents excessive API calls during typing
  • Custom form components enable consistency and reusability
  • v-model works with custom components via modelValue prop
  • File uploads need type and size validation
  • Multi-step forms improve UX for complex data entry
  • Disabled states prevent invalid form submission
  • Real-time validation balances UX and feedback

9. Further Resources

Official Documentation:

Validation Libraries:


Next Steps

Excellent work! You now know how to handle events and build complex forms in Vue.

In Lesson 5.5: Composables & Lifecycle, you'll learn about Vue's composition API patterns and component lifecycle hooks.