protonemedia/laravel-splade-core
Splade Core is the engine behind Laravel Splade, letting you build reactive single-page applications using Blade and Vue 3 without writing Vue components. It provides the low-level rendering, routing, and component internals that power the broader Splade ecosystem.
Understand what this package actually is before installing it. protonemedia/laravel-splade-core is not a standalone feature package — it's the engine layer extracted from Splade. It ships the Blade compilation/directive layer, the Vue 3 bridge, and the Composition API composables (useForm, useTable, useToast, useModal, useEventBus) that Splade exposes to <script setup> blocks inside Blade files. In practice you almost never install it directly; you install the umbrella package and it arrives as a transitive dependency.
# Typical path — core comes along for the ride
composer require protonemedia/laravel-splade
# Only if you're deliberately building on the core primitives
composer require protonemedia/laravel-splade-core
Where to look first in the repo:
composer.json — confirms the PHP/Laravel version constraints and whether a JS build step is bundled.src/ — the service provider and the class that registers Blade directives. This is where @splade, @js, and component aliasing are wired up. Anything you'd extend or override lives here.resources/js/ (and package.json) — the entry point that exports the composables your <script setup> blocks import.tests/ — the fastest way to learn the contract for directives and payload shapes, since documentation for the core layer is thin.First use case — a reactive form in a Blade view. This is the 80% case and the reason anyone reaches for Splade:
{{-- resources/views/users/edit.blade.php --}}
<x-splade-form :default="$user" action="{{ route('users.update', $user) }}" method="put">
<x-splade-input name="name" label="Name" />
<x-splade-input name="email" type="email" label="Email" />
<x-splade-submit />
</x-splade-form>
Or, using the Composition API directly, which is what the core package exists to enable:
<script setup>
import { useForm } from '@protonemedia/laravel-splade-core'
const form = useForm({ name: '', email: '' })
const submit = () => form.put('/users/1')
</script>
<template>
<input v-model="form.name" />
<p v-if="form.errors.name">{{ form.errors.name }}</p>
<button :disabled="form.processing" @click="submit">Save</button>
</template>
You get Laravel validation errors back into form.errors without writing a line of fetch/axios glue. That's the value proposition — start there.
Set expectations early: the repo is archived (last release Jan 2024) and has essentially no dependent ecosystem. Treat a version pin as mandatory:
composer require protonemedia/laravel-splade-core:1.4.* --prefer-dist
The dominant pattern is "Laravel owns validation, Vue owns the interaction." Keep your FormRequest exactly as you would in a normal controller — Splade reads the 422 JSON error bag and maps keys onto form fields. Two things make this work reliably:
name must match the validation key exactly, including nested syntax (items.0.name for array fields).public function update(UpdateUserRequest $request, User $user)
{
$user->update($request->validated());
return redirect()
->route('users.edit', $user)
->with('success', 'User updated.');
}
Wire flash data once (in your layout or AppServiceProvider) and consume it via the composable rather than hand-rolling a notification component:
<script setup>
import { useToast } from '@protonemedia/laravel-splade-core'
const toast = useToast()
toast.success(@js(session('success')))
</script>
@js() is the sanctioned way to hand PHP values to the client — it JSON-encodes with escaping. Use it instead of string-interpolating into a JS expression.
For admin screens, useTable is the highest-leverage composable in the package. Instead of paginating in PHP and re-rendering, sorting/searching/paging round-trips to your controller and Splade swaps the rows:
public function index(Request $request)
{
return view('users.index', [
'users' => QueryBuilder::for(User::class)
->allowedFilters(['name', 'email'])
->allowedSorts(['name', 'created_at'])
->paginate(15)
->withQueryString(),
]);
}
The corresponding <x-splade-table> binds to the paginator automatically. The important habit: keep one source of truth for query state (the query string), so a page refresh or a shared URL reproduces the exact view. Splade's partial-reload behaviour depends on this.
Two patterns worth adopting as defaults:
<x-splade-link> for internal navigation — it fetches and swaps only what changed, preserving scroll and component state. Use it on anything list-like or tab-like where a full page load feels heavy.<x-splade-defer> for expensive blocks (dashboards, aggregates, external API calls). The shell renders immediately, the heavy fragment streams in. Wrap the underlying query in a route or __invoke so it's independently addressable and cacheable.For cross-component communication (a sidebar toggling a modal, a table filtering a chart), use the event bus rather than lifting state into a global store:
<script setup>
import { useEventBus } from '@protonemedia/laravel-splade-core'
const emit = useEventBus()
emit('filters-updated', { status: 'active' })
</script>
This keeps Blade partials from silently depending on shared module-level state — the failure mode of that approach is subtle and painful to debug.
v-if. The Blade-rendered markup is the client contract; if the server sends it, the client can act on it.The repo is archived. There is no upstream fix coming for a Vue minor-version incompatibility, an SSR memory leak, or a change in Laravel's Blade compiler internals. Concretely:
vue or vite range will eventually break the build with nobody to file a bug against.npm install can't silently change behaviour under you.php artisan view:cache explicitly on upgrades — it's the canary.<script setup>).<script setup> block is not interpreted at runtime — it must pass through the Splade/Vite pipeline. php artisan view:clear alone won't help; you need npm run dev (or a rebuild) after editing a view's script block.vite build is the #1 cause of "my form is dead." Wire the asset build into your deploy before view:cache.php artisan optimize:clear && npm run build.const { name } = useForm(...) breaks reactivity in templates. Keep the object and use form.name.useForm returns a deep-reactive proxy — mutating nested arrays (form.items.push(...)) works, but reassigning the whole object (form = {...}) does not unless you keep a ref.form.processing and form.errors are read-only in practice. Treat them as derived state; don't assign to them.items.*.name produces keys like items.0.name; your bound name attribute must match byte-for-byte.FormRequest messages() are fine — only the keys matter.{ errors: { field: [msg] } } envelope. Don't assume Splade will normalize it.PUT/PATCH with uploads (_method), and the request must be multipart. The <x-splade-form> component handles this; a hand-rolled useForm + fetch will not. If uploads silently fail, check the request's Content-Type in the network tab first.upload_max_filesize / post_max_size limits — Splade changes nothing there. A 413 looks like a Vue bug but is a php.ini bug.@js output. View-source the rendered page. If PHP data looks mangled, you're interpolating into a JS string somewhere instead of using @js.Date.now(), Math.random(), or auth-dependent branching inside a component. Move that logic server-side or gate it behind onMounted.vendor/ means the next composer install wipes your work. Confirm the publish tags in the service provider before touching anything.resources/js entry after the package's registration — order matters, and it's not obvious from the docs.config/app.php / package discovery determines who wins. Don't run both on the same views.@yourdirective-style helpers that compile to JS.useForm with your own useAppForm that injects CSRF headers, a base URL, or default toast behaviour. This is the cleanest way to add cross-cutting concerns without forking.@js(window.__app = ...) block in the layout) rather than sprinkling @js through views.How can I help you explore Laravel packages today?