Skip to content

Commit

Permalink
Merge pull request #486 from HugoDF/patch-2
Browse files Browse the repository at this point in the history
Remove attribute when set to null/undefined/false
  • Loading branch information
calebporzio committed May 15, 2020
2 parents 1775699 + afc4d9c commit 408d8d6
Show file tree
Hide file tree
Showing 6 changed files with 126 additions and 79 deletions.
15 changes: 12 additions & 3 deletions README.md
Expand Up @@ -252,14 +252,23 @@ In this example, the "hidden" class will only be applied when the value of the `

**`x-bind` for boolean attributes**

`x-bind` supports boolean attributes in the same way that value attributes, using a variable as the condition or any JavaScript expression that resolves to `true` or `false`.
`x-bind` supports boolean attributes in the same way as value attributes, using a variable as the condition or any JavaScript expression that resolves to `true` or `false`.

For example:
`<button x-bind:disabled="myVar">Click me</button>`
```html
<!-- Given: -->
<button x-bind:disabled="myVar">Click me</button>

<!-- When myVar == true: -->
<button disabled="disabled">Click me</button>

<!-- When myVar == false: -->
<button>Click me</button>
```

This will add or remove the `disabled` attribute when `myVar` is true or false respectively.

Boolean attributes are supported as per the [HTML specification](https://html.spec.whatwg.org/multipage/indices.html#attributes-3:boolean-attribute), for example `disabled`, `readonly`, `required`, `checked`, `hidden`, `selected`, `open` etc.
Boolean attributes are supported as per the [HTML specification](https://html.spec.whatwg.org/multipage/indices.html#attributes-3:boolean-attribute), for example `disabled`, `readonly`, `required`, `checked`, `hidden`, `selected`, `open`, etc.

---

Expand Down
12 changes: 5 additions & 7 deletions dist/alpine-ie11.js
Expand Up @@ -6029,15 +6029,13 @@
var newClasses = value.split(' ').filter(Boolean);
el.setAttribute('class', arrayUnique(_originalClasses.concat(newClasses)).join(' '));
}
} else if (isBooleanAttr(attrName)) {
// Boolean attributes have to be explicitly added and removed, not just set.
if (!!value) {
el.setAttribute(attrName, '');
} else {
} else {
// If an attribute's bound value is null, undefined or false, remove the attribute
if ([null, undefined, false].includes(value)) {
el.removeAttribute(attrName);
} else {
isBooleanAttr(attrName) ? el.setAttribute(attrName, attrName) : el.setAttribute(attrName, value);
}
} else {
el.setAttribute(attrName, value);
}
}

Expand Down
12 changes: 5 additions & 7 deletions dist/alpine.js
Expand Up @@ -575,15 +575,13 @@
const newClasses = value.split(' ').filter(Boolean);
el.setAttribute('class', arrayUnique(originalClasses.concat(newClasses)).join(' '));
}
} else if (isBooleanAttr(attrName)) {
// Boolean attributes have to be explicitly added and removed, not just set.
if (!!value) {
el.setAttribute(attrName, '');
} else {
} else {
// If an attribute's bound value is null, undefined or false, remove the attribute
if ([null, undefined, false].includes(value)) {
el.removeAttribute(attrName);
} else {
isBooleanAttr(attrName) ? el.setAttribute(attrName, attrName) : el.setAttribute(attrName, value);
}
} else {
el.setAttribute(attrName, value);
}
}

Expand Down
12 changes: 5 additions & 7 deletions src/directives/bind.js
Expand Up @@ -68,15 +68,13 @@ export function handleAttributeBindingDirective(component, el, attrName, express
const newClasses = value.split(' ').filter(Boolean)
el.setAttribute('class', arrayUnique(originalClasses.concat(newClasses)).join(' '))
}
} else if (isBooleanAttr(attrName)) {
// Boolean attributes have to be explicitly added and removed, not just set.
if (!! value) {
el.setAttribute(attrName, '')
} else {
} else {
// If an attribute's bound value is null, undefined or false, remove the attribute
if ([null, undefined, false].includes(value)) {
el.removeAttribute(attrName)
} else {
isBooleanAttr(attrName) ? el.setAttribute(attrName, attrName) : el.setAttribute(attrName, value)
}
} else {
el.setAttribute(attrName, value)
}
}

Expand Down
152 changes: 98 additions & 54 deletions test/bind.spec.js
Expand Up @@ -175,14 +175,46 @@ test('class attribute bindings are synced by string syntax', async () => {
expect(document.querySelector('span').classList.contains('baz')).toBeTruthy()
})

test('boolean attributes set to false are removed from element', async () => {
test('non-boolean attributes set to null/undefined/false are removed from the element', async () => {
document.body.innerHTML = `
<div x-data="{ isSet: false }">
<div x-data="{}">
<a href="#hello" x-bind:href="null"></a>
<a href="#hello" x-bind:href="false"></a>
<a href="#hello" x-bind:href="undefined"></a>
<!-- custom attribute see https://github.com/alpinejs/alpine/issues/280 -->
<span visible="true" x-bind:visible="null"></span>
<span visible="true" x-bind:visible="false"></span>
<span visible="true" x-bind:visible="undefined"></span>
</div>
`
Alpine.start()

expect(document.querySelectorAll('a')[0].getAttribute('href')).toBeNull()
expect(document.querySelectorAll('a')[1].getAttribute('href')).toBeNull()
expect(document.querySelectorAll('a')[2].getAttribute('href')).toBeNull()
expect(document.querySelectorAll('span')[0].getAttribute('visible')).toBeNull()
expect(document.querySelectorAll('span')[1].getAttribute('visible')).toBeNull()
expect(document.querySelectorAll('span')[2].getAttribute('visible')).toBeNull()
})

test('non-boolean empty string attributes are not removed', async () => {
document.body.innerHTML = `
<div x-data="{}">
<a href="#hello" x-bind:href="''"></a>
</div>
`
Alpine.start()

expect(document.querySelectorAll('a')[0].getAttribute('href')).toEqual('')
})

test('truthy boolean attribute values are set to their attribute name', async () => {
document.body.innerHTML = `
<div x-data="{ isSet: true }">
<input x-bind:disabled="isSet"></input>
<input x-bind:checked="isSet"></input>
<input x-bind:required="isSet"></input>
<input x-bind:readonly="isSet"></input>
<input x-bind:hidden="isSet"></input>
<details x-bind:open="isSet"></details>
<select x-bind:multiple="isSet"></select>
<option x-bind:selected="isSet"></option>
Expand Down Expand Up @@ -211,42 +243,43 @@ test('boolean attributes set to false are removed from element', async () => {
></script>
</div>
`

Alpine.start()

expect(document.querySelectorAll('input')[0].disabled).toBeFalsy()
expect(document.querySelectorAll('input')[1].checked).toBeFalsy()
expect(document.querySelectorAll('input')[2].required).toBeFalsy()
expect(document.querySelectorAll('input')[3].readOnly).toBeFalsy()
expect(document.querySelectorAll('input')[4].hidden).toBeFalsy()
expect(document.querySelectorAll('details')[0].open).toBeFalsy()
expect(document.querySelectorAll('option')[0].selected).toBeFalsy()
expect(document.querySelectorAll('select')[0].multiple).toBeFalsy()
expect(document.querySelectorAll('textarea')[0].autofocus).toBeFalsy()
expect(document.querySelectorAll('dl')[0].attributes.itemscope).toBeFalsy()
expect(document.querySelectorAll('form')[0].attributes.novalidate).toBeFalsy()
expect(document.querySelectorAll('iframe')[0].attributes.allowfullscreen).toBeFalsy()
expect(document.querySelectorAll('iframe')[0].attributes.allowpaymentrequest).toBeFalsy()
expect(document.querySelectorAll('button')[0].attributes.formnovalidate).toBeFalsy()
expect(document.querySelectorAll('audio')[0].attributes.autoplay).toBeFalsy()
expect(document.querySelectorAll('audio')[0].attributes.controls).toBeFalsy()
expect(document.querySelectorAll('audio')[0].attributes.loop).toBeFalsy()
expect(document.querySelectorAll('audio')[0].attributes.muted).toBeFalsy()
expect(document.querySelectorAll('video')[0].attributes.playsinline).toBeFalsy()
expect(document.querySelectorAll('track')[0].attributes.default).toBeFalsy()
expect(document.querySelectorAll('img')[0].attributes.ismap).toBeFalsy()
expect(document.querySelectorAll('ol')[0].attributes.reversed).toBeFalsy()
expect(document.querySelectorAll('script')[0].attributes.async).toBeFalsy()
expect(document.querySelectorAll('script')[0].attributes.defer).toBeFalsy()
expect(document.querySelectorAll('script')[0].attributes.nomodule).toBeFalsy()
expect(document.querySelectorAll('input')[0].disabled).toBeTruthy()
expect(document.querySelectorAll('input')[1].checked).toBeTruthy()
expect(document.querySelectorAll('input')[2].required).toBeTruthy()
expect(document.querySelectorAll('input')[3].readOnly).toBeTruthy()
expect(document.querySelectorAll('details')[0].open).toBeTruthy()
expect(document.querySelectorAll('option')[0].selected).toBeTruthy()
expect(document.querySelectorAll('select')[0].multiple).toBeTruthy()
expect(document.querySelectorAll('textarea')[0].autofocus).toBeTruthy()
expect(document.querySelectorAll('dl')[0].attributes.itemscope).toBeTruthy()
expect(document.querySelectorAll('form')[0].attributes.novalidate).toBeTruthy()
expect(document.querySelectorAll('iframe')[0].attributes.allowfullscreen).toBeTruthy()
expect(document.querySelectorAll('iframe')[0].attributes.allowpaymentrequest).toBeTruthy()
expect(document.querySelectorAll('button')[0].attributes.formnovalidate).toBeTruthy()
expect(document.querySelectorAll('audio')[0].attributes.autoplay).toBeTruthy()
expect(document.querySelectorAll('audio')[0].attributes.controls).toBeTruthy()
expect(document.querySelectorAll('audio')[0].attributes.loop).toBeTruthy()
expect(document.querySelectorAll('audio')[0].attributes.muted).toBeTruthy()
expect(document.querySelectorAll('video')[0].attributes.playsinline).toBeTruthy()
expect(document.querySelectorAll('track')[0].attributes.default).toBeTruthy()
expect(document.querySelectorAll('img')[0].attributes.ismap).toBeTruthy()
expect(document.querySelectorAll('ol')[0].attributes.reversed).toBeTruthy()
expect(document.querySelectorAll('script')[0].attributes.async).toBeTruthy()
expect(document.querySelectorAll('script')[0].attributes.defer).toBeTruthy()
expect(document.querySelectorAll('script')[0].attributes.nomodule).toBeTruthy()
})

test('boolean attributes set to true are added to element', async () => {
test('null, undefined, or false boolean attribute values are removed', async () => {
document.body.innerHTML = `
<div x-data="{ isSet: true }">
<div x-data="{ isSet: false }">
<input x-bind:disabled="isSet"></input>
<input x-bind:checked="isSet"></input>
<input x-bind:required="isSet"></input>
<input x-bind:readonly="isSet"></input>
<input x-bind:hidden="isSet"></input>
<details x-bind:open="isSet"></details>
<select x-bind:multiple="isSet"></select>
<option x-bind:selected="isSet"></option>
Expand Down Expand Up @@ -275,33 +308,44 @@ test('boolean attributes set to true are added to element', async () => {
></script>
</div>
`
Alpine.start()

expect(document.querySelectorAll('input')[0].getAttribute('disabled')).toBeNull()
expect(document.querySelectorAll('input')[1].getAttribute('checked')).toBeNull()
expect(document.querySelectorAll('input')[2].getAttribute('required')).toBeNull()
expect(document.querySelectorAll('input')[3].getAttribute('readOnly')).toBeNull()
expect(document.querySelectorAll('input')[4].getAttribute('hidden')).toBeNull()
expect(document.querySelectorAll('details')[0].getAttribute('open')).toBeNull()
expect(document.querySelectorAll('option')[0].getAttribute('selected')).toBeNull()
expect(document.querySelectorAll('select')[0].getAttribute('multiple')).toBeNull()
expect(document.querySelectorAll('textarea')[0].getAttribute('autofocus')).toBeNull()
expect(document.querySelectorAll('dl')[0].getAttribute('itemscope')).toBeNull()
expect(document.querySelectorAll('form')[0].getAttribute('novalidate')).toBeNull()
expect(document.querySelectorAll('iframe')[0].getAttribute('allowfullscreen')).toBeNull()
expect(document.querySelectorAll('iframe')[0].getAttribute('allowpaymentrequest')).toBeNull()
expect(document.querySelectorAll('button')[0].getAttribute('formnovalidate')).toBeNull()
expect(document.querySelectorAll('audio')[0].getAttribute('autoplay')).toBeNull()
expect(document.querySelectorAll('audio')[0].getAttribute('controls')).toBeNull()
expect(document.querySelectorAll('audio')[0].getAttribute('loop')).toBeNull()
expect(document.querySelectorAll('audio')[0].getAttribute('muted')).toBeNull()
expect(document.querySelectorAll('video')[0].getAttribute('playsinline')).toBeNull()
expect(document.querySelectorAll('track')[0].getAttribute('default')).toBeNull()
expect(document.querySelectorAll('img')[0].getAttribute('ismap')).toBeNull()
expect(document.querySelectorAll('ol')[0].getAttribute('reversed')).toBeNull()
expect(document.querySelectorAll('script')[0].getAttribute('async')).toBeNull()
expect(document.querySelectorAll('script')[0].getAttribute('defer')).toBeNull()
expect(document.querySelectorAll('script')[0].getAttribute('nomodule')).toBeNull()
})

test('boolean empty string attributes are not removed', async () => {
document.body.innerHTML = `
<div x-data="{}">
<input x-bind:disabled="''">
</div>
`
Alpine.start()

expect(document.querySelectorAll('input')[0].disabled).toBeTruthy()
expect(document.querySelectorAll('input')[1].checked).toBeTruthy()
expect(document.querySelectorAll('input')[2].required).toBeTruthy()
expect(document.querySelectorAll('input')[3].readOnly).toBeTruthy()
expect(document.querySelectorAll('details')[0].open).toBeTruthy()
expect(document.querySelectorAll('option')[0].selected).toBeTruthy()
expect(document.querySelectorAll('select')[0].multiple).toBeTruthy()
expect(document.querySelectorAll('textarea')[0].autofocus).toBeTruthy()
expect(document.querySelectorAll('dl')[0].attributes.itemscope).toBeTruthy()
expect(document.querySelectorAll('form')[0].attributes.novalidate).toBeTruthy()
expect(document.querySelectorAll('iframe')[0].attributes.allowfullscreen).toBeTruthy()
expect(document.querySelectorAll('iframe')[0].attributes.allowpaymentrequest).toBeTruthy()
expect(document.querySelectorAll('button')[0].attributes.formnovalidate).toBeTruthy()
expect(document.querySelectorAll('audio')[0].attributes.autoplay).toBeTruthy()
expect(document.querySelectorAll('audio')[0].attributes.controls).toBeTruthy()
expect(document.querySelectorAll('audio')[0].attributes.loop).toBeTruthy()
expect(document.querySelectorAll('audio')[0].attributes.muted).toBeTruthy()
expect(document.querySelectorAll('video')[0].attributes.playsinline).toBeTruthy()
expect(document.querySelectorAll('track')[0].attributes.default).toBeTruthy()
expect(document.querySelectorAll('img')[0].attributes.ismap).toBeTruthy()
expect(document.querySelectorAll('ol')[0].attributes.reversed).toBeTruthy()
expect(document.querySelectorAll('script')[0].attributes.async).toBeTruthy()
expect(document.querySelectorAll('script')[0].attributes.defer).toBeTruthy()
expect(document.querySelectorAll('script')[0].attributes.nomodule).toBeTruthy()
expect(document.querySelectorAll('input')[0].disabled).toEqual(true)
})

test('binding supports short syntax', async () => {
Expand Down
2 changes: 1 addition & 1 deletion test/model.spec.js
Expand Up @@ -131,7 +131,7 @@ test('x-model binds checkbox value', async () => {
<div x-data="{ foo: true }">
<input type="checkbox" x-model="foo"></input>
<span x-bind:bar="foo"></span>
<span x-bind:bar="JSON.stringify(foo)"></span>
</div>
`

Expand Down

0 comments on commit 408d8d6

Please sign in to comment.