File Uploads
Gale provides seamless file uploads through the x-files directive.
When files are selected, HTTP magics automatically switch from JSON to FormData,
enabling standard Laravel file handling without configuration.
Basic File Upload
Add the x-files directive to any file input. The file will be
automatically included when you submit with an HTTP magic.
<form x-data="{ title: '' }"
@submit.prevent="$postx('/upload')">
<input type="text" x-model="title" placeholder="Title">
<!-- x-files marks this for upload -->
<input type="file" x-files="document">
<button type="submit">Upload</button>
</form>
<form x-data="{ title: '' }"
@submit.prevent="$postx('/upload')">
<input type="text" x-model="title" placeholder="Title">
<!-- x-files marks this for upload -->
<input type="file" x-files="document">
<button type="submit">Upload</button>
</form>
// Controller - Standard Laravel file handling
public function upload(Request $request)
{
$request->validate([
'title' => 'required|string',
'document' => 'required|file|max:10240',
]);
$path = $request->file('document')->store('documents');
Document::create([
'title' => $request->input('title'),
'path' => $path,
]);
return gale()
->state('title', '')
->messages(['_success' => 'Document uploaded!']);
}
// Controller - Standard Laravel file handling
public function upload(Request $request)
{
$request->validate([
'title' => 'required|string',
'document' => 'required|file|max:10240',
]);
$path = $request->file('document')->store('documents');
Document::create([
'title' => $request->input('title'),
'path' => $path,
]);
return gale()
->state('title', '')
->messages(['_success' => 'Document uploaded!']);
}
The x-files Directive
The x-files directive marks a file input for inclusion in Gale requests.
The expression becomes the field name.
<!-- Named via expression (recommended) -->
<input type="file" x-files="avatar">
<!-- Falls back to name attribute -->
<input type="file" name="avatar" x-files>
<!-- Multiple files -->
<input type="file" x-files="photos" multiple>
<!-- With accept filter -->
<input type="file" x-files="image" accept="image/*">
<!-- Named via expression (recommended) --> <input type="file" x-files="avatar"> <!-- Falls back to name attribute --> <input type="file" name="avatar" x-files> <!-- Multiple files --> <input type="file" x-files="photos" multiple> <!-- With accept filter --> <input type="file" x-files="image" accept="image/*">
File Magics
Gale provides several magic properties for working with selected files:
| Magic | Description | Returns |
|---|---|---|
$file(name) |
Get single file info | Object or null |
$files(name) |
Get array of file info | Array |
$filePreview(name, index?) |
Get preview URL | String (blob URL) |
$clearFiles(name?) |
Reset file input(s) | void |
$formatBytes(size) |
Format bytes to readable | String ("1.5 MB") |
Getting File Information
<div x-data>
<input type="file" x-files="avatar">
<!-- Show file info when selected -->
<template x-if="$file('avatar')">
<div>
<p>File: <span x-text="$file('avatar').name"></span></p>
<p>Size: <span x-text="$formatBytes($file('avatar').size)"></span></p>
<p>Type: <span x-text="$file('avatar').type"></span></p>
</div>
</template>
</div>
<div x-data>
<input type="file" x-files="avatar">
<!-- Show file info when selected -->
<template x-if="$file('avatar')">
<div>
<p>File: <span x-text="$file('avatar').name"></span></p>
<p>Size: <span x-text="$formatBytes($file('avatar').size)"></span></p>
<p>Type: <span x-text="$file('avatar').type"></span></p>
</div>
</template>
</div>
Image Preview
Use $filePreview() to display image previews before upload:
<div x-data>
<input type="file" x-files="avatar" accept="image/*">
<!-- Live image preview -->
<img
x-show="$filePreview('avatar')"
:src="$filePreview('avatar')"
class="w-32 h-32 object-cover rounded">
<!-- Clear button -->
<button
x-show="$file('avatar')"
@click="$clearFiles('avatar')"
type="button">
Remove
</button>
</div>
<div x-data>
<input type="file" x-files="avatar" accept="image/*">
<!-- Live image preview -->
<img
x-show="$filePreview('avatar')"
:src="$filePreview('avatar')"
class="w-32 h-32 object-cover rounded">
<!-- Clear button -->
<button
x-show="$file('avatar')"
@click="$clearFiles('avatar')"
type="button">
Remove
</button>
</div>
Multiple File Uploads
Add multiple to accept multiple files. Use $files()
to access the array:
<div x-data>
<input type="file" x-files="photos" multiple accept="image/*">
<!-- Count selected files -->
<p x-show="$files('photos').length > 0">
Selected: <span x-text="$files('photos').length"></span> files
</p>
<!-- Preview gallery -->
<div class="grid grid-cols-4 gap-2">
<template x-for="(file, index) in $files('photos')" :key="index">
<div class="relative">
<img :src="$filePreview('photos', index)" class="w-full h-24 object-cover">
<span x-text="$formatBytes(file.size)" class="text-xs"></span>
</div>
</template>
</div>
<button @click="$postx('/upload-photos')">Upload All</button>
</div>
<div x-data>
<input type="file" x-files="photos" multiple accept="image/*">
<!-- Count selected files -->
<p x-show="$files('photos').length > 0">
Selected: <span x-text="$files('photos').length"></span> files
</p>
<!-- Preview gallery -->
<div class="grid grid-cols-4 gap-2">
<template x-for="(file, index) in $files('photos')" :key="index">
<div class="relative">
<img :src="$filePreview('photos', index)" class="w-full h-24 object-cover">
<span x-text="$formatBytes(file.size)" class="text-xs"></span>
</div>
</template>
</div>
<button @click="$postx('/upload-photos')">Upload All</button>
</div>
// Controller - Multiple file handling
public function uploadPhotos(Request $request)
{
$request->validate([
'photos' => 'required|array|max:10',
'photos.*' => 'image|max:5120',
]);
foreach ($request->file('photos') as $photo) {
$path = $photo->store('photos');
Photo::create(['path' => $path]);
}
return gale()->messages(['_success' => 'Photos uploaded!']);
}
// Controller - Multiple file handling
public function uploadPhotos(Request $request)
{
$request->validate([
'photos' => 'required|array|max:10',
'photos.*' => 'image|max:5120',
]);
foreach ($request->file('photos') as $photo) {
$path = $photo->store('photos');
Photo::create(['path' => $path]);
}
return gale()->messages(['_success' => 'Photos uploaded!']);
}
Client-Side Validation
Use modifiers for client-side validation before upload:
<!-- Max file size: 5MB -->
<input type="file" x-files.max-size-5mb="document">
<!-- Max 3 files -->
<input type="file" x-files.max-files-3="photos" multiple>
<!-- Combined: max 5MB each, max 10 files -->
<input type="file" x-files.max-size-5mb.max-files-10="attachments" multiple>
<!-- Max file size: 5MB --> <input type="file" x-files.max-size-5mb="document"> <!-- Max 3 files --> <input type="file" x-files.max-files-3="photos" multiple> <!-- Combined: max 5MB each, max 10 files --> <input type="file" x-files.max-size-5mb.max-files-10="attachments" multiple>
Handling Validation Errors
Listen for the gale:file-error event for validation failures:
<div x-data="{ fileError: null }">
<input
type="file"
x-files.max-size-2mb="document"
:file-error="fileError = $event.detail.message">
<p x-show="fileError" x-text="fileError" class="text-red-500"></p>
</div>
<div x-data="{ fileError: null }">
<input
type="file"
x-files.max-size-2mb="document"
:file-error="fileError = $event.detail.message">
<p x-show="fileError" x-text="fileError" class="text-red-500"></p>
</div>
Upload Progress
Track upload progress using the upload state magics:
<div x-data>
<input type="file" x-files="video">
<!-- Progress bar during upload -->
<div x-show="$uploading" class="progress-container">
<div
class="progress-bar"
:style="{ width: $uploadProgress + '%' }"></div>
<span x-text="$uploadProgress + '%'"></span>
</div>
<!-- Upload error -->
<p x-show="$uploadError" x-text="$uploadError" class="text-red-500"></p>
<button
@click="$postx('/upload-video')"
:disabled="$uploading">
<span x-show="!$uploading">Upload</span>
<span x-show="$uploading">Uploading...</span>
</button>
</div>
<div x-data>
<input type="file" x-files="video">
<!-- Progress bar during upload -->
<div x-show="$uploading" class="progress-container">
<div
class="progress-bar"
:style="{ width: $uploadProgress + '%' }"></div>
<span x-text="$uploadProgress + '%'"></span>
</div>
<!-- Upload error -->
<p x-show="$uploadError" x-text="$uploadError" class="text-red-500"></p>
<button
@click="$postx('/upload-video')"
:disabled="$uploading">
<span x-show="!$uploading">Upload</span>
<span x-show="$uploading">Uploading...</span>
</button>
</div>
Complete Upload Example
<form
x-data="{ title: '', description: '', _fileError: null }"
@submit.prevent="$postx('/documents')"
class="space-y-4">
<!-- Success message -->
<div x-message="_success" class="bg-green-50 p-3 rounded"></div>
<!-- Title -->
<div>
<label>Title</label>
<input type="text" x-model="title" class="w-full border p-2">
<span x-message="title" class="text-red-500"></span>
</div>
<!-- Description -->
<div>
<label>Description</label>
<textarea x-model="description" class="w-full border p-2"></textarea>
</div>
<!-- File input -->
<div>
<label>Document (PDF, max 10MB)</label>
<input
type="file"
x-files.max-size-10mb="document"
accept=".pdf"
:file-error="_fileError = $event.detail.message"
:file-change="_fileError = null">
<span x-show="_fileError" x-text="_fileError" class="text-red-500"></span>
<span x-message="document" class="text-red-500"></span>
</div>
<!-- File info -->
<div x-show="$file('document')" class="bg-gray-50 p-3 rounded">
<p><strong>File:</strong> <span x-text="$file('document')?.name"></span></p>
<p><strong>Size:</strong> <span x-text="$formatBytes($file('document')?.size || 0)"></span></p>
<button type="button" @click="$clearFiles('document')" class="text-sm text-red-500">Remove</button>
</div>
<!-- Upload progress -->
<div x-show="$uploading" class="w-full bg-gray-200 rounded h-2">
<div
class="bg-blue-500 h-2 rounded"
:style="{ width: $uploadProgress + '%' }"></div>
</div>
<!-- Submit -->
<button
type="submit"
:disabled="$uploading"
class="bg-blue-500 text-white px-4 py-2 rounded">
<span x-show="!$uploading">Upload Document</span>
<span x-show="$uploading">Uploading... <span x-text="$uploadProgress + '%'"></span></span>
</button>
</form>
<form
x-data="{ title: '', description: '', _fileError: null }"
@submit.prevent="$postx('/documents')"
class="space-y-4">
<!-- Success message -->
<div x-message="_success" class="bg-green-50 p-3 rounded"></div>
<!-- Title -->
<div>
<label>Title</label>
<input type="text" x-model="title" class="w-full border p-2">
<span x-message="title" class="text-red-500"></span>
</div>
<!-- Description -->
<div>
<label>Description</label>
<textarea x-model="description" class="w-full border p-2"></textarea>
</div>
<!-- File input -->
<div>
<label>Document (PDF, max 10MB)</label>
<input
type="file"
x-files.max-size-10mb="document"
accept=".pdf"
:file-error="_fileError = $event.detail.message"
:file-change="_fileError = null">
<span x-show="_fileError" x-text="_fileError" class="text-red-500"></span>
<span x-message="document" class="text-red-500"></span>
</div>
<!-- File info -->
<div x-show="$file('document')" class="bg-gray-50 p-3 rounded">
<p><strong>File:</strong> <span x-text="$file('document')?.name"></span></p>
<p><strong>Size:</strong> <span x-text="$formatBytes($file('document')?.size || 0)"></span></p>
<button type="button" @click="$clearFiles('document')" class="text-sm text-red-500">Remove</button>
</div>
<!-- Upload progress -->
<div x-show="$uploading" class="w-full bg-gray-200 rounded h-2">
<div
class="bg-blue-500 h-2 rounded"
:style="{ width: $uploadProgress + '%' }"></div>
</div>
<!-- Submit -->
<button
type="submit"
:disabled="$uploading"
class="bg-blue-500 text-white px-4 py-2 rounded">
<span x-show="!$uploading">Upload Document</span>
<span x-show="$uploading">Uploading... <span x-text="$uploadProgress + '%'"></span></span>
</button>
</form>