x-widget
Version:
Adds the ability to define reusable Widgets (WebComponents) using Alpinejs.
150 lines (116 loc) • 4.1 kB
JavaScript
import { expect } from '@esm-bundle/chai/esm/chai.js'
import { xPropDirective, safeLeftHandSide } from '../src/x-prop-directive.mjs'
import Alpine from 'alpinejs'
const waitUntil = (predicate, timeout = 10000) =>
new Promise((resolve, reject) => {
setTimeout(() => reject(new Error('timeout')), timeout)
const waitId = setInterval(() => {
const result = predicate()
if (result) {
clearInterval(waitId)
resolve(result)
}
}, 1)
})
const waitForEl = (selector) =>
waitUntil(() => document.querySelector(selector))
// so html gets formatted in literals in vscode
const html = String.raw
describe('x-prop', () => {
before(() => {
document.body.setAttribute('x-data', '')
Alpine.directive('prop', xPropDirective)
window.Alpine = Alpine
Alpine.start()
})
beforeEach(() => (document.body.innerHTML = ''))
it('works in basic case', async () => {
document.body.innerHTML = `
<div id="root" x-data="{x: 10}">
<div x-prop:y="x">
<span x-text="y"></span>
</div>
</div>
`
const spanEl = await waitForEl('span')
expect(spanEl.innerText).to.equal('10')
Alpine.evaluate(spanEl, 'y = 20')
await new Promise((r) => setTimeout(r, 0))
const rootEl = await waitForEl('#root')
expect(Alpine.evaluate(rootEl, 'x')).to.equal(20)
})
it('sees props in scope', async () => {
document.body.innerHTML = `
<div id="root" x-data="{x: 10}">
<div x-prop:y="x">
<span x-text="JSON.stringify({x,y})"></span>
</div>
</div>
`
const spanEl = await waitForEl('span')
expect(spanEl.innerText).to.equal('{"x":10,"y":10}')
})
it('sets props on element', async () => {
document.body.innerHTML = `
<div id="root" x-data="{x: true}">
<input type="checkbox" x-prop:indeterminate="x">
</div>
`
const inputEl = await waitForEl('input')
expect(inputEl.indeterminate).to.be.true
})
it('supports deep refs', async () => {
document.body.innerHTML = html`
<div id="root" x-data="{a: {b: 10}}">
<div class="inner" x-prop:x="a.b"></div>
</div>
`
const rootEl = await waitForEl('#root')
const innerEl = await waitForEl('.inner')
await new Promise((r) => setTimeout(r, 0))
Alpine.evaluate(innerEl, 'x = 20')
await new Promise((r) => setTimeout(r, 0))
expect(Alpine.evaluate(rootEl, 'a.b')).to.equal(20)
})
it('works with non left hand side expressions', async () => {
document.body.innerHTML = html`
<div id="root" x-prop:x="10" x-prop:y="20" x-text="x + y"></div>
`
const rootEl = await waitForEl('#root')
expect(rootEl.innerText).to.equal('30')
})
it('supports circular prop binding', async () => {
document.body.innerHTML = html`
<div x-data="{x: 'hello'}">
<div id="root" x-prop:x="x" x-text="x"></div>
</div>
`
const rootEl = await waitForEl('#root')
expect(rootEl.innerText).to.equal('hello')
})
it('supports self prop binding', async () => {
document.body.innerHTML = html`
<div id="root" x-data="{y: 'hello'}" x-prop:x="y" x-text="x"></div>
`
const rootEl = await waitForEl('#root')
expect(rootEl.innerText).to.equal('hello')
})
it('assigning to non left hand side expressions works', async () => {
document.body.innerHTML = html` <div id="root" x-prop:x="10"></div> `
const spanEl = await waitForEl('#root')
Alpine.evaluate(spanEl, 'x = 20')
expect(Alpine.evaluate(spanEl, 'x')).to.equal(20)
})
})
it('can tell if something is safe left hand side', () => {
expect(safeLeftHandSide('x')).to.be.true
expect(safeLeftHandSide('xYz')).to.be.true
expect(safeLeftHandSide('x.y.c')).to.be.true
expect(safeLeftHandSide('x[y]')).to.be.true
expect(safeLeftHandSide('x[y.z][b]')).to.be.true
// TODO: expect(safeLeftHandSide('x[0]')).to.be.true
// TODO: expect(safeLeftHandSide('var')).to.be.false
// expect(safeLeftHandSide('x()')).to.be.false
// expect(safeLeftHandSide('x.0')).to.be.false
// expect(safeLeftHandSide('x.true')).to.be.false
})