Building Complete Forms

+15 Mana ✨

Let's combine everything to build real-world forms with validation, submission handling, and user feedback.

A Complete Registration Form

vue
<script setup lang="ts">
import { ref, computed } from 'vue'

// Form data
const form = ref({
  email: '',
  password: '',
  confirmPassword: '',
  name: '',
  agreeToTerms: false
})

// Form state
const isSubmitting = ref(false)
const submitError = ref('')
const submitSuccess = ref(false)

// Validation
const errors = computed(() => {
  const errs: Record<string, string> = {}
  
  // Email validation
  if (!form.value.email) {
    errs.email = 'Email is required'
  } else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(form.value.email)) {
    errs.email = 'Please enter a valid email'
  }
  
  // Name validation
  if (!form.value.name) {
    errs.name = 'Name is required'
  } else if (form.value.name.length < 2) {
    errs.name = 'Name must be at least 2 characters'
  }
  
  // Password validation
  if (!form.value.password) {
    errs.password = 'Password is required'
  } else if (form.value.password.length < 8) {
    errs.password = 'Password must be at least 8 characters'
  }
  
  // Confirm password
  if (form.value.password !== form.value.confirmPassword) {
    errs.confirmPassword = 'Passwords do not match'
  }
  
  // Terms agreement
  if (!form.value.agreeToTerms) {
    errs.agreeToTerms = 'You must agree to the terms'
  }
  
  return errs
})

const isValid = computed(() => Object.keys(errors.value).length === 0)

// Track which fields have been touched
const touched = ref<Record<string, boolean>>({})

function markTouched(field: string) {
  touched.value[field] = true
}

function shouldShowError(field: string): boolean {
  return touched.value[field] && !!errors.value[field]
}

// Form submission
async function handleSubmit() {
  // Mark all fields as touched to show all errors
  Object.keys(form.value).forEach(key => {
    touched.value[key] = true
  })
  
  if (!isValid.value) {
    return
  }
  
  isSubmitting.value = true
  submitError.value = ''
  
  try {
    // Simulate API call
    await new Promise(resolve => setTimeout(resolve, 1500))
    
    // Simulate occasional error
    if (Math.random() < 0.2) {
      throw new Error('Email already registered')
    }
    
    submitSuccess.value = true
    console.log('Form submitted:', form.value)
  } catch (error) {
    submitError.value = error instanceof Error ? error.message : 'Submission failed'
  } finally {
    isSubmitting.value = false
  }
}

// Reset form
function resetForm() {
  form.value = {
    email: '',
    password: '',
    confirmPassword: '',
    name: '',
    agreeToTerms: false
  }
  touched.value = {}
  submitError.value = ''
  submitSuccess.value = false
}
</script>

<template>
  <div class="form-container">
    <h2>Create Account</h2>
    
    <!-- Success message -->
    <div v-if="submitSuccess" class="success-message">
      <p>Account created successfully!</p>
      <button @click="resetForm">Create Another</button>
    </div>
    
    <!-- Registration form -->
    <form v-else @submit.prevent="handleSubmit">
      <!-- Name field -->
      <div class="form-group">
        <label for="name">Name</label>
        <input
          id="name"
          v-model.trim="form.name"
          type="text"
          :class="{ error: shouldShowError('name') }"
          @blur="markTouched('name')"
        />
        <span v-if="shouldShowError('name')" class="error-text">
          {{ errors.name }}
        </span>
      </div>
      
      <!-- Email field -->
      <div class="form-group">
        <label for="email">Email</label>
        <input
          id="email"
          v-model.trim="form.email"
          type="email"
          :class="{ error: shouldShowError('email') }"
          @blur="markTouched('email')"
        />
        <span v-if="shouldShowError('email')" class="error-text">
          {{ errors.email }}
        </span>
      </div>
      
      <!-- Password field -->
      <div class="form-group">
        <label for="password">Password</label>
        <input
          id="password"
          v-model="form.password"
          type="password"
          :class="{ error: shouldShowError('password') }"
          @blur="markTouched('password')"
        />
        <span v-if="shouldShowError('password')" class="error-text">
          {{ errors.password }}
        </span>
      </div>
      
      <!-- Confirm password -->
      <div class="form-group">
        <label for="confirmPassword">Confirm Password</label>
        <input
          id="confirmPassword"
          v-model="form.confirmPassword"
          type="password"
          :class="{ error: shouldShowError('confirmPassword') }"
          @blur="markTouched('confirmPassword')"
        />
        <span v-if="shouldShowError('confirmPassword')" class="error-text">
          {{ errors.confirmPassword }}
        </span>
      </div>
      
      <!-- Terms checkbox -->
      <div class="form-group checkbox">
        <label>
          <input
            v-model="form.agreeToTerms"
            type="checkbox"
            @change="markTouched('agreeToTerms')"
          />
          I agree to the Terms of Service
        </label>
        <span v-if="shouldShowError('agreeToTerms')" class="error-text">
          {{ errors.agreeToTerms }}
        </span>
      </div>
      
      <!-- Error message -->
      <div v-if="submitError" class="error-message">
        {{ submitError }}
      </div>
      
      <!-- Submit button -->
      <button 
        type="submit" 
        :disabled="isSubmitting"
        class="submit-btn"
      >
        {{ isSubmitting ? 'Creating Account...' : 'Create Account' }}
      </button>
    </form>
  </div>
</template>

<style scoped>
.form-container {
  max-width: 400px;
  margin: 0 auto;
  padding: 2rem;
}

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

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

input[type="text"],
input[type="email"],
input[type="password"] {
  width: 100%;
  padding: 0.75rem;
  border: 1px solid #ddd;
  border-radius: 4px;
  font-size: 1rem;
}

input.error {
  border-color: #e53e3e;
}

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

.checkbox label {
  display: flex;
  align-items: center;
  gap: 0.5rem;
}

.submit-btn {
  width: 100%;
  padding: 0.75rem;
  background: #42b883;
  color: white;
  border: none;
  border-radius: 4px;
  font-size: 1rem;
  cursor: pointer;
}

.submit-btn:disabled {
  opacity: 0.6;
  cursor: not-allowed;
}

.error-message {
  background: #fed7d7;
  color: #c53030;
  padding: 0.75rem;
  border-radius: 4px;
  margin-bottom: 1rem;
}

.success-message {
  background: #c6f6d5;
  color: #276749;
  padding: 1rem;
  border-radius: 4px;
  text-align: center;
}
</style>

This form demonstrates:

  • Reactive form state with ref()
  • Computed validation that updates automatically
  • Touch tracking to show errors only after interaction
  • Loading states during submission
  • Error handling with user feedback
  • Form reset functionality
  • Accessibility with proper labels and IDs
✓ Completed