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

Filament Unsaved Changes Modal Laravel Package

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+.

View on GitHub
Deep Wiki
Context7

Getting Started

Two Composer lines and two panel lines. Both panel lines are required — the plugin only swaps the confirmation UI, it does not turn on dirty-state tracking.

composer require azgasim/filament-unsaved-changes-modal
// app/Providers/Filament/AdminPanelProvider.php
use AzGasim\FilamentUnsavedChangesModal\FilamentUnsavedChangesModalPlugin;

return $panel
    // … your existing panel configuration …
    ->unsavedChangesAlerts()   // Filament's own dirty detection — keep this
    ->plugin(FilamentUnsavedChangesModalPlugin::make());

The array form is equivalent: ->plugins([FilamentUnsavedChangesModalPlugin::make(), ...]). Register it on each panel where you want the behavior — a second panel (customer portal, staff panel) does not inherit it.

Where to read first, in order:

  1. src/FilamentUnsavedChangesModalPlugin.php — the entire public surface is in this one short file: the five fluent setters plus DEFAULT_MODAL_WIDTH, DEFAULT_MODAL_ICON_COLOR, DEFAULT_STAY_BUTTON_COLOR, DEFAULT_LEAVE_BUTTON_COLOR, and MODAL_DOM_ID. Read it before anything else; it is the contract.
  2. resources/views/hooks/unsaved-changes-script-overrides.blade.php — the JavaScript that intercepts navigation, opens the modal by DOM id, and closes it. Every behavioral surprise you hit will be debugged here.
  3. resources/lang/en/unsaved-changes-modal.php — the navigation.* keys holding all user-facing copy. There are no plugin methods for the heading, description, or button labels.

First use case to validate: open any Edit page in the panel, type into a field, then click a sidebar link. You should get a Filament-styled Stay / Leave modal instead of the browser confirm(). Then repeat by clicking a link into an edit page from a table row — that exercises the SPA path (livewire:navigate) while the sidebar click exercises the same-origin full-page path. Both passing means interception is wired for both navigation styles, which is the whole point of the package.

Do this on a scratch panel first if you are on Filament 5.3.x below 5.3.5 — the package pins ^5.3.5 for the filament/tables XSS fix (CVE-2026-33080), so an older panel will fail composer require rather than install quietly.

Implementation Patterns

The panel provider is the only wiring point. No middleware, no facade, no per-resource opt-in, no beforeLeave on individual forms. The feature lands uniformly on every resource, custom page, and relation-manager form in that panel. That uniformity is the reason to install this rather than hand-roll a form-level hook — you get one implementation to test instead of one per form.

Theme as one declarative chain, colocated with registration. Keep it in the same call so the entire visual contract shows up in a single diff:

->plugin(
    FilamentUnsavedChangesModalPlugin::make()
        ->modalWidth('lg')
        ->modalIcon('OutlinedExclamationTriangle')
        ->modalIconColor('warning')
        ->stayButtonColor('gray')
        ->leaveButtonColor('danger')
)

Every setter returns $this. If you run two panels with different tones (a calm admin vs. a louder customer portal), extract a small private factory on the provider and pass a palette array rather than copy-pasting the chain.

Accepted values, so you do not have to guess:

  • modalWidth() — the same strings as Filament's Filament\Support\Enums\Width: xs … 7xl, full, min, max, fit, prose, screen-sm … screen-2xl, screen.
  • modalIcon() — Heroicon::OutlinedExclamationTriangle or the string 'OutlinedExclamationTriangle'. Not the CSS-style o-exclamation-triangle; that will not resolve.
  • modalIconColor(), stayButtonColor(), leaveButtonColor() — panel color keys: primary, success, danger, warning, info, gray, plus any custom colors registered on the panel.

data-skip-unsaved-changes-modal is an escape hatch, not a default. The listener resolves the attribute with closest(...), so putting it on a wrapping <nav> or a dropdown container exempts an entire cluster of links at once. Good candidates:

  • A "Cancel and return to list" link in a custom header action — blocking the user there is actively wrong.
  • Logout or help-desk links, where you never want to trap someone.
  • Links in an ephemeral preview toolbar where the form is intentionally throwaway.

Keep that list short. The value of the package collapses proportionally to how many panel links bypass it.

Localization is normal app copy — publish once, then edit.

php artisan vendor:publish --tag="filament-unsaved-changes-modal-translations"

Files land under lang/vendor/filament-unsaved-changes-modal/{locale}/unsaved-changes-modal.php, keys under navigation.*. The package already ships en, de, ckb, and fr; check those before writing your own, and send new locales upstream — for a single-maintainer package it is the cheapest possible contribution.

Publish views only when you actually need to change markup.

php artisan vendor:publish --tag="filament-unsaved-changes-modal-views"

If you rename the modal's DOM id in the published view, you must keep it in sync with MODAL_DOM_ID in the plugin class and the script hook view — the script opens and closes the modal purely by that id. This is the one place where "just publish the views and tweak" can silently break the feature.

Rollout on an existing panel. Branch, install, then walk the three or four highest-traffic dirty-form flows — edit user, edit order, a settings page, and any wizard-style custom page. For each, confirm that Leave actually performs the intended navigation rather than swallowing it. Once green, ship it; this is not a feature flag worth maintaining.

Your own coverage. composer test runs the package's suite, not your integration. Add one browser assertion per critical flow: dirty a form, click a nav link, assert the modal's DOM id is present, click Leave, assert the target URL loaded. That one test is what protects you across Filament minor upgrades.

Gotchas and Tips

Omitting ->unsavedChangesAlerts() produces silence, not an error. The plugin hooks Filament's existing dirty-state machinery; without it, forms never register as dirty, the modal never appears, and nothing is logged. This is the single most common misconfiguration and it looks exactly like "the package doesn't work."

Reload and tab-close are still the native browser prompt. Browsers do not permit customizing beforeunload, and no package can change that. Set expectations with stakeholders up front: the win is on in-app navigation, which is where users actually lose work. Do not file a bug asking for a Filament modal on refresh.

Only same-origin links inside the panel body are intercepted. External links, and any link rendered outside the panel body that the script does not see, fall through to normal browser behavior. If a custom page builds its navigation with raw anchors outside the panel's DOM scope, test it explicitly.

There is no config file — and there used to be. The published config/unsaved-changes-modal.php was removed as a breaking change in 1.0.0. If you are following a blog post or an older README that says to publish config and set keys there, ignore it; configure exclusively through the fluent plugin methods. php artisan vendor:publish will not show you a config tag.

Copy strings are translation-only. There are no ->heading(), ->description(), or ->stayLabel() methods. If a product owner wants different wording, the answer is the translations publish tag, not the plugin API. Budget that as a small task rather than assuming it is a one-liner on the plugin chain.

Icon strings are Heroicon names, not CSS classes. 'o-exclamation-triangle' fails silently back to the default. Use 'OutlinedExclamationTriangle' or the Heroicon enum case. The plugin also exposes a HeroiconResolver, so if you are passing icons around from config or a settings table, resolve them rather than trusting raw strings.

Debug order when the modal does not appear: (1) is unsavedChangesAlerts() on this panel? (2) is the plugin registered on this panel, not a different one? (3) does the form actually become dirty — check for fields with deferred state or custom components that bypass Filament's dirty tracking? (4) does the clicked link, or any ancestor, carry data-skip-unsaved-changes-modal? (5) only then open the script hook view and check the DOM id still matches MODAL_DOM_ID.

The plugin class is final and make() uses new self. You cannot subclass it to override the modal behavior or defaults. Extend by publishing the view, by wrapping registration in your own helper, or by contributing upstream — not by inheritance.

Version ceiling is real. Filament ^5.3.5 and PHP ^8.2 are hard requirements. On a mixed-version monorepo, a prefer-lowest CI job will fail the security audit if you try to run the plugin against an earlier 5.3.x. Pin deliberately and let the composer constraint tell you no rather than discovering it in staging.

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.
abbasudo/artisan-route
mirvan/ippanel
ignibyte/aic-framework
glavweb/data-schema-bundle
glavweb/datagrid-bundle
tacticmedia/scim-server
uni-method/frame
neika-ai/sdk
adachsoft/php-code-style
pharaonic/laravel-notification-dependencies
beautinow/translug
dev-brainiac/bot-cloaked
octoberrain/html
octoberrain/extension
octoberrain/exception
octoberrain/contracts
adachsoft/api-version-bundle
cgv/localized-resource
noith/settings
kda/laravel-package