diff options
Diffstat (limited to 'alpinejs/packages/alpinejs/src/directives/x-for.js')
-rw-r--r-- | alpinejs/packages/alpinejs/src/directives/x-for.js | 262 |
1 files changed, 262 insertions, 0 deletions
diff --git a/alpinejs/packages/alpinejs/src/directives/x-for.js b/alpinejs/packages/alpinejs/src/directives/x-for.js new file mode 100644 index 0000000..504306d --- /dev/null +++ b/alpinejs/packages/alpinejs/src/directives/x-for.js @@ -0,0 +1,262 @@ +import { addScopeToNode, refreshScope } from '../scope' +import { evaluateLater } from '../evaluator' +import { directive } from '../directives' +import { reactive } from '../reactivity' +import { initTree } from '../lifecycle' +import { mutateDom } from '../mutation' +import { flushJobs } from '../scheduler' +import { warn } from '../utils/warn' + +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_prevKeys = [] + el._x_lookup = {} + + effect(() => loop(el, iteratorNames, evaluateItems, evaluateKey)) + + cleanup(() => { + Object.values(el._x_lookup).forEach(el => el.remove()) + + delete el._x_prevKeys + delete el._x_lookup + }) +}) + +let shouldFastRender = true + +function loop(el, iteratorNames, evaluateItems, evaluateKey) { + let isObject = i => typeof i === 'object' && ! Array.isArray(i) + let templateEl = el + + 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 >= 0) { + items = Array.from(Array(items).keys(), i => i + 1) + } + + if (items === undefined) items = [] + + let lookup = el._x_lookup + let prevKeys = el._x_prevKeys + let scopes = [] + let keys = [] + + // In order to preserve DOM elements (move instead of replace) + // we need to generate all the keys for every iteration up + // front. These will be our source of truth for diffing. + if (isObject(items)) { + items = Object.entries(items).map(([key, value]) => { + let scope = getIterationScopeVariables(iteratorNames, value, key, items) + + evaluateKey(value => keys.push(value), { scope: { index: key, ...scope} }) + + scopes.push(scope) + }) + } else { + for (let i = 0; i < items.length; i++) { + let scope = getIterationScopeVariables(iteratorNames, items[i], i, items) + + evaluateKey(value => keys.push(value), { scope: { index: i, ...scope} }) + + scopes.push(scope) + } + } + + // Rather than making DOM manipulations inside one large loop, we'll + // instead track which mutations need to be made in the following + // arrays. After we're finished, we can batch them at the end. + let adds = [] + let moves = [] + let removes = [] + let sames = [] + + // First, we track elements that will need to be removed. + for (let i = 0; i < prevKeys.length; i++) { + let key = prevKeys[i] + + if (keys.indexOf(key) === -1) removes.push(key) + } + + // Notice we're mutating prevKeys as we go. This makes it + // so that we can efficiently make incremental comparisons. + prevKeys = prevKeys.filter(key => ! removes.includes(key)) + + let lastKey = 'template' + + // This is the important part of the diffing algo. Identifying + // which keys (future DOM elements) are new, which ones have + // or haven't moved (noting where they moved to / from). + for (let i = 0; i < keys.length; i++) { + let key = keys[i] + + let prevIndex = prevKeys.indexOf(key) + + if (prevIndex === -1) { + // New key found. + prevKeys.splice(i, 0, key) + + adds.push([lastKey, i]) + } else if (prevIndex !== i) { + // A key has moved. + let keyInSpot = prevKeys.splice(i, 1)[0] + let keyForSpot = prevKeys.splice(prevIndex - 1, 1)[0] + + prevKeys.splice(i, 0, keyForSpot) + prevKeys.splice(prevIndex, 0, keyInSpot) + + moves.push([keyInSpot, keyForSpot]) + } else { + // This key hasn't moved, but we'll still keep track + // so that we can refresh it later on. + sames.push(key) + } + + lastKey = key + } + + // Now that we've done the diffing work, we can apply the mutations + // in batches for both separating types work and optimizing + // for browser performance. + + // We'll remove all the nodes that need to be removed, + // letting the mutation observer pick them up and + // clean up any side effects they had. + for (let i = 0; i < removes.length; i++) { + let key = removes[i] + + lookup[key].remove() + + lookup[key] = null + delete lookup[key] + } + + // Here we'll move elements around, skipping + // mutation observer triggers by using "mutateDom". + for (let i = 0; i < moves.length; i++) { + let [keyInSpot, keyForSpot] = moves[i] + + let elInSpot = lookup[keyInSpot] + let elForSpot = lookup[keyForSpot] + + let marker = document.createElement('div') + + mutateDom(() => { + elForSpot.after(marker) + elInSpot.after(elForSpot) + marker.before(elInSpot) + marker.remove() + }) + + refreshScope(elForSpot, scopes[keys.indexOf(keyForSpot)]) + } + + // We can now create and add new elements. + for (let i = 0; i < adds.length; i++) { + let [lastKey, index] = adds[i] + + let lastEl = (lastKey === 'template') ? templateEl : lookup[lastKey] + + let scope = scopes[index] + let key = keys[index] + + let clone = document.importNode(templateEl.content, true).firstElementChild + + addScopeToNode(clone, reactive(scope), templateEl) + + mutateDom(() => { + lastEl.after(clone) + + initTree(clone) + }) + + if (typeof key === 'object') { + warn('x-for key cannot be an object, it must be a string or an integer', templateEl) + } + + lookup[key] = clone + } + + // If an element hasn't changed, we still want to "refresh" the + // data it depends on in case the data has changed in an + // "unobservable" way. + for (let i = 0; i < sames.length; i++) { + refreshScope(lookup[sames[i]], scopes[keys.indexOf(sames[i])]) + } + + // Now we'll log the keys (and the order they're in) for comparing + // against next time. + templateEl._x_prevKeys = keys + }) +} + +// 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) +} |