Skip to content

Fix x-bind:value on <option> losing attribute when value is an empty string#4831

Merged
calebporzio merged 2 commits into
mainfrom
josh/fix-bind-value-empty-string-on-option
May 8, 2026
Merged

Fix x-bind:value on <option> losing attribute when value is an empty string#4831
calebporzio merged 2 commits into
mainfrom
josh/fix-bind-value-empty-string-on-option

Conversation

@joshhanley
Copy link
Copy Markdown
Collaborator

The Scenario

When using x-bind:value on an <option> element inside x-for and the bound value is an empty string, the rendered <option> is missing its value attribute. The option's .value property then reflects whatever text content x-text writes into it, so a form submission for the "empty" option sends the option's label instead of "".

<select x-data="{
    options: [
        { value: '', label: 'Empty' },
        { value: 'foo', label: 'Foo' },
        { value: 'bar', label: 'Bar' },
    ]
}">
    <template x-for="option in options" :key="option.label">
        <option :value="option.value" x-text="option.label"></option>
    </template>
</select>

The first option ends up as <option>Empty</option> (no value attribute), and optionEl.value === 'Empty', not ''.

Adding any text inside the <option> template (e.g. &nbsp;) masks the bug, which was the clue in the original report.

The Problem

bindInputValue in packages/alpinejs/src/utils/bind.js has an early-return optimisation:

} else {
    if (el.value === value) return

    el.value = value === undefined ? '' : value
}

For most elements, el.value is anchored to the value attribute, so this short-circuit is safe. For <option> it isn't: per the HTML spec, when an <option> has no value attribute, its .value property reflects the element's text content.

Because bind runs before text in Alpine's directive order, the first pass sees an option with no text yet (el.value === ''), the bound value is also '', and the function returns early without setting the attribute. x-text then writes "Empty" into the option, and el.value silently becomes "Empty" because there's no value attribute holding it to "".

The Solution

Special-case <option> in bindInputValue so the value attribute is written or removed deterministically:

} else if (el.tagName === 'OPTION') {
    if (value === null || value === undefined || value === false) {
        if (el.hasAttribute('value')) el.removeAttribute('value')
    } else {
        setIfChanged(el, 'value', value)
    }
}

For non-nullish values, the attribute is set explicitly via setIfChanged. This anchors .value so it can't drift when sibling directives write text.

For null/undefined/false, the attribute is removed, matching the convention bindAttribute already uses for other attributes. This preserves the DOM's text-as-value fallback, so :value="someUndefinedKey" opts back into label-as-value behaviour.

The change is scoped to <option> and leaves the text/checkbox/radio/select paths untouched.

Fixes #4830

@hirasso
Copy link
Copy Markdown
Contributor

hirasso commented May 7, 2026

😳 wow, @joshhanley ... how did you find this so quickly? Amazing work!

@joshhanley
Copy link
Copy Markdown
Collaborator Author

Thanks!

Instead of reimplementing attribute set/remove logic inline,
delegate to the existing bindAttribute function which already
handles null/undefined/false removal and setIfChanged.

Also reverts the unrelated isObjectAttr removal to keep the
diff focused on the option fix.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@calebporzio calebporzio merged commit 3b125f9 into main May 8, 2026
2 checks passed
@calebporzio calebporzio deleted the josh/fix-bind-value-empty-string-on-option branch May 8, 2026 16:52
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.

x-bind:value on <option> losing attribute when value is an empty string

3 participants