Compare commits
4 Commits
afff93196a
...
75a37043b7
Author | SHA1 | Date | |
---|---|---|---|
75a37043b7 | |||
0e22cbf130 | |||
ba82252dd7 | |||
0ce566f502 |
87
src/main.ts
87
src/main.ts
|
@ -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) => {
|
||||||
|
|
Loading…
Reference in New Issue
Block a user