Enhance condition rendering: defer processing of %if directives until after initial DOM traversal

This commit is contained in:
Astrian Zheng 2025-05-15 12:40:06 +10:00
parent 4a7737c74f
commit 6f3d40754a
Signed by: Astrian
SSH Key Fingerprint: SHA256:rVnhx3DAKjujCwWE13aDl7uV6+9U1MvydLkNRXJrBiA

View File

@ -208,6 +208,7 @@ export default (options: ComponentOptions) => {
// Store nodes and expressions that need to be updated // Store nodes and expressions that need to be updated
const textBindings: Array<{ node: Text, expr: string, originalContent: string }> = [] const textBindings: Array<{ node: Text, expr: string, originalContent: string }> = []
const ifDirectivesToProcess: Array<{ element: Element, expr: string }> = []
// Traverse the DOM tree // Traverse the DOM tree
let currentNode: Node | null let currentNode: Node | null
@ -247,66 +248,68 @@ export default (options: ComponentOptions) => {
// Handle element nodes (can extend to handle attribute bindings, etc.) // Handle element nodes (can extend to handle attribute bindings, etc.)
else if (currentNode.nodeType === Node.ELEMENT_NODE) { else if (currentNode.nodeType === Node.ELEMENT_NODE) {
// Handle element attribute bindings, such as <img src="{{ imageUrl }}">
const element = currentNode as Element const currentElementNode = currentNode as Element // Renamed to avoid conflict with outer 'element'
// Traverse all marco attributes // Traverse all marco attributes
// Detect :attr="" bindings, such as :src="imageUrl" // Detect :attr="" bindings, such as :src="imageUrl"
Array.from(element.attributes).forEach(attr => { Array.from(currentElementNode.attributes).forEach(attr => {
if (attr.name.startsWith(':')) { if (attr.name.startsWith(':')) {
const attrName = attr.name.substring(1) // Remove ':' const attrName = attr.name.substring(1) // Remove ':'
const expr = attr.value.trim() const expr = attr.value.trim()
// Remove the attribute, as it is not a standard HTML attribute // Remove the attribute, as it is not a standard HTML attribute
element.removeAttribute(attr.name) currentElementNode.removeAttribute(attr.name)
// Set up attribute binding // Set up attribute binding
this._setupAttributeBinding(element, attrName, expr, attr.value) this._setupAttributeBinding(currentElementNode, attrName, expr, attr.value)
} }
}) })
// Process @event bindings, such as @click="handleClick" // Process @event bindings, such as @click="handleClick"
const eventBindings = Array.from(element.attributes).filter(attr => attr.name.startsWith('@')) const eventBindings = Array.from(currentElementNode.attributes).filter(attr => attr.name.startsWith('@'))
eventBindings.forEach(attr => { eventBindings.forEach(attr => {
const eventName = attr.name.substring(1) // Remove '@' const eventName = attr.name.substring(1) // Remove '@'
const handlerValue = attr.value.trim() const handlerValue = attr.value.trim()
// Remove the attribute, as it is not a standard HTML attribute // Remove the attribute, as it is not a standard HTML attribute
element.removeAttribute(attr.name) currentElementNode.removeAttribute(attr.name)
// Handle different types of event handlers // Handle different types of event handlers
if (handlerValue.includes('=>')) { if (handlerValue.includes('=>')) {
// Handle arrow function: @click="e => setState('count', count + 1)" // Handle arrow function: @click="e => setState('count', count + 1)"
this._setupArrowFunctionHandler(element, eventName, handlerValue) this._setupArrowFunctionHandler(currentElementNode, eventName, handlerValue)
} else if (handlerValue.includes('(') && handlerValue.includes(')')) { } else if (handlerValue.includes('(') && handlerValue.includes(')')) {
// Handle function call: @click="increment(5)" // Handle function call: @click="increment(5)"
this._setupFunctionCallHandler(element, eventName, handlerValue) this._setupFunctionCallHandler(currentElementNode, eventName, handlerValue)
} else if (typeof (this as any)[handlerValue] === 'function') { } else if (typeof (this as any)[handlerValue] === 'function') {
// Handle method reference: @click="handleClick" // Handle method reference: @click="handleClick"
element.addEventListener(eventName, (this as any)[handlerValue].bind(this)) currentElementNode.addEventListener(eventName, (this as any)[handlerValue].bind(this))
} else { } else {
// Handle simple expression: @click="count++" or @input="name = $event.target.value" // Handle simple expression: @click="count++" or @input="name = $event.target.value"
this._setupExpressionHandler(element, eventName, handlerValue) this._setupExpressionHandler(currentElementNode, eventName, handlerValue)
} }
}) })
// Process %-started marcos, such as %connect="stateName", %if="condition", %for="item in items" // Process %-started marcos, such as %connect="stateName", %if="condition", %for="item in items"
const macroBindings = Array.from(element.attributes).filter(attr => attr.name.startsWith('%')) const macroBindings = Array.from(currentElementNode.attributes).filter(attr => attr.name.startsWith('%'))
macroBindings.forEach(attr => { macroBindings.forEach(attr => {
const macroName = attr.name.substring(1) // Remove '%' const macroName = attr.name.substring(1) // Remove '%'
const expr = attr.value.trim() const expr = attr.value.trim()
// Remove the attribute, as it is not a standard HTML attribute // Remove the attribute, as it is not a standard HTML attribute
element.removeAttribute(attr.name) currentElementNode.removeAttribute(attr.name)
// Handle different types of macros // Handle different types of macros
if (macroName === 'connect') // Handle state connection: %connect="stateName" if (macroName === 'connect') // Handle state connection: %connect="stateName"
this._setupTwoWayBinding(element, expr) this._setupTwoWayBinding(currentElementNode, expr)
else if (macroName === 'if') else if (macroName === 'if') {
this._setupConditionRendering(element, expr) // Defer %if processing until after the initial traversal
ifDirectivesToProcess.push({ element: currentElementNode, expr })
}
else if (macroName === 'for') else if (macroName === 'for')
this._setupAttributeBinding(element, 'data-laterano-for', expr, attr.value) this._setupAttributeBinding(currentElementNode, 'data-laterano-for', expr, attr.value) // Placeholder, actual %for needs more complex logic
else else
console.warn(`Unknown macro: %${macroName}`) console.warn(`Unknown macro: %${macroName}`)
}) })
@ -317,6 +320,11 @@ export default (options: ComponentOptions) => {
// Save text binding relationships for updates // Save text binding relationships for updates
this._textBindings = textBindings this._textBindings = textBindings
// Process all collected %if directives after the main traversal
for (const { element: ifElement, expr } of ifDirectivesToProcess) {
this._setupConditionRendering(ifElement, expr)
}
} }
// Handle two-way data binding (%connect marco) // Handle two-way data binding (%connect marco)