Requires Alpine
Most logic is borrowed from Statamic Peak
/routes/web.php
use App\\Http\\Controllers\\DynamicToken;
// Dynamic Token route for posting a form with Ajax.
Route::get('/!/DynamicToken/refresh', [DynamicToken::class, 'getRefresh']);
/app/Http/Controllers/DynamicToken.php
<?php
namespace App\\Http\\Controllers;
use Illuminate\\Http\\Request;
class DynamicToken extends Controller
{
/**
* Get refreshed CSRF token.
*
* @return string
*/
public function getRefresh(Request $request)
{
$referer = request()->headers->get('referer');
$contains = str_contains($referer, request()->getHttpHost());
if (empty($referer) || !$contains) {
abort(404);
}
return response()->json([
'csrf_token' => csrf_token()
]);
}
}
import Alpine from 'alpinejs';
window.Alpine = Alpine
Alpine.start()
// Global get CSRF token function (used by forms).
window.getToken = async () => {
return await fetch('/!/DynamicToken/refresh')
.then((res) => res.json())
.then((data) => {
return data.csrf_token
})
.catch(function (error) {
this.error = 'Something went wrong. Please try again later.'
})
}
(just add to wherever your form is invoked)
{{# The form script handling validation and submission via AJAX with `fetch()`. #}}
<script>
document.addEventListener('alpine:initializing', () => {
Alpine.data('sending', () => {
return {
error: false,
errors: [],
sending: false,
success: false,
sendForm: async function() {
this.sending = true
// Post the form.
fetch(this.$refs.form.action, {
headers: {
'X-Requested-With' : 'XMLHttpRequest',
'X-CSRF-Token' : await getToken(),
},
method: 'POST',
body: new FormData(this.$refs.form)})
.then(res => res.json())
.then(json => {
if (json['success']) {
this.errors = []
this.success = true
this.error = false
this.sending = false
this.$refs.form.reset()
setTimeout(function(){
this.success = false
}, 4500)
}
if (json['error']) {
this.sending = false
this.error = true
this.success = false
this.errors = json['error']
}
})
.catch(err => {
err.text().then( errorMessage => {
this.sending = false
})
})
}
}
})
})
</script>
{{# Create the selected form and reference Alpine data in `sending()`. Prevent form from submitting with POST as we will submit with AJAX. #}}
<div x-data="sending">
{{ form:create in="{ form:handle }"
id="form-{form:handle}"
class="flex flex-wrap"
x-ref="form"
@submit.prevent="sendForm()"
js="alpine:dynamic_form"
}}
</div>
On the parent to show the error message
<div x-data="{ id: $id('error-label') }" class="flex flex-col space-y-1">
{{ field }}
{{# Display error label when there is a validation error with the name field. #}}
<template x-if="errors.{{ handle }}">
<div class="block text-sm text-red-600" :id="id" x-text="errors.{{ handle}}"></div>test
</template>
{{# Instructions Below #}}
{{ if instructions && instructions_position == "below" }}
<div class="text-sm font-normal prose">{{ instructions | markdown }}</div>
{{ /if }}
</div>
And on the field itself to show the error border state
{{# Bind custom error classses when there is a validation error with the field. #}}
<!-- /vendor/statamic/forms/fields/text.antlers.html -->
<input
class="w-full rounded border-neutral focus-visible:border-primary focus-visible:ring focus-visible:ring-primary motion-safe:transition caret-primary"
:class="{ '!border-red-600 !focus-visible:border-red-600': errors.{{ handle }} }"
id="{{ handle }}"
name="{{ handle }}"
type="{{ input_type ?? 'text' }}"
placeholder="{{ placeholder }}"
:aria-invalid="errors.hasOwnProperty('{{ handle }}')"
:aria-describedBy="id"
{{ if character_limit }}maxlength="{{ character_limit }}"{{ /if }}
{{ old ?= 'value="{old}"' }}
{{ js_attributes }}
/>