Sfoglia il codice sorgente

`x-model.boolean` modifier (#3532)

* Add x-model.boolean

* fix

---------

Co-authored-by: Caleb Porzio <calebporzio@gmail.com>
Günther Debrauwer 1 anno fa
parent
commit
95b4b7f34c

+ 41 - 11
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
 }

+ 13 - 0
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
 <span x-text="typeof age"></span>
 ```
 
+<a name="boolean"></a>
+### `.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
+<select x-model.boolean="isActive">
+    <option value="true">Yes</option>
+    <option value="false">No</option>
+</select>
+<span x-text="typeof isActive"></span>
+```
+
 <a name="debounce"></a>
 ### `.debounce`
 

+ 55 - 0
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`
+    <div x-data="{ foo: null, bar: null, baz: [] }">
+        <input type="text" x-model.boolean="foo"></input>
+        <select x-model.boolean="bar">
+            <option value="true">yes</option>
+            <option value="false">no</option>
+        </select>
+    </div>
+    `,
+    ({ 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`
+    <div x-data="{ foo: 0, bar: '' }">
+        <input x-model.boolean="foo"></input>
+    </div>
+    `,
+    ({ 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`
     <div x-data="{ foo: '' }">