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>
// 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/*">

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>

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>

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>
// 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>

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>

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>

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>

On this page