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