From 95b4b7f34c17e751b6543e1fb78910d33d7d1886 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=BCnther=20Debrauwer?= Date: Sat, 11 Nov 2023 16:24:51 +0100 Subject: [PATCH] `x-model.boolean` modifier (#3532) * Add x-model.boolean * fix --------- Co-authored-by: Caleb Porzio --- packages/alpinejs/src/directives/x-model.js | 52 ++++++++++++++---- packages/docs/src/en/directives/model.md | 13 +++++ .../integration/directives/x-model.spec.js | 55 +++++++++++++++++++ 3 files changed, 109 insertions(+), 11 deletions(-) diff --git a/packages/alpinejs/src/directives/x-model.js b/packages/alpinejs/src/directives/x-model.js index 5ed630cd2..c53053b8c 100644 --- a/packages/alpinejs/src/directives/x-model.js +++ b/packages/alpinejs/src/directives/x-model.js @@ -46,7 +46,7 @@ directive('model', (el, { modifiers, expression }, { effect, cleanup }) => { }) } } - + if (typeof expression === 'string' && el.type === 'radio') { // Radio buttons only work properly when they share a name attribute. // People might assume we take care of that for them, because @@ -69,7 +69,7 @@ directive('model', (el, { modifiers, expression }, { effect, cleanup }) => { let removeListener = isCloning ? () => {} : on(el, event, modifiers, (e) => { setValue(getInputValue(el, modifiers, e, getValue())) }) - + if (modifiers.includes('fill')) if ([null, ''].includes(getValue()) || (el.type === 'checkbox' && Array.isArray(getValue()))) { @@ -138,26 +138,44 @@ function getInputValue(el, modifiers, event, currentValue) { else if (el.type === 'checkbox') { // If the data we are binding to is an array, toggle its value inside the array. if (Array.isArray(currentValue)) { - let newValue = modifiers.includes('number') ? safeParseNumber(event.target.value) : event.target.value + let newValue = null; + + if (modifiers.includes('number')) { + newValue = safeParseNumber(event.target.value) + } else if (modifiers.includes('boolean')) { + newValue = safeParseBoolean(event.target.value) + } else { + newValue = event.target.value + } return event.target.checked ? currentValue.concat([newValue]) : currentValue.filter(el => ! checkedAttrLooseCompare(el, newValue)) } else { return event.target.checked } } else if (el.tagName.toLowerCase() === 'select' && el.multiple) { - return modifiers.includes('number') - ? Array.from(event.target.selectedOptions).map(option => { + if (modifiers.includes('number')) { + return Array.from(event.target.selectedOptions).map(option => { let rawValue = option.value || option.text return safeParseNumber(rawValue) }) - : Array.from(event.target.selectedOptions).map(option => { - return option.value || option.text + } else if (modifiers.includes('boolean')) { + return Array.from(event.target.selectedOptions).map(option => { + let rawValue = option.value || option.text + return safeParseBoolean(rawValue) }) + } + + return Array.from(event.target.selectedOptions).map(option => { + return option.value || option.text + }) } else { - let rawValue = event.target.value - return modifiers.includes('number') - ? safeParseNumber(rawValue) - : (modifiers.includes('trim') ? rawValue.trim() : rawValue) + if (modifiers.includes('number')) { + return safeParseNumber(event.target.value) + } else if (modifiers.includes('boolean')) { + return safeParseBoolean(event.target.value) + } + + return modifiers.includes('trim') ? event.target.value.trim() : event.target.value } }) } @@ -168,6 +186,18 @@ function safeParseNumber(rawValue) { return isNumeric(number) ? number : rawValue } +function safeParseBoolean(rawValue) { + if ([1, '1', 'true', true].includes(rawValue)) { + return true + } + + if ([0, '0', 'false', false].includes(rawValue)) { + return false + } + + return rawValue ? Boolean(rawValue) : null +} + function checkedAttrLooseCompare(valueA, valueB) { return valueA == valueB } diff --git a/packages/docs/src/en/directives/model.md b/packages/docs/src/en/directives/model.md index 4df45e6fe..91bbfa952 100644 --- a/packages/docs/src/en/directives/model.md +++ b/packages/docs/src/en/directives/model.md @@ -307,6 +307,19 @@ By default, any data stored in a property via `x-model` is stored as a string. T ``` + +### `.boolean` + +By default, any data stored in a property via `x-model` is stored as a string. To force Alpine to store the value as a JavaScript boolean, add the `.boolean` modifier. Both integers (1/0) and strings (true/false) are valid boolean values. + +```alpine + + +``` + ### `.debounce` diff --git a/tests/cypress/integration/directives/x-model.spec.js b/tests/cypress/integration/directives/x-model.spec.js index d2af463a0..575d4107d 100644 --- a/tests/cypress/integration/directives/x-model.spec.js +++ b/tests/cypress/integration/directives/x-model.spec.js @@ -79,6 +79,61 @@ test('x-model with number modifier returns: null if empty, original value if cas } ) +test('x-model casts value to boolean if boolean modifier is present', + html` +
+ + +
+ `, + ({ get }) => { + get('input[type=text]').type('1') + get('div').should(haveData('foo', true)) + + get('input[type=text]').clear().type('0') + get('div').should(haveData('foo', false)) + + get('input[type=text]').clear().type('true') + get('div').should(haveData('foo', true)) + + get('input[type=text]').clear().type('false') + get('div').should(haveData('foo', false)) + + get('select').select('no') + get('div').should(haveData('bar', false)) + + get('select').select('yes') + get('div').should(haveData('bar', true)) + } +) + +test('x-model with boolean modifier returns: null if empty, original value if casting fails, numeric value if casting passes', + html` +
+ +
+ `, + ({ get }) => { + get('input').clear() + get('div').should(haveData('foo', null)) + get('input').clear().type('bar') + get('div').should(haveData('foo', 'bar')) + get('input').clear().type('1') + get('div').should(haveData('foo', true)) + get('input').clear().type('1').clear() + get('div').should(haveData('foo', null)) + get('input').clear().type('0') + get('div').should(haveData('foo', false)) + get('input').clear().type('bar') + get('div').should(haveData('foo', 'bar')) + get('input').clear().type('0').clear() + get('div').should(haveData('foo', null)) + } +) + test('x-model trims value if trim modifier is present', html`