Skip to content

Commit

Permalink
Merge pull request #229 from HugoDF/feat-boolean-attributes
Browse files Browse the repository at this point in the history
Feature: Support all HTML boolean attributes as per WHATWG HTML Standard
  • Loading branch information
calebporzio authored Mar 7, 2020
2 parents 3fab0cc + 2cd56d4 commit 88ea0ea
Show file tree
Hide file tree
Showing 6 changed files with 119 additions and 6 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -244,7 +244,7 @@ For example:

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

The following boolean attributes are currently supported: disabled, readonly, required, checked, hidden, selected, open.
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
8 changes: 7 additions & 1 deletion dist/alpine-ie11.js
Original file line number Diff line number Diff line change
Expand Up @@ -4978,6 +4978,12 @@
return i.type === type;
}.bind(this));
}
function isBooleanAttr(attrName) {
// As per HTML spec table https://html.spec.whatwg.org/multipage/indices.html#attributes-3:boolean-attribute
// Array roughly ordered by estimated usage
var booleanAttributes = ['disabled', 'checked', 'required', 'readonly', 'hidden', 'open', 'selected', 'autofocus', 'itemscope', 'multiple', 'novalidate', 'allowfullscreen', 'allowpaymentrequest', 'formnovalidate', 'autoplay', 'controls', 'loop', 'muted', 'playsinline', 'default', 'ismap', 'reversed', 'async', 'defer', 'nomodule'];
return booleanAttributes.includes(attrName);
}
function replaceAtAndColonWithStandardSyntax(name) {
if (name.startsWith('@')) {
return name.replace('@', 'x-on:');
Expand Down Expand Up @@ -5565,7 +5571,7 @@
var newClasses = value.split(' ');
el.setAttribute('class', arrayUnique(_originalClasses.concat(newClasses)).join(' '));
}
} else if (['disabled', 'readonly', 'required', 'checked', 'hidden', 'selected', 'open'].includes(attrName)) {
} else if (isBooleanAttr(attrName)) {
// Boolean attributes have to be explicitly added and removed, not just set.
if (!!value) {
el.setAttribute(attrName, '');
Expand Down
8 changes: 7 additions & 1 deletion dist/alpine.js
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,12 @@
return i.type === type;
});
}
function isBooleanAttr(attrName) {
// As per HTML spec table https://html.spec.whatwg.org/multipage/indices.html#attributes-3:boolean-attribute
// Array roughly ordered by estimated usage
const booleanAttributes = ['disabled', 'checked', 'required', 'readonly', 'hidden', 'open', 'selected', 'autofocus', 'itemscope', 'multiple', 'novalidate', 'allowfullscreen', 'allowpaymentrequest', 'formnovalidate', 'autoplay', 'controls', 'loop', 'muted', 'playsinline', 'default', 'ismap', 'reversed', 'async', 'defer', 'nomodule'];
return booleanAttributes.includes(attrName);
}
function replaceAtAndColonWithStandardSyntax(name) {
if (name.startsWith('@')) {
return name.replace('@', 'x-on:');
Expand Down Expand Up @@ -579,7 +585,7 @@
const newClasses = value.split(' ');
el.setAttribute('class', arrayUnique(originalClasses.concat(newClasses)).join(' '));
}
} else if (['disabled', 'readonly', 'required', 'checked', 'hidden', 'selected', 'open'].includes(attrName)) {
} else if (isBooleanAttr(attrName)) {
// Boolean attributes have to be explicitly added and removed, not just set.
if (!!value) {
el.setAttribute(attrName, '');
Expand Down
4 changes: 2 additions & 2 deletions src/directives/bind.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { arrayUnique } from '../utils'
import { arrayUnique , isBooleanAttr } from '../utils'

export function handleAttributeBindingDirective(component, el, attrName, expression, extraVars) {
var value = component.evaluateReturnExpression(el, expression, extraVars)
Expand Down Expand Up @@ -55,7 +55,7 @@ export function handleAttributeBindingDirective(component, el, attrName, express
const newClasses = value.split(' ')
el.setAttribute('class', arrayUnique(originalClasses.concat(newClasses)).join(' '))
}
} else if (['disabled', 'readonly', 'required', 'checked', 'hidden', 'selected', 'open'].includes(attrName)) {
} else if (isBooleanAttr(attrName)) {
// Boolean attributes have to be explicitly added and removed, not just set.
if (!! value) {
el.setAttribute(attrName, '')
Expand Down
14 changes: 14 additions & 0 deletions src/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,20 @@ export function getXAttrs(el, type) {
})
}

export function isBooleanAttr(attrName) {
// As per HTML spec table https://html.spec.whatwg.org/multipage/indices.html#attributes-3:boolean-attribute
// Array roughly ordered by estimated usage
const booleanAttributes = [
'disabled','checked','required','readonly','hidden','open', 'selected',
'autofocus', 'itemscope', 'multiple', 'novalidate','allowfullscreen',
'allowpaymentrequest', 'formnovalidate', 'autoplay', 'controls', 'loop',
'muted', 'playsinline', 'default', 'ismap', 'reversed', 'async', 'defer',
'nomodule'
]

return booleanAttributes.includes(attrName)
}

export function replaceAtAndColonWithStandardSyntax(name) {
if (name.startsWith('@')) {
return name.replace('@', 'x-on:')
Expand Down
89 changes: 88 additions & 1 deletion test/bind.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -184,9 +184,33 @@ test('boolean attributes set to false are removed from element', async () => {
<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>
<textarea x-bind:autofocus="isSet"></textarea>
<dl x-bind:itemscope="isSet"></dl>
<form x-bind:novalidate="isSet"></form>
<iframe
x-bind:allowfullscreen="isSet"
x-bind:allowpaymentrequest="isSet"
></iframe>
<button x-bind:formnovalidate="isSet"></button>
<audio
x-bind:autoplay="isSet"
x-bind:controls="isSet"
x-bind:loop="isSet"
x-bind:muted="isSet"
></audio>
<video x-bind:playsinline="isSet"></video>
<track x-bind:default="isSet" />
<img x-bind:ismap="isSet" />
<ol x-bind:reversed="isSet"></ol>
<script
x-bind:async="isSet"
x-bind:defer="isSet"
x-bind:nomodule="isSet"
></script>
</div>
`

Alpine.start()

expect(document.querySelectorAll('input')[0].disabled).toBeFalsy()
Expand All @@ -195,6 +219,25 @@ test('boolean attributes set to false are removed from element', async () => {
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()
})

test('boolean attributes set to true are added to element', async () => {
Expand All @@ -205,6 +248,31 @@ test('boolean attributes set to true are added to element', async () => {
<input x-bind:required="isSet"></input>
<input x-bind:readonly="isSet"></input>
<details x-bind:open="isSet"></details>
<select x-bind:multiple="isSet"></select>
<option x-bind:selected="isSet"></option>
<textarea x-bind:autofocus="isSet"></textarea>
<dl x-bind:itemscope="isSet"></dl>
<form x-bind:novalidate="isSet"></form>
<iframe
x-bind:allowfullscreen="isSet"
x-bind:allowpaymentrequest="isSet"
></iframe>
<button x-bind:formnovalidate="isSet"></button>
<audio
x-bind:autoplay="isSet"
x-bind:controls="isSet"
x-bind:loop="isSet"
x-bind:muted="isSet"
></audio>
<video x-bind:playsinline="isSet"></video>
<track x-bind:default="isSet" />
<img x-bind:ismap="isSet" />
<ol x-bind:reversed="isSet"></ol>
<script
x-bind:async="isSet"
x-bind:defer="isSet"
x-bind:nomodule="isSet"
></script>
</div>
`

Expand All @@ -215,6 +283,25 @@ test('boolean attributes set to true are added to element', async () => {
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('binding supports short syntax', async () => {
Expand Down

0 comments on commit 88ea0ea

Please sign in to comment.