Comparing version
{ | ||
"main": "dist/alpine.js", | ||
"name": "alpinejs", | ||
"version": "2.2.5", | ||
"version": "2.3.0", | ||
"repository": { | ||
@@ -6,0 +6,0 @@ "type": "git", |
# Alpine.js | ||
 | ||
 | ||
@@ -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
572471
6.33%55
3.77%11757
6.23%598
3.46%