Skip to content

Commit

Permalink
x-model.boolean modifier (#3532)
Browse files Browse the repository at this point in the history
* Add x-model.boolean

* fix

---------

Co-authored-by: Caleb Porzio <calebporzio@gmail.com>
  • Loading branch information
gdebrauwer and calebporzio authored Nov 11, 2023
1 parent 3a19b0f commit 95b4b7f
Show file tree
Hide file tree
Showing 3 changed files with 109 additions and 11 deletions.
52 changes: 41 additions & 11 deletions packages/alpinejs/src/directives/x-model.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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()))) {
Expand Down Expand Up @@ -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
}
})
}
Expand All @@ -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
}
Expand Down
13 changes: 13 additions & 0 deletions packages/docs/src/en/directives/model.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`

Expand Down
55 changes: 55 additions & 0 deletions tests/cypress/integration/directives/x-model.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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: '' }">
Expand Down

0 comments on commit 95b4b7f

Please sign in to comment.