Quickstart
Build a complete task list with server-side validation in under 10 minutes.
Note
What We're Building
We'll create a task list that demonstrates the core Gale patterns:
- State updates — Change Alpine data from your Laravel controllers
- Form validation — Server-side rules with reactive error messages
- DOM manipulation — Add and remove HTML without page reloads
- Loading states — Show feedback during server requests
By the end, you'll understand the patterns used in every Gale application.
Step 1: Create the Route
Add a simple route to display our task list. In routes/web.php:
1use Illuminate\Support\Facades\Route;
2
3Route::get('/tasks', fn () => view('tasks'));
use Illuminate\Support\Facades\Route;
Route::get('/tasks', fn () => view('tasks'));
Step 2: Create the View
Create resources/views/tasks.blade.php:
1<!DOCTYPE html>
2<html lang="en">
3<head>
4 <meta charset="UTF-8">
5 <meta name="viewport" content="width=device-width, initial-scale=1.0">
6 <title>Task List</title>
7 @gale
8 <script src="https://cdn.tailwindcss.com"></script>
9</head>
10<body class="bg-gray-100 min-h-screen py-8">
11
12<div x-data="{ title: '' }"
13 class="max-w-md mx-auto bg-white rounded-lg shadow p-6">
14
15 <h1 class="text-2xl font-bold mb-4">Task List</h1>
16
17 <!-- Add Task Form -->
18 <form @submit.prevent="$postx('/tasks')" class="mb-6">
19 <div class="flex gap-2">
20 <input type="text"
21 x-model="title"
22 placeholder="Add a task..."
23 class="flex-1 px-3 py-2 border rounded focus:outline-none focus:ring-2 focus:ring-blue-500">
24 <button type="submit"
25 x-loading.attr="disabled"
26 x-loading.class="opacity-50"
27 class="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600">
28 Add
29 </button>
30 </div>
31 <p x-message="title" class="text-red-500 text-sm mt-1"></p>
32 </form>
33
34 <!-- Task List -->
35 <ul id="task-list" class="space-y-2">
36 <!-- Tasks will be added here -->
37 </ul>
38
39</div>
40
41</body>
42</html>
Task List
Task List
Breaking Down the View
Let's examine the key Gale features in this template:
The @gale Directive
Loads Alpine.js, the Alpine Morph plugin, and the Gale plugin. It also outputs the CSRF meta tag that Gale uses for secure requests.
The x-data State
Standard Alpine.js. We define title for the form input. Gale sends this entire object with each request.
The $postx() Magic
Gale's HTTP magic. The x suffix means "with CSRF protection". When the form submits, Gale serializes the Alpine state and POSTs it to /tasks.
The x-loading Directive
Shows loading feedback during requests. x-loading.attr="disabled" disables the button, and x-loading.class="opacity-50" adds a visual cue.
The x-message Directive
Displays validation errors from the server. When Laravel validation fails, Gale automatically manages a global messages state and this directive shows the error for the specified field.
Step 3: Handle Adding Tasks
Now add the POST route that validates input and adds tasks. In routes/web.php:
1Route::post('/tasks', function () {
2 // Validate the incoming state
3 $validated = request()->validateState([
4 'title' => 'required|string|min:3|max:255',
5 ]);
6
7 // Generate a unique ID for this task
8 $taskId = uniqid('task-');
9
10 // Build the HTML for the new task
11 $html = '
12 <li id="' . $taskId . '" class="flex items-center gap-2 p-2 bg-gray-50 rounded">
13 <span class="flex-1">' . e($validated['title']) . '</span>
14 <button @click="$postx(\'/tasks/' . $taskId . '/delete\')"
15 class="text-red-500 hover:text-red-700">
16 Delete
17 </button>
18 </li>
19 ';
20
21 return gale()
22 ->state('title', '') // Clear the input
23 ->append('#task-list', $html); // Add the task to the list
24});
Route::post('/tasks', function () {
// Validate the incoming state
$validated = request()->validateState([
'title' => 'required|string|min:3|max:255',
]);
// Generate a unique ID for this task
$taskId = uniqid('task-');
// Build the HTML for the new task
$html = '
What's Happening Here
request()->validateState()
Validates the Alpine state sent from the frontend. If validation fails, Gale automatically returns the error messages to Gale's global messages state, which x-message reads from.
gale()
Returns the GaleResponse builder. This is your interface for sending updates back to the browser via Server-Sent Events.
->state('title', '')
Updates the title property in Alpine's state, clearing the form input.
->append('#task-list', $html)
Appends the new task HTML to the element matching the selector. The browser receives this and Alpine reactively updates the DOM.
Step 4: Handle Deleting Tasks
Add the delete route:
1Route::post('/tasks/{taskId}/delete', function ($taskId) {
2 return gale()->remove('#' . $taskId);
3});
Route::post('/tasks/{taskId}/delete', function ($taskId) {
return gale()->remove('#' . $taskId);
});
The remove() method tells the browser to remove the element matching the CSS selector. Clean and simple.
Step 5: Test It
Visit /tasks in your browser and try:
- Submitting an empty form (see validation errors appear)
- Entering a task shorter than 3 characters
- Adding valid tasks (watch them appear instantly)
- Deleting tasks (watch them disappear)
It works!
How It All Works
Here's the complete request/response cycle when you add a task:
Frontend (Alpine Gale)
-
1
User types in input, Alpine updates
title -
2
Form submits,
$postx()serializes state - 3 POST request sent with JSON body + CSRF
- 6 SSE events received, state & DOM updated
- 7 Alpine reactivity triggers UI refresh
Backend (Laravel Gale)
- 4 Route receives request, validates state
-
5
gale()builds SSE response with updates
The key insight: your Laravel code decides what changes, and Alpine automatically applies them. You never write JavaScript to handle responses—Gale does that for you.
Complete Code
Here's everything together for easy copying:
routes/web.php
1use Illuminate\Support\Facades\Route;
2
3// Display the task list
4Route::get('/tasks', fn () => view('tasks'));
5
6// Add a new task
7Route::post('/tasks', function () {
8 $validated = request()->validateState([
9 'title' => 'required|string|min:3|max:255',
10 ]);
11
12 $taskId = uniqid('task-');
13
14 $html = '
15 <li id="' . $taskId . '" class="flex items-center gap-2 p-2 bg-gray-50 rounded">
16 <span class="flex-1">' . e($validated['title']) . '</span>
17 <button @click="$postx(\'/tasks/' . $taskId . '/delete\')"
18 class="text-red-500 hover:text-red-700">
19 Delete
20 </button>
21 </li>
22 ';
23
24 return gale()
25 ->state('title', '')
26 ->append('#task-list', $html);
27});
28
29// Delete a task
30Route::post('/tasks/{taskId}/delete', function ($taskId) {
31 return gale()->remove('#' . $taskId);
32});
use Illuminate\Support\Facades\Route;
// Display the task list
Route::get('/tasks', fn () => view('tasks'));
// Add a new task
Route::post('/tasks', function () {
$validated = request()->validateState([
'title' => 'required|string|min:3|max:255',
]);
$taskId = uniqid('task-');
$html = '
resources/views/tasks.blade.php
1<!DOCTYPE html>
2<html lang="en">
3<head>
4 <meta charset="UTF-8">
5 <meta name="viewport" content="width=device-width, initial-scale=1.0">
6 <title>Task List</title>
7 @gale
8 <script src="https://cdn.tailwindcss.com"></script>
9</head>
10<body class="bg-gray-100 min-h-screen py-8">
11
12<div x-data="{ title: '' }"
13 class="max-w-md mx-auto bg-white rounded-lg shadow p-6">
14
15 <h1 class="text-2xl font-bold mb-4">Task List</h1>
16
17 <!-- Add Task Form -->
18 <form @submit.prevent="$postx('/tasks')" class="mb-6">
19 <div class="flex gap-2">
20 <input type="text"
21 x-model="title"
22 placeholder="Add a task..."
23 class="flex-1 px-3 py-2 border rounded focus:outline-none focus:ring-2 focus:ring-blue-500">
24 <button type="submit"
25 x-loading.attr="disabled"
26 x-loading.class="opacity-50"
27 class="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600">
28 Add
29 </button>
30 </div>
31 <p x-message="title" class="text-red-500 text-sm mt-1"></p>
32 </form>
33
34 <!-- Task List -->
35 <ul id="task-list" class="space-y-2">
36 <!-- Tasks will be added here -->
37 </ul>
38
39</div>
40
41</body>
42</html>
Task List
Task List
Key Concepts Summary
| Concept | Frontend | Backend |
|---|---|---|
| HTTP Requests | $postx(), $get() |
Standard Laravel routes |
| State Access | Alpine's x-data |
request()->state() |
| State Updates | Automatic from SSE | gale()->state() |
| Validation | x-message directive |
request()->validateState() |
| DOM Updates | Automatic morphing | append(), remove(), view() |
| Loading States | x-loading directive |
N/A (automatic) |
Next Steps
You've learned the fundamental patterns of Gale. Here's where to go deeper:
State Management
Deep dive into RFC 7386 merge semantics, dot notation, and advanced state patterns
DOM Manipulation
Learn all 8 morph modes, Blade fragments, and view transitions
Form Handling
Build complex forms with array validation and file uploads
HTTP Magics
Explore $get, $post, $postx, request options, and retry logic