fix: boolean props w/ bare attr as true
This commit is contained in:
parent
5635e6eb4a
commit
737ef54763
4 changed files with 57 additions and 5 deletions
|
|
@ -101,7 +101,7 @@
|
||||||
"size-limit": [
|
"size-limit": [
|
||||||
{
|
{
|
||||||
"path": "./dist/WebComponent.js",
|
"path": "./dist/WebComponent.js",
|
||||||
"limit": "1.35 KB"
|
"limit": "1.4 KB"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "./dist/html.js",
|
"path": "./dist/html.js",
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,11 @@
|
||||||
*/
|
*/
|
||||||
export function deserialize(value, type) {
|
export function deserialize(value, type) {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'number':
|
|
||||||
case 'boolean':
|
case 'boolean':
|
||||||
|
// bare presence follows the HTML convention: `<el flag>` / `flag=""` is true.
|
||||||
|
if (value === '') return true
|
||||||
|
// falls through
|
||||||
|
case 'number':
|
||||||
case 'object':
|
case 'object':
|
||||||
case 'undefined':
|
case 'undefined':
|
||||||
return JSON.parse(value)
|
return JSON.parse(value)
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,11 @@ describe('WebComponent', () => {
|
||||||
* and returns it. Remove it with `el.remove()` to trigger disconnect.
|
* and returns it. Remove it with `el.remove()` to trigger disconnect.
|
||||||
*/
|
*/
|
||||||
let tagSeq = 0
|
let tagSeq = 0
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param Class
|
||||||
|
* @param attributes
|
||||||
|
*/
|
||||||
function mount(Class, attributes = {}) {
|
function mount(Class, attributes = {}) {
|
||||||
const tag = `wc-test-${tagSeq++}`
|
const tag = `wc-test-${tagSeq++}`
|
||||||
window.customElements.define(tag, Class)
|
window.customElements.define(tag, Class)
|
||||||
|
|
@ -265,6 +270,38 @@ describe('reactive props (documented contract)', () => {
|
||||||
expect(el.props.count).toBe(5)
|
expect(el.props.count).toBe(5)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('turns a boolean prop on from a bare attribute (<el active>)', () => {
|
||||||
|
const el = connected()
|
||||||
|
// `<el active>` and `<el active="">` are the same to the DOM
|
||||||
|
el.setAttribute('active', '')
|
||||||
|
expect(el.props.active).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('honors an explicit "true"/"false" boolean attribute value', () => {
|
||||||
|
const el = connected()
|
||||||
|
el.setAttribute('active', 'true')
|
||||||
|
expect(el.props.active).toBe(true)
|
||||||
|
el.setAttribute('active', 'false')
|
||||||
|
expect(el.props.active).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('resets a boolean prop to its default when the attribute is removed', () => {
|
||||||
|
const el = connected()
|
||||||
|
el.setAttribute('active', '')
|
||||||
|
el.removeAttribute('active')
|
||||||
|
expect(el.props.active).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('round-trips a boolean prop write through its reflected attribute', () => {
|
||||||
|
const el = connected()
|
||||||
|
el.props.active = true
|
||||||
|
expect(el.getAttribute('active')).toBe('true')
|
||||||
|
expect(el.props.active).toBe(true)
|
||||||
|
el.props.active = false
|
||||||
|
expect(el.getAttribute('active')).toBe('false')
|
||||||
|
expect(el.props.active).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
it('fires onChanges with property/attribute/previousValue/currentValue', () => {
|
it('fires onChanges with property/attribute/previousValue/currentValue', () => {
|
||||||
const changes = []
|
const changes = []
|
||||||
class WithChanges extends WebComponent {
|
class WithChanges extends WebComponent {
|
||||||
|
|
@ -475,7 +512,10 @@ describe('attribute value & removal handling', () => {
|
||||||
expect(el.props.label).toBe('')
|
expect(el.props.label).toBe('')
|
||||||
// regression: must NOT echo back as "true"
|
// regression: must NOT echo back as "true"
|
||||||
expect(el.getAttribute('label')).toBe('')
|
expect(el.getAttribute('label')).toBe('')
|
||||||
expect(changes.at(-1)).toMatchObject({ property: 'label', currentValue: '' })
|
expect(changes.at(-1)).toMatchObject({
|
||||||
|
property: 'label',
|
||||||
|
currentValue: '',
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('resets a prop to its static default when the attribute is removed', () => {
|
it('resets a prop to its static default when the attribute is removed', () => {
|
||||||
|
|
@ -518,7 +558,10 @@ describe('attribute value & removal handling', () => {
|
||||||
|
|
||||||
// malformed value must not throw and must still fire onChanges/render
|
// malformed value must not throw and must still fire onChanges/render
|
||||||
expect(() => el.setAttribute('count', 'abc')).not.toThrow()
|
expect(() => el.setAttribute('count', 'abc')).not.toThrow()
|
||||||
expect(changes.at(-1)).toMatchObject({ property: 'count', currentValue: 'abc' })
|
expect(changes.at(-1)).toMatchObject({
|
||||||
|
property: 'count',
|
||||||
|
currentValue: 'abc',
|
||||||
|
})
|
||||||
|
|
||||||
error.mockRestore()
|
error.mockRestore()
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -13,8 +13,14 @@ describe('deserialize', () => {
|
||||||
expect(deserialize('false', 'boolean')).toBe(false)
|
expect(deserialize('false', 'boolean')).toBe(false)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('treats a bare/empty boolean attribute as true', () => {
|
||||||
|
expect(deserialize('', 'boolean')).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
test('parses object strings', () => {
|
test('parses object strings', () => {
|
||||||
expect(deserialize('{"hello":"world"}', 'object')).toEqual({ hello: 'world' })
|
expect(deserialize('{"hello":"world"}', 'object')).toEqual({
|
||||||
|
hello: 'world',
|
||||||
|
})
|
||||||
expect(deserialize('[1,2,3]', 'object')).toEqual([1, 2, 3])
|
expect(deserialize('[1,2,3]', 'object')).toEqual([1, 2, 3])
|
||||||
expect(deserialize('null', 'object')).toBeNull()
|
expect(deserialize('null', 'object')).toBeNull()
|
||||||
})
|
})
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue