Refactor %for macro handling: improve list rendering logic and key expression evaluation
This commit is contained in:
parent
89bb2ea27d
commit
42aa5a1d75
408
src/main.ts
408
src/main.ts
|
@ -306,7 +306,7 @@ export default (options: ComponentOptions) => {
|
|||
this._setupTwoWayBinding(currentElementNode, expr)
|
||||
else if (macroName === 'if')
|
||||
ifDirectivesToProcess.push({ element: currentElementNode, expr })
|
||||
else if (macroName === 'for')
|
||||
else if (macroName === 'for')
|
||||
this._setupListRendering(currentElementNode, expr)
|
||||
else if (macroName === 'key')
|
||||
return
|
||||
|
@ -381,213 +381,213 @@ export default (options: ComponentOptions) => {
|
|||
}
|
||||
|
||||
// Handle list rendering (%for macro)
|
||||
private _setupListRendering(element: Element, expr: string) {
|
||||
// Parse the expression (e.g., "item in items" or "(item, index) in items")
|
||||
const match = expr.match(/(?:\(([^,]+),\s*([^)]+)\)|([^,\s]+))\s+in\s+(.+)/)
|
||||
if (!match) {
|
||||
console.error(`Invalid %for expression: ${expr}`)
|
||||
return
|
||||
}
|
||||
private _setupListRendering(element: Element, expr: string) {
|
||||
// Parse the expression (e.g., "item in items" or "(item, index) in items")
|
||||
const match = expr.match(/(?:\(([^,]+),\s*([^)]+)\)|([^,\s]+))\s+in\s+(.+)/)
|
||||
if (!match) {
|
||||
console.error(`Invalid %for expression: ${expr}`)
|
||||
return
|
||||
}
|
||||
|
||||
// Extract the item variable name, index variable name (optional), and collection expression
|
||||
const itemVar = match[3] || match[1]
|
||||
const indexVar = match[2] || null
|
||||
const collectionExpr = match[4].trim()
|
||||
// Extract the item variable name, index variable name (optional), and collection expression
|
||||
const itemVar = match[3] || match[1]
|
||||
const indexVar = match[2] || null
|
||||
const collectionExpr = match[4].trim()
|
||||
|
||||
// Create a placeholder comment to mark where the list should be rendered
|
||||
const placeholder = document.createComment(` %for: ${expr} `)
|
||||
element.parentNode?.insertBefore(placeholder, element)
|
||||
|
||||
// Remove the original template element from the DOM
|
||||
const template = element.cloneNode(true) as Element
|
||||
element.parentNode?.removeChild(element)
|
||||
|
||||
// Store current rendered items
|
||||
const renderedItems: Array<{
|
||||
element: Element,
|
||||
key: any,
|
||||
data: any,
|
||||
index: number
|
||||
}> = []
|
||||
|
||||
// Create a function to update the list when the collection changes
|
||||
const updateList = () => {
|
||||
const collection = this._getNestedState(collectionExpr)
|
||||
if (!collection || !Array.isArray(collection)) {
|
||||
console.warn(`Collection "${collectionExpr}" is not an array or does not exist`)
|
||||
return
|
||||
}
|
||||
|
||||
// Get key attribute if available
|
||||
const keyAttr = template.getAttribute('data-laterano-for')
|
||||
|
||||
// Store a map of existing items by key for reuse
|
||||
const existingElementsByKey = new Map()
|
||||
renderedItems.forEach(item => {
|
||||
if (item.key !== undefined) {
|
||||
existingElementsByKey.set(item.key, item)
|
||||
}
|
||||
})
|
||||
|
||||
// Clear rendered items
|
||||
renderedItems.length = 0
|
||||
|
||||
// Create or update items in the list
|
||||
collection.forEach((item, index) => {
|
||||
// Determine the key for this item
|
||||
const key = keyAttr ? this._evaluateKeyExpression(keyAttr, item, index, itemVar) : index
|
||||
|
||||
// Check if we can reuse an existing element
|
||||
const existingItem = existingElementsByKey.get(key)
|
||||
let itemElement: Element
|
||||
|
||||
if (existingItem) {
|
||||
// Reuse existing element
|
||||
itemElement = existingItem.element
|
||||
existingElementsByKey.delete(key) // Remove from map so we know it's been used
|
||||
} else {
|
||||
// Create a new element
|
||||
itemElement = template.cloneNode(true) as Element
|
||||
|
||||
// Process template macros for this new element
|
||||
this._processTemplateMacros(itemElement)
|
||||
}
|
||||
|
||||
// Update item data
|
||||
renderedItems.push({
|
||||
element: itemElement,
|
||||
key,
|
||||
data: item,
|
||||
index
|
||||
})
|
||||
|
||||
// Create rendering context for this item
|
||||
const itemContext = { [itemVar]: item }
|
||||
if (indexVar) {
|
||||
itemContext[indexVar] = index
|
||||
}
|
||||
|
||||
// Apply the item context to the element
|
||||
this._applyItemContext(itemElement, itemContext)
|
||||
|
||||
// Insert the element at the correct position in the DOM
|
||||
placeholder.parentNode?.insertBefore(itemElement, placeholder.nextSibling)
|
||||
})
|
||||
|
||||
// Remove any remaining unused items
|
||||
existingElementsByKey.forEach(item => {
|
||||
if (item.element.parentNode) {
|
||||
item.element.parentNode.removeChild(item.element)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Initial render
|
||||
updateList()
|
||||
|
||||
// Set up state dependency for collection changes
|
||||
if (!this._stateToElementsMap[collectionExpr]) {
|
||||
this._stateToElementsMap[collectionExpr] = new Set()
|
||||
}
|
||||
// Using a unique identifier for this list rendering instance
|
||||
const listVirtualElement = document.createElement('div')
|
||||
this._stateToElementsMap[collectionExpr].add(listVirtualElement as HTMLElement)
|
||||
|
||||
// Add listener for state changes
|
||||
this._statesListeners[collectionExpr] = () => {
|
||||
updateList()
|
||||
}
|
||||
}
|
||||
// Create a placeholder comment to mark where the list should be rendered
|
||||
const placeholder = document.createComment(` %for: ${expr} `)
|
||||
element.parentNode?.insertBefore(placeholder, element)
|
||||
|
||||
// Helper method to evaluate key expressions for list items
|
||||
private _evaluateKeyExpression(keyExpr: string, itemData: any, index: number, itemVar: string): any {
|
||||
try {
|
||||
// If keyExpr is directly the item property, return it
|
||||
if (keyExpr === itemVar) {
|
||||
return itemData
|
||||
}
|
||||
|
||||
// If keyExpr is a property path like "item.id", extract it
|
||||
if (keyExpr.startsWith(itemVar + '.')) {
|
||||
const propertyPath = keyExpr.substring(itemVar.length + 1)
|
||||
const parts = propertyPath.split('.')
|
||||
let value = itemData
|
||||
|
||||
for (const part of parts) {
|
||||
if (value === undefined || value === null) {
|
||||
return undefined
|
||||
}
|
||||
value = value[part]
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
// Otherwise, evaluate as an expression
|
||||
const func = new Function(itemVar, 'index', `return ${keyExpr}`)
|
||||
return func(itemData, index)
|
||||
} catch (error) {
|
||||
console.error(`Error evaluating key expression: ${keyExpr}`, error)
|
||||
return index // Fallback to index as key
|
||||
}
|
||||
}
|
||||
// Remove the original template element from the DOM
|
||||
const template = element.cloneNode(true) as Element
|
||||
element.parentNode?.removeChild(element)
|
||||
|
||||
// Helper method to apply item context to elements
|
||||
private _applyItemContext(element: Element, itemContext: Record<string, any>) {
|
||||
// Store the item context on the element
|
||||
(element as any)._itemContext = itemContext
|
||||
|
||||
// Update text nodes with handlebars expressions
|
||||
const updateTextNodes = (node: Node) => {
|
||||
if (node.nodeType === Node.TEXT_NODE) {
|
||||
const textContent = node.textContent || ''
|
||||
|
||||
if (textContent.includes('{{')) {
|
||||
const textNode = node as Text
|
||||
const originalContent = textContent
|
||||
|
||||
// Replace expressions with values from item context
|
||||
let newContent = originalContent.replace(/\{\{\s*([^}]+)\s*\}\}/g, (match, expr) => {
|
||||
// Check if expression references item context
|
||||
const contextVarNames = Object.keys(itemContext)
|
||||
const usesContext = contextVarNames.some(varName => expr.includes(varName))
|
||||
|
||||
if (usesContext) {
|
||||
try {
|
||||
// Create a function that evaluates the expression with the item context
|
||||
const contextValues = Object.values(itemContext)
|
||||
const func = new Function(...contextVarNames, `return ${expr.trim()}`)
|
||||
const result = func(...contextValues)
|
||||
return result !== undefined ? String(result) : ''
|
||||
} catch (error) {
|
||||
console.error(`Error evaluating expression in list item: ${expr}`, error)
|
||||
return ''
|
||||
}
|
||||
} else {
|
||||
// Use the regular state value if not from item context
|
||||
const value = this._getNestedState(expr.trim())
|
||||
return value !== undefined ? String(value) : ''
|
||||
}
|
||||
})
|
||||
|
||||
textNode.textContent = newContent
|
||||
}
|
||||
}
|
||||
|
||||
// Recursively process child nodes
|
||||
const childNodes = node.childNodes
|
||||
for (let i = 0; i < childNodes.length; i++) {
|
||||
updateTextNodes(childNodes[i])
|
||||
}
|
||||
}
|
||||
|
||||
// Update text nodes
|
||||
updateTextNodes(element)
|
||||
|
||||
// Also handle event handlers and other bindings if needed
|
||||
// This is more complex and would require extending other methods
|
||||
// to be aware of the item context
|
||||
}
|
||||
// Store current rendered items
|
||||
const renderedItems: Array<{
|
||||
element: Element,
|
||||
key: any,
|
||||
data: any,
|
||||
index: number
|
||||
}> = []
|
||||
|
||||
// Create a function to update the list when the collection changes
|
||||
const updateList = () => {
|
||||
const collection = this._getNestedState(collectionExpr)
|
||||
if (!collection || !Array.isArray(collection)) {
|
||||
console.warn(`Collection "${collectionExpr}" is not an array or does not exist`)
|
||||
return
|
||||
}
|
||||
|
||||
// Get key attribute if available
|
||||
const keyAttr = template.getAttribute('data-laterano-for')
|
||||
|
||||
// Store a map of existing items by key for reuse
|
||||
const existingElementsByKey = new Map()
|
||||
renderedItems.forEach(item => {
|
||||
if (item.key !== undefined) {
|
||||
existingElementsByKey.set(item.key, item)
|
||||
}
|
||||
})
|
||||
|
||||
// Clear rendered items
|
||||
renderedItems.length = 0
|
||||
|
||||
// Create or update items in the list
|
||||
collection.forEach((item, index) => {
|
||||
// Determine the key for this item
|
||||
const key = keyAttr ? this._evaluateKeyExpression(keyAttr, item, index, itemVar) : index
|
||||
|
||||
// Check if we can reuse an existing element
|
||||
const existingItem = existingElementsByKey.get(key)
|
||||
let itemElement: Element
|
||||
|
||||
if (existingItem) {
|
||||
// Reuse existing element
|
||||
itemElement = existingItem.element
|
||||
existingElementsByKey.delete(key) // Remove from map so we know it's been used
|
||||
} else {
|
||||
// Create a new element
|
||||
itemElement = template.cloneNode(true) as Element
|
||||
|
||||
// Process template macros for this new element
|
||||
this._processTemplateMacros(itemElement)
|
||||
}
|
||||
|
||||
// Update item data
|
||||
renderedItems.push({
|
||||
element: itemElement,
|
||||
key,
|
||||
data: item,
|
||||
index
|
||||
})
|
||||
|
||||
// Create rendering context for this item
|
||||
const itemContext = { [itemVar]: item }
|
||||
if (indexVar) {
|
||||
itemContext[indexVar] = index
|
||||
}
|
||||
|
||||
// Apply the item context to the element
|
||||
this._applyItemContext(itemElement, itemContext)
|
||||
|
||||
// Insert the element at the correct position in the DOM
|
||||
placeholder.parentNode?.insertBefore(itemElement, placeholder.nextSibling)
|
||||
})
|
||||
|
||||
// Remove any remaining unused items
|
||||
existingElementsByKey.forEach(item => {
|
||||
if (item.element.parentNode) {
|
||||
item.element.parentNode.removeChild(item.element)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Initial render
|
||||
updateList()
|
||||
|
||||
// Set up state dependency for collection changes
|
||||
if (!this._stateToElementsMap[collectionExpr]) {
|
||||
this._stateToElementsMap[collectionExpr] = new Set()
|
||||
}
|
||||
// Using a unique identifier for this list rendering instance
|
||||
const listVirtualElement = document.createElement('div')
|
||||
this._stateToElementsMap[collectionExpr].add(listVirtualElement as HTMLElement)
|
||||
|
||||
// Add listener for state changes
|
||||
this._statesListeners[collectionExpr] = () => {
|
||||
updateList()
|
||||
}
|
||||
}
|
||||
|
||||
// Helper method to evaluate key expressions for list items
|
||||
private _evaluateKeyExpression(keyExpr: string, itemData: any, index: number, itemVar: string): any {
|
||||
try {
|
||||
// If keyExpr is directly the item property, return it
|
||||
if (keyExpr === itemVar) {
|
||||
return itemData
|
||||
}
|
||||
|
||||
// If keyExpr is a property path like "item.id", extract it
|
||||
if (keyExpr.startsWith(itemVar + '.')) {
|
||||
const propertyPath = keyExpr.substring(itemVar.length + 1)
|
||||
const parts = propertyPath.split('.')
|
||||
let value = itemData
|
||||
|
||||
for (const part of parts) {
|
||||
if (value === undefined || value === null) {
|
||||
return undefined
|
||||
}
|
||||
value = value[part]
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
// Otherwise, evaluate as an expression
|
||||
const func = new Function(itemVar, 'index', `return ${keyExpr}`)
|
||||
return func(itemData, index)
|
||||
} catch (error) {
|
||||
console.error(`Error evaluating key expression: ${keyExpr}`, error)
|
||||
return index // Fallback to index as key
|
||||
}
|
||||
}
|
||||
|
||||
// Helper method to apply item context to elements
|
||||
private _applyItemContext(element: Element, itemContext: Record<string, any>) {
|
||||
// Store the item context on the element
|
||||
(element as any)._itemContext = itemContext
|
||||
|
||||
// Update text nodes with handlebars expressions
|
||||
const updateTextNodes = (node: Node) => {
|
||||
if (node.nodeType === Node.TEXT_NODE) {
|
||||
const textContent = node.textContent || ''
|
||||
|
||||
if (textContent.includes('{{')) {
|
||||
const textNode = node as Text
|
||||
const originalContent = textContent
|
||||
|
||||
// Replace expressions with values from item context
|
||||
let newContent = originalContent.replace(/\{\{\s*([^}]+)\s*\}\}/g, (match, expr) => {
|
||||
// Check if expression references item context
|
||||
const contextVarNames = Object.keys(itemContext)
|
||||
const usesContext = contextVarNames.some(varName => expr.includes(varName))
|
||||
|
||||
if (usesContext) {
|
||||
try {
|
||||
// Create a function that evaluates the expression with the item context
|
||||
const contextValues = Object.values(itemContext)
|
||||
const func = new Function(...contextVarNames, `return ${expr.trim()}`)
|
||||
const result = func(...contextValues)
|
||||
return result !== undefined ? String(result) : ''
|
||||
} catch (error) {
|
||||
console.error(`Error evaluating expression in list item: ${expr}`, error)
|
||||
return ''
|
||||
}
|
||||
} else {
|
||||
// Use the regular state value if not from item context
|
||||
const value = this._getNestedState(expr.trim())
|
||||
return value !== undefined ? String(value) : ''
|
||||
}
|
||||
})
|
||||
|
||||
textNode.textContent = newContent
|
||||
}
|
||||
}
|
||||
|
||||
// Recursively process child nodes
|
||||
const childNodes = node.childNodes
|
||||
for (let i = 0; i < childNodes.length; i++) {
|
||||
updateTextNodes(childNodes[i])
|
||||
}
|
||||
}
|
||||
|
||||
// Update text nodes
|
||||
updateTextNodes(element)
|
||||
|
||||
// Also handle event handlers and other bindings if needed
|
||||
// This is more complex and would require extending other methods
|
||||
// to be aware of the item context
|
||||
}
|
||||
|
||||
private _evaluateIfCondition(element: Element, condition: string) {
|
||||
const info = this._conditionalElements.get(element)
|
||||
|
|
Loading…
Reference in New Issue
Block a user