ivanfuhr/bladex

Extend Laravel Blade with HTTP-aware components and server-driven interactions.

Maintainers

Package info

github.com/ivanfuhr/bladex

pkg:composer/ivanfuhr/bladex

Transparency log

Fund package maintenance!

ivanfuhr

Statistics

Installs: 1

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 1

dev-main 2026-08-06 03:08 UTC

This package is auto-updated.

Last update: 2026-08-06 03:09:34 UTC


README

BladeX

Packagist PHP from Packagist Laravel versions GitHub Workflow Status (main) Total Downloads

Extend Laravel Blade with HTTP-aware components and server-driven interactions.

Installation

You can install the package via Composer:

composer require ivanfuhr/bladex

After installing or updating the package (especially from a path repository), run composer dump-autoload in your application.

IDE support (macros on response())

BladeX registers refresh, remove, navigate, and related methods as runtime macros on Laravel's response factory. Static analysis does not see them unless you add the package IDE stub:

VS Code / Cursor (Intelephense) — in your app's .vscode/settings.json:

{
    "intelephense.environment.includePaths": [
        "vendor/ivanfuhr/bladex/ide"
    ]
}

PHPStorm — add vendor/ivanfuhr/bladex/ide under Settings → PHP → Include Path, or use the Laravel Idea plugin. When developing BladeX from a path repository, the repo root .phpstorm.meta.php and .vscode/settings.json already include the local ide/ folder.

Then restart the PHP language server. Chained calls such as response()->remove($component) should resolve to BladeXResponseBuilder.

You may publish all of the package's resources at once:

php artisan vendor:publish --tag="bladex"

Or, you may publish each resource individually:

Publishing the Configuration File

php artisan vendor:publish --tag="bladex-config"

Publishing and Running the Migrations

php artisan vendor:publish --tag="bladex-migrations"
php artisan migrate

Publishing the Views

php artisan vendor:publish --tag="bladex-views"

Publishing the Translations

php artisan vendor:publish --tag="bladex-lang"

Publishing the Public Assets

php artisan vendor:publish --tag="bladex-assets"

Usage

Include the scripts directive in your layout before </body>:

@bladexScripts

No asset publishing is required. BladeX serves bladex.js from the /bladex/bladex.js route, similar to how Livewire serves its runtime script. The ?v= query string is a short hash of that file and changes when the bundle changes.

To point at a different URL (for example a published CDN asset), pass the url option:

@bladexScripts(['url' => asset('vendor/bladex/bladex.js')])

In your application JavaScript, resolve a BladeX component root from a DOM node or identifier:

const component = Bladex.find(document.querySelector('button'));

const alert = Bladex.find('ui.alert');

Bladex.find() returns { element, identifier } when a [data-component-identifier] root is found, or null otherwise.

Operations

Return BladeX operations from a route or controller with response()->refresh(), response()->replace(), and the other operation macros on Laravel's response factory. Pass optional root-level JSON keys with response()->with([...]) before chaining (reserved keys operations and errors are ignored). Each operation targets a component by the identifier() of the Component instance you pass in.

use App\View\Components\RandomSentence;

return response()->refresh(new RandomSentence());

refresh re-renders the given component and updates the existing root with the same resolvedIdentifier() on the page.

use App\View\Components\LoadingSpinner;
use App\View\Components\RandomSentence;

return response()->replace(new LoadingSpinner(), new RandomSentence());

replace finds the root for $from and swaps it with the HTML rendered from $to. The DOM will then expose the identifier of $to.

use App\View\Components\OldBanner;

return response()->remove(new OldBanner());

remove deletes the root that matches the given component’s resolvedIdentifier().

use App\View\Components\ListContainer;
use App\View\Components\ListItem;

return response()->append(new ListContainer(), new ListItem($id));

append inserts the rendered HTML of $content as the last child inside the root of $into.

use App\View\Components\ListContainer;
use App\View\Components\ListItem;

return response()->prepend(new ListContainer(), new ListItem($id));

prepend inserts the rendered HTML as the first child inside the root of $into.

return response()->navigate(route('items.index'));

navigate sends a client-side redirect operation (location.assign()). Operations run in order; put navigate last so earlier DOM updates are not skipped.

Use when and unless to queue operations conditionally without breaking the chain:

return response()->with()
    ->remove(new TodoItem($todo))
    ->when(Todo::query()->count() === 0, fn ($bx) => $bx
        ->append(new TodoList, new TodoEmptyState));

The response is JSON with an operations array and an X-BladeX: true header. When validation errors are present, the payload may also include an errors list ([{ "name": "field", "messages": ["..."] }]) from the session default bag and/or failed validation (see include_session_errors in config).

return response()
    ->refresh(new OrderForm($order))
    ->status(422);

Note: ->withErrors() is optional — failed Form Requests on JSON requests are handled automatically.

HTTP status and response customization

Set the HTTP status with response()->status($code) or ->status($code) on the builder (same idea as the status argument to response()->json(..., $status)):

return response()
    ->status(422)
    ->refresh(new OrderForm($order))
    ->withErrors($validator);

For headers, cookies, or any other JsonResponse API, use usingResponse() — BladeX does not reimplement response():

use Ivanfuhr\BladeX\Http\BladeXJsonResponse;

return response()
    ->refresh(new OrderForm($order))
    ->status(422)
    ->usingResponse(fn (BladeXJsonResponse $response) => $response
        ->header('X-Request-Id', $requestId)
        ->cookie('flash', 'saved', 60));

The JSON body stays { "operations": [...] } and may include validation data when applicable:

  • errors — list of { "name": "title", "messages": ["The message."] }

The fetch proxy applies operations whenever X-BladeX: true is present, even if the status is not 2xx — your JavaScript can still branch on response.ok for validation or error handling.

For declarative <form data-fetch data-method="post|put|..."> requests, BladeX dispatches a validation-failed custom event on each matching form control when the response includes validation errors (after operations run). The event bubbles and includes detail.field (Laravel error key), detail.messages, detail.control, and detail.form. Successful responses (response.ok) dispatch validation-cleared on each control in the form with detail.reason set to success; a new submit dispatches the same event with reason submit. Listen on inputs or use event delegation on the form or document:

document.addEventListener('validation-failed', (event) => {
    const { control, messages } = event.detail;

    control.setAttribute('aria-invalid', 'true');
    // show messages[0], messages.join(' '), etc.
});

document.addEventListener('validation-cleared', (event) => {
    event.detail.control.removeAttribute('aria-invalid');
});

You can also call Bladex.dispatchValidationFailed(form, errors) manually or use Bladex.normalizeErrors(payload) after your own fetch calls.

Failed Form Request validation on JSON / BladeX requests is converted automatically to the same payload shape (operations: [], errors, X-BladeX: true) — you do not need ->withErrors() in the controller. Disable with bladex.treat_json_validation_as_bladex or use non-JSON requests for Laravel's default validation JSON.

Automatic fetch handling

@bladexScripts installs a fetch proxy. When a response includes the X-BladeX: true header, BladeX applies the operations automatically — you do not need to call Bladex.apply() yourself.

Put a CSRF meta tag in your layout (Laravel’s default app layout already does):

<meta name="csrf-token" content="{{ csrf_token() }}">

Then a normal fetch is enough:

fetch('/your-endpoint', { method: 'POST' });

The proxy also sets Accept: application/json, X-Requested-With: XMLHttpRequest, and X-CSRF-TOKEN (from the meta tag) on mutating requests when those headers are missing.

To disable the proxy (for example if you manage fetch yourself), pass:

@bladexScripts(['fetchProxy' => false])

By default, refresh and replace reconcile the matched component root with server HTML using DOM morphing (preserves focus and local state when possible). To restore legacy full replacement (outerHTML), publish config and set dom_update to replace, or pass:

@bladexScripts(['domUpdate' => 'replace'])

You can still use Bladex.fetch() explicitly, or call Bladex.apply(payload) when the proxy is off.

Declarative actions

After @bladexScripts, you can trigger BladeX requests from HTML attributes instead of writing fetch() or onclick handlers. The server still decides which components to update through response()->refresh() (and related macros) plus identifier() — there is no hx-target in the markup.

Put data-fetch with the request URL on the element. HTTP method defaults to GET; set data-method for mutating verbs.

Attribute Behavior
data-fetch Request URL (required for declarative actions).
data-method HTTP method: get, post, put, patch, or delete. Defaults to get when omitted.

Optional attributes:

Attribute Behavior
data-trigger Events to listen for (default: submit on <form>, click elsewhere). Comma-separated list, with optional once and delay:300ms modifiers per event (for example click once or change delay:200ms).
data-loading Set automatically on the element while its declarative request is in flight (removed when the request finishes). On <form>, BladeX also disables that form’s controls until the request completes (fields that were already disabled stay disabled). Style with [data-loading] in your CSS.

Forms use FormData as the request body for mutating methods. Links and non-submit buttons call preventDefault on the configured trigger so navigation does not occur.

Example:

<button
    type="button"
    data-fetch="{{ route('items.store') }}"
    data-method="post"
>
    Add item
</button>

Both refresh and replace update the matched root by morphing server HTML into the existing element (see dom_update in config). With dom_update set to replace, they use outerHTML instead. remove calls element.remove(). append and prepend insert $content inside the root of $into (beforeend / afterbegin).

If more than one element shares the same data-component-identifier, BladeX logs an error and skips the operation. Make identifier() unique per mounted instance when you render multiple copies of the same component (for example by including a model id in the identifier).

Changelog

Please see CHANGELOG for more information on what has changed recently.

Contributing

Thank you for considering contributing to BladeX! Please review our contributing guide to get started. Package development requires Node.js: run npm ci && npm run build after cloning so packages/bladex/dist/bladex.js exists for PHP feature tests and the workbench.

Security Vulnerabilities

Please review our security policy on how to report security vulnerabilities.

Credits

License

BladeX is open-sourced software licensed under the MIT license.