UNPKG

alpinejs

Version:
147 lines (112 loc) 5.93 kB
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 } }