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