✅ 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
handleSubmitfor 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
modefor conditional UI
❌ Don't
- Handle loading state manually (let
handleSubmitmanage 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!')
})
})