alpinejs
Version:
The rugged, minimal JavaScript framework
197 lines (157 loc) • 6.73 kB
JavaScript
import { addScopeToNode } from '../scope'
import { evaluateLater } from '../evaluator'
import { directive } from '../directives'
import { reactive } from '../reactivity'
import { initTree, destroyTree } from '../lifecycle'
import { mutateDom } from '../mutation'
import { warn } from '../utils/warn'
import { skipDuringClone } from '../clone'
directive('for', (el, { expression }, { effect, cleanup }) => {
let iteratorNames = parseForExpression(expression)
let evaluateItems = evaluateLater(el, iteratorNames.items)
let evaluateKey = evaluateLater(el,
// the x-bind:key expression is stored for our use instead of evaluated.
el._x_keyExpression || 'index'
)
el._x_lookup = new Map()
effect(() => loop(el, iteratorNames, evaluateItems, evaluateKey))
cleanup(() => {
el._x_lookup.forEach(el =>
mutateDom(() => {
destroyTree(el)
el.remove()
})
)
delete el._x_lookup
})
})
function refreshScope(scope) {
return (newScope) => {
Object.entries(newScope).forEach(([key, value]) => {
scope[key] = value
})
}
}
function loop(templateEl, iteratorNames, evaluateItems, evaluateKey) {
evaluateItems(items => {
// Prepare yourself. There's a lot going on here. Take heart,
// every bit of complexity in this function was added for
// the purpose of making Alpine fast with large datas.
// Support number literals. Ex: x-for="i in 100"
if (isNumeric(items))
items = Array.from({ length: items }, (_, i) => i + 1)
if (items === undefined) items = []
// Support Set and Map objects by converting to arrays.
if (items instanceof Set) items = Array.from(items)
if (items instanceof Map) items = Array.from(items)
// In order to remove elements early we need to generate the key/scope
// pairs up front, moving existing elements from the old lookup to the
// new. This leaves only the elements to be removed in the old lookup.
let oldLookup = templateEl._x_lookup
let lookup = new Map()
templateEl._x_lookup = lookup
let hasStringKeys = isObject(items)
let scopeEntries = Object.entries(items).map(([index, item]) => {
if (! hasStringKeys) index = parseInt(index)
let scope = getIterationScopeVariables(iteratorNames, item, index, items)
let key
evaluateKey(innerKey => {
if (typeof innerKey === 'object')
warn('x-for key cannot be an object, it must be a string or an integer', templateEl)
if (oldLookup.has(innerKey)) {
lookup.set(innerKey, oldLookup.get(innerKey))
oldLookup.delete(innerKey)
}
key = innerKey
}, { scope: { index, ...scope } })
return [key, scope]
})
mutateDom(() => {
oldLookup.forEach((el) => {
destroyTree(el)
el.remove()
})
let added = new Set()
let prev = templateEl
scopeEntries.forEach(([key, scope]) => {
if (lookup.has(key)) {
let el = lookup.get(key)
el._x_refreshXForScope(scope)
if (prev.nextElementSibling !== el) {
if (prev.nextElementSibling)
el.replaceWith(prev.nextElementSibling)
prev.after(el)
}
prev = el
if (el._x_currentIfEl) {
if (el.nextElementSibling !== el._x_currentIfEl)
prev.after(el._x_currentIfEl)
prev = el._x_currentIfEl
}
return
}
if (templateEl.content.children.length > 1)
warn('x-for templates require a single root element, additional elements will be ignored.', templateEl)
let clone = document.importNode(templateEl.content, true).firstElementChild
let reactiveScope = reactive(scope)
addScopeToNode(clone, reactiveScope, templateEl)
clone._x_refreshXForScope = refreshScope(reactiveScope)
lookup.set(key, clone)
added.add(clone)
prev.after(clone)
prev = clone
})
skipDuringClone(() => added.forEach(clone => initTree(clone)))()
})
})
}
// This was taken from VueJS 2.* core. Thanks Vue!
function parseForExpression(expression) {
let forIteratorRE = /,([^,\}\]]*)(?:,([^,\}\]]*))?$/
let stripParensRE = /^\s*\(|\)\s*$/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].replace(stripParensRE, '').trim()
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) {
// We must create a new object, so each iteration has a new scope
let scopeVariables = {}
// Support array destructuring ([foo, bar]).
if (/^\[.*\]$/.test(iteratorNames.item) && Array.isArray(item)) {
let names = iteratorNames.item.replace('[', '').replace(']', '').split(',').map(i => i.trim())
names.forEach((name, i) => {
scopeVariables[name] = item[i]
})
// Support object destructuring ({ foo: 'oof', bar: 'rab' }).
} else if (/^\{.*\}$/.test(iteratorNames.item) && ! Array.isArray(item) && typeof item === 'object') {
let names = iteratorNames.item.replace('{', '').replace('}', '').split(',').map(i => i.trim())
names.forEach(name => {
scopeVariables[name] = item[name]
})
} else {
scopeVariables[iteratorNames.item] = item
}
if (iteratorNames.index) scopeVariables[iteratorNames.index] = index
if (iteratorNames.collection) scopeVariables[iteratorNames.collection] = items
return scopeVariables
}
function isNumeric(subject){
return ! Array.isArray(subject) && ! isNaN(subject)
}
function isObject(subject) {
return typeof subject === 'object' && ! Array.isArray(subject)
}