alpinejs
Version:
<div dir="rtl">
147 lines (112 loc) • 5.93 kB
JavaScript
import { transitionIn, transitionOut, getXAttrs, warnIfMalformedTemplate, isNumeric } from '../utils'
export function handleForDirective(component, templateEl, expression, initialUpdate, extraVars) {
warnIfMalformedTemplate(templateEl, 'x-for')
let iteratorNames = typeof expression === 'function'
? parseForExpression(component.evaluateReturnExpression(templateEl, expression))
: parseForExpression(expression)
let items = evaluateItemsAndReturnEmptyIfXIfIsPresentAndFalseOnElement(component, templateEl, iteratorNames, extraVars)
// As we walk the array, we'll also walk the DOM (updating/creating as we go).
let currentEl = templateEl
items.forEach((item, index) => {
let iterationScopeVariables = getIterationScopeVariables(iteratorNames, item, index, items, extraVars())
let currentKey = generateKeyForIteration(component, templateEl, index, iterationScopeVariables)
let nextEl = lookAheadForMatchingKeyedElementAndMoveItIfFound(currentEl.nextElementSibling, currentKey)
// If we haven't found a matching key, insert the element at the current position.
if (! nextEl) {
nextEl = addElementInLoopAfterCurrentEl(templateEl, currentEl)
// And transition it in if it's not the first page load.
transitionIn(nextEl, () => {}, () => {}, component, initialUpdate)
nextEl.__x_for = iterationScopeVariables
component.initializeElements(nextEl, () => nextEl.__x_for)
// Otherwise update the element we found.
} else {
// Temporarily remove the key indicator to allow the normal "updateElements" to work.
delete nextEl.__x_for_key
nextEl.__x_for = iterationScopeVariables
component.updateElements(nextEl, () => nextEl.__x_for)
}
currentEl = nextEl
currentEl.__x_for_key = currentKey
})
removeAnyLeftOverElementsFromPreviousUpdate(currentEl, component)
}
// 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 = String(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)
if (iteratorMatch) {
res.item = item.replace(forIteratorRE, '').trim()
res.index = iteratorMatch[1].trim()
if (iteratorMatch[2]) {
res.collection = iteratorMatch[2].trim()
}
} else {
res.item = item
}
return res
}
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
return scopeVariables
}
function generateKeyForIteration(component, el, index, iterationScopeVariables) {
let bindKeyAttribute = getXAttrs(el, component, 'bind').filter(attr => attr.value === 'key')[0]
// If the dev hasn't specified a key, just return the index of the iteration.
if (! bindKeyAttribute) return index
return component.evaluateReturnExpression(el, bindKeyAttribute.expression, () => iterationScopeVariables)
}
function evaluateItemsAndReturnEmptyIfXIfIsPresentAndFalseOnElement(component, el, iteratorNames, extraVars) {
let ifAttribute = getXAttrs(el, component, 'if')[0]
if (ifAttribute && ! component.evaluateReturnExpression(el, ifAttribute.expression)) {
return []
}
let items = component.evaluateReturnExpression(el, iteratorNames.items, extraVars)
// This adds support for the `i in n` syntax.
if (isNumeric(items) && items >= 0) {
items = Array.from(Array(items).keys(), i => i + 1)
}
return items
}
function addElementInLoopAfterCurrentEl(templateEl, currentEl) {
let clone = document.importNode(templateEl.content, true )
currentEl.parentElement.insertBefore(clone, currentEl.nextElementSibling)
return currentEl.nextElementSibling
}
function lookAheadForMatchingKeyedElementAndMoveItIfFound(nextEl, currentKey) {
if (! nextEl) return
// If we are already past the x-for generated elements, we don't need to look ahead.
if (nextEl.__x_for_key === undefined) return
// 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)
}
tmpNextEl = (tmpNextEl.nextElementSibling && tmpNextEl.nextElementSibling.__x_for_key !== undefined) ? tmpNextEl.nextElementSibling : false
}
}
function removeAnyLeftOverElementsFromPreviousUpdate(currentEl, component) {
var nextElementFromOldLoop = (currentEl.nextElementSibling && currentEl.nextElementSibling.__x_for_key !== undefined) ? currentEl.nextElementSibling : false
while (nextElementFromOldLoop) {
let nextElementFromOldLoopImmutable = nextElementFromOldLoop
let nextSibling = nextElementFromOldLoop.nextElementSibling
transitionOut(nextElementFromOldLoop, () => {
nextElementFromOldLoopImmutable.remove()
}, () => {}, component)
nextElementFromOldLoop = (nextSibling && nextSibling.__x_for_key !== undefined) ? nextSibling : false
}
}