|
| 1 | +# Livewire Media Uploader |
| 2 | + |
| 3 | +Reusable **Livewire v3** media uploader with **TailwindCSS** UI, Alpine-powered overlays, and first-class integration with **Spatie Laravel Media Library**. Ships with a **publishable Blade view** so each app can theme it as needed. |
| 4 | + |
| 5 | +--- |
| 6 | + |
| 7 | +## Table of Contents |
| 8 | + |
| 9 | +- [Features](#features) |
| 10 | +- [Requirements](#requirements) |
| 11 | +- [Installation](#installation) |
| 12 | +- [Publishing Assets](#publishing-assets) |
| 13 | +- [Quick Start](#quick-start) |
| 14 | +- [Usage Examples](#usage-examples) |
| 15 | +- [Configuration](#configuration) |
| 16 | +- [Props](#props) |
| 17 | +- [Events](#events) |
| 18 | +- [Model Setup (Spatie Media Library)](#model-setup-spatie-media-library) |
| 19 | +- [Overlays & UX Notes](#overlays--ux-notes) |
| 20 | +- [Troubleshooting](#troubleshooting) |
| 21 | +- [Roadmap](#roadmap) |
| 22 | +- [License](#license) |
| 23 | + |
| 24 | +--- |
| 25 | + |
| 26 | +## Features |
| 27 | + |
| 28 | +- ✅ Livewire v3 component with Tailwind-only Blade (no UI dependency) |
| 29 | +- ✅ Spatie Media Library integration (attach, list, edit meta, delete) |
| 30 | +- ✅ **Publishable view** for per-project customization |
| 31 | +- ✅ Drag & drop uploads + progress bar |
| 32 | +- ✅ Inline edit of **caption / description / order** |
| 33 | +- ✅ Name-conflict strategies: **rename | replace | skip | allow** |
| 34 | +- ✅ Optional **exact duplicate** detection via SHA-256 |
| 35 | +- ✅ Collection → preset mapping (auto `accept` attribute) |
| 36 | +- ✅ Image preview **overlay** + delete confirmation **modal** |
| 37 | +- ✅ Works with: |
| 38 | + - Saved model instance (`:for="$model"`) |
| 39 | + - String model + id (`model="user" :id="1"`) |
| 40 | + - FQCN, morph map alias, or dotted paths with custom namespaces |
| 41 | + - Local alias map |
| 42 | + |
| 43 | +--- |
| 44 | + |
| 45 | +## Requirements |
| 46 | + |
| 47 | +- PHP **8.1+** |
| 48 | +- Laravel **10.x | 11.x | 12.x** |
| 49 | +- Livewire **^3.0** |
| 50 | +- spatie/laravel-medialibrary **^10.12** |
| 51 | +- TailwindCSS (optional but recommended for the default view) |
| 52 | +- Alpine.js (used by overlays/progress; see [Overlays & UX Notes](#overlays--ux-notes)) |
| 53 | + |
| 54 | +--- |
| 55 | + |
| 56 | +## Installation |
| 57 | + |
| 58 | +```bash |
| 59 | +composer require codebyray/livewire-media-uploader |
| 60 | +``` |
| 61 | + |
| 62 | +Auto-discovery will register the service provider. If you disable discovery, add: |
| 63 | + |
| 64 | +```php |
| 65 | +// config/app.php |
| 66 | +'providers' => [ |
| 67 | + // ... |
| 68 | + Codebyray\LivewireMediaUploader\MediaUploaderServiceProvider::class, |
| 69 | +], |
| 70 | +``` |
| 71 | + |
| 72 | +The component is registered under **both** aliases: |
| 73 | + |
| 74 | +- `<livewire:media-uploader ... />` |
| 75 | +- `<livewire:media.media-uploader ... />` |
| 76 | + |
| 77 | +--- |
| 78 | + |
| 79 | +## Publishing Assets |
| 80 | + |
| 81 | +**Config:** |
| 82 | +```bash |
| 83 | +php artisan vendor:publish --tag=media-uploader-config |
| 84 | +``` |
| 85 | + |
| 86 | +**Views:** |
| 87 | +```bash |
| 88 | +php artisan vendor:publish --tag=media-uploader-views |
| 89 | +``` |
| 90 | + |
| 91 | +After publishing, customize the Blade at: |
| 92 | +``` |
| 93 | +resources/views/vendor/media-uploader/livewire/media-uploader.blade.php |
| 94 | +``` |
| 95 | + |
| 96 | +--- |
| 97 | + |
| 98 | +## Quick Start |
| 99 | + |
| 100 | +1) Ensure your target Eloquent model implements `Spatie\MediaLibrary\HasMedia` and is **saved**. |
| 101 | + |
| 102 | +2) Include Livewire & Alpine (usually in your app layout): |
| 103 | + |
| 104 | +```html |
| 105 | +@livewireStyles |
| 106 | +<style>[x-cloak]{ display:none !important; }</style> |
| 107 | +@livewireScripts |
| 108 | +``` |
| 109 | + |
| 110 | +3) Drop the component into your Blade: |
| 111 | + |
| 112 | +```html |
| 113 | +<livewire:media-uploader :for="$user" collection="avatars" preset="images" /> |
| 114 | +``` |
| 115 | + |
| 116 | +--- |
| 117 | + |
| 118 | +## Usage Examples |
| 119 | + |
| 120 | +**1) Pass a saved model instance** |
| 121 | +```html |
| 122 | +<livewire:media-uploader :for="$user" collection="avatars" preset="images" /> |
| 123 | +``` |
| 124 | + |
| 125 | +**2) Short string model + id** |
| 126 | +```html |
| 127 | +<livewire:media-uploader model="user" :id="$user->id" collection="images" preset="images" /> |
| 128 | +``` |
| 129 | + |
| 130 | +**3) Morph map alias** |
| 131 | +```html |
| 132 | +<livewire:media-uploader model="users" :id="$user->id" collection="profile" preset="images" /> |
| 133 | +``` |
| 134 | + |
| 135 | +**4) FQCN** |
| 136 | +```html |
| 137 | +<livewire:media-uploader model="\App\Models\User" :id="$user->id" collection="documents" /> |
| 138 | +``` |
| 139 | + |
| 140 | +**5) Dotted path + custom namespaces** |
| 141 | +```html |
| 142 | +<livewire:media-uploader |
| 143 | + model="crm.contact" |
| 144 | + :id="$contactId" |
| 145 | + :namespaces="['App\\Domain\\Crm\\Models', 'App\\Models']" |
| 146 | + collection="images" |
| 147 | + preset="images" |
| 148 | +/> |
| 149 | +``` |
| 150 | + |
| 151 | +**6) Local aliases (per-instance)** |
| 152 | +```html |
| 153 | +<livewire:media-uploader |
| 154 | + model="profile" |
| 155 | + :id="$user->id" |
| 156 | + :aliases="['profile' => \App\Models\User::class]" |
| 157 | + collection="gallery" |
| 158 | +/> |
| 159 | +``` |
| 160 | + |
| 161 | +**7) Single-file mode + hide list** |
| 162 | +```html |
| 163 | +<livewire:media-uploader |
| 164 | + :for="$user" |
| 165 | + collection="avatar" |
| 166 | + :multiple="false" |
| 167 | + :showList="false" |
| 168 | + preset="images" |
| 169 | +/> |
| 170 | +``` |
| 171 | + |
| 172 | +**8) Name conflict strategies** |
| 173 | +```html |
| 174 | +<livewire:media-uploader :for="$user" collection="files" onNameConflict="rename" /> |
| 175 | +<livewire:media-uploader :for="$user" collection="files" onNameConflict="replace" /> |
| 176 | +<livewire:media-uploader :for="$user" collection="files" onNameConflict="skip" /> |
| 177 | +<livewire:media-uploader :for="$user" collection="files" onNameConflict="allow" /> |
| 178 | +``` |
| 179 | + |
| 180 | +**9) Duplicate detection by SHA-256** |
| 181 | +```html |
| 182 | +<livewire:media-uploader :for="$user" collection="images" preset="images" :skipExactDuplicates="true" /> |
| 183 | +``` |
| 184 | + |
| 185 | +**10) Restrict types/mimes/max size manually** |
| 186 | +```html |
| 187 | +<livewire:media-uploader |
| 188 | + :for="$user" |
| 189 | + collection="documents" |
| 190 | + :accept="'.pdf,.doc,.docx,application/pdf,application/msword,application/vnd.openxmlformats-officedocument.wordprocessingml.document'" |
| 191 | + :allowedTypes="['pdf','doc','docx']" |
| 192 | + :allowedMimes="['application/pdf','application/msword','application/vnd.openxmlformats-officedocument.wordprocessingml.document']" |
| 193 | + :maxSizeKb="5120" |
| 194 | +/> |
| 195 | +``` |
| 196 | + |
| 197 | +--- |
| 198 | + |
| 199 | +## Configuration |
| 200 | + |
| 201 | +The package merges `config/media-uploader.php`: |
| 202 | + |
| 203 | +- `accept_from_config` — if `true`, auto-fills `<input accept>` from the selected preset |
| 204 | +- `collections` — map collection name → preset key |
| 205 | +- `presets.*.types` — extensions (comma-separated) |
| 206 | +- `presets.*.mimes` — MIME types (comma-separated) |
| 207 | +- `presets.*.max_kb` — max file size per file in KB |
| 208 | + |
| 209 | +Example: |
| 210 | +```php |
| 211 | +'collections' => [ |
| 212 | + 'avatars' => 'images', |
| 213 | + 'images' => 'images', |
| 214 | + 'attachments' => 'docs', |
| 215 | +], |
| 216 | +``` |
| 217 | + |
| 218 | +The component decides the active preset in this order: |
| 219 | +1. Explicit `$preset` prop |
| 220 | +2. Mapping from `collections` |
| 221 | +3. Fallback to `default` |
| 222 | + |
| 223 | +--- |
| 224 | + |
| 225 | +## Props |
| 226 | + |
| 227 | +| Prop | Type | Default | Description | |
| 228 | +|---|---|---|---| |
| 229 | +| `for` | `Model` | — | Saved Eloquent model instance implementing `HasMedia`. | |
| 230 | +| `model` | `string` | — | Model resolver: alias, FQCN, morph alias, or dotted path. | |
| 231 | +| `id` | `int|string` | — | Target model id (used with `model`). | |
| 232 | +| `collection` | `string` | `images` | Media collection name. | |
| 233 | +| `disk` | `?string` | `null` | Storage disk (e.g. `s3`). | |
| 234 | +| `multiple` | `bool` | `true` | Toggle multi-file input. | |
| 235 | +| `accept` | `?string` | `null` | `<input accept>` override (otherwise may be auto from config). | |
| 236 | +| `showList` | `bool` | `true` | Show the attached media list. | |
| 237 | +| `maxSizeKb` | `int` | `500` (overridden to preset’s `max_kb` if empty) | Max file size (KB). | |
| 238 | +| `preset` | `?string` | `null` | Choose a preset (`images`, `docs`, `videos`, `default`, etc.). | |
| 239 | +| `allowedTypes` | `array` | `[]` | Extensions filter (e.g. `['jpg','png']`). | |
| 240 | +| `allowedMimes` | `array` | `[]` | MIME filter (e.g. `['image/jpeg']`). | |
| 241 | +| `onNameConflict` | `string` | `rename` | Strategy: `rename` \| `replace` \| `skip` \| `allow`. | |
| 242 | +| `skipExactDuplicates` | `bool` | `false` | Uses SHA-256 stored in `custom_properties->sha256`. | |
| 243 | +| `namespaces` | `array` | `['App\\Models']` | Namespaces for dotted-path resolution. | |
| 244 | +| `aliases` | `array` | `[]` | Local alias map, e.g. `['profile' => \App\Models\User::class]`. | |
| 245 | +| `attachedFilesTitle` | `string` | `"Current gallery"` | Heading text in the list card. | |
| 246 | + |
| 247 | +--- |
| 248 | + |
| 249 | +## Events |
| 250 | + |
| 251 | +The component dispatches browser events you can listen for: |
| 252 | + |
| 253 | +- `media-uploaded` — after an upload completes |
| 254 | +- `media-deleted` — after a deletion (`detail.id` contains the Media ID) |
| 255 | +- `media-meta-updated` — after saving inline metadata |
| 256 | + |
| 257 | +Example: |
| 258 | +```html |
| 259 | +<div |
| 260 | + x-data |
| 261 | + x-on:media-uploaded.window="console.log('uploaded!')" |
| 262 | + x-on:media-deleted.window="console.log('deleted', $event.detail?.id)" |
| 263 | +> |
| 264 | + <livewire:media-uploader :for="$user" collection="images" preset="images" /> |
| 265 | +</div> |
| 266 | +``` |
| 267 | + |
| 268 | +--- |
| 269 | + |
| 270 | +## Model Setup (Spatie Media Library) |
| 271 | + |
| 272 | +Your model must implement `HasMedia` and be **saved** before attaching media. |
| 273 | + |
| 274 | +```php |
| 275 | +use Spatie\MediaLibrary\HasMedia; |
| 276 | +use Spatie\MediaLibrary\InteractsWithMedia; |
| 277 | + |
| 278 | +class User extends Model implements HasMedia |
| 279 | +{ |
| 280 | + use InteractsWithMedia; |
| 281 | + |
| 282 | + public function registerMediaCollections(): void |
| 283 | + { |
| 284 | + $this->addMediaCollection('images'); |
| 285 | + $this->addMediaCollection('avatars'); |
| 286 | + } |
| 287 | + |
| 288 | + // Optional thumbnail conversion for the list |
| 289 | + public function registerMediaConversions(\Spatie\MediaLibrary\MediaCollections\Models\Media $media = null): void |
| 290 | + { |
| 291 | + $this->addMediaConversion('thumb') |
| 292 | + ->fit('contain', 256, 256) |
| 293 | + ->nonQueued(); |
| 294 | + } |
| 295 | +} |
| 296 | +``` |
| 297 | + |
| 298 | +> The list view tries `getUrl('thumb')` and falls back to `getUrl()` if no conversion is available. |
| 299 | +
|
| 300 | +--- |
| 301 | + |
| 302 | +## Overlays & UX Notes |
| 303 | + |
| 304 | +- **Image Preview Overlay** (lightbox): toggled by `x-show="preview.open"`. |
| 305 | +- **Delete Confirmation Modal**: toggled by `$wire.confirmingDeleteId !== null`. |
| 306 | +- Add once in your layout to prevent flash-of-overlay: |
| 307 | + ```html |
| 308 | + <style>[x-cloak]{ display:none !important; }</style> |
| 309 | + ``` |
| 310 | +- Z-index defaults: preview `z-[60]`, delete modal `z-50`. Adjust to your stack if you have higher layers. |
| 311 | + |
| 312 | +--- |
| 313 | + |
| 314 | +## Troubleshooting |
| 315 | + |
| 316 | +- **“Target model must be saved…”** |
| 317 | + Ensure the model exists in DB (`$model->exists === true`) before rendering the component. |
| 318 | + |
| 319 | +- **“must implement Spatie\MediaLibrary\HasMedia”** |
| 320 | + Add `implements HasMedia` + `InteractsWithMedia` to your model. |
| 321 | + |
| 322 | +- **Unknown model class/alias** |
| 323 | + If using `model="something"` + `:id`, make sure: |
| 324 | + - It’s a valid FQCN, morph alias, or maps via dotted path within `namespaces`, or |
| 325 | + - You passed a local alias via `:aliases="['something' => \App\Models\YourModel::class]`. |
| 326 | + |
| 327 | +- **`accept` not applied** |
| 328 | + Set `accept_from_config=true` and ensure your preset has `types`/`mimes`. Or override via `accept` prop. |
| 329 | + |
| 330 | +- **No thumbnails** |
| 331 | + Add a `thumb` conversion (see [Model Setup](#model-setup-spatie-media-library)). |
| 332 | + |
| 333 | +--- |
| 334 | + |
| 335 | +## Roadmap |
| 336 | + |
| 337 | +- Drag-to-reorder (update `order_column`) |
| 338 | +- Optional queued conversions hints |
| 339 | + |
| 340 | +PRs welcome! |
| 341 | + |
| 342 | +--- |
| 343 | + |
| 344 | +## License |
| 345 | + |
| 346 | +**MIT** © Ray Cuzzart II |
| 347 | + |
| 348 | +--- |
| 349 | + |
| 350 | +**Component aliases:** `media-uploader` and `media.media-uploader` |
| 351 | +**View namespace:** `media-uploader::livewire.media-uploader` |
0 commit comments