Skip to content

Fix x-mask attribute changes on x-model inputs#4852

Open
joshhanley wants to merge 2 commits into
mainfrom
josh/dynamic-mask-model-regression
Open

Fix x-mask attribute changes on x-model inputs#4852
joshhanley wants to merge 2 commits into
mainfrom
josh/dynamic-mask-model-regression

Conversation

@joshhanley

@joshhanley joshhanley commented Jul 1, 2026

Copy link
Copy Markdown
Collaborator

The Scenario

When Livewire morphs an input that uses both wire:model and x-mask, changing the server-rendered x-mask attribute can leave the input using a previous mask.

In this example, the input starts with a five-digit French postal code mask, changes to a four-digit Belgian mask, then changes back to the five-digit French mask. The x-mask attribute updates, but the input remains limited by the older four-digit mask.

use Livewire\Attributes\Computed;
use Livewire\Component;

new class extends Component {
    public string $country = 'FR';
    public string $postalCode = '12345';

    #[Computed]
    public function mask()
    {
        return $this->country === 'FR' ? '99999' : '9999';
    }
};
<form>
    <select wire:model.live="country">
        <option value="FR">France</option>
        <option value="BE">Belgium</option>
    </select>

    <input wire:model="postalCode" x-mask="{{ $this->mask }}">
</form>

The Problem

The mask directive wraps el._x_forceModelUpdate so programmatic model updates are formatted before they are applied to the input.

When an existing x-mask attribute changes, Alpine treats that as directive cleanup followed by directive initialisation. The cleanup aborts the directive's event listeners, but it does not restore el._x_forceModelUpdate.

That means every reinitialised mask wraps the updater currently on the element. Previous masks stay in the update chain, so an older four-digit mask can still trim a value after the element has changed back to a five-digit mask.

The Solution

The solution is to let the mask directive unregister the model updater wrapper it installs.

The directive now keeps a reference to its wrapper and restores the previous updater during cleanup, but only when the element is still using that exact wrapper. This avoids removing a newer wrapper that may have been installed after another directive initialisation.

Fixes livewire/livewire#10377

@joshhanley joshhanley changed the title Fix x-mask attribute changes with x-model Fix x-mask attribute changes on x-model inputs Jul 1, 2026
@calebporzio

Copy link
Copy Markdown
Collaborator

Traced this through mutation.js (removed-attrs cleanup runs before added-attrs init, so the wrapper chain stays depth-1) and verified the Cypress test fails on main and passes with the fix. The identity guard on restore handles the x-model-reinit case nicely, and dropping the cleanedUp microtask guard is safe since any dead wrapper gets clobbered by x-model re-init. Ready to merge.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Dynamic x-mask broken when used together with wire:model

2 participants