React and Svelte components
The three upload components also come for React (18 or later) and Svelte 5: Dropzone, DropzoneSingle and Uploader. They render the same markup as the Livewire ones, so laracrate.css, the themes and the appearance props apply as they are, and they put the files in the same page queue (window.Laracrate.uploads), so an upload keeps going when the component unmounts on an Inertia or SPA navigation.
<Uploader target="avatar" layout="portrait" rounded="full" />
<Dropzone target="venue.gallery" params={{ venue: venue.id }} />
Setup
They ship as source in resources/js and your app's Vite builds them, with the React or Svelte plugin it already has. There is no npm package. An alias keeps the imports short:
// vite.config.js import path from 'node:path'; export default defineConfig({ resolve: { alias: { '@laracrate': path.resolve(__dirname, 'vendor/edulazaro/laracrate/resources/js') }, }, });
/* resources/css/app.css */ @import '../../vendor/edulazaro/laracrate/resources/css/laracrate.css';
import { Dropzone, DropzoneSingle, Uploader } from '@laracrate/react/index.js'; // React import { Dropzone, DropzoneSingle, Uploader } from '@laracrate/svelte/index.js'; // Svelte
The endpoints take the CSRF token from <meta name="csrf-token">, as the Livewire components do. If you changed a route prefix, tell the components once with configure({ signUrl, confirmUrl, registerUrl, targetUrl, removeUrl, statusUrl }), exported from the same index.js.
Upload targets
A Livewire component builds its own upload grant on the server. These run in the browser, so the app declares where they may upload, and who may, once, in routes/uploads.php, the way it declares its broadcast channels:
// routes/uploads.php use App\Models\User; use App\Models\Venue; use EduLazaro\Laracrate\Uploads\UploadGrant; use EduLazaro\Laracrate\Uploads\UploadTargets; UploadTargets::define('avatar', fn (User $user) => UploadGrant::for($user, 'avatar', replace: true)); UploadTargets::define('venue.gallery', fn (User $user, Venue $venue) => $user->can('update', $venue) ? UploadGrant::for($venue, 'gallery') : null); UploadTargets::define('venue.cover', fn (User $user, Venue $venue) => $user->can('update', $venue) ? UploadGrant::for($venue, 'cover', replace: true) : null);
The callback gets the signed in user, then the component's params by name: a model by its route key (a slug if the model routes by slug), a scalar cast to its type. It returns a grant, or null to refuse. The params come from the browser: the callback is where the app checks that this user may upload there, as a channel checks who may listen. A param that does not resolve, a guest when the first argument is not nullable (?User $user), or a user of another class refuses the target too.
The file is read the first time a component asks for a target (laracrate.uploads.targets); a service provider can call UploadTargets::define() too. The endpoint lives in the uploads route group, so it takes its middleware (auth by default).
When it mounts, a component reads what to show, GET /laracrate/uploads/targets/{target}?params[venue]=12, with no grant yet:
- the collection's rules: the mime types for the file picker, the extensions and the largest size for the hint, and the kind of file (
image,video,audio,documentormixed) for the icon. The server checks them again on every upload; - for a target that replaces its file (
replace: true), the current file; - the target's scope, to show again the uploads still running for it.
The callback runs there too, so a user it refuses sees why when the component mounts, not after picking a file. The grant is asked for only when the user picks or drops files (or presses Upload in a deferred one), POST /laracrate/uploads/targets/{target}, and used right away: no grant goes out to whoever only looks at the page, and none is ever old. Meanwhile the files wait in the queue with their spinner.
From there it does the rest: it shows the file just uploaded (the upload endpoints answer it whole, as File::toComponentArray()), polls GET /laracrate/files/{slug}/status while it is processed, and its delete button removes it with POST /laracrate/uploads/remove. Only a grant that replaces can remove, and only the current file of its target: that grant deletes it anyway with its next upload.
Dropzone
Several files, as a grid of tiles or a list of rows.
<Dropzone target="case.documents" params={{ case: caseId }} layout="list" maxFiles={10}
onUploaded={(file) => ...} onBatchCompleted={({ ok, error }) => ...} />
DropzoneSingle
One file in a card: the drop area, or the current file to replace, view or remove.
<DropzoneSingle target="venue.cover" params={{ venue: venue.id }} />
Uploader
One file shown big, an avatar or a logo, in a row or portrait layout. rounded rounds the preview, not the card.
<Uploader target="avatar" layout="portrait" rounded="full" onuploaded={(file) => (me.avatar = file.preview)} />
Props
Svelte takes the same props, with the callbacks in lower case (onuploaded, onbatchcompleted) and class instead of className.
| Prop | Components | Default | |
|---|---|---|---|
target, params |
all | the upload target and its params | |
deferred |
all | false |
stage the files until the Upload button, or startBatch() |
hideActions |
Dropzone, DropzoneSingle |
false |
no Upload and Cancel buttons: the page calls startBatch() |
multiple |
Dropzone |
true |
|
maxFiles |
Dropzone |
how many files it takes; it closes once it holds them | |
layout |
Dropzone, Uploader |
grid, row |
grid or list; row or portrait |
persistQueue |
Dropzone |
false |
keep the uploaded files listed |
removable |
DropzoneSingle, Uploader |
true |
the delete button |
pollMs |
DropzoneSingle, Uploader |
1500 |
how often a file being processed is asked about |
accept, maxSizeKb |
all | the target's | to show other rules than the collection's |
extensions |
DropzoneSingle |
the target's | shown in the hint |
kind |
DropzoneSingle, Uploader |
the target's | what the collection takes, for the icon and the preview |
icon |
Dropzone, DropzoneSingle |
the drop area's icon: upload, image, video, audio, document, file |
|
locale, labels |
all | en |
en or es, and any text of your own: labels={{ upload: 'Add photos' }} |
theme, rounded, bordered, shadow, size, className, style |
all | as in Themes and customising |
| Callback | |
|---|---|
onUploaded(file, item) |
once per saved file, as File::toComponentArray() plus collection, contextKey and scope |
onRejected(item) |
a file the server refused, with its reason in item.error |
onBatchCompleted({ ok, error }) |
when a batch ends |
onMaxFiles({ max, rejected }) |
Dropzone: files left out for maxFiles |
onRemoved(file) |
DropzoneSingle, Uploader: the current file, once removed |
A ref in React, or bind:this in Svelte, gives startBatch() and clear().
What runs after an upload no matter what (attach the file, start a job) belongs on the server, as with the Livewire components, not in onUploaded.
Without a target
An app can also issue the grant itself and pass it as grant: { grant, scope }, or a function that takes the files picked and returns them (or a promise of them). Then it also passes the current file as file, File::toComponentArray() or its own JSON with url, name and size (and preview, type, processing_status, slug to be polled):
$grant = UploadGrant::for($user, 'avatar', replace: true); return Inertia::render('Profile/Edit', [ 'avatarGrant' => ['grant' => $grant->token(), 'scope' => $grant->scope()], 'avatar' => $user->file('avatar')?->toComponentArray(), ]);
<Uploader grant={avatarGrant} file={avatar} />
Such a grant lives 24 hours. file also works with a target, to show another file than the target's (null for none).