Skip to content

Fix :value bindings not updating custom elements when bound object data mutates#4833

Open
joshhanley wants to merge 1 commit into
mainfrom
josh/fix-bind-input-value-object-equality-short-circuit
Open

Fix :value bindings not updating custom elements when bound object data mutates#4833
joshhanley wants to merge 1 commit into
mainfrom
josh/fix-bind-input-value-object-equality-short-circuit

Conversation

@joshhanley
Copy link
Copy Markdown
Collaborator

The Scenario

When :value (or x-model) binds an object to a custom element, mutations to nested properties don't propagate to the element:

<script>
    customElements.define('value-display', class extends HTMLElement {
        get value() { return this._value }
        set value(v) {
            this._value = v
            this.textContent = JSON.stringify(v)
        }
    })
</script>

<div x-data="{ data: [{ label: 'Mon', value: 100 }] }">
    <value-display :value="data"></value-display>
    <button @click="data[0].value = 999">Mutate</button>
</div>

After clicking the button the display still shows "value":100 instead of 999.

The Problem

bindInputValue short-circuits with a reference-equality check:

if (el.value === value) return

The custom element's value getter returns the reactive proxy that was last set. When the proxy is the same reference between renders (its contents mutate but the top-level reference is preserved), the equality check is true and the setter is never re-invoked.

The Solution

Skip the short-circuit when value is a non-null object:

if (el.value === value && (typeof value !== 'object' || value === null)) return

Strings, numbers, booleans, and null/undefined still short-circuit as before.

Fixes livewire/flux#2414

…data mutates

`bindInputValue` short-circuits with a reference-equality check
(`el.value === value`). For standard inputs this is fine because
`el.value` is always a string, but for custom elements that store the
bound value as a property, the element's `value` getter returns the
reactive proxy that was last set. When the proxy is the same reference
between renders (its internals mutate but the top-level reference is
preserved), the equality check is true and the setter is never
re-invoked.

Skip the short-circuit when the value is a non-null object so updates
propagate to the custom element.
joshhanley added a commit to livewire/livewire that referenced this pull request May 11, 2026
The reference-equality short-circuit that hid these updates was in
Alpine's `bindInputValue`, not Livewire. With that fixed upstream
(alpinejs/alpine#4833), the Livewire-side workaround is no longer
needed. The test in `MergeSnapshotBrowserTest.php` stays as a
regression guard.
@ekwoka
Copy link
Copy Markdown
Contributor

ekwoka commented May 11, 2026

This is curious, since, of course, normal attributes wouldn't be allowed to be objects, but it is definitely what someone might expect for this kind of thing, especially coming from less "web-native" frameworks.

The alternative would be forcefully stringifying it into the attribute...which would then require the element to parse that...

But also the fact it's a reactive object being passed could cause other issues just passing the proxy as is...but it would play nice with mutatibility, and leave making it raw up to the end user, which would be simpler than the inverse of making it raw and trying to make the proxy pass be in user land...

Question: Does this same issue occur for attributes other than value or just for value? Does this change make value behave the same as other attributes?

I assume this change doesn't change the behavior of assigning to value on normal elements, since the assignment doesn't actually change at all, so it gets turned into a string tag...

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.

Charts do not update when data is updated

2 participants