feat: refactor template processing by consolidating utility functions and adding processTemplateMacros
This commit is contained in:
parent
1b8b61a8d9
commit
df92e93e7c
204
src/main.ts
204
src/main.ts
|
@ -1,4 +1,4 @@
|
||||||
import { parseTemplate } from './utils/parseTemplate'
|
import utils from './utils/index'
|
||||||
|
|
||||||
interface ComponentOptions {
|
interface ComponentOptions {
|
||||||
tag: string
|
tag: string
|
||||||
|
@ -140,10 +140,24 @@ export default (options: ComponentOptions) => {
|
||||||
this.shadowRoot?.appendChild(styleElement)
|
this.shadowRoot?.appendChild(styleElement)
|
||||||
}
|
}
|
||||||
|
|
||||||
const rootElement = parseTemplate(template)
|
const rootElement = utils.parseTemplate(template)
|
||||||
shadow.appendChild(rootElement)
|
shadow.appendChild(rootElement)
|
||||||
|
|
||||||
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) {
|
private _triggerDomUpdates(keyPath: string) {
|
||||||
|
@ -215,190 +229,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)
|
// Handle two-way data binding (%connect macro)
|
||||||
private _setupTwoWayBinding(element: Element, expr: string) {
|
private _setupTwoWayBinding(element: Element, expr: string) {
|
||||||
// Get the initial value
|
// Get the initial value
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
import { parseTemplate } from './parseTemplate'
|
import { parseTemplate } from './parseTemplate'
|
||||||
|
import { processTemplateMacros } from './processTemplateMarco'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
parseTemplate,
|
parseTemplate,
|
||||||
|
processTemplateMacros,
|
||||||
}
|
}
|
||||||
|
|
218
src/utils/processTemplateMarco.ts
Normal file
218
src/utils/processTemplateMarco.ts
Normal file
|
@ -0,0 +1,218 @@
|
||||||
|
export 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)
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user