From 36156c2f31a63af3f7e13893da0e0d2a527f67ae Mon Sep 17 00:00:00 2001 From: Chris Morrell Date: Wed, 10 May 2023 09:34:31 -0400 Subject: [PATCH] Handle special binding case for 'checked' and 'selected' (#3535) * 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> --- packages/alpinejs/src/utils/bind.js | 19 +++++++++++++++++ .../integration/directives/x-bind.spec.js | 21 ++++++++++++++++++- tests/cypress/utils.js | 2 ++ 3 files changed, 41 insertions(+), 1 deletion(-) diff --git a/packages/alpinejs/src/utils/bind.js b/packages/alpinejs/src/utils/bind.js index 256cbc888..c6d2261b1 100644 --- a/packages/alpinejs/src/utils/bind.js +++ b/packages/alpinejs/src/utils/bind.js @@ -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) @@ -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) @@ -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 + '' }) diff --git a/tests/cypress/integration/directives/x-bind.spec.js b/tests/cypress/integration/directives/x-bind.spec.js index 7b95c3ad9..615ffb061 100644 --- a/tests/cypress/integration/directives/x-bind.spec.js +++ b/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` @@ -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` +
+ + +
+ `, + ({ 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)) + } +) diff --git a/tests/cypress/utils.js b/tests/cypress/utils.js index ef126a3a4..7a1b4560e 100644 --- a/tests/cypress/utils.js +++ b/tests/cypress/utils.js @@ -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)