azgasim/filament-unsaved-changes-modal
Replaces Filament's browser confirmation dialog with a native Filament modal when leaving a dirty form. Works with SPA and full-page navigation, using the same dirty-state rules as unsavedChangesAlerts(). Requires Filament 5.3.5+ and PHP 8.2+.
Architecture fit. The package is an extremely thin, surgical extension of an existing Filament capability rather than a new subsystem. It does not introduce middleware, a service provider contract, a facade, a database table, a queue job, or a config file. Its entire surface is one final plugin class (FilamentUnsavedChangesModalPlugin) with five fluent setters, plus a Blade hook that injects client-side interception JavaScript and a modal view keyed by a fixed DOM id (MODAL_DOM_ID). That means it composes with the panel provider the same way a theme or a color palette does — declarative, one wiring point, uniformly applied to every resource, custom page, and relation-manager form in that panel. For a TPM, the important architectural property is negative footprint: adopting it cannot perturb routing, auth, persistence, or the Livewire component tree. It only replaces the presentation of a confirmation that Filament already produces.
The dependency it leans on is the real architectural fact: the plugin is a UI swap over Filament's unsavedChangesAlerts(). Filament owns dirty-state detection; this package owns the dialog. That is a clean separation of concerns and a good sign for maintainability, but it also means the value proposition is bounded — you are buying styling and UX consistency, not capability. The underlying capability (confirm() on dirty navigation, browser prompt on reload/tab-close) exists for free in Filament 5.
Integration feasibility. High. Two commands and two chained lines in the panel provider. No migration path, no data model, no backfill, no feature flag machinery required. Both an existing panel and a greenfield panel adopt it identically. The data-skip-unsaved-changes-modal attribute (resolved via closest(...)) gives per-link and per-cluster opt-out without touching the plugin API, which covers the realistic edge cases: explicit "cancel and return to list" links, logout, help-desk links, and ephemeral preview toolbars. Localization is standard Laravel vendor-publish; en, de, ckb, and fr ship in the box.
The hard constraint is version: filament/filament ^5.3.5 and php ^8.2. The README is explicit that the floor exists because of a known XSS advisory (CVE-2026-33080) in earlier Filament 5.x, and the release notes mention Composer security-audit blocks under prefer-lowest. Practically, this package forces your panel to Filament 5.3.5+, which is either a non-issue (you are current) or a scoping decision (you are not). Any team still on Filament 3.x or 4.x is out of scope entirely, and the plugin offers no backport.
Technical risk. Moderate-low in blast radius, moderate in dependency risk. The specific risks a TPM should log:
| Risk | Assessment |
|---|---|
| Single maintainer, 12 stars, 0 dependents | The dominant risk. Bus factor of one. The mitigating factor is that the package is small, MIT-licensed, and the critical logic is a single Blade/JS hook you could fork into your own repo in an afternoon. Treat it as vendorable source, not as critical infrastructure. |
| Silent misconfiguration | Omitting ->unsavedChangesAlerts() produces no error, no log line, and no modal — the feature just quietly does nothing. This is the most likely production surprise and needs an explicit smoke test in CI, not a code review. |
DOM id coupling |
The script opens and closes the modal by MODAL_DOM_ID. If you publish views and change the id, or if a future Filament theme collides, the modal silently fails to open while navigation is already suppressed — a potential "user is stuck on the form" failure. |
| JS interception correctness | The listener must distinguish same-origin http(s) links from external, mailto:, tel:, download, and programmatic navigations. Release notes show hardening here (non-http(s) hrefs, same-origin checks, defensive try/catch), which is reassuring but also indicates this is the historically fragile surface. |
| Incomplete coverage by design | Reload and tab-close still fall back to the browser's native prompt. That is a browser platform limit, not a package defect — but it means the UX is inconsistent, and stakeholders who see the styled modal will ask why the reload prompt looks different. Set that expectation up front. |
| Filament major-version treadmill | Interception depends on Filament's livewire:navigate behavior and its Blade hook points. A Filament 6 upgrade is a likely breaking event with no guaranteed maintainer response. |
Key questions to resolve before committing.
$this->redirect() / redirect() calls from Livewire actions. The latter is very likely not intercepted, and that is often the dominant navigation path on an edit page after save.Content-Security-Policy. Verify against your actual headers, not a local dev environment.Escape per Filament's own modal conventions? Since it reuses Filament modal primitives it probably inherits this — confirm rather than assume.SECURITY.md?^5.3.5 constraint drag you forward into an incompatible minor?Stack fit. The package targets exactly one stack shape: a Filament 5 panel (5.3.5+), PHP 8.2+, Composer-managed, using unsavedChangesAlerts() on the panel. If your application is a standard Laravel + Filament admin, fit is essentially perfect — the plugin registers on the panel object alongside your existing ->plugins([...]) array and inherits panel-wide scope. If you run multiple panels (admin, customer portal, partner console), each panel opts in independently, which is the right granularity: you can ship it on the internal admin panel first and leave customer-facing surfaces untouched.
Migration path. Not a migration in the data sense — a configuration change with a one-line removal as rollback. Concretely:
composer require azgasim/filament-unsaved-changes-modal on a short-lived branch.->unsavedChangesAlerts() (if absent) and ->plugin(FilamentUnsavedChangesModalPlugin::make()) to the target panel provider, colocated so the visual contract is one readable chain.Rollback is deleting two chained lines and running composer remove. That reversibility is the strongest argument for trying it: the cost of being wrong is an afternoon, not a sprint.
Compatibility. The binding constraint is Filament's minor floor. Verify your composer.lock resolves filament/filament to >=5.3.5 before adding the package, because the plugin's constraint will otherwise force an upgrade of Filament itself as a side effect of a UI change — a scoping surprise in a release window. The PHP 8.2 floor is unremarkable for 2026. Note the 1.0.0 breaking change (published config file removed in favor of plugin-only configuration): if anyone prototypes from a pre-1.0 blog post or example, they will write a config file that is silently ignored.
Sequencing. Adopt in this order, because each step de-risks the next:
unsavedChangesAlerts() state per panel, confirm CSP headers permit the injected script. All three are cheap checks that can kill the initiative early.en user base, audit the four shipped locales for tone and completeness before publishing overrides — the copy is user-facing and legally mundane but visible.data-skip-unsaved-changes-modal to the short list of links where blocking is hostile (cancel/discard, logout, support). Keep it short; the feature's value erodes proportionally to bypass coverage.MODAL_DOM_ID must stay in sync with, and who owns it if upstream goes quiet. Cheap to write, expensive to reconstruct during an incident.Maintenance. Near-zero steady state, with two named exceptions. First, translation drift: every new locale or copy change is a publish-and-edit cycle, and user-facing text in four languages with no owner becomes stale. Assign the published lang files to whoever owns panel copy. Second, Filament upgrade review: any Filament minor that touches modal internals, Blade hook ordering, or livewire:navigate semantics requires re-walking the pilot flows. Budget this as a line item in your Filament upgrade checklist rather than as a surprise. There is no config file to drift, no migrations to run, no scheduled task, no cache to clear — the maintenance surface is genuinely small.
Support. Low tier-1 volume, but with a specific and predictable shape: users who reload or close a tab will see the old browser prompt while everything else is styled. That is a browser limitation, not a bug, and it will generate tickets phrased as "the modal doesn't work sometimes." Pre-empt it with a short note in internal release communications. A second support pattern is the "stuck on form" report caused by a published-view id mismatch or a JS interception edge case; the triage path should be documented as "check MODAL_DOM_ID sync, then check the script hook view" — which is exactly the debugging order the implementation guidance recommends.
Scaling. Operationally flat. The cost is client-side only: one small script evaluated per panel page load, one modal rendered on demand. No server-side per-request overhead, no query cost, no memory footprint, no concurrency consideration. It scales to any number of resources and forms trivially because it applies at the panel level. The only "scaling" dimension that matters is panel count and team count — more panels means more registration sites to keep in sync, and more teams means more people who need to know the data-skip-unsaved-changes-modal convention exists. Address the first with a small private factory method or palette helper; address the second with a line in the frontend conventions doc.
Failure modes. Enumerated for the runbook, in descending likelihood:
| Failure | Symptom | Detection | Mitigation |
|---|---|---|---|
unsavedChangesAlerts() missing |
Feature silently absent, no logs | Smoke test in CI | Explicit test; README's most-flagged gotcha |
Published view id drift |
Modal never opens; navigation suppressed → user feels stuck | Manual QA of dirty-form exit | Keep id in sync with MODAL_DOM_ID; avoid publishing views unless necessary |
| JS interception too broad | External links or downloads blocked or misrouted | Link-audit test | Rely on the same-origin / non-http(s) hardening in 1.0.0+; keep current |
| Programmatic redirect not intercepted | Save-then-redirect bypasses the modal entirely | Flow walkthrough | Expected behavior for post-save; confirm it is acceptable UX |
| Filament minor upgrade breaks hook | Modal stops appearing after upgrade | Dusk regression test | Pin Filament minor; re-walk flows on upgrade |
| Upstream abandonment | No fixes for future Filament majors | Quarterly dependency review | Fork-and-own contingency documented; MIT license permits it |
| CSP violation on inline script | Script blocked; feature absent; console errors | CSP report-only rollout | Nonce/hash the hook or verify compatibility in Phase 1 |
The compounding observation across all of these: every failure mode is a silent UI degradation, never a data-integrity or availability event. Worst case, users see the browser's confirm() again — which is the pre-adoption baseline. That asymmetry is what makes this a low-risk adoption for a TPM to approve.
Ramp-up. Effectively immediate for engineers — the README plus the single plugin class is a complete mental model, and the "first use case to validate" is spelled out (dirty an Edit page, click a sidebar link). The real ramp-up cost is organizational, not technical: (1) teaching the team that data-skip-unsaved-changes-modal exists and when not to use it, (2) teaching the two-or-three-person group who owns panel frontend that a Filament upgrade now includes a modal regression walk, and (3) writing the one Dusk test that converts "I think it works" into "CI knows it works." Total onboarding is measured in hours.
Recommendation. Adopt, with conditions: pin the Filament minor, vendor the source or document the fork procedure, add one browser-level regression test per critical dirty-form flow, and treat reload/tab-close inconsistency as an accepted, communicated limitation. The package buys genuine UX polish — a native Filament modal instead of a jarring browser confirm() — at a dependency cost that is small, reversible, and fully contained within the presentation layer. Given a score profile of high opportunity against a thin maturity signal (12 stars, 0 dependents, single maintainer), the correct posture is low-commitment adoption with a documented exit, not a strategic bet on the package's longevity.
How can I help you explore Laravel packages today?