Comparing version 2.2.5 to 2.3.0
{ | ||
"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
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
572471
55
11757
598