Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

alpinejs

Package Overview
Dependencies
Maintainers
1
Versions
134
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

alpinejs - npm Package Compare versions

Comparing version 2.2.5 to 2.3.0

src/directives/html.js

2

package.json
{
"main": "dist/alpine.js",
"name": "alpinejs",
"version": "2.2.5",
"version": "2.3.0",
"repository": {

@@ -6,0 +6,0 @@ "type": "git",

# Alpine.js
![npm bundle size](https://img.shields.io/bundlephobia/minzip/alpinejs)
![npm version](https://img.shields.io/npm/v/alpinejs)

@@ -34,7 +35,10 @@ Alpine.js offers you the reactive and declarative nature of big frameworks like Vue or React at a much lower cost.

**For IE11 support** Use the following script instead.
**For IE11 support** Use the following scripts instead.
```html
<script src="https://cdn.jsdelivr.net/gh/alpinejs/alpine@v2.x.x/dist/alpine-ie11.min.js" defer></script>
<script type="module" src="https://cdn.jsdelivr.net/gh/alpinejs/alpine@v2.x.x/dist/alpine.min.js"></script>
<script nomodule src="https://cdn.jsdelivr.net/gh/alpinejs/alpine@v2.x.x/dist/alpine-ie11.min.js" defer></script>
```
The pattern above is the [module/nomodule pattern](https://philipwalton.com/articles/deploying-es2015-code-in-production-today/) that will result in the modern bundle automatically loaded on modern browsers, and the IE11 bundle loaded automatically on IE11 and other legacy browsers.
## Use

@@ -157,2 +161,5 @@

> **For bundler users**, note that Alpine.js accesses functions that are in the global scope (`window`), you'll need to explicitly assign your functions to `window` in order to use them with `x-data` for example `window.dropdown = function () {}` (this is because with Webpack, Rollup, Parcel etc. `function`'s you define will default to the module's scope not `window`).
You can also mix-in multiple data objects using object destructuring:

@@ -199,4 +206,4 @@

| `x-show.transition` | A simultanious fade and scale. (opacity, scale: 0.95, timing-function: cubic-bezier(0.4, 0.0, 0.2, 1), duration-in: 150ms, duration-out: 75ms)
| `x-show.transition.in` | Ony transition in. |
| `x-show.transition.out` | Ony transition out. |
| `x-show.transition.in` | Only transition in. |
| `x-show.transition.out` | Only transition out. |
| `x-show.transition.opacity` | Only use the fade. |

@@ -295,2 +302,7 @@ | `x-show.transition.scale` | Only use the scale. |

**`.self` modifier**
**Example:** `<div x-on:click.self="foo = 'bar'"><button></button></div>`
Adding `.self` to an event listener will only trigger the handler if the `$event.target` is the element itself. In the above example, this means the "click" event that bubbles from the button to the outer `<div>` will **not** run the handler.
**`.window` modifier**

@@ -414,2 +426,4 @@ **Example:** `<div x-on:resize.window="isOpen = window.outerWidth > 768 ? false : open"></div>`

> Note: `x-for` must have a single element root inside of the `<template></template>` tag.
#### Nesting `x-for`s

@@ -457,2 +471,4 @@ You can nest `x-for` loops, but you MUST wrap each loop in an element. For example:

> The example above uses classes from [Tailwind CSS](https://tailwindcss.com)
Alpine offers 6 different transition directives for applying classes to various stages of an element's transition between "hidden" and "shown" states. These directives work both with `x-show` AND `x-if`.

@@ -575,5 +591,9 @@

## v3 Roadmap
* Move from `x-ref` to `ref` for Vue parity
* Move from `x-ref` to `ref` for Vue parity?
* Add `Alpine.directive()`
* Add `Alpine.component('foo', {...})` (With magic `__init()` method)
* Dispatch Alpine events for "loaded", "transition-start", etc... (Original PR: #299) ?
* Remove "object" (and array) syntax from `x-bind:class="{ 'foo': true }"` (PR to add support for object syntax for the `style` attribute: #236)
* Improve `x-for` mutation reactivity (#165)
* Add "deep watching" support in V3 (#294)

@@ -580,0 +600,0 @@ ## License

import { walk, saferEval, saferEvalNoReturn, getXAttrs, debounce } from './utils'
import { handleForDirective } from './directives/for'
import { handleAttributeBindingDirective } from './directives/bind'
import { handleTextDirective } from './directives/text'
import { handleHtmlDirective } from './directives/html'
import { handleShowDirective } from './directives/show'

@@ -72,3 +74,3 @@ import { handleIfDirective } from './directives/if'

if (typeof initReturnedCallback === 'function') {
// Run the callback returned form the "x-init" hook to allow the user to do stuff after
// Run the callback returned from the "x-init" hook to allow the user to do stuff after
// Alpine's got it's grubby little paws all over everything.

@@ -148,2 +150,5 @@ initReturnedCallback.call(this.$data)

// Don't touch spawns from if directives
if (el.__x_inserted_me !== undefined) return false
this.initializeElement(el, extraVars)

@@ -156,6 +161,3 @@ }, el => {

// Walk through the $nextTick stack and clear it as we go.
while (this.nextTickStack.length > 0) {
this.nextTickStack.shift()()
}
this.executeAndClearNextTickStack(rootEl)
}

@@ -186,5 +188,12 @@

// Walk through the $nextTick stack and clear it as we go.
while (this.nextTickStack.length > 0) {
this.nextTickStack.shift()()
this.executeAndClearNextTickStack(rootEl)
}
executeAndClearNextTickStack(el) {
// Skip spawns from alpine directives
if (el === this.$el) {
// Walk through the $nextTick stack and clear it as we go.
while (this.nextTickStack.length > 0) {
this.nextTickStack.shift()()
}
}

@@ -236,2 +245,10 @@ }

let attrs = getXAttrs(el)
if (el.type !== undefined && el.type === 'radio') {
// If there's an x-model on a radio input, move it to end of attribute list
// to ensure that x-bind:value (if present) is processed first.
const modelIdx = attrs.findIndex((attr) => attr.type === 'model')
if (modelIdx > -1) {
attrs.push(attrs.splice(modelIdx, 1)[0])
}
}

@@ -241,3 +258,3 @@ attrs.forEach(({ type, value, modifiers, expression }) => {

case 'model':
handleAttributeBindingDirective(this, el, 'value', expression, extraVars)
handleAttributeBindingDirective(this, el, 'value', expression, extraVars, type)
break;

@@ -249,3 +266,3 @@

handleAttributeBindingDirective(this, el, value, expression, extraVars)
handleAttributeBindingDirective(this, el, value, expression, extraVars, type)
break;

@@ -256,12 +273,7 @@

// If nested model key is undefined, set the default value to empty string.
if (output === undefined && expression.match(/\./).length) {
output = ''
}
el.innerText = output
handleTextDirective(el, output, expression)
break;
case 'html':
el.innerHTML = this.evaluateReturnExpression(el, expression, extraVars)
handleHtmlDirective(this, el, expression, extraVars)
break;

@@ -268,0 +280,0 @@

import { arrayUnique , isBooleanAttr } from '../utils'
export function handleAttributeBindingDirective(component, el, attrName, expression, extraVars) {
export function handleAttributeBindingDirective(component, el, attrName, expression, extraVars, attrType) {
var value = component.evaluateReturnExpression(el, expression, extraVars)

@@ -13,3 +13,10 @@

if (el.type === 'radio') {
el.checked = el.value == value
// Set radio value from x-bind:value, if no "value" attribute exists.
// If there are any initial state values, radio will have a correct
// "checked" value since x-bind:value is processed before x-model.
if (el.attributes.value === undefined && attrType === 'bind') {
el.value = value
} else if (attrType !== 'bind') {
el.checked = el.value == value
}
} else if (el.type === 'checkbox') {

@@ -40,3 +47,8 @@ if (Array.isArray(value)) {

} else {
// Cursor position should be restored back to origin due to a safari bug
const cursorPosition = el.selectionStart
el.value = value
if(el === document.activeElement) {
el.setSelectionRange(cursorPosition, cursorPosition)
}
}

@@ -48,3 +60,7 @@ } else if (attrName === 'class') {

} else if (typeof value === 'object') {
Object.keys(value).forEach(classNames => {
// Sorting the keys / class names by their boolean value will ensure that
// anything that evaluates to `false` and needs to remove classes is run first.
const keysSortedByBooleanValue = Object.keys(value).sort((a, b) => value[a] - value[b]);
keysSortedByBooleanValue.forEach(classNames => {
if (value[classNames]) {

@@ -51,0 +67,0 @@ classNames.split(' ').forEach(className => el.classList.add(className))

import { transitionIn, transitionOut, getXAttrs } from '../utils'
export function handleForDirective(component, el, expression, initialUpdate, extraVars) {
if (el.tagName.toLowerCase() !== 'template') console.warn('Alpine: [x-for] directive should only be added to <template> tags.')
export function handleForDirective(component, templateEl, expression, initialUpdate, extraVars) {
warnIfNotTemplateTag(templateEl)
const { single, bunch, iterator1, iterator2 } = parseFor(expression)
let iteratorNames = parseForExpression(expression)
var items
const ifAttr = getXAttrs(el, 'if')[0]
if (ifAttr && ! component.evaluateReturnExpression(el, ifAttr.expression)) {
// If there is an "x-if" attribute in conjunction with an x-for,
// AND x-if resolves to false, just pretend the x-for is
// empty, effectively hiding it.
items = []
} else {
items = component.evaluateReturnExpression(el, bunch, extraVars)
}
let items = evaluateItemsAndReturnEmptyIfXIfIsPresentAndFalseOnElement(component, templateEl, iteratorNames, extraVars)
// As we walk the array, we'll also walk the DOM (updating/creating as we go).
var previousEl = el
items.forEach((i, index, group) => {
const currentKey = getThisIterationsKeyFromTemplateTag(component, el, single, iterator1, iterator2, i, index, group)
let currentEl = previousEl.nextElementSibling
let currentEl = templateEl
items.forEach((item, index) => {
let iterationScopeVariables = getIterationScopeVariables(iteratorNames, item, index, items, extraVars())
let currentKey = generateKeyForIteration(component, templateEl, index, iterationScopeVariables)
let nextEl = currentEl.nextElementSibling
// Let's check and see if the x-for has already generated an element last time it ran.
if (currentEl && currentEl.__x_for_key !== undefined) {
// If the the key's don't match.
if (currentEl.__x_for_key !== currentKey) {
// We'll look ahead to see if we can find it further down.
var tmpCurrentEl = currentEl
while(tmpCurrentEl) {
// If we found it later in the DOM.
if (tmpCurrentEl.__x_for_key === currentKey) {
// Move it to where it's supposed to be in the DOM.
el.parentElement.insertBefore(tmpCurrentEl, currentEl)
// And set it as the current element as if we just created it.
currentEl = tmpCurrentEl
break
}
// If there's no previously x-for processed element ahead, add one.
if (! nextEl || nextEl.__x_for_key === undefined) {
nextEl = addElementInLoopAfterCurrentEl(templateEl, currentEl)
tmpCurrentEl = (tmpCurrentEl.nextElementSibling && tmpCurrentEl.nextElementSibling.__x_for_key !== undefined) ? tmpCurrentEl.nextElementSibling : false
}
// And transition it in if it's not the first page load.
transitionIn(nextEl, () => {}, initialUpdate)
nextEl.__x_for = iterationScopeVariables
component.initializeElements(nextEl, () => nextEl.__x_for)
} else {
nextEl = lookAheadForMatchingKeyedElementAndMoveItIfFound(nextEl, currentKey)
// If we haven't found a matching key, just insert the element at the current position
if (! nextEl) {
nextEl = addElementInLoopAfterCurrentEl(templateEl, currentEl)
}
// Temporarily remove the key indicator to allow the normal "updateElements" to work
delete currentEl.__x_for_key
delete nextEl.__x_for_key
let xForVars = {}
xForVars[single] = i
if (iterator1) xForVars[iterator1] = index
if (iterator2) xForVars[iterator2] = group
currentEl.__x_for = xForVars
component.updateElements(currentEl, () => {
return currentEl.__x_for
})
} else {
// There are no more .__x_for_key elements, meaning the page is first loading, OR, there are
// extra items in the array that need to be added as new elements.
nextEl.__x_for = iterationScopeVariables
component.updateElements(nextEl, () => nextEl.__x_for)
}
// Let's create a clone from the template.
const clone = document.importNode(el.content, true)
currentEl = nextEl
currentEl.__x_for_key = currentKey
})
if (clone.childElementCount !== 1) console.warn('Alpine: <template> tag with [x-for] encountered with multiple element roots. Make sure <template> only has a single child node.')
removeAnyLeftOverElementsFromPreviousUpdate(currentEl)
}
// Insert it where we are in the DOM.
el.parentElement.insertBefore(clone, currentEl)
// This was taken from VueJS 2.* core. Thanks Vue!
function parseForExpression(expression) {
let forIteratorRE = /,([^,\}\]]*)(?:,([^,\}\]]*))?$/
let stripParensRE = /^\(|\)$/g
let forAliasRE = /([\s\S]*?)\s+(?:in|of)\s+([\s\S]*)/
let inMatch = expression.match(forAliasRE)
if (! inMatch) return
let res = {}
res.items = inMatch[2].trim()
let item = inMatch[1].trim().replace(stripParensRE, '')
let iteratorMatch = item.match(forIteratorRE)
// Set it as the current element.
currentEl = previousEl.nextElementSibling
if (iteratorMatch) {
res.item = item.replace(forIteratorRE, '').trim()
res.index = iteratorMatch[1].trim()
// And transition it in if it's not the first page load.
transitionIn(currentEl, () => {}, initialUpdate)
// Now, let's walk the new DOM node and initialize everything,
// including new nested components.
// Note we are resolving the "extraData" alias stuff from the dom element value so that it's
// always up to date for listener handlers that don't get re-registered.
let xForVars = {}
xForVars[single] = i
if (iterator1) xForVars[iterator1] = index
if (iterator2) xForVars[iterator2] = group
currentEl.__x_for = xForVars
component.initializeElements(currentEl, () => {
return currentEl.__x_for
})
if (iteratorMatch[2]) {
res.collection = iteratorMatch[2].trim()
}
} else {
res.item = item
}
return res
}
currentEl.__x_for_key = currentKey
function getIterationScopeVariables(iteratorNames, item, index, items, extraVars) {
// We must create a new object, so each iteration has a new scope
let scopeVariables = extraVars ? {...extraVars} : {}
scopeVariables[iteratorNames.item] = item
if (iteratorNames.index) scopeVariables[iteratorNames.index] = index
if (iteratorNames.collection) scopeVariables[iteratorNames.collection] = items
previousEl = currentEl
})
return scopeVariables
}
// Now that we've added/updated/moved all the elements for the current state of the loop.
// Anything left over, we can get rid of.
var nextElementFromOldLoop = (previousEl.nextElementSibling && previousEl.nextElementSibling.__x_for_key !== undefined) ? previousEl.nextElementSibling : false
function generateKeyForIteration(component, el, index, iterationScopeVariables) {
let bindKeyAttribute = getXAttrs(el, 'bind').filter(attr => attr.value === 'key')[0]
while(nextElementFromOldLoop) {
const nextElementFromOldLoopImmutable = nextElementFromOldLoop
const nextSibling = nextElementFromOldLoop.nextElementSibling
// If the dev hasn't specified a key, just return the index of the iteration.
if (! bindKeyAttribute) return index
transitionOut(nextElementFromOldLoop, () => {
nextElementFromOldLoopImmutable.remove()
})
return component.evaluateReturnExpression(el, bindKeyAttribute.expression, () => iterationScopeVariables)
}
nextElementFromOldLoop = (nextSibling && nextSibling.__x_for_key !== undefined) ? nextSibling : false
function warnIfNotTemplateTag(el) {
if (el.tagName.toLowerCase() !== 'template') console.warn('Alpine: [x-for] directive should only be added to <template> tags.')
}
function evaluateItemsAndReturnEmptyIfXIfIsPresentAndFalseOnElement(component, el, iteratorNames, extraVars) {
let ifAttribute = getXAttrs(el, 'if')[0]
if (ifAttribute && ! component.evaluateReturnExpression(el, ifAttribute.expression)) {
return []
}
return component.evaluateReturnExpression(el, iteratorNames.items, extraVars)
}
// This was taken from VueJS 2.* core. Thanks Vue!
function parseFor (expression) {
const forIteratorRE = /,([^,\}\]]*)(?:,([^,\}\]]*))?$/
const stripParensRE = /^\(|\)$/g
const forAliasRE = /([\s\S]*?)\s+(?:in|of)\s+([\s\S]*)/
function addElementInLoopAfterCurrentEl(templateEl, currentEl) {
let clone = document.importNode(templateEl.content, true )
const inMatch = expression.match(forAliasRE)
if (! inMatch) return
const res = {}
res.bunch = inMatch[2].trim()
const single = inMatch[1].trim().replace(stripParensRE, '')
const iteratorMatch = single.match(forIteratorRE)
if (iteratorMatch) {
res.single = single.replace(forIteratorRE, '').trim()
res.iterator1 = iteratorMatch[1].trim()
if (iteratorMatch[2]) {
res.iterator2 = iteratorMatch[2].trim()
if (clone.childElementCount !== 1) console.warn('Alpine: <template> tag with [x-for] encountered with multiple element roots. Make sure <template> only has a single child node.')
currentEl.parentElement.insertBefore(clone, currentEl.nextElementSibling)
return currentEl.nextElementSibling
}
function lookAheadForMatchingKeyedElementAndMoveItIfFound(nextEl, currentKey) {
// If the the key's DO match, no need to look ahead.
if (nextEl.__x_for_key === currentKey) return nextEl
// If they don't, we'll look ahead for a match.
// If we find it, we'll move it to the current position in the loop.
let tmpNextEl = nextEl
while(tmpNextEl) {
if (tmpNextEl.__x_for_key === currentKey) {
return tmpNextEl.parentElement.insertBefore(tmpNextEl, nextEl)
}
} else {
res.single = single
tmpNextEl = (tmpNextEl.nextElementSibling && tmpNextEl.nextElementSibling.__x_for_key !== undefined) ? tmpNextEl.nextElementSibling : false
}
return res
}
}
function getThisIterationsKeyFromTemplateTag(component, el, single, iterator1, iterator2, i, index, group) {
const keyAttr = getXAttrs(el, 'bind').filter(attr => attr.value === 'key')[0]
function removeAnyLeftOverElementsFromPreviousUpdate(currentEl) {
var nextElementFromOldLoop = (currentEl.nextElementSibling && currentEl.nextElementSibling.__x_for_key !== undefined) ? currentEl.nextElementSibling : false
let keyAliases = { [single]: i }
if (iterator1) keyAliases[iterator1] = index
if (iterator2) keyAliases[iterator2] = group
return keyAttr
? component.evaluateReturnExpression(el, keyAttr.expression, () => keyAliases)
: index
while (nextElementFromOldLoop) {
let nextElementFromOldLoopImmutable = nextElementFromOldLoop
let nextSibling = nextElementFromOldLoop.nextElementSibling
transitionOut(nextElementFromOldLoop, () => {
nextElementFromOldLoopImmutable.remove()
})
nextElementFromOldLoop = (nextSibling && nextSibling.__x_for_key !== undefined) ? nextSibling : false
}
}

@@ -13,7 +13,7 @@ import { transitionIn, transitionOut } from '../utils'

el.nextElementSibling.__x_inserted_me = true
transitionIn(el.nextElementSibling, () => {}, initialUpdate)
component.initializeElements(el.nextElementSibling, extraVars)
el.nextElementSibling.__x_inserted_me = true
} else if (! expressionResult && elementHasAlreadyBeenAdded) {

@@ -20,0 +20,0 @@ transitionOut(el.nextElementSibling, () => {

@@ -46,9 +46,14 @@ import { kebabCase, debounce, isNumeric } from '../utils'

const returnValue = runListenerHandler(component, expression, e, extraVars)
// If the .self modifier isn't present, or if it is present and
// the target element matches the element we are registering the
// event on, run the handler
if (! modifiers.includes('self') || e.target === el) {
const returnValue = runListenerHandler(component, expression, e, extraVars)
if (returnValue === false) {
e.preventDefault()
} else {
if (modifiers.includes('once')) {
listenerTarget.removeEventListener(event, handler)
if (returnValue === false) {
e.preventDefault()
} else {
if (modifiers.includes('once')) {
listenerTarget.removeEventListener(event, handler)
}
}

@@ -127,4 +132,4 @@ }

default:
return kebabCase(key)
return key && kebabCase(key)
}
}

@@ -15,11 +15,3 @@

export function arrayUnique(array) {
var a = array.concat();
for(var i=0; i<a.length; ++i) {
for(var j=i+1; j<a.length; ++j) {
if(a[i] === a[j])
a.splice(j--, 1);
}
}
return a;
return Array.from(new Set(array))
}

@@ -26,0 +18,0 @@

@@ -162,3 +162,3 @@ import Alpine from 'alpinejs'

expect(document.querySelector('span').classList.contains('foo')).toBeTruthy
expect(document.querySelector('span').classList.contains('foo')).toBeTruthy()
})

@@ -175,4 +175,4 @@

expect(document.querySelector('span').classList.contains('bar')).toBeTruthy
expect(document.querySelector('span').classList.contains('baz')).toBeTruthy
expect(document.querySelector('span').classList.contains('bar')).toBeTruthy()
expect(document.querySelector('span').classList.contains('baz')).toBeTruthy()
})

@@ -335,2 +335,46 @@

expect(document.querySelector('input[name="stringCheckbox"]').value).toEqual('foo')
});
});
test('radio values are set correctly', async () => {
document.body.innerHTML = `
<div x-data="{lists: [{id: 1}, {id: 8}], selectedListID: '8'}">
<template x-for="list in lists" :key="list.id">
<input x-model="selectedListID" type="radio" :value="list.id.toString()" :id="'list-' + list.id">
</template>
<input type="radio" id="list-test" value="test" x-model="selectedListID">
</div>
`
Alpine.start()
expect(document.querySelector('#list-1').value).toEqual('1')
expect(document.querySelector('#list-1').checked).toBeFalsy()
expect(document.querySelector('#list-8').value).toEqual('8')
expect(document.querySelector('#list-8').checked).toBeTruthy()
expect(document.querySelector('#list-test').value).toEqual('test')
expect(document.querySelector('#list-test').checked).toBeFalsy()
});
test('classes are removed before being added', async () => {
document.body.innerHTML = `
<div x-data="{ isOpen: true }">
<span :class="{ 'text-red block': isOpen, 'text-red hidden': !isOpen }">
Span
</span>
<button @click="isOpen = !isOpen"></button>
</div>
`
Alpine.start()
expect(document.querySelector('span').classList.contains('block')).toBeTruthy()
expect(document.querySelector('span').classList.contains('text-red')).toBeTruthy()
document.querySelector('button').click()
await wait(() => {
expect(document.querySelector('span').classList.contains('block')).toBeFalsy()
expect(document.querySelector('span').classList.contains('hidden')).toBeTruthy()
expect(document.querySelector('span').classList.contains('text-red')).toBeTruthy
})
})

@@ -322,1 +322,130 @@ import Alpine from 'alpinejs'

})
test('x-for updates the right elements when new item are inserted at the beginning of the list', async () => {
document.body.innerHTML = `
<div x-data="{ items: [{name: 'one', key: '1'}, {name: 'two', key: '2'}] }">
<button x-on:click="items = [{name: 'zero', key: '0'}, {name: 'one', key: '1'}, {name: 'two', key: '2'}]"></button>
<template x-for="item in items" :key="item.key">
<span x-text="item.name"></span>
</template>
</div>
`
Alpine.start()
expect(document.querySelectorAll('span').length).toEqual(2)
const itemA = document.querySelectorAll('span')[0]
itemA.setAttribute('order', 'first')
const itemB = document.querySelectorAll('span')[1]
itemB.setAttribute('order', 'second')
document.querySelector('button').click()
await wait(() => { expect(document.querySelectorAll('span').length).toEqual(3) })
expect(document.querySelectorAll('span')[0].innerText).toEqual('zero')
expect(document.querySelectorAll('span')[1].innerText).toEqual('one')
expect(document.querySelectorAll('span')[2].innerText).toEqual('two')
// Make sure states are preserved
expect(document.querySelectorAll('span')[0].getAttribute('order')).toEqual(null)
expect(document.querySelectorAll('span')[1].getAttribute('order')).toEqual('first')
expect(document.querySelectorAll('span')[2].getAttribute('order')).toEqual('second')
})
test('nested x-for access outer loop variable', async () => {
document.body.innerHTML = `
<div x-data="{ foos: [ {name: 'foo', bars: ['bob', 'lob']}, {name: 'baz', bars: ['bab', 'lab']} ] }">
<template x-for="foo in foos">
<h1>
<template x-for="bar in foo.bars">
<h2 x-text="foo.name+': '+bar"></h2>
</template>
</h1>
</template>
</div>
`
Alpine.start()
await wait(() => { expect(document.querySelectorAll('h1').length).toEqual(2) })
await wait(() => { expect(document.querySelectorAll('h2').length).toEqual(4) })
expect(document.querySelectorAll('h2')[0].innerText).toEqual('foo: bob')
expect(document.querySelectorAll('h2')[1].innerText).toEqual('foo: lob')
expect(document.querySelectorAll('h2')[2].innerText).toEqual('baz: bab')
expect(document.querySelectorAll('h2')[3].innerText).toEqual('baz: lab')
})
test('nested x-for event listeners', async () => {
document._alerts = []
document.body.innerHTML = `
<div x-data="{ foos: [
{name: 'foo', bars: [{name: 'bob', count: 0}, {name: 'lob', count: 0}]},
{name: 'baz', bars: [{name: 'bab', count: 0}, {name: 'lab', count: 0}]}
], fnText: function(foo, bar) { return foo.name+': '+bar.name+' = '+bar.count; } }">
<template x-for="foo in foos">
<h1>
<template x-for="bar in foo.bars">
<h2 x-text="fnText(foo, bar)"
x-on:click="bar.count += 1; document._alerts.push(fnText(foo, bar))"
></h2>
</template>
</h1>
</template>
</div>
`
Alpine.start()
await wait(() => { expect(document.querySelectorAll('h1').length).toEqual(2) })
await wait(() => { expect(document.querySelectorAll('h2').length).toEqual(4) })
expect(document.querySelectorAll('h2')[0].innerText).toEqual('foo: bob = 0')
expect(document.querySelectorAll('h2')[1].innerText).toEqual('foo: lob = 0')
expect(document.querySelectorAll('h2')[2].innerText).toEqual('baz: bab = 0')
expect(document.querySelectorAll('h2')[3].innerText).toEqual('baz: lab = 0')
expect(document._alerts.length).toEqual(0)
document.querySelectorAll('h2')[0].click()
await wait(() => {
expect(document.querySelectorAll('h2')[0].innerText).toEqual('foo: bob = 1')
expect(document.querySelectorAll('h2')[1].innerText).toEqual('foo: lob = 0')
expect(document.querySelectorAll('h2')[2].innerText).toEqual('baz: bab = 0')
expect(document.querySelectorAll('h2')[3].innerText).toEqual('baz: lab = 0')
expect(document._alerts.length).toEqual(1)
expect(document._alerts[0]).toEqual('foo: bob = 1')
})
document.querySelectorAll('h2')[2].click()
await wait(() => {
expect(document.querySelectorAll('h2')[0].innerText).toEqual('foo: bob = 1')
expect(document.querySelectorAll('h2')[1].innerText).toEqual('foo: lob = 0')
expect(document.querySelectorAll('h2')[2].innerText).toEqual('baz: bab = 1')
expect(document.querySelectorAll('h2')[3].innerText).toEqual('baz: lab = 0')
expect(document._alerts.length).toEqual(2)
expect(document._alerts[0]).toEqual('foo: bob = 1')
expect(document._alerts[1]).toEqual('baz: bab = 1')
})
document.querySelectorAll('h2')[0].click()
await wait(() => {
expect(document.querySelectorAll('h2')[0].innerText).toEqual('foo: bob = 2')
expect(document.querySelectorAll('h2')[1].innerText).toEqual('foo: lob = 0')
expect(document.querySelectorAll('h2')[2].innerText).toEqual('baz: bab = 1')
expect(document.querySelectorAll('h2')[3].innerText).toEqual('baz: lab = 0')
expect(document._alerts.length).toEqual(3)
expect(document._alerts[0]).toEqual('foo: bob = 1')
expect(document._alerts[1]).toEqual('baz: bab = 1')
expect(document._alerts[2]).toEqual('foo: bob = 2')
})
})

@@ -79,1 +79,22 @@ import Alpine from 'alpinejs'

})
test('event listeners are attached once', async () => {
document.body.innerHTML = `
<div x-data="{ count: 0 }">
<span x-text="count"></span>
<template x-if="true">
<button @click="count += 1">Click me</button>
</template>
</div>
`
Alpine.start()
expect(document.querySelector('span').innerText).toEqual(0)
document.querySelector('button').click()
await wait(() => {
expect(document.querySelector('span').innerText).toEqual(1)
})
})

@@ -23,3 +23,25 @@ import Alpine from 'alpinejs'

await wait(() => { expect(document.querySelector('span').innerText).toEqual('bob') })
await wait(() => expect(document.querySelector('span').innerText).toEqual('bob'))
})
test('nextTick wait for x-for to finish rendering', async () => {
document.body.innerHTML = `
<div x-data="{list: ['one', 'two'], check: 2}">
<template x-for="item in list">
<span x-text="item"></span>
</template>
<p x-text="check"></p>
<button x-on:click="list = ['one', 'two', 'three']; $nextTick(() => {check = document.querySelectorAll('span').length})"></button>
</div>
`
Alpine.start()
expect(document.querySelector('p').innerText).toEqual(2)
document.querySelector('button').click()
await wait(() => { expect(document.querySelector('p').innerText).toEqual(3) })
})

@@ -66,2 +66,29 @@ import Alpine from 'alpinejs'

test('.self modifier', async () => {
document.body.innerHTML = `
<div x-data="{ foo: 'bar' }">
<div x-on:click.self="foo = 'baz'" id="selfTarget">
<button></button>
</div>
<span x-text="foo"></span>
</div>
`
Alpine.start()
expect(document.querySelector('span').innerText).toEqual('bar')
document.querySelector('button').click()
await wait(() => {
expect(document.querySelector('span').innerText).toEqual('bar')
})
document.querySelector('#selfTarget').click()
await wait(() => {
expect(document.querySelector('span').innerText).toEqual('baz')
})
})
test('.prevent modifier', async () => {

@@ -412,1 +439,29 @@ document.body.innerHTML = `

})
test('autocomplete event does not trigger keydown with modifier callback', async () => {
document.body.innerHTML = `
<div x-data="{ count: 0 }">
<input type="text" x-on:keydown.?="count++">
<span x-text="count"></span>
</div>
`
Alpine.start()
expect(document.querySelector('span').innerText).toEqual(0)
const autocompleteEvent = new Event('keydown')
fireEvent.keyDown(document.querySelector('input'), { key: 'Enter' })
await wait(() => { expect(document.querySelector('span').innerText).toEqual(0) })
fireEvent.keyDown(document.querySelector('input'), { key: '?' })
await wait(() => { expect(document.querySelector('span').innerText).toEqual(1) })
fireEvent(document.querySelector('input'), autocompleteEvent)
await wait(() => { expect(document.querySelector('span').innerText).toEqual(1) })
})

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is too big to display

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc