Composition API
Copy
<script setup lang="ts">
import { ref } from 'vue';
const status = ref<'idle' | 'loading' | 'success' | 'error'>('idle');
const error = ref<string | null>(null);
async function handleSubmit(e: Event) {
e.preventDefault();
status.value = 'loading';
error.value = null;
const form = e.target as HTMLFormElement;
const data = new FormData(form);
try {
const response = await fetch('https://spike.ac/api/f/YOUR_FORM_SLUG', {
method: 'POST',
body: data,
headers: { 'Accept': 'application/json' }
});
if (response.ok) {
status.value = 'success';
form.reset();
} else {
const result = await response.json();
error.value = result.error || 'Submission failed';
status.value = 'error';
}
} catch {
error.value = 'Network error';
status.value = 'error';
}
}
function reset() {
status.value = 'idle';
error.value = null;
}
</script>
<template>
<div v-if="status === 'success'">
<p>Thanks for your message!</p>
<button @click="reset">Send another</button>
</div>
<form v-else @submit="handleSubmit">
<input type="email" name="email" placeholder="Email" required />
<textarea name="message" placeholder="Message" required></textarea>
<button type="submit" :disabled="status === 'loading'">
{{ status === 'loading' ? 'Sending...' : 'Send' }}
</button>
<p v-if="error" class="error">{{ error }}</p>
</form>
</template>
Composable
Create a reusable composable:Copy
// composables/useSpike.ts
import { ref } from 'vue';
export function useSpike(formSlug: string) {
const status = ref<'idle' | 'loading' | 'success' | 'error'>('idle');
const error = ref<string | null>(null);
async function submit(data: Record<string, unknown>) {
status.value = 'loading';
error.value = null;
try {
const response = await fetch(`https://spike.ac/api/f/${formSlug}`, {
method: 'POST',
body: JSON.stringify(data),
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
}
});
if (response.ok) {
status.value = 'success';
} else {
const result = await response.json();
error.value = result.error || 'Submission failed';
status.value = 'error';
}
} catch {
error.value = 'Network error';
status.value = 'error';
}
}
function reset() {
status.value = 'idle';
error.value = null;
}
return { status, error, submit, reset };
}
Copy
<script setup>
import { useSpike } from '@/composables/useSpike';
const { status, error, submit, reset } = useSpike('YOUR_FORM_SLUG');
function handleSubmit(e) {
e.preventDefault();
const form = e.target;
const data = Object.fromEntries(new FormData(form));
submit(data);
}
</script>
Options API
Copy
<script>
export default {
data() {
return {
status: 'idle',
error: null
};
},
methods: {
async handleSubmit(e) {
e.preventDefault();
this.status = 'loading';
this.error = null;
const form = e.target;
const data = new FormData(form);
try {
const response = await fetch('https://spike.ac/api/f/YOUR_FORM_SLUG', {
method: 'POST',
body: data,
headers: { 'Accept': 'application/json' }
});
if (response.ok) {
this.status = 'success';
form.reset();
} else {
const result = await response.json();
this.error = result.error || 'Submission failed';
this.status = 'error';
}
} catch {
this.error = 'Network error';
this.status = 'error';
}
}
}
};
</script>
Nuxt 3
Works the same way in Nuxt 3:Copy
<!-- pages/contact.vue -->
<script setup>
const status = ref('idle');
async function handleSubmit(e) {
status.value = 'loading';
const { data, error } = await useFetch('https://spike.ac/api/f/YOUR_FORM_SLUG', {
method: 'POST',
body: Object.fromEntries(new FormData(e.target)),
headers: { 'Accept': 'application/json' }
});
status.value = error.value ? 'error' : 'success';
}
</script>