Compare commits

..

4 Commits

View File

@ -38,7 +38,7 @@ export default (options: ComponentOptions) => {
private _states: Record<string, any> = {} private _states: Record<string, any> = {}
private _stateToElementsMap: Record<string, Set<HTMLElement>> = {} private _stateToElementsMap: Record<string, Set<HTMLElement>> = {}
private _currentRenderingElement: HTMLElement | null = null private _currentRenderingElement: HTMLElement | null = null
private _statesListeners: Record<string, Function> = {} private _statesListeners: Record<string, (...args: any[]) => void> = {}
private _textBindings: Array<{ private _textBindings: Array<{
node: Text node: Text
expr: string expr: string
@ -69,9 +69,9 @@ export default (options: ComponentOptions) => {
set: (target: Record<string, any>, keyPath: string, value: any) => { set: (target: Record<string, any>, keyPath: string, value: any) => {
const valueRoute = keyPath.split('.') const valueRoute = keyPath.split('.')
let currentTarget = target let currentTarget = target
for (let i in valueRoute) { for (const i in valueRoute) {
const key = valueRoute[i] const key = valueRoute[i]
if (parseInt(i) === valueRoute.length - 1) { if (Number.parseInt(i) === valueRoute.length - 1) {
currentTarget[key] = value currentTarget[key] = value
} else { } else {
if (!currentTarget[key]) { if (!currentTarget[key]) {
@ -93,8 +93,7 @@ export default (options: ComponentOptions) => {
}) })
// trigger state update events // trigger state update events
if (statesListeners && statesListeners[keyPath]) statesListeners?.[keyPath]?.(value)
statesListeners[keyPath](value)
return true return true
}, },
@ -110,16 +109,14 @@ export default (options: ComponentOptions) => {
const valueRoute = keyPath.split('.') const valueRoute = keyPath.split('.')
let currentTarget = target let currentTarget = target
for (let i in valueRoute) { for (const i in valueRoute) {
const key = valueRoute[i] const key = valueRoute[i]
if (parseInt(i) === valueRoute.length - 1) { if (Number.parseInt(i) === valueRoute.length - 1)
return currentTarget[key] return currentTarget[key]
} else {
if (!currentTarget[key]) { if (!currentTarget[key])
currentTarget[key] = {} currentTarget[key] = {}
} currentTarget = currentTarget[key]
currentTarget = currentTarget[key]
}
} }
return undefined return undefined
}, },
@ -144,7 +141,7 @@ export default (options: ComponentOptions) => {
const doc = parser.parseFromString(template, 'text/html') const doc = parser.parseFromString(template, 'text/html')
const mainContent = doc.body.firstElementChild const mainContent = doc.body.firstElementChild
let rootElement let rootElement: Element
if (mainContent) { if (mainContent) {
rootElement = document.importNode(mainContent, true) rootElement = document.importNode(mainContent, true)
@ -163,50 +160,47 @@ export default (options: ComponentOptions) => {
if (this._stateToElementsMap[keyPath]) { if (this._stateToElementsMap[keyPath]) {
const updateQueue = new Set<HTMLElement>() const updateQueue = new Set<HTMLElement>()
this._stateToElementsMap[keyPath].forEach((element) => { for (const element of this._stateToElementsMap[keyPath]) {
updateQueue.add(element) updateQueue.add(element)
}) }
this._scheduleUpdate(updateQueue) this._scheduleUpdate(updateQueue)
} }
// Update text bindings that depend on this state // Update text bindings that depend on this state
if (this._textBindings) { if (this._textBindings) {
this._textBindings.forEach((binding) => { // this._textBindings.forEach((binding) => {
for (const binding of this._textBindings)
if ( if (
binding.expr === keyPath || binding.expr === keyPath ||
binding.expr.startsWith(keyPath + '.') binding.expr.startsWith(`${keyPath}.`)
) { )
this._updateTextNode( this._updateTextNode(
binding.node, binding.node,
binding.expr, binding.expr,
binding.originalContent, binding.originalContent,
) )
}
})
} }
// Update attribute bindings that depend on this state // Update attribute bindings that depend on this state
if (this._attributeBindings) { if (this._attributeBindings) {
this._attributeBindings.forEach((binding) => { for (const binding of this._attributeBindings)
if ( if (
binding.expr === keyPath || binding.expr === keyPath ||
binding.expr.startsWith(keyPath + '.') binding.expr.startsWith(`${keyPath}.`)
) { ) {
const value = this._getNestedState(binding.expr) const value = this._getNestedState(binding.expr)
if (value !== undefined) { if (value !== undefined)
binding.element.setAttribute(binding.attrName, String(value)) binding.element.setAttribute(binding.attrName, String(value))
}
} }
})
} }
} }
private _scheduleUpdate(elements: Set<HTMLElement>) { private _scheduleUpdate(elements: Set<HTMLElement>) {
requestAnimationFrame(() => { requestAnimationFrame(() => {
elements.forEach((element) => { for (const element of elements)
this._updateElement(element) this._updateElement(element)
})
}) })
} }
@ -258,7 +252,14 @@ export default (options: ComponentOptions) => {
// Traverse the DOM tree // Traverse the DOM tree
let currentNode: Node | null let currentNode: Node | null
while ((currentNode = walker.nextNode())) { let flag = true
while (flag) {
currentNode = walker.nextNode()
if (!currentNode) {
flag = false
break
}
// Handle text nodes // Handle text nodes
if (currentNode.nodeType === Node.TEXT_NODE) { if (currentNode.nodeType === Node.TEXT_NODE) {
const textContent = currentNode.textContent || '' const textContent = currentNode.textContent || ''
@ -272,7 +273,7 @@ export default (options: ComponentOptions) => {
// Record nodes and expressions that need to be updated // Record nodes and expressions that need to be updated
const matches = textContent.match(/\{\{\s*([^}]+)\s*\}\}/g) const matches = textContent.match(/\{\{\s*([^}]+)\s*\}\}/g)
if (matches) { if (matches) {
matches.forEach((match) => { for (const match of matches) {
// Extract the expression content, removing {{ }} and spaces // Extract the expression content, removing {{ }} and spaces
const expr = match.replace(/\{\{\s*|\s*\}\}/g, '').trim() const expr = match.replace(/\{\{\s*|\s*\}\}/g, '').trim()
@ -289,7 +290,7 @@ export default (options: ComponentOptions) => {
this._stateToElementsMap[expr].add( this._stateToElementsMap[expr].add(
textNode as unknown as HTMLElement, textNode as unknown as HTMLElement,
) )
}) }
} }
} }
} }
@ -301,7 +302,7 @@ export default (options: ComponentOptions) => {
// Traverse all macro attributes // Traverse all macro attributes
// Detect :attr="" bindings, such as :src="imageUrl" // Detect :attr="" bindings, such as :src="imageUrl"
Array.from(currentElementNode.attributes).forEach((attr) => { for (const attr of Array.from(currentElementNode.attributes)) {
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()
@ -317,13 +318,14 @@ export default (options: ComponentOptions) => {
attr.value, attr.value,
) )
} }
}) }
// Process @event bindings, such as @click="handleClick" // Process @event bindings, such as @click="handleClick"
const eventBindings = Array.from( const eventBindings = Array.from(
currentElementNode.attributes, currentElementNode.attributes,
).filter((attr) => attr.name.startsWith('@')) ).filter((attr) => attr.name.startsWith('@'))
eventBindings.forEach((attr) => { // eventBindings.forEach((attr) => {
for (const attr of eventBindings) {
const eventName = attr.name.substring(1) // Remove '@' const eventName = attr.name.substring(1) // Remove '@'
const handlerValue = attr.value.trim() const handlerValue = attr.value.trim()
@ -362,12 +364,13 @@ export default (options: ComponentOptions) => {
handlerValue, handlerValue,
) )
} }
}) }
// Process %-started macros, such as %connect="stateName", %if="condition", %for="item in items" // Process %-started macros, such as %connect="stateName", %if="condition", %for="item in items"
const macroBindings = Array.from( const macroBindings = Array.from(
currentElementNode.attributes, currentElementNode.attributes,
).filter((attr) => attr.name.startsWith('%')) ).filter((attr) => attr.name.startsWith('%'))
// biome-ignore lint/complexity/noForEach: TODO: will cause a bug, need to be fixed
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()
@ -533,12 +536,12 @@ export default (options: ComponentOptions) => {
// Determine the key for this item // Determine the key for this item
const key = keyAttr const key = keyAttr
? this._evaluateExpressionWithItemContext( ? this._evaluateExpressionWithItemContext(
keyAttr ?? '', keyAttr ?? '',
item, item,
index, index,
itemVar, itemVar,
indexVar ? indexVar : undefined, indexVar ? indexVar : undefined,
) )
: index : index
// Check if we can reuse an existing element // Check if we can reuse an existing element
@ -624,7 +627,7 @@ export default (options: ComponentOptions) => {
itemContext: Record<string, any>, itemContext: Record<string, any>,
) { ) {
// 1. Store the item context of the element so that subsequent updates can find it // 1. Store the item context of the element so that subsequent updates can find it
;(element as any)._itemContext = itemContext ; (element as any)._itemContext = itemContext
// 2. Process bindings in text nodes // 2. Process bindings in text nodes
const processTextNodes = (node: Node) => { const processTextNodes = (node: Node) => {