refactor: remove list rendering setup from component options and related utility
This commit is contained in:
		
							parent
							
								
									3b344af52d
								
							
						
					
					
						commit
						8f6b4a6cd9
					
				
							
								
								
									
										428
									
								
								src/main.ts
									
									
									
									
									
								
							
							
						
						
									
										428
									
								
								src/main.ts
									
									
									
									
									
								
							| 
						 | 
					@ -140,7 +140,6 @@ export default (options: ComponentOptions) => {
 | 
				
			||||||
				setupArrowFunctionHandler: this._setupArrowFunctionHandler.bind(this),
 | 
									setupArrowFunctionHandler: this._setupArrowFunctionHandler.bind(this),
 | 
				
			||||||
				setupExpressionHandler: this._setupExpressionHandler.bind(this),
 | 
									setupExpressionHandler: this._setupExpressionHandler.bind(this),
 | 
				
			||||||
				setupFunctionCallHandler: this._setupFunctionCallHandler.bind(this),
 | 
									setupFunctionCallHandler: this._setupFunctionCallHandler.bind(this),
 | 
				
			||||||
				setupListRendering: this._setupListRendering.bind(this),
 | 
					 | 
				
			||||||
				stateToElementsMap: this._stateToElementsMap,
 | 
									stateToElementsMap: this._stateToElementsMap,
 | 
				
			||||||
				textBindings: this._textBindings,
 | 
									textBindings: this._textBindings,
 | 
				
			||||||
				availableFuncs: Object.getOwnPropertyNames(
 | 
									availableFuncs: Object.getOwnPropertyNames(
 | 
				
			||||||
| 
						 | 
					@ -189,433 +188,6 @@ 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
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			// 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: unknown
 | 
					 | 
				
			||||||
				data: unknown
 | 
					 | 
				
			||||||
				index: number
 | 
					 | 
				
			||||||
			}> = []
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			// Create a function to update the list when the collection changes
 | 
					 | 
				
			||||||
			const updateList = () => {
 | 
					 | 
				
			||||||
				const collection = this._evaluateExpression(collectionExpr)
 | 
					 | 
				
			||||||
				if (!collection || !Array.isArray(collection)) {
 | 
					 | 
				
			||||||
					console.warn(
 | 
					 | 
				
			||||||
						`Collection "${collectionExpr}" is not an array or does not exist`,
 | 
					 | 
				
			||||||
					)
 | 
					 | 
				
			||||||
					return
 | 
					 | 
				
			||||||
				}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
				const parentNode = placeholder.parentNode
 | 
					 | 
				
			||||||
				if (!parentNode) {
 | 
					 | 
				
			||||||
					console.error("Placeholder's parentNode is null. Cannot update list.")
 | 
					 | 
				
			||||||
					return
 | 
					 | 
				
			||||||
				}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
				// Detach all currently rendered DOM items managed by this instance.
 | 
					 | 
				
			||||||
				for (const item of renderedItems)
 | 
					 | 
				
			||||||
					if (item.element.parentNode === parentNode)
 | 
					 | 
				
			||||||
						parentNode.removeChild(item.element)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
				// Get key attribute if available
 | 
					 | 
				
			||||||
				const keyAttr = template.getAttribute('%key')
 | 
					 | 
				
			||||||
				if (!keyAttr)
 | 
					 | 
				
			||||||
					console.warn(
 | 
					 | 
				
			||||||
						'%key attribute not found in the template, which is not a recommended practice.',
 | 
					 | 
				
			||||||
					)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
				// Store a map of existing items by key for reuse
 | 
					 | 
				
			||||||
				const existingElementsByKey = new Map()
 | 
					 | 
				
			||||||
				// renderedItems.forEach((item) => {
 | 
					 | 
				
			||||||
				for (const item of renderedItems)
 | 
					 | 
				
			||||||
					if (item.key !== undefined) existingElementsByKey.set(item.key, item)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
				// Clear rendered items
 | 
					 | 
				
			||||||
				renderedItems.length = 0
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
				// document fragment
 | 
					 | 
				
			||||||
				const fragment = document.createDocumentFragment()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
				// Create or update items in the list
 | 
					 | 
				
			||||||
				collection.forEach((item, index) => {
 | 
					 | 
				
			||||||
					// Determine the key for this item
 | 
					 | 
				
			||||||
					const key = keyAttr
 | 
					 | 
				
			||||||
						? this._evaluateExpressionWithItemContext(
 | 
					 | 
				
			||||||
								keyAttr ?? '',
 | 
					 | 
				
			||||||
								item,
 | 
					 | 
				
			||||||
								index,
 | 
					 | 
				
			||||||
								itemVar,
 | 
					 | 
				
			||||||
								indexVar ? indexVar : undefined,
 | 
					 | 
				
			||||||
							)
 | 
					 | 
				
			||||||
						: 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
 | 
					 | 
				
			||||||
					}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
					// Update item data
 | 
					 | 
				
			||||||
					renderedItems.push({
 | 
					 | 
				
			||||||
						element: itemElement,
 | 
					 | 
				
			||||||
						key,
 | 
					 | 
				
			||||||
						data: item,
 | 
					 | 
				
			||||||
						index,
 | 
					 | 
				
			||||||
					})
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
					// Create item context for this item
 | 
					 | 
				
			||||||
					const itemContext = {
 | 
					 | 
				
			||||||
						[itemVar]: item,
 | 
					 | 
				
			||||||
					}
 | 
					 | 
				
			||||||
					if (indexVar) itemContext[indexVar] = index
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
					// insert %key attribute, which dynamically bind the key
 | 
					 | 
				
			||||||
					if (keyAttr) {
 | 
					 | 
				
			||||||
						const keyValue = this._evaluateExpressionWithItemContext(
 | 
					 | 
				
			||||||
							keyAttr,
 | 
					 | 
				
			||||||
							itemContext,
 | 
					 | 
				
			||||||
						)
 | 
					 | 
				
			||||||
						itemElement.setAttribute('data-laterano-key', String(keyValue))
 | 
					 | 
				
			||||||
					}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
					// remove original %key attribute
 | 
					 | 
				
			||||||
					itemElement.removeAttribute('%key')
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
					// Apply the item context to the element
 | 
					 | 
				
			||||||
					// We will use recursive processing here!
 | 
					 | 
				
			||||||
					this._processElementWithItemContext(itemElement, itemContext)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
					// Insert the element to the document fragment
 | 
					 | 
				
			||||||
					fragment.appendChild(itemElement)
 | 
					 | 
				
			||||||
				})
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
				// Insert the document fragment into the DOM
 | 
					 | 
				
			||||||
				placeholder.parentNode?.insertBefore(fragment, placeholder.nextSibling)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
				// Remove any remaining unused items
 | 
					 | 
				
			||||||
				// existingElementsByKey.forEach((item) => {
 | 
					 | 
				
			||||||
				for (const item of existingElementsByKey.values())
 | 
					 | 
				
			||||||
					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()
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		// Recursively process the element and its children, applying the item context
 | 
					 | 
				
			||||||
		private _processElementWithItemContext(
 | 
					 | 
				
			||||||
			element: Element,
 | 
					 | 
				
			||||||
			itemContext: Record<string, unknown>,
 | 
					 | 
				
			||||||
		) {
 | 
					 | 
				
			||||||
			// 1. Store the item context of the element so that subsequent updates can find it
 | 
					 | 
				
			||||||
			;(element as { _itemContext?: Record<string, unknown> })._itemContext =
 | 
					 | 
				
			||||||
				itemContext
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			// 2. Process bindings in text nodes
 | 
					 | 
				
			||||||
			const processTextNodes = (node: Node) => {
 | 
					 | 
				
			||||||
				if (node.nodeType === Node.TEXT_NODE) {
 | 
					 | 
				
			||||||
					const textContent = node.textContent || ''
 | 
					 | 
				
			||||||
					if (textContent.includes('{{')) {
 | 
					 | 
				
			||||||
						const textNode = node as Text
 | 
					 | 
				
			||||||
						const updatedContent = textContent.replace(
 | 
					 | 
				
			||||||
							/\{\{\s*([^}]+)\s*\}\}/g,
 | 
					 | 
				
			||||||
							(match, expr) => {
 | 
					 | 
				
			||||||
								const value = this._evaluateExpressionWithItemContext(
 | 
					 | 
				
			||||||
									expr.trim(),
 | 
					 | 
				
			||||||
									itemContext,
 | 
					 | 
				
			||||||
								)
 | 
					 | 
				
			||||||
								return value !== undefined ? String(value) : ''
 | 
					 | 
				
			||||||
							},
 | 
					 | 
				
			||||||
						)
 | 
					 | 
				
			||||||
						textNode.textContent = updatedContent
 | 
					 | 
				
			||||||
					}
 | 
					 | 
				
			||||||
				}
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			// Process the text nodes of the element itself
 | 
					 | 
				
			||||||
			// Array.from(element.childNodes).forEach((node) => {
 | 
					 | 
				
			||||||
			for (const node of Array.from(element.childNodes))
 | 
					 | 
				
			||||||
				if (node.nodeType === Node.TEXT_NODE) processTextNodes(node)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			// 3. Process attribute bindings (:attr)
 | 
					 | 
				
			||||||
			// Array.from(element.attributes).forEach((attr) => {
 | 
					 | 
				
			||||||
			for (const attr of Array.from(element.attributes)) {
 | 
					 | 
				
			||||||
				if (attr.name.startsWith(':')) {
 | 
					 | 
				
			||||||
					const attrName = attr.name.substring(1)
 | 
					 | 
				
			||||||
					const expr = attr.value.trim()
 | 
					 | 
				
			||||||
					const value = this._evaluateExpressionWithItemContext(
 | 
					 | 
				
			||||||
						expr,
 | 
					 | 
				
			||||||
						itemContext,
 | 
					 | 
				
			||||||
					)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
					if (value !== undefined) element.setAttribute(attrName, String(value))
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
					// Remove the original binding attribute (execute only for cloned templates once)
 | 
					 | 
				
			||||||
					element.removeAttribute(attr.name)
 | 
					 | 
				
			||||||
				}
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			// 4. Process event bindings (@event)
 | 
					 | 
				
			||||||
			// Array.from(element.attributes).forEach((attr) => {
 | 
					 | 
				
			||||||
			for (const attr of Array.from(element.attributes)) {
 | 
					 | 
				
			||||||
				if (attr.name.startsWith('@')) {
 | 
					 | 
				
			||||||
					const eventName = attr.name.substring(1)
 | 
					 | 
				
			||||||
					const handlerValue = attr.value.trim()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
					// Remove the original binding attribute
 | 
					 | 
				
			||||||
					element.removeAttribute(attr.name)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
					// Add event listener
 | 
					 | 
				
			||||||
					element.addEventListener(eventName, (event: Event) => {
 | 
					 | 
				
			||||||
						try {
 | 
					 | 
				
			||||||
							// Create a merged context
 | 
					 | 
				
			||||||
							const mergedContext = {
 | 
					 | 
				
			||||||
								...this._createHandlerContext(event, element),
 | 
					 | 
				
			||||||
								...itemContext,
 | 
					 | 
				
			||||||
								$event: event,
 | 
					 | 
				
			||||||
								$el: element,
 | 
					 | 
				
			||||||
							}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
							// Execute the expression
 | 
					 | 
				
			||||||
							const fnStr = `with(this) { ${handlerValue} }`
 | 
					 | 
				
			||||||
							new Function(fnStr).call(mergedContext)
 | 
					 | 
				
			||||||
						} catch (err) {
 | 
					 | 
				
			||||||
							console.error(
 | 
					 | 
				
			||||||
								`Error executing event handler with item context: ${handlerValue}`,
 | 
					 | 
				
			||||||
								err,
 | 
					 | 
				
			||||||
							)
 | 
					 | 
				
			||||||
						}
 | 
					 | 
				
			||||||
					})
 | 
					 | 
				
			||||||
				}
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			// 5. Process conditional rendering (%if)
 | 
					 | 
				
			||||||
			let isConditional = false
 | 
					 | 
				
			||||||
			let shouldDisplay = true
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			// Array.from(element.attributes).forEach((attr) => {
 | 
					 | 
				
			||||||
			for (const attr of Array.from(element.attributes)) {
 | 
					 | 
				
			||||||
				if (attr.name === '%if') {
 | 
					 | 
				
			||||||
					isConditional = true
 | 
					 | 
				
			||||||
					const expr = attr.value.trim()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
					// Remove the original binding attribute
 | 
					 | 
				
			||||||
					element.removeAttribute(attr.name)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
					// Calculate the condition
 | 
					 | 
				
			||||||
					const result = this._evaluateExpressionWithItemContext(
 | 
					 | 
				
			||||||
						expr,
 | 
					 | 
				
			||||||
						itemContext,
 | 
					 | 
				
			||||||
					)
 | 
					 | 
				
			||||||
					shouldDisplay = Boolean(result)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
					// Apply the condition (in the list item context, we use display style to simplify)
 | 
					 | 
				
			||||||
					if (!shouldDisplay) (element as HTMLElement).style.display = 'none'
 | 
					 | 
				
			||||||
				}
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			// If the condition evaluates to false, skip further processing of this element
 | 
					 | 
				
			||||||
			if (isConditional && !shouldDisplay) {
 | 
					 | 
				
			||||||
				return
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			// 6. Process nested list rendering (%for)
 | 
					 | 
				
			||||||
			let hasForDirective = false
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			// Array.from(element.attributes).forEach((attr) => {
 | 
					 | 
				
			||||||
			for (const attr of Array.from(element.attributes)) {
 | 
					 | 
				
			||||||
				if (attr.name === '%for') {
 | 
					 | 
				
			||||||
					hasForDirective = true
 | 
					 | 
				
			||||||
					const forExpr = attr.value.trim()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
					// Remove the original binding attribute
 | 
					 | 
				
			||||||
					element.removeAttribute(attr.name)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
					// Here we will create a new nested list
 | 
					 | 
				
			||||||
					// Note: We need to evaluate the collection expression through the current item context here
 | 
					 | 
				
			||||||
					this._setupNestedListRendering(element, forExpr, itemContext)
 | 
					 | 
				
			||||||
				}
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			// If this element is a list element, skip child element processing (they will be processed by the list processor)
 | 
					 | 
				
			||||||
			if (hasForDirective) return
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			// 7. Recursively process all child elements
 | 
					 | 
				
			||||||
			// Array.from(element.children).forEach((child) => {
 | 
					 | 
				
			||||||
			for (const child of Array.from(element.children))
 | 
					 | 
				
			||||||
				this._processElementWithItemContext(child, itemContext)
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		// Set up nested list rendering
 | 
					 | 
				
			||||||
		private _setupNestedListRendering(
 | 
					 | 
				
			||||||
			element: Element,
 | 
					 | 
				
			||||||
			expr: string,
 | 
					 | 
				
			||||||
			parentItemContext: Record<string, unknown>,
 | 
					 | 
				
			||||||
		) {
 | 
					 | 
				
			||||||
			// Similar to _setupListRendering, but applies to nested situations
 | 
					 | 
				
			||||||
			// Parse the expression (e.g., "subItem in item.subItems")
 | 
					 | 
				
			||||||
			const match = expr.match(
 | 
					 | 
				
			||||||
				/(?:\(([^,]+),\s*([^)]+)\)|([^,\s]+))\s+in\s+(.+)/,
 | 
					 | 
				
			||||||
			)
 | 
					 | 
				
			||||||
			if (!match) {
 | 
					 | 
				
			||||||
				console.error(`Invalid nested %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()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			// Evaluate the collection expression, using the parent item context
 | 
					 | 
				
			||||||
			const collection = this._evaluateExpressionWithItemContext(
 | 
					 | 
				
			||||||
				collectionExpr,
 | 
					 | 
				
			||||||
				parentItemContext,
 | 
					 | 
				
			||||||
			)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			if (!collection || !Array.isArray(collection)) {
 | 
					 | 
				
			||||||
				console.warn(
 | 
					 | 
				
			||||||
					`Nested collection "${collectionExpr}" is not an array or does not exist`,
 | 
					 | 
				
			||||||
				)
 | 
					 | 
				
			||||||
				return
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			// Create a placeholder comment
 | 
					 | 
				
			||||||
			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)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			// Create an element for each item
 | 
					 | 
				
			||||||
			collection.forEach((item, index) => {
 | 
					 | 
				
			||||||
				const itemElement = template.cloneNode(true) as Element
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
				// Create a nested item context, merging the parent context
 | 
					 | 
				
			||||||
				const nestedItemContext = {
 | 
					 | 
				
			||||||
					...parentItemContext,
 | 
					 | 
				
			||||||
					[itemVar]: item,
 | 
					 | 
				
			||||||
				}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
				if (indexVar) {
 | 
					 | 
				
			||||||
					nestedItemContext[indexVar] = index
 | 
					 | 
				
			||||||
				}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
				// Recursively process this item and its children
 | 
					 | 
				
			||||||
				this._processElementWithItemContext(itemElement, nestedItemContext)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
				// TODO: detect list items existed inside the view, use replace instead of remove and re-add,
 | 
					 | 
				
			||||||
				// to improve performance
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
				// Insert the item element into the DOM
 | 
					 | 
				
			||||||
				placeholder.parentNode?.insertBefore(
 | 
					 | 
				
			||||||
					itemElement,
 | 
					 | 
				
			||||||
					placeholder.nextSibling,
 | 
					 | 
				
			||||||
				)
 | 
					 | 
				
			||||||
			})
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		// Evaluate expressions using the item context
 | 
					 | 
				
			||||||
		private _evaluateExpressionWithItemContext(
 | 
					 | 
				
			||||||
			expression: string,
 | 
					 | 
				
			||||||
			itemContext: Record<string, unknown>,
 | 
					 | 
				
			||||||
			index?: number,
 | 
					 | 
				
			||||||
			itemVar?: string,
 | 
					 | 
				
			||||||
			indexVar?: string,
 | 
					 | 
				
			||||||
		): unknown {
 | 
					 | 
				
			||||||
			try {
 | 
					 | 
				
			||||||
				// Check if the expression directly references the item variable
 | 
					 | 
				
			||||||
				if (itemVar && expression === itemVar) {
 | 
					 | 
				
			||||||
					return itemContext[itemVar]
 | 
					 | 
				
			||||||
				}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
				// Check if the expression is an item property path
 | 
					 | 
				
			||||||
				if (itemVar && expression.startsWith(`${itemVar}.`)) {
 | 
					 | 
				
			||||||
					const propertyPath = expression.substring(itemVar.length + 1)
 | 
					 | 
				
			||||||
					const parts = propertyPath.split('.')
 | 
					 | 
				
			||||||
					let value = itemContext[itemVar]
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
					for (const part of parts) {
 | 
					 | 
				
			||||||
						if (value === undefined || value === null) return undefined
 | 
					 | 
				
			||||||
						value = (value as { [key: string]: unknown })[part]
 | 
					 | 
				
			||||||
					}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
					return value
 | 
					 | 
				
			||||||
				}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
				// Check if the expression directly references the index variable
 | 
					 | 
				
			||||||
				if (indexVar && expression === indexVar) {
 | 
					 | 
				
			||||||
					return index
 | 
					 | 
				
			||||||
				}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
				// Create a merged context (component state + item context)
 | 
					 | 
				
			||||||
				const mergedContext = { ...this._states, ...itemContext }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
				// Create a function to evaluate the expression
 | 
					 | 
				
			||||||
				const contextKeys = Object.keys(mergedContext)
 | 
					 | 
				
			||||||
				const contextValues = Object.values(mergedContext)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
				// Use the with statement to allow the expression to access all properties in the context
 | 
					 | 
				
			||||||
				const func = new Function(...contextKeys, `return ${expression}`)
 | 
					 | 
				
			||||||
				return func(...contextValues)
 | 
					 | 
				
			||||||
			} catch (error) {
 | 
					 | 
				
			||||||
				console.error(
 | 
					 | 
				
			||||||
					`Error evaluating expression with item context: ${expression}`,
 | 
					 | 
				
			||||||
					error,
 | 
					 | 
				
			||||||
				)
 | 
					 | 
				
			||||||
				return undefined
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		private _evaluateIfCondition(element: Element, condition: string) {
 | 
							private _evaluateIfCondition(element: Element, condition: string) {
 | 
				
			||||||
			const info = this._conditionalElements.get(element)
 | 
								const info = this._conditionalElements.get(element)
 | 
				
			||||||
			if (!info) return
 | 
								if (!info) return
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -24,7 +24,6 @@ export default function processTemplateMacros(
 | 
				
			||||||
			eventName: string,
 | 
								eventName: string,
 | 
				
			||||||
			handlerValue: string,
 | 
								handlerValue: string,
 | 
				
			||||||
		) => void
 | 
							) => void
 | 
				
			||||||
		setupListRendering: (element: Element, expr: string) => void
 | 
					 | 
				
			||||||
		stateToElementsMap: Record<string, Set<HTMLElement>>
 | 
							stateToElementsMap: Record<string, Set<HTMLElement>>
 | 
				
			||||||
		textBindings: {
 | 
							textBindings: {
 | 
				
			||||||
			node: Text
 | 
								node: Text
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
		Reference in New Issue
	
	Block a user