Skip to content

<div style="display: none;" hidden="true" aria-hidden="true">Are you an LLM? You can read better optimized documentation at /guides/vue/saas/wizard.md for this page in Markdown format</div>

Vue.js SaaS Wizard - Developer Wiki

Table of Contents

  1. Getting Started
  2. Architecture Deep Dive
  3. Creating Custom Wizards
  4. Validation System
  5. State Management
  6. API Integration

Getting Started

Understanding the Wizard System

The application provides two main wizard types:

  1. Initial Wizard (InitialWizard.vue): Full-page onboarding experience
  2. Tasks Wizard (TasksWizard.vue): Modal-based task completion

Both wizards share common functionality through the createWizardStore factory and validation system.

Basic Setup

javascript
// 1. Import the main app
import { advisableSaasApp } from './apps'

// 2. Set up global data
window.vueData = {
  initialData: {
    // Your wizard data structure
    generalSettings: {},
    paymentSettings: {},
    // ... other steps
  },
  wizardType: 'initial' // or 'tasks'
}

// 3. Mount the app (automatic)
window.advisableSaasApp = advisableSaasApp

Architecture Deep Dive

Component Hierarchy

AdvisableSaasApp (Root)
├── InitialWizard
│   ├── Step Components (dynamic)
│   └── Progress Indicators
├── TasksWizard
│   ├── Modal Container
│   ├── Step Components (dynamic)
│   └── Navigation Controls
└── TasksButton
    └── Progress Ring

Data Flow

  1. User Input → Component
  2. Component → Store Action (updateStepData)
  3. Store → Validation System
  4. Validation → Error State
  5. Auto-save → API Persistence

Store Architecture

javascript
// Store modules are created using the factory pattern
const wizardStore = createWizardStore({
  name: 'myWizard',
  steps: [
    { name: 'step1', component: 'Step1Component' },
    { name: 'step2', component: 'Step2Component' }
  ],
  createSchemas: (l10n, steps) => ({
    step1: { /* validation schema */ },
    step2: { /* validation schema */ }
  })
})

Creating Custom Wizards

Step 1: Define Your Wizard Configuration

javascript
// store/myCustomWizard.js
import { createWizardStore } from './createWizardStore'
import Step1 from '../components/steps/Step1.vue'
import Step2 from '../components/steps/Step2.vue'

const steps = [
  {
    name: 'basicInfo',
    component: Step1,
    title: 'wizard.steps.basicInfo.title',
    label: 'wizard.steps.basicInfo.label',
    icon: 'fa-solid fa-info-circle'
  },
  {
    name: 'advanced',
    component: Step2,
    title: 'wizard.steps.advanced.title',
    label: 'wizard.steps.advanced.label',
    icon: 'fa-solid fa-cog'
  }
]

const createSchemas = (l10n, steps) => ({
  basicInfo: {
    name: {
      type: 'string',
      required: true,
      minLength: 2,
      maxLength: 50
    },
    email: {
      type: 'string',
      required: true,
      pattern: ValidationRules.email()
    }
  },
  advanced: {
    settings: {
      type: 'object',
      required: false
    }
  }
})

export default createWizardStore({
  name: 'myCustomWizard',
  steps,
  createSchemas
})

Step 2: Create Step Components

vue
<!-- components/steps/Step1.vue -->
<template>
  <div class="step-container">
    <div class="step-header">
      <h2 class="step-title">{{ l10n.t('wizard.steps.basicInfo.title') }}</h2>
    </div>
    
    <form class="step-form">
      <div class="form-group">
        <label class="form-label" :data-required="true">
          {{ l10n.t('wizard.fields.name.label') }}
        </label>
        <input
          v-model="stepData.name"
          type="text"
          class="form-control"
          :class="{ error: hasFieldError('name') }"
          @blur="validateField('name')"
        />
        <span v-if="hasFieldError('name')" class="error-message">
          {{ getFieldError('name') }}
        </span>
      </div>
      
      <div class="form-actions">
        <button type="button" class="btn btn-outline" @click="$emit('previous')">
          Previous
        </button>
        <button type="button" class="btn btn-primary" @click="handleNext">
          Next
        </button>
      </div>
    </form>
  </div>
</template>

<script>
import { useWizardStore } from '../../composables/useWizardStore'
import { useFormValidation } from '../../composables/useFormValidation'
import useLocalization from '../../../productBundles/composables/useLocalization'

export default {
  name: 'Step1',
  emits: ['next', 'previous'],
  setup(props, { emit }) {
    const stepName = 'basicInfo'
    const { stepData } = useWizardStore(stepName)
    const { validateField, hasFieldError, getFieldError, validateStep } = useFormValidation(stepName, 'myCustomWizard')
    const l10n = useLocalization(['wizard'], ['wizard.'])
    
    const handleNext = async () => {
      const result = await validateStep()
      if (result.isValid) {
        emit('next')
      }
    }
    
    return {
      stepData,
      validateField,
      hasFieldError,
      getFieldError,
      handleNext,
      l10n
    }
  }
}
</script>

Step 3: Register Your Wizard

javascript
// store/index.js
import myCustomWizard from './myCustomWizard'

const store = new Vuex.Store({
  modules: {
    common,
    initialWizard,
    tasksWizard,
    myCustomWizard // Add your wizard here
  }
})

Validation System

Schema Definition

javascript
const schema = {
  // Basic field validation
  fieldName: {
    type: 'string',        // 'string', 'number', 'boolean', 'array', 'object'
    required: true,        // or function: (formData) => boolean
    minLength: 5,
    maxLength: 100,
    pattern: /^[A-Za-z]+$/ // RegExp or ValidationRule function
  },
  
  // Nested object validation
  address: {
    street: { type: 'string', required: true },
    city: { type: 'string', required: true },
    zipCode: { type: 'string', pattern: /^\d{5}$/ }
  },
  
  // Array validation
  items: {
    type: 'array',
    required: true
  },
  
  // Dynamic field validation (using dot notation)
  'items[0].name': {
    type: 'string',
    required: true
  }
}

Custom Validation Rules

javascript
import { ValidationRules } from '../validation'

// Using built-in rules
const emailRule = ValidationRules.email('Please enter a valid email')
const phoneRule = ValidationRules.phone('Invalid phone number')

// Custom validation function
const customRule = ValidationRules.custom(
  (value, formData) => {
    // Your validation logic
    if (formData.type === 'premium' && !value) {
      return false
    }
    return true
  },
  'This field is required for premium accounts'
)

// Pattern-based validation
const schema = {
  email: {
    type: 'string',
    required: true,
    pattern: emailRule
  },
  phone: {
    type: 'string',
    pattern: phoneRule
  },
  premiumFeature: {
    type: 'string',
    pattern: customRule
  }
}

Dynamic Validation

For validation rules that change based on form data:

javascript
// Register dynamic validation generators
import { dynamicValidationRegistry } from '../validation/dynamicValidationRegistry'

dynamicValidationRegistry.register('myWizard', {
  stepName: new Map([
    ['paymentFields', (formData) => {
      const fields = {}
      
      if (formData.paymentMethod === 'credit_card') {
        fields['cardNumber'] = {
          type: 'string',
          required: true,
          pattern: /^\d{16}$/
        }
        fields['expiryDate'] = {
          type: 'string',
          required: true,
          pattern: /^\d{2}\/\d{2}$/
        }
      }
      
      if (formData.paymentMethod === 'paypal') {
        fields['paypalEmail'] = {
          type: 'string',
          required: true,
          pattern: ValidationRules.email()
        }
      }
      
      return fields
    }]
  ])
})

State Management

Store Structure

Each wizard store contains:

javascript
state: {
  currentStep: 1,
  steps: [...],           // Step definitions
  wizardData: {...},      // Form data
  validationErrors: {...}, // Validation state
  touchedFields: {...},   // Field interaction tracking
  validators: {...},      // Validator instances
  isLoading: false,
  isSaving: false,
  wizardProgress: {...},  // Progress tracking
  wizardConfig: {...}     // External configuration
}

Key Actions

javascript
// Navigation
await store.dispatch('myWizard/setCurrentStep', 2)
await store.dispatch('myWizard/completeStep', 1)

// Data management
store.dispatch('myWizard/updateStepData', {
  section: 'stepName',
  data: { field: 'value' }
})

// Validation
const result = await store.dispatch('myWizard/validateStep', {
  stepName: 'basicInfo',
  l10n
})

const fieldResult = await store.dispatch('myWizard/validateField', {
  stepName: 'basicInfo',
  fieldName: 'email',
  l10n
})

// Persistence
await store.dispatch('myWizard/saveWizardProgress')
await store.dispatch('myWizard/loadWizardProgress', { l10n })

Using Composables

javascript
// In your component
import { useWizardStore } from '../composables/useWizardStore'
import { useFormValidation } from '../composables/useFormValidation'

export default {
  setup() {
    const stepName = 'myStep'
    const wizardNamespace = 'myWizard'
    
    // Get reactive step data
    const { stepData } = useWizardStore(stepName)
    
    // Get validation methods
    const {
      validateField,
      validateStep,
      hasFieldError,
      getFieldError,
      isStepValid
    } = useFormValidation(stepName, wizardNamespace)
    
    return {
      stepData,
      validateField,
      validateStep,
      hasFieldError,
      getFieldError,
      isStepValid
    }
  }
}

API Integration

Expected Endpoints

javascript
// Configuration endpoint
GET /saas/api/wizard/data
Response: {
  success: true,
  data: {
    paymentGateways: [...],
    transporters: [...],
    countries: [...],
    languages: [...]
  }
}

// State persistence
POST /saas/api/wizard/saveState
Body: {
  wizardName: 'initialWizard',
  currentStep: 2,
  steps: [...],
  wizardData: {...},
  wizardProgress: {...}
}

// State loading
GET /saas/api/wizard/getState?wizard_name=initialWizard
Response: {
  success: true,
  data: {
    currentStep: 2,
    wizardData: {...},
    // ... saved state
  }
}

// Wizard completion
POST /saas/api/wizard/complete
Body: FormData with wizardData and files
Response: {
  success: true,
  redirect: '/dashboard'
}