Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Handle special binding case for 'checked' and 'selected' #3535

Merged
merged 2 commits into from
May 10, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
19 changes: 19 additions & 0 deletions packages/alpinejs/src/utils/bind.js
Expand Up @@ -22,6 +22,14 @@ export default function bind(el, name, value, modifiers = []) {
case 'class':
bindClasses(el, value)
break;

// 'selected' and 'checked' are special attributes that aren't necessarily
// synced with their corresponding properties when updated, so both the
// attribute and property need to be updated when bound.
case 'selected':
case 'checked':
bindAttributeAndPropery(el, name, value)
inxilpro marked this conversation as resolved.
Show resolved Hide resolved
break;

default:
bindAttribute(el, name, value)
Expand Down Expand Up @@ -78,6 +86,11 @@ function bindStyles(el, value) {
el._x_undoAddedStyles = setStyles(el, value)
}

function bindAttributeAndPropery(el, name, value) {
inxilpro marked this conversation as resolved.
Show resolved Hide resolved
bindAttribute(el, name, value)
setPropertyIfChanged(el, name, value)
}

function bindAttribute(el, name, value) {
if ([null, undefined, false].includes(value) && attributeShouldntBePreservedIfFalsy(name)) {
el.removeAttribute(name)
Expand All @@ -94,6 +107,12 @@ function setIfChanged(el, attrName, value) {
}
}

function setPropertyIfChanged(el, propName, value) {
if (el[propName] !== value) {
el[propName] = value
}
}

function updateSelect(el, value) {
const arrayWrappedValue = [].concat(value).map(value => { return value + '' })

Expand Down
21 changes: 20 additions & 1 deletion tests/cypress/integration/directives/x-bind.spec.js
@@ -1,4 +1,4 @@
import { beHidden, beVisible, haveText, beChecked, haveAttribute, haveClasses, haveValue, notBeChecked, notHaveAttribute, notHaveClasses, test, html } from '../../utils'
import { beHidden, beVisible, haveText, beChecked, haveAttribute, haveClasses, haveProperty, haveValue, notBeChecked, notHaveAttribute, notHaveClasses, test, html } from '../../utils';

test('sets attribute bindings on initialize',
html`
Expand Down Expand Up @@ -452,3 +452,22 @@ test('Can retrieve Alpine bound data with global bound method',
get('#6').should(haveText('bar'))
}
)

test('x-bind updates checked attribute and property after user interaction',
html`
<div x-data="{ checked: true }">
<button @click="checked = !checked">toggle</button>
<input type="checkbox" x-bind:checked="checked" @change="checked = $event.target.checked" />
</div>
`,
({ get }) => {
get('input').should(haveAttribute('checked', 'checked'))
get('input').should(haveProperty('checked', true))
get('input').click()
get('input').should(notHaveAttribute('checked'))
get('input').should(haveProperty('checked', false))
get('button').click()
get('input').should(haveAttribute('checked', 'checked'))
get('input').should(haveProperty('checked', true))
}
)
2 changes: 2 additions & 0 deletions tests/cypress/utils.js
Expand Up @@ -97,6 +97,8 @@ export let haveAttribute = (name, value) => el => expect(el).to.have.attr(name,

export let notHaveAttribute = (name, value) => el => expect(el).not.to.have.attr(name, value)

export let haveProperty = (name, value) => el => expect(el).to.have.prop(name, value)

export let haveText = text => el => expect(el).to.have.text(text)

export let notHaveText = text => el => expect(el).not.to.have.text(text)
Expand Down