Skip to content

✅ Submitting Forms

This guide shows how to handle form submission using arkform's handleSubmit method, which provides automatic loading states, validation, and error handling.

🎯 The handleSubmit Pattern

The handleSubmit method is the recommended way to handle form submissions. It automatically:

  • Manages loading state
  • Validates all form fields
  • Provides typed validation results
  • Handles errors gracefully

🔁 Basic Template

vue
<template>
  <arkform name="login" theme="default" :submit="handleSubmit">
    <header>
      <h2>Login</h2>
      <p>Enter your email and password to continue.</p>
    </header>

    <field name="email" placeholder="Enter your email" />
    <field name="password" placeholder="Enter your password" />

    <!-- Show form-level errors -->
    <div v-if="form.mode.value === 'error'" class="error-messages">
      <p v-for="error in form.errors.value" :key="error">{{ error }}</p>
    </div>

    <!-- Show success messages -->
    <div v-if="form.mode.value === 'success'" class="success-messages">
      <p v-for="message in form.messages.value" :key="message">{{ message }}</p>
    </div>

    <button type="submit" :disabled="form.isLoading.value">
      {{ form.isLoading.value ? 'Logging in...' : 'Login' }}
    </button>
  </arkform>
</template>

<script setup>
import { defineArkform } from "#arkform"
import { type as arktype } from "arktype"

// Define form schema
const { $form } = defineArkform({
  login: {
    schema: arktype({
      email: "string.email",
      password: "string"
    })
  }
})

const form = $form("login")

// Create submit handler
const handleSubmit = form.handleSubmit(async (result) => {
  if (!result.ok) {
    // Validation failed - errors are automatically shown
    form.errors.value = result.message
    return
  }

  try {
    // Submit the validated data
    const response = await $fetch('/api/auth/login', {
      method: 'POST',
      body: result.data // { email: string, password: string }
    })

    // Success
    form.messages.value = ['Login successful!']
    await navigateTo('/dashboard')
  } catch (error) {
    // Handle API errors
    form.errors.value = ['Login failed. Please check your credentials.']
  }
})
</script>

🧠 Advanced Pattern: Custom Composable

For reusable form logic, create a custom composable:

ts
// composables/useLoginForm.ts
import { defineArkform } from "#arkform"
import { type as arktype } from "arktype"

export function useLoginForm() {
  const { $form } = defineArkform({
    login: {
      schema: arktype({
        email: "string.email",
        password: "string"
      })
    }
  })

  const form = $form("login")

  const handleSubmit = form.handleSubmit(async (result) => {
    if (!result.ok) {
      // Set form-level errors
      form.errors.value = result.message
      return
    }

    try {
      const { email, password } = result.data
      
      // Call your API
      const response = await AuthService.login(email, password)
      
      // Handle success
      form.messages.value = ['Welcome back!']
      await navigateTo('/dashboard')
      
    } catch (error) {
      // Handle errors
      form.errors.value = ['Invalid credentials. Please try again.']
    }
  })

  return {
    form,
    handleSubmit,
    // Expose convenient properties
    isLoading: form.isLoading,
    errors: form.errors,
    messages: form.messages
  }
}

Then use it in your component:

vue
<template>
  <arkform name="login" :submit="handleSubmit">
    <field name="email" />
    <field name="password" />
    
    <div v-if="errors.length" class="errors">
      <p v-for="error in errors">{{ error }}</p>
    </div>
    
    <button type="submit" :disabled="isLoading">
      {{ isLoading ? 'Logging in...' : 'Login' }}
    </button>
  </arkform>
</template>

<script setup>
const { handleSubmit, isLoading, errors } = useLoginForm()
</script>

🔄 Handling Different Result Types

The handleSubmit callback receives a Result<FormData, string[]>:

ts
const handleSubmit = form.handleSubmit(async (result) => {
  // Type-safe result handling
  if (!result.ok) {
    // result.message is string[] - validation errors
    console.log('Validation errors:', result.message)
    form.errors.value = result.message
    return
  }

  // result.data is strongly typed based on your schema
  const formData = result.data // { email: string, password: string }
  
  // Submit to API
  await submitForm(formData)
})

🎨 Loading States & UI Feedback

vue
<template>
  <arkform name="contact" :submit="handleSubmit">
    <field name="name" />
    <field name="email" />
    <field name="message" is="textarea" />
    
    <!-- Loading overlay -->
    <div v-if="form.isLoading.value" class="loading-overlay">
      <p>Submitting...</p>
    </div>
    
    <!-- Form mode-based styling -->
    <div class="form-status" :class="form.mode.value">
      <div v-if="form.mode.value === 'error'" class="error-state">
        <h4>Validation Errors:</h4>
        <ul>
          <li v-for="error in form.errors.value">{{ error }}</li>
        </ul>
      </div>
      
      <div v-if="form.mode.value === 'success'" class="success-state">
        <h4>Success!</h4>
        <p v-for="message in form.messages.value">{{ message }}</p>
      </div>
    </div>
    
    <button 
      type="submit" 
      :disabled="form.isLoading.value"
      :class="{ loading: form.isLoading.value }"
    >
      <span v-if="!form.isLoading.value">Submit</span>
      <span v-else>
        <LoadingSpinner />
        Submitting...
      </span>
    </button>
  </arkform>
</template>

⚡ Best Practices

✅ Do

  • Use handleSubmit for consistent loading and error handling
  • Keep form composables focused on a single form
  • Validate on the client and server
  • Provide clear success and error feedback
  • Use the form's reactive mode for conditional UI

❌ Don't

  • Handle loading state manually (let handleSubmit manage it)
  • Put business logic directly in the submit handler
  • Forget to handle both validation and API errors
  • Block the UI unnecessarily during submission

🧪 Testing

Form submission logic is easy to test:

ts
// tests/useLoginForm.test.ts
import { describe, it, expect, vi } from 'vitest'
import { useLoginForm } from '~/composables/useLoginForm'

describe('useLoginForm', () => {
  it('should handle successful login', async () => {
    const { form, handleSubmit } = useLoginForm()
    
    // Set form values
    form.set({ email: 'test@example.com', password: 'password123' })
    
    // Mock API call
    const mockLogin = vi.fn().mockResolvedValue({ success: true })
    
    // Execute submit
    const submit = handleSubmit
    await submit()
    
    expect(form.messages.value).toContain('Welcome back!')
  })
})