Compare commits
3 Commits
5344a58e10
...
4aed034100
Author | SHA1 | Date | |
---|---|---|---|
4aed034100 | |||
df92e93e7c | |||
1b8b61a8d9 |
294
src/main.ts
294
src/main.ts
|
@ -1,3 +1,5 @@
|
|||
import utils from './utils/index'
|
||||
|
||||
interface ComponentOptions {
|
||||
tag: string
|
||||
template: string
|
||||
|
@ -57,6 +59,11 @@ export default (options: ComponentOptions) => {
|
|||
constructor() {
|
||||
super()
|
||||
|
||||
// initialize dom tree and append to shadow root, as well as initialize state
|
||||
this._initialize()
|
||||
}
|
||||
|
||||
private _initState() {
|
||||
// copy state from options
|
||||
this._states = new Proxy(
|
||||
{ ...(states || {}) },
|
||||
|
@ -78,7 +85,14 @@ export default (options: ComponentOptions) => {
|
|||
}
|
||||
}
|
||||
// trigger dom updates
|
||||
this._triggerDomUpdates(keyPath)
|
||||
utils.triggerDomUpdates(keyPath, {
|
||||
stateToElementsMap: this._stateToElementsMap,
|
||||
textBindings: this._textBindings,
|
||||
attributeBindings: this._attributeBindings,
|
||||
updateTextNode: this._updateTextNode.bind(this),
|
||||
getNestedState: this._getNestedState.bind(this),
|
||||
scheduleUpdate: this._scheduleUpdate.bind(this),
|
||||
})
|
||||
if (this._statesListeners[keyPath])
|
||||
this._statesListeners[keyPath](value)
|
||||
|
||||
|
@ -118,12 +132,12 @@ export default (options: ComponentOptions) => {
|
|||
},
|
||||
},
|
||||
)
|
||||
|
||||
// initialize dom tree and append to shadow root
|
||||
this._initialize()
|
||||
}
|
||||
|
||||
private _initialize() {
|
||||
// initialize state
|
||||
this._initState()
|
||||
|
||||
// initialize shadow dom
|
||||
const shadow = this.attachShadow({ mode: 'open' })
|
||||
|
||||
|
@ -133,64 +147,26 @@ export default (options: ComponentOptions) => {
|
|||
this.shadowRoot?.appendChild(styleElement)
|
||||
}
|
||||
|
||||
const parser = new DOMParser()
|
||||
const doc = parser.parseFromString(template, 'text/html')
|
||||
const rootElement = utils.parseTemplate(template)
|
||||
shadow.appendChild(rootElement)
|
||||
|
||||
const mainContent = doc.body.firstElementChild
|
||||
let rootElement: Element
|
||||
|
||||
if (mainContent) {
|
||||
rootElement = document.importNode(mainContent, true)
|
||||
shadow.appendChild(rootElement)
|
||||
} else {
|
||||
const container = document.createElement('div')
|
||||
container.innerHTML = template
|
||||
rootElement = container
|
||||
shadow.appendChild(container)
|
||||
}
|
||||
|
||||
this._processTemplateMacros(rootElement)
|
||||
utils.processTemplateMacros(rootElement, this, {
|
||||
updateTextNode: this._updateTextNode.bind(this),
|
||||
setupAttributeBinding: this._setupAttributeBinding.bind(this),
|
||||
setupArrowFunctionHandler: this._setupArrowFunctionHandler.bind(this),
|
||||
setupFunctionCallHandler: this._setupFunctionCallHandler.bind(this),
|
||||
setupExpressionHandler: this._setupExpressionHandler.bind(this),
|
||||
setupTwoWayBinding: this._setupTwoWayBinding.bind(this),
|
||||
setupConditionRendering: this._setupConditionRendering.bind(this),
|
||||
setupListRendering: this._setupListRendering.bind(this),
|
||||
stateToElementsMap: this._stateToElementsMap,
|
||||
textBindings: this._textBindings,
|
||||
availableFuncs: Object.getOwnPropertyNames(Object.getPrototypeOf(this)).filter(
|
||||
name => typeof (this as Record<string, unknown>)[name] === 'function' && name !== 'constructor'
|
||||
),
|
||||
})
|
||||
}
|
||||
|
||||
private _triggerDomUpdates(keyPath: string) {
|
||||
if (this._stateToElementsMap[keyPath]) {
|
||||
const updateQueue = new Set<HTMLElement>()
|
||||
|
||||
for (const element of this._stateToElementsMap[keyPath]) {
|
||||
updateQueue.add(element)
|
||||
}
|
||||
|
||||
this._scheduleUpdate(updateQueue)
|
||||
}
|
||||
|
||||
// Update text bindings that depend on this state
|
||||
if (this._textBindings) {
|
||||
// this._textBindings.forEach((binding) => {
|
||||
for (const binding of this._textBindings)
|
||||
if (
|
||||
binding.expr === keyPath ||
|
||||
binding.expr.startsWith(`${keyPath}.`)
|
||||
)
|
||||
this._updateTextNode(
|
||||
binding.node,
|
||||
binding.expr,
|
||||
binding.originalContent,
|
||||
)
|
||||
}
|
||||
|
||||
// Update attribute bindings that depend on this state
|
||||
if (this._attributeBindings) {
|
||||
for (const binding of this._attributeBindings)
|
||||
if (
|
||||
binding.expr === keyPath ||
|
||||
binding.expr.startsWith(`${keyPath}.`)
|
||||
) {
|
||||
const value = this._getNestedState(binding.expr)
|
||||
if (value !== undefined)
|
||||
binding.element.setAttribute(binding.attrName, String(value))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _scheduleUpdate(elements: Set<HTMLElement>) {
|
||||
requestAnimationFrame(() => {
|
||||
|
@ -221,190 +197,6 @@ export default (options: ComponentOptions) => {
|
|||
}
|
||||
}
|
||||
|
||||
private _processTemplateMacros(element: Element) {
|
||||
/*
|
||||
* We define that those prefix are available as macros:
|
||||
* - @ means event binding macro, such as @click="handleClick"
|
||||
* - : means dynamic attribute macro, such as :src="imageUrl"
|
||||
* - % means component controlling macro, such as %if="condition", %for="item in items" and %connect="stateName"
|
||||
*/
|
||||
|
||||
// Traverse all child nodes, including text nodes
|
||||
const walker = document.createTreeWalker(
|
||||
element,
|
||||
NodeFilter.SHOW_TEXT | NodeFilter.SHOW_ELEMENT,
|
||||
null,
|
||||
)
|
||||
|
||||
// Store nodes and expressions that need to be updated
|
||||
const textBindings: Array<{
|
||||
node: Text
|
||||
expr: string
|
||||
originalContent: string
|
||||
}> = []
|
||||
const ifDirectivesToProcess: Array<{ element: Element; expr: string }> =
|
||||
[]
|
||||
|
||||
// Traverse the DOM tree
|
||||
let currentNode: Node | null
|
||||
let flag = true
|
||||
while (flag) {
|
||||
currentNode = walker.nextNode()
|
||||
if (!currentNode) {
|
||||
flag = false
|
||||
break
|
||||
}
|
||||
|
||||
// Handle text nodes
|
||||
if (currentNode.nodeType === Node.TEXT_NODE) {
|
||||
const textContent = currentNode.textContent || ''
|
||||
const textNode = currentNode as Text
|
||||
|
||||
// Check if it contains Handlebars expressions {{ xxx }}
|
||||
if (textContent.includes('{{')) {
|
||||
// Save the original content, including expressions
|
||||
const originalContent = textContent
|
||||
|
||||
// Record nodes and expressions that need to be updated
|
||||
const matches = textContent.match(/\{\{\s*([^}]+)\s*\}\}/g)
|
||||
if (matches) {
|
||||
for (const match of matches) {
|
||||
// Extract the expression content, removing {{ }} and spaces
|
||||
const expr = match.replace(/\{\{\s*|\s*\}\}/g, '').trim()
|
||||
|
||||
// Store the node, expression, and original content for later updates
|
||||
textBindings.push({ node: textNode, expr, originalContent })
|
||||
|
||||
// Set the initial value
|
||||
this._updateTextNode(textNode, expr, originalContent)
|
||||
|
||||
// Add dependency relationship for this state path
|
||||
if (!this._stateToElementsMap[expr])
|
||||
this._stateToElementsMap[expr] = new Set()
|
||||
|
||||
this._stateToElementsMap[expr].add(
|
||||
textNode as unknown as HTMLElement,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle element nodes (can extend to handle attribute bindings, etc.)
|
||||
else if (currentNode.nodeType === Node.ELEMENT_NODE) {
|
||||
const currentElementNode = currentNode as Element // Renamed to avoid conflict with outer 'element'
|
||||
|
||||
// Traverse all macro attributes
|
||||
|
||||
// Detect :attr="" bindings, such as :src="imageUrl"
|
||||
for (const attr of Array.from(currentElementNode.attributes)) {
|
||||
if (attr.name.startsWith(':')) {
|
||||
const attrName = attr.name.substring(1) // Remove ':'
|
||||
const expr = attr.value.trim()
|
||||
|
||||
// Remove the attribute, as it is not a standard HTML attribute
|
||||
currentElementNode.removeAttribute(attr.name)
|
||||
|
||||
// Set up attribute binding
|
||||
this._setupAttributeBinding(
|
||||
currentElementNode,
|
||||
attrName,
|
||||
expr,
|
||||
attr.value,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Process @event bindings, such as @click="handleClick"
|
||||
const eventBindings = Array.from(
|
||||
currentElementNode.attributes,
|
||||
).filter((attr) => attr.name.startsWith('@'))
|
||||
// eventBindings.forEach((attr) => {
|
||||
for (const attr of eventBindings) {
|
||||
const eventName = attr.name.substring(1) // Remove '@'
|
||||
const handlerValue = attr.value.trim()
|
||||
|
||||
// Remove the attribute, as it is not a standard HTML attribute
|
||||
currentElementNode.removeAttribute(attr.name)
|
||||
|
||||
// Handle different types of event handlers
|
||||
if (handlerValue.includes('=>')) {
|
||||
// Handle arrow function: @click="e => setState('count', count + 1)"
|
||||
this._setupArrowFunctionHandler(
|
||||
currentElementNode,
|
||||
eventName,
|
||||
handlerValue,
|
||||
)
|
||||
} else if (
|
||||
handlerValue.includes('(') &&
|
||||
handlerValue.includes(')')
|
||||
) {
|
||||
// Handle function call: @click="increment(5)"
|
||||
this._setupFunctionCallHandler(
|
||||
currentElementNode,
|
||||
eventName,
|
||||
handlerValue,
|
||||
)
|
||||
} else if (
|
||||
typeof (this as Record<string, unknown>)[handlerValue] ===
|
||||
'function'
|
||||
) {
|
||||
// Handle method reference: @click="handleClick"
|
||||
currentElementNode.addEventListener(
|
||||
eventName,
|
||||
(
|
||||
this as unknown as Record<
|
||||
string,
|
||||
(...args: unknown[]) => void
|
||||
>
|
||||
)[handlerValue].bind(this),
|
||||
)
|
||||
} else {
|
||||
// Handle simple expression: @click="count++" or @input="name = $event.target.value"
|
||||
this._setupExpressionHandler(
|
||||
currentElementNode,
|
||||
eventName,
|
||||
handlerValue,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Process %-started macros, such as %connect="stateName", %if="condition", %for="item in items"
|
||||
const macroBindings = Array.from(
|
||||
currentElementNode.attributes,
|
||||
).filter((attr) => attr.name.startsWith('%'))
|
||||
|
||||
// macroBindings.forEach((attr) => {
|
||||
for (const attr of macroBindings) {
|
||||
const macroName = attr.name.substring(1) // Remove '%'
|
||||
const expr = attr.value.trim()
|
||||
|
||||
// Remove the attribute, as it is not a standard HTML attribute
|
||||
currentElementNode.removeAttribute(attr.name)
|
||||
|
||||
// Handle different types of macros
|
||||
if (macroName === 'connect')
|
||||
// Handle state connection: %connect="stateName"
|
||||
this._setupTwoWayBinding(currentElementNode, expr)
|
||||
else if (macroName === 'if') {
|
||||
ifDirectivesToProcess.push({ element: currentElementNode, expr })
|
||||
} else if (macroName === 'for')
|
||||
this._setupListRendering(currentElementNode, expr)
|
||||
else if (macroName === 'key') continue
|
||||
else console.warn(`Unknown macro: %${macroName}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Save text binding relationships for updates
|
||||
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 macro)
|
||||
private _setupTwoWayBinding(element: Element, expr: string) {
|
||||
// Get the initial value
|
||||
|
@ -533,12 +325,12 @@ export default (options: ComponentOptions) => {
|
|||
// Determine the key for this item
|
||||
const key = keyAttr
|
||||
? this._evaluateExpressionWithItemContext(
|
||||
keyAttr ?? '',
|
||||
item,
|
||||
index,
|
||||
itemVar,
|
||||
indexVar ? indexVar : undefined,
|
||||
)
|
||||
keyAttr ?? '',
|
||||
item,
|
||||
index,
|
||||
itemVar,
|
||||
indexVar ? indexVar : undefined,
|
||||
)
|
||||
: index
|
||||
|
||||
// Check if we can reuse an existing element
|
||||
|
@ -623,7 +415,7 @@ export default (options: ComponentOptions) => {
|
|||
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 =
|
||||
; (element as { _itemContext?: Record<string, unknown> })._itemContext =
|
||||
itemContext
|
||||
|
||||
// 2. Process bindings in text nodes
|
||||
|
@ -1201,4 +993,4 @@ export default (options: ComponentOptions) => {
|
|||
}
|
||||
|
||||
customElements.define(tag, CustomElementImpl)
|
||||
}
|
||||
}
|
9
src/utils/index.ts
Normal file
9
src/utils/index.ts
Normal file
|
@ -0,0 +1,9 @@
|
|||
import parseTemplate from './parseTemplate'
|
||||
import processTemplateMacros from './processTemplateMarco'
|
||||
import triggerDomUpdates from './triggerDomUpdates'
|
||||
|
||||
export default {
|
||||
parseTemplate,
|
||||
processTemplateMacros,
|
||||
triggerDomUpdates,
|
||||
}
|
16
src/utils/parseTemplate.ts
Normal file
16
src/utils/parseTemplate.ts
Normal file
|
@ -0,0 +1,16 @@
|
|||
export default function parseTemplate(template: string): Element {
|
||||
const parser = new DOMParser()
|
||||
const doc = parser.parseFromString(template, 'text/html')
|
||||
|
||||
const mainContent = doc.body.firstElementChild
|
||||
let rootElement: Element
|
||||
|
||||
if (mainContent) rootElement = document.importNode(mainContent, true)
|
||||
else {
|
||||
const container = document.createElement('div')
|
||||
container.innerHTML = template
|
||||
rootElement = container
|
||||
}
|
||||
|
||||
return rootElement
|
||||
}
|
218
src/utils/processTemplateMarco.ts
Normal file
218
src/utils/processTemplateMarco.ts
Normal file
|
@ -0,0 +1,218 @@
|
|||
export default function processTemplateMacros(
|
||||
element: Element,
|
||||
context: CustomElement,
|
||||
options: {
|
||||
updateTextNode: (node: Text, expr: string, originalContent: string) => void
|
||||
setupAttributeBinding: (
|
||||
element: Element,
|
||||
attrName: string,
|
||||
expr: string,
|
||||
attrValue: string,
|
||||
) => void
|
||||
setupArrowFunctionHandler: (
|
||||
element: Element,
|
||||
eventName: string,
|
||||
handlerValue: string,
|
||||
) => void
|
||||
setupFunctionCallHandler: (
|
||||
element: Element,
|
||||
eventName: string,
|
||||
handlerValue: string,
|
||||
) => void
|
||||
setupExpressionHandler: (
|
||||
element: Element,
|
||||
eventName: string,
|
||||
handlerValue: string,
|
||||
) => void
|
||||
setupTwoWayBinding: (element: Element, expr: string) => void
|
||||
setupConditionRendering: (element: Element, expr: string) => void
|
||||
setupListRendering: (element: Element, expr: string) => void
|
||||
stateToElementsMap: Record<string, Set<HTMLElement>>
|
||||
textBindings: {
|
||||
node: Text
|
||||
expr: string
|
||||
originalContent: string
|
||||
}[],
|
||||
availableFuncs: string[]
|
||||
},
|
||||
) {
|
||||
/*
|
||||
* We define that those prefix are available as macros:
|
||||
* - @ means event binding macro, such as @click="handleClick"
|
||||
* - : means dynamic attribute macro, such as :src="imageUrl"
|
||||
* - % means component controlling macro, such as %if="condition", %for="item in items" and %connect="stateName"
|
||||
*/
|
||||
|
||||
// Traverse all child nodes, including text nodes
|
||||
const walker = document.createTreeWalker(
|
||||
element,
|
||||
NodeFilter.SHOW_TEXT | NodeFilter.SHOW_ELEMENT,
|
||||
null,
|
||||
)
|
||||
|
||||
// Store nodes and expressions that need to be updated
|
||||
const textBindings: Array<{
|
||||
node: Text
|
||||
expr: string
|
||||
originalContent: string
|
||||
}> = []
|
||||
const ifDirectivesToProcess: Array<{ element: Element; expr: string }> = []
|
||||
|
||||
// Traverse the DOM tree
|
||||
let currentNode: Node | null
|
||||
let flag = true
|
||||
while (flag) {
|
||||
currentNode = walker.nextNode()
|
||||
if (!currentNode) {
|
||||
flag = false
|
||||
break
|
||||
}
|
||||
|
||||
// Handle text nodes
|
||||
if (currentNode.nodeType === Node.TEXT_NODE) {
|
||||
const textContent = currentNode.textContent || ''
|
||||
const textNode = currentNode as Text
|
||||
|
||||
// Check if it contains Handlebars expressions {{ xxx }}
|
||||
if (textContent.includes('{{')) {
|
||||
// Save the original content, including expressions
|
||||
const originalContent = textContent
|
||||
|
||||
// Record nodes and expressions that need to be updated
|
||||
const matches = textContent.match(/\{\{\s*([^}]+)\s*\}\}/g)
|
||||
if (matches) {
|
||||
for (const match of matches) {
|
||||
// Extract the expression content, removing {{ }} and spaces
|
||||
const expr = match.replace(/\{\{\s*|\s*\}\}/g, '').trim()
|
||||
|
||||
// Store the node, expression, and original content for later updates
|
||||
textBindings.push({ node: textNode, expr, originalContent })
|
||||
|
||||
// Set the initial value
|
||||
options.updateTextNode(textNode, expr, originalContent)
|
||||
|
||||
// Add dependency relationship for this state path
|
||||
if (!options.stateToElementsMap[expr])
|
||||
options.stateToElementsMap[expr] = new Set()
|
||||
|
||||
options.stateToElementsMap[expr].add(
|
||||
textNode as unknown as HTMLElement,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle element nodes (can extend to handle attribute bindings, etc.)
|
||||
else if (currentNode.nodeType === Node.ELEMENT_NODE) {
|
||||
const currentElementNode = currentNode as Element // Renamed to avoid conflict with outer 'element'
|
||||
|
||||
// Traverse all macro attributes
|
||||
|
||||
// Detect :attr="" bindings, such as :src="imageUrl"
|
||||
for (const attr of Array.from(currentElementNode.attributes)) {
|
||||
if (attr.name.startsWith(':')) {
|
||||
const attrName = attr.name.substring(1) // Remove ':'
|
||||
const expr = attr.value.trim()
|
||||
|
||||
// Remove the attribute, as it is not a standard HTML attribute
|
||||
currentElementNode.removeAttribute(attr.name)
|
||||
|
||||
// Set up attribute binding
|
||||
options.setupAttributeBinding(
|
||||
currentElementNode,
|
||||
attrName,
|
||||
expr,
|
||||
attr.value,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Process @event bindings, such as @click="handleClick"
|
||||
const eventBindings = Array.from(
|
||||
currentElementNode.attributes,
|
||||
).filter((attr) => attr.name.startsWith('@'))
|
||||
// eventBindings.forEach((attr) => {
|
||||
for (const attr of eventBindings) {
|
||||
const eventName = attr.name.substring(1) // Remove '@'
|
||||
const handlerValue = attr.value.trim()
|
||||
|
||||
// Remove the attribute, as it is not a standard HTML attribute
|
||||
currentElementNode.removeAttribute(attr.name)
|
||||
|
||||
// Handle different types of event handlers
|
||||
if (handlerValue.includes('=>')) { // Handle arrow function: @click="e => setState('count', count + 1)"
|
||||
options.setupArrowFunctionHandler(
|
||||
currentElementNode,
|
||||
eventName,
|
||||
handlerValue,
|
||||
)
|
||||
} else if (
|
||||
handlerValue.includes('(') &&
|
||||
handlerValue.includes(')')
|
||||
) { // Handle function call: @click="increment(5)"
|
||||
options.setupFunctionCallHandler(
|
||||
currentElementNode,
|
||||
eventName,
|
||||
handlerValue,
|
||||
)
|
||||
} else if (
|
||||
options.availableFuncs.includes(handlerValue) &&
|
||||
typeof (context as unknown as Record<string, unknown>)[
|
||||
handlerValue
|
||||
] === 'function'
|
||||
) { // Handle method reference: @click="handleClick"
|
||||
currentElementNode.addEventListener(
|
||||
eventName,
|
||||
(
|
||||
context as unknown as Record<
|
||||
string,
|
||||
(...args: unknown[]) => void
|
||||
>
|
||||
)[handlerValue].bind(context),
|
||||
)
|
||||
} else {
|
||||
// Handle simple expression: @click="count++" or @input="name = $event.target.value"
|
||||
options.setupExpressionHandler(
|
||||
currentElementNode,
|
||||
eventName,
|
||||
handlerValue,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Process %-started macros, such as %connect="stateName", %if="condition", %for="item in items"
|
||||
const macroBindings = Array.from(
|
||||
currentElementNode.attributes,
|
||||
).filter((attr) => attr.name.startsWith('%'))
|
||||
|
||||
// macroBindings.forEach((attr) => {
|
||||
for (const attr of macroBindings) {
|
||||
const macroName = attr.name.substring(1) // Remove '%'
|
||||
const expr = attr.value.trim()
|
||||
|
||||
// Remove the attribute, as it is not a standard HTML attribute
|
||||
currentElementNode.removeAttribute(attr.name)
|
||||
|
||||
// Handle different types of macros
|
||||
if (macroName === 'connect')
|
||||
// Handle state connection: %connect="stateName"
|
||||
options.setupTwoWayBinding(currentElementNode, expr)
|
||||
else if (macroName === 'if') {
|
||||
ifDirectivesToProcess.push({ element: currentElementNode, expr })
|
||||
} else if (macroName === 'for')
|
||||
options.setupListRendering(currentElementNode, expr)
|
||||
else if (macroName === 'key') continue
|
||||
else console.warn(`Unknown macro: %${macroName}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Save text binding relationships for updates
|
||||
options.textBindings = textBindings
|
||||
|
||||
// Process all collected %if directives after the main traversal
|
||||
for (const { element: ifElement, expr } of ifDirectivesToProcess) {
|
||||
options.setupConditionRendering(ifElement, expr)
|
||||
}
|
||||
}
|
54
src/utils/triggerDomUpdates.ts
Normal file
54
src/utils/triggerDomUpdates.ts
Normal file
|
@ -0,0 +1,54 @@
|
|||
export default function triggerDomUpdates(keyPath: string, ops: {
|
||||
stateToElementsMap: Record<string, Set<HTMLElement>>,
|
||||
scheduleUpdate: (elements: Set<HTMLElement>) => void,
|
||||
textBindings: Array<{
|
||||
node: Text
|
||||
expr: string
|
||||
originalContent: string
|
||||
}> | undefined,
|
||||
attributeBindings: Array<{
|
||||
element: Element
|
||||
attrName: string
|
||||
expr: string
|
||||
template: string
|
||||
}> | undefined,
|
||||
updateTextNode: (node: Text, expr: string, template: string) => void,
|
||||
getNestedState: (path: string) => unknown,
|
||||
}) {
|
||||
if (ops.stateToElementsMap[keyPath]) {
|
||||
const updateQueue = new Set<HTMLElement>()
|
||||
|
||||
for (const element of ops.stateToElementsMap[keyPath])
|
||||
updateQueue.add(element)
|
||||
|
||||
ops.scheduleUpdate(updateQueue)
|
||||
}
|
||||
|
||||
// Update text bindings that depend on this state
|
||||
if (ops.textBindings) {
|
||||
// this._textBindings.forEach((binding) => {
|
||||
for (const binding of ops.textBindings)
|
||||
if (
|
||||
binding.expr === keyPath ||
|
||||
binding.expr.startsWith(`${keyPath}.`)
|
||||
)
|
||||
ops.updateTextNode(
|
||||
binding.node,
|
||||
binding.expr,
|
||||
binding.originalContent,
|
||||
)
|
||||
}
|
||||
|
||||
// Update attribute bindings that depend on this state
|
||||
if (ops.attributeBindings) {
|
||||
for (const binding of ops.attributeBindings)
|
||||
if (
|
||||
binding.expr === keyPath ||
|
||||
binding.expr.startsWith(`${keyPath}.`)
|
||||
) {
|
||||
const value = ops.getNestedState(binding.expr)
|
||||
if (value !== undefined)
|
||||
binding.element.setAttribute(binding.attrName, String(value))
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user