Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Ajax Modals Bundle Laravel Package

jagilpe/ajax-modals-bundle

A Symfony bundle for opening Bootstrap modals via AJAX, letting controllers return modal content and handle form submissions without full page reloads. It provides reusable helpers and response classes to integrate dynamic modals into Twig templates.

View on GitHub
Deep Wiki
Context7

Getting Started

Recognize the stack mismatch first. jagilpe/ajax-modals-bundle is a Symfony bundle — it registers through config/bundles.php, ships a DependencyInjection extension, and exposes Twig helpers. None of that contract exists in Laravel, so composer require jagilpe/ajax-modals-bundle will install files you cannot boot. Read the source as a reference implementation, not as a dependency.

Two realistic entry points for a Laravel developer:

  1. Use the frontend half directly. The bundle wraps the standalone @jagilpe/ajax-modals JS library. That library is framework-agnostic: it fetches a URL, injects the returned HTML fragment into a Bootstrap modal, and executes a small command protocol on the response. npm install @jagilpe/ajax-modals and skip Composer entirely.
  2. Reimplement the pattern in Blade. The server-side idea — controller returns a bare fragment, not a full layout — is one route + one view.

Where to look first:

  • The bundle's Resources/views/ templates show the exact contract: the server must return a fragment, never a full page.
  • The Twig helper functions are the entire public API; there are only a handful, and they map one-to-one onto a Blade component.
  • The JS library source is where the real behavior lives: the response/command protocol (e.g. close, reload-region, replace-content) and how the opener's data-* attributes select the target URL.

First use case — the one flow that exercises the whole contract: a "Create project" button that opens an empty Bootstrap modal, fetches a form via AJAX into the body, submits without a page reload, closes on success, and re-renders inline validation errors on failure. Get that working and you understand the package.

In Laravel the equivalent skeleton is:

// routes/web.php
Route::get('/projects/create',  [ProjectController::class, 'create'])->name('projects.create');
Route::post('/projects',        [ProjectController::class, 'store'])->name('projects.store');
// ProjectController
public function create()
{
    return view('projects._form', ['project' => new Project]); // fragment only
}

public function store(Request $request)
{
    $validated = $request->validate([...]);

    if ($request->wantsJson()) {
        return response()->json(['command' => 'close-and-reload', 'target' => '#projects']);
    }

    return redirect()->route('projects.index');
}

Implementation Patterns

Fragment-in, command-out. The core contract across the whole package is: opener element carries a data-* URL → JS fetches it → HTML is swapped into .modal-body → on submit the server replies with either a re-rendered fragment (validation errors) or a JSON command (close, redirect, refresh a region). Keep that shape when porting; it is the pattern's entire value.

Form re-render on validation failure. This is the highest-leverage reuse. The controller detects the AJAX request and returns the same partial with errors instead of a redirect. In Blade:

{{-- projects/_form.blade.php --}}
<form method="POST" action="{{ route('projects.store') }}" data-ajax-modal>
  @csrf
  <input name="name" value="{{ old('name', $project->name ?? '') }}">
  @error('name') <span class="invalid-feedback">{{ $message }}</span> @enderror
</form>

Chained modals. The bundle supports opening a second modal over the first (e.g. "add client" from inside a "create project" flow). Handle that by keeping the previous modal mounted and stacking, rather than destroying state. In Laravel, one <x-modal> component with a slot per instance is cleaner than a stack manager.

Reusable Blade modal component. The Twig helper is really just a wrapper around Bootstrap's data-bs-toggle="modal" markup with a dynamically-set data-url. Mirror it:

{{-- resources/views/components/ajax-modal.blade.php --}}
<div class="modal fade" id="{{ $id }}" tabindex="-1" aria-hidden="true">
  <div class="modal-dialog {{ $size ?? '' }}">
    <div class="modal-content">
      <div class="modal-body" data-src="{{ $src }}"></div>
    </div>
  </div>
</div>

And a small vanilla/Alpine controller that fetches data-src on shown.bs.modal and interprets the response command.

Region refresh after success. Instead of reloading the page after a successful create, return a JSON command telling the client to swap a specific DOM region (#projects). This is what makes the modal feel fast and is the one UX detail worth copying verbatim.

Integration tips.

  • Put the modal-opening JS in a single Alpine component or a tiny Modal.js module; don't scatter fetch() calls across views.
  • Return fragments from a dedicated route (/projects/create returns a partial; /projects returns a full page). Never branch on X-Requested-With inside one giant action.
  • Keep CSRF out of the fragment fetch (GET), and use the standard @csrf hidden field inside form fragments — Laravel's token is per-session, so it works across modal loads without special-casing.

Gotchas and Tips

No upstream support. One star, zero dependents, single maintainer. If you vendor or fork anything here, pin the commit and name an owner. The likely outcomes are Bootstrap-version drift, Twig/Symfony API drift, and an archived repo — none of which you can influence.

Bootstrap version coupling is the silent killer. The bundle's templates target a specific Bootstrap major (Bootstrap 3 vs 4 vs 5 changed data-toggle → data-bs-toggle, and modal JS semantics). If your design system is Bootstrap 5, expect the shipped markup to be stale. Verify bootstrap in your package.json against what the templates emit before assuming it will render.

Authorization on every modal endpoint. The bundle returns arbitrary fragments via AJAX. In a Laravel port, every fragment route needs the same can: / authorize() check as its full-page sibling — an IDOR here is trivially exploitable because the route often looks like "just a partial." Use a policy in the controller, not in the view.

Do not inject user-supplied HTML. This is the classic modal pitfall. If the fragment URL is built from user input, you have an SSRF/redirect surface. Always whitelist the endpoint pattern; never let the client pick an arbitrary URL for fetch().

Focus trap and aria-modal. The bundle predates strict a11y expectations. Bootstrap 5 handles focus trapping for the standard modal flow, but dynamically injecting content after shown.bs.modal can leave focus outside the dialog. Call bootstrap.Modal.getInstance(el).handleUpdate() (or re-focus the first field) after each fragment swap.

CSRF on form fragments is easy to forget. Because the modal body is fetched over GET, you can accidentally ship a form fragment without @csrf. Symptom: 419 Page Expired only on modal submits, never on regular pages. Always @csrf inside the fragment.

Debug the protocol, not the framework. When a modal "silently does nothing," the cause is almost always a response the JS didn't recognize as a command (an HTML error page instead of a JSON command, or a redirect the fetch followed). Add X-Requested-With: XMLHttpRequest to the fetch, and in the controller return response()->json(...) explicitly rather than redirect() for AJAX submits.

Extension point worth keeping: the command protocol. Whether you use the JS library or your own, keep the JSON envelope ({ command, target, html?, message? }) stable. New behaviors (toast on success, redirect after save) get added as new commands without touching the modal controller — this is the one design decision from the bundle that ages well.

Don't reach for this if you need more than simple dialogs. Nested state, dirty-form guards, optimistic updates, upload progress — all of these are outside the package's scope and outside what a ~1-day Blade port will handle well. At that point use Livewire, which gives you the same server-driven feel with maintained tooling.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console