Skip to content

Commit

Permalink
Handle special binding case for 'checked' and 'selected' (#3535)
Browse files Browse the repository at this point in the history
* Handle special binding case for 'checked' and 'selected'

* Fix typo in "bindAttributeAndProperty"

Co-authored-by: Eric Kwoka <43540491+ekwoka@users.noreply.github.com>

---------

Co-authored-by: Eric Kwoka <43540491+ekwoka@users.noreply.github.com>
  • Loading branch information
inxilpro and ekwoka committed May 10, 2023
1 parent 6d23457 commit 36156c2
Show file tree
Hide file tree
Showing 3 changed files with 41 additions and 1 deletion.
19 changes: 19 additions & 0 deletions packages/alpinejs/src/utils/bind.js
Original file line number Diff line number Diff line change
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':
bindAttributeAndProperty(el, name, value)
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 bindAttributeAndProperty(el, name, value) {
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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

0 comments on commit 36156c2

Please sign in to comment.