feat: add biome configuration and update dependencies
- Introduced a new `biome.json` configuration file for Biome setup. - Added `@biomejs/biome` as a development dependency in `package.json` and `package-lock.json`. - Updated `main.ts` to improve code formatting and readability by adjusting indentation and line breaks. - Enhanced state management and event handling within the component to ensure better performance and maintainability.
This commit is contained in:
parent
3521ee0cbf
commit
4def8050a1
31
biome.json
Normal file
31
biome.json
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
{
|
||||||
|
"$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
|
||||||
|
"vcs": {
|
||||||
|
"enabled": true,
|
||||||
|
"clientKind": "git",
|
||||||
|
"useIgnoreFile": true
|
||||||
|
},
|
||||||
|
"files": {
|
||||||
|
"ignoreUnknown": false,
|
||||||
|
"ignore": []
|
||||||
|
},
|
||||||
|
"formatter": {
|
||||||
|
"enabled": true,
|
||||||
|
"indentStyle": "tab"
|
||||||
|
},
|
||||||
|
"organizeImports": {
|
||||||
|
"enabled": true
|
||||||
|
},
|
||||||
|
"linter": {
|
||||||
|
"enabled": true,
|
||||||
|
"rules": {
|
||||||
|
"recommended": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"javascript": {
|
||||||
|
"formatter": {
|
||||||
|
"quoteStyle": "single",
|
||||||
|
"semicolons": "asNeeded"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
165
package-lock.json
generated
165
package-lock.json
generated
|
@ -9,6 +9,7 @@
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@biomejs/biome": "1.9.4",
|
||||||
"@rollup/plugin-node-resolve": "^16.0.1",
|
"@rollup/plugin-node-resolve": "^16.0.1",
|
||||||
"@rollup/plugin-terser": "^0.4.4",
|
"@rollup/plugin-terser": "^0.4.4",
|
||||||
"@rollup/plugin-typescript": "^12.1.2",
|
"@rollup/plugin-typescript": "^12.1.2",
|
||||||
|
@ -49,6 +50,170 @@
|
||||||
"node": ">=6.9.0"
|
"node": ">=6.9.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@biomejs/biome": {
|
||||||
|
"version": "1.9.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-1.9.4.tgz",
|
||||||
|
"integrity": "sha512-1rkd7G70+o9KkTn5KLmDYXihGoTaIGO9PIIN2ZB7UJxFrWw04CZHPYiMRjYsaDvVV7hP1dYNRLxSANLaBFGpog==",
|
||||||
|
"dev": true,
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"license": "MIT OR Apache-2.0",
|
||||||
|
"bin": {
|
||||||
|
"biome": "bin/biome"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.21.3"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/biome"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@biomejs/cli-darwin-arm64": "1.9.4",
|
||||||
|
"@biomejs/cli-darwin-x64": "1.9.4",
|
||||||
|
"@biomejs/cli-linux-arm64": "1.9.4",
|
||||||
|
"@biomejs/cli-linux-arm64-musl": "1.9.4",
|
||||||
|
"@biomejs/cli-linux-x64": "1.9.4",
|
||||||
|
"@biomejs/cli-linux-x64-musl": "1.9.4",
|
||||||
|
"@biomejs/cli-win32-arm64": "1.9.4",
|
||||||
|
"@biomejs/cli-win32-x64": "1.9.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@biomejs/cli-darwin-arm64": {
|
||||||
|
"version": "1.9.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-1.9.4.tgz",
|
||||||
|
"integrity": "sha512-bFBsPWrNvkdKrNCYeAp+xo2HecOGPAy9WyNyB/jKnnedgzl4W4Hb9ZMzYNbf8dMCGmUdSavlYHiR01QaYR58cw==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT OR Apache-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.21.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@biomejs/cli-darwin-x64": {
|
||||||
|
"version": "1.9.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-1.9.4.tgz",
|
||||||
|
"integrity": "sha512-ngYBh/+bEedqkSevPVhLP4QfVPCpb+4BBe2p7Xs32dBgs7rh9nY2AIYUL6BgLw1JVXV8GlpKmb/hNiuIxfPfZg==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT OR Apache-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.21.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@biomejs/cli-linux-arm64": {
|
||||||
|
"version": "1.9.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-1.9.4.tgz",
|
||||||
|
"integrity": "sha512-fJIW0+LYujdjUgJJuwesP4EjIBl/N/TcOX3IvIHJQNsAqvV2CHIogsmA94BPG6jZATS4Hi+xv4SkBBQSt1N4/g==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT OR Apache-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.21.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@biomejs/cli-linux-arm64-musl": {
|
||||||
|
"version": "1.9.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-1.9.4.tgz",
|
||||||
|
"integrity": "sha512-v665Ct9WCRjGa8+kTr0CzApU0+XXtRgwmzIf1SeKSGAv+2scAlW6JR5PMFo6FzqqZ64Po79cKODKf3/AAmECqA==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT OR Apache-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.21.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@biomejs/cli-linux-x64": {
|
||||||
|
"version": "1.9.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-1.9.4.tgz",
|
||||||
|
"integrity": "sha512-lRCJv/Vi3Vlwmbd6K+oQ0KhLHMAysN8lXoCI7XeHlxaajk06u7G+UsFSO01NAs5iYuWKmVZjmiOzJ0OJmGsMwg==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT OR Apache-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.21.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@biomejs/cli-linux-x64-musl": {
|
||||||
|
"version": "1.9.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-1.9.4.tgz",
|
||||||
|
"integrity": "sha512-gEhi/jSBhZ2m6wjV530Yy8+fNqG8PAinM3oV7CyO+6c3CEh16Eizm21uHVsyVBEB6RIM8JHIl6AGYCv6Q6Q9Tg==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT OR Apache-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.21.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@biomejs/cli-win32-arm64": {
|
||||||
|
"version": "1.9.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-1.9.4.tgz",
|
||||||
|
"integrity": "sha512-tlbhLk+WXZmgwoIKwHIHEBZUwxml7bRJgk0X2sPyNR3S93cdRq6XulAZRQJ17FYGGzWne0fgrXBKpl7l4M87Hg==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT OR Apache-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.21.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@biomejs/cli-win32-x64": {
|
||||||
|
"version": "1.9.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-1.9.4.tgz",
|
||||||
|
"integrity": "sha512-8Y5wMhVIPaWe6jw2H+KlEm4wP/f7EW3810ZLmDlrEEy5KvBsb9ECEfu/kMWD484ijfQ8+nIi0giMgu9g1UAuuA==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT OR Apache-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.21.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@isaacs/cliui": {
|
"node_modules/@isaacs/cliui": {
|
||||||
"version": "8.0.2",
|
"version": "8.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
|
||||||
|
|
|
@ -17,6 +17,7 @@
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"description": "",
|
"description": "",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@biomejs/biome": "1.9.4",
|
||||||
"@rollup/plugin-node-resolve": "^16.0.1",
|
"@rollup/plugin-node-resolve": "^16.0.1",
|
||||||
"@rollup/plugin-terser": "^0.4.4",
|
"@rollup/plugin-terser": "^0.4.4",
|
||||||
"@rollup/plugin-typescript": "^12.1.2",
|
"@rollup/plugin-typescript": "^12.1.2",
|
||||||
|
|
504
src/main.ts
504
src/main.ts
|
@ -9,14 +9,28 @@ interface ComponentOptions {
|
||||||
style?: string
|
style?: string
|
||||||
onMount?: (this: CustomElement) => void
|
onMount?: (this: CustomElement) => void
|
||||||
onUnmount?: () => void
|
onUnmount?: () => void
|
||||||
onAttributeChanged?: (attrName: string, oldValue: string, newValue: string) => void
|
onAttributeChanged?: (
|
||||||
|
attrName: string,
|
||||||
|
oldValue: string,
|
||||||
|
newValue: string,
|
||||||
|
) => void
|
||||||
states?: Record<string, any>
|
states?: Record<string, any>
|
||||||
statesListeners?: { [key: string]: (value: any) => void }
|
statesListeners?: { [key: string]: (value: any) => void }
|
||||||
funcs?: { [key: string]: (...args: any[]) => void }
|
funcs?: { [key: string]: (...args: any[]) => void }
|
||||||
}
|
}
|
||||||
|
|
||||||
export default (options: ComponentOptions) => {
|
export default (options: ComponentOptions) => {
|
||||||
const { tag, template, style, onMount, onUnmount, onAttributeChanged, states, statesListeners, funcs } = options
|
const {
|
||||||
|
tag,
|
||||||
|
template,
|
||||||
|
style,
|
||||||
|
onMount,
|
||||||
|
onUnmount,
|
||||||
|
onAttributeChanged,
|
||||||
|
states,
|
||||||
|
statesListeners,
|
||||||
|
funcs,
|
||||||
|
} = options
|
||||||
const componentRegistry = new Map()
|
const componentRegistry = new Map()
|
||||||
componentRegistry.set(tag, options)
|
componentRegistry.set(tag, options)
|
||||||
|
|
||||||
|
@ -25,75 +39,92 @@ export default (options: ComponentOptions) => {
|
||||||
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, Function> = {}
|
||||||
private _textBindings: Array<{ node: Text, expr: string, originalContent: string }> = []
|
private _textBindings: Array<{
|
||||||
private _attributeBindings: Array<{ element: Element, attrName: string, expr: string, template: string }> = []
|
node: Text
|
||||||
private _conditionalElements: Map<Element, {
|
expr: string
|
||||||
expr: string,
|
originalContent: string
|
||||||
placeholder: Comment,
|
}> = []
|
||||||
isPresent: boolean
|
private _attributeBindings: Array<{
|
||||||
}> = new Map()
|
element: Element
|
||||||
|
attrName: string
|
||||||
|
expr: string
|
||||||
|
template: string
|
||||||
|
}> = []
|
||||||
|
private _conditionalElements: Map<
|
||||||
|
Element,
|
||||||
|
{
|
||||||
|
expr: string
|
||||||
|
placeholder: Comment
|
||||||
|
isPresent: boolean
|
||||||
|
}
|
||||||
|
> = new Map()
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super()
|
super()
|
||||||
|
|
||||||
// copy state from options
|
// copy state from options
|
||||||
this._states = new Proxy({ ...(states || {}) }, {
|
this._states = new Proxy(
|
||||||
set: (target: Record<string, any>, keyPath: string, value: any) => {
|
{ ...(states || {}) },
|
||||||
const valueRoute = keyPath.split('.')
|
{
|
||||||
let currentTarget = target
|
set: (target: Record<string, any>, keyPath: string, value: any) => {
|
||||||
for (let i in valueRoute) {
|
const valueRoute = keyPath.split('.')
|
||||||
const key = valueRoute[i]
|
let currentTarget = target
|
||||||
if (parseInt(i) === valueRoute.length - 1) {
|
for (let i in valueRoute) {
|
||||||
currentTarget[key] = value
|
const key = valueRoute[i]
|
||||||
} else {
|
if (parseInt(i) === valueRoute.length - 1) {
|
||||||
if (!currentTarget[key]) {
|
currentTarget[key] = value
|
||||||
currentTarget[key] = {}
|
} else {
|
||||||
|
if (!currentTarget[key]) {
|
||||||
|
currentTarget[key] = {}
|
||||||
|
}
|
||||||
|
currentTarget = currentTarget[key]
|
||||||
}
|
}
|
||||||
currentTarget = currentTarget[key]
|
|
||||||
}
|
}
|
||||||
}
|
// trigger dom updates
|
||||||
// trigger dom updates
|
this._triggerDomUpdates(keyPath)
|
||||||
this._triggerDomUpdates(keyPath)
|
if (this._statesListeners[keyPath])
|
||||||
if (this._statesListeners[keyPath])
|
this._statesListeners[keyPath](value)
|
||||||
this._statesListeners[keyPath](value)
|
|
||||||
|
|
||||||
// trigger %if macros
|
// trigger %if macros
|
||||||
if (this._conditionalElements.size > 0)
|
if (this._conditionalElements.size > 0)
|
||||||
this._conditionalElements.forEach((info, element) => {
|
this._conditionalElements.forEach((info, element) => {
|
||||||
if (info.expr.includes(keyPath))
|
if (info.expr.includes(keyPath))
|
||||||
this._evaluateIfCondition(element, info.expr)
|
this._evaluateIfCondition(element, info.expr)
|
||||||
})
|
})
|
||||||
|
|
||||||
// trigger state update events
|
// trigger state update events
|
||||||
if (statesListeners && statesListeners[keyPath])
|
if (statesListeners && statesListeners[keyPath])
|
||||||
statesListeners[keyPath](value)
|
statesListeners[keyPath](value)
|
||||||
|
|
||||||
return true
|
return true
|
||||||
|
},
|
||||||
|
get: (target: Record<string, any>, keyPath: string) => {
|
||||||
|
// collect state dependencies
|
||||||
|
if (this._currentRenderingElement) {
|
||||||
|
if (!this._stateToElementsMap[keyPath])
|
||||||
|
this._stateToElementsMap[keyPath] = new Set()
|
||||||
|
this._stateToElementsMap[keyPath].add(
|
||||||
|
this._currentRenderingElement,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const valueRoute = keyPath.split('.')
|
||||||
|
let currentTarget = target
|
||||||
|
for (let i in valueRoute) {
|
||||||
|
const key = valueRoute[i]
|
||||||
|
if (parseInt(i) === valueRoute.length - 1) {
|
||||||
|
return currentTarget[key]
|
||||||
|
} else {
|
||||||
|
if (!currentTarget[key]) {
|
||||||
|
currentTarget[key] = {}
|
||||||
|
}
|
||||||
|
currentTarget = currentTarget[key]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return undefined
|
||||||
|
},
|
||||||
},
|
},
|
||||||
get: (target: Record<string, any>, keyPath: string) => {
|
)
|
||||||
// collect state dependencies
|
|
||||||
if (this._currentRenderingElement) {
|
|
||||||
if (!this._stateToElementsMap[keyPath])
|
|
||||||
this._stateToElementsMap[keyPath] = new Set()
|
|
||||||
this._stateToElementsMap[keyPath].add(this._currentRenderingElement)
|
|
||||||
}
|
|
||||||
|
|
||||||
const valueRoute = keyPath.split('.')
|
|
||||||
let currentTarget = target
|
|
||||||
for (let i in valueRoute) {
|
|
||||||
const key = valueRoute[i]
|
|
||||||
if (parseInt(i) === valueRoute.length - 1) {
|
|
||||||
return currentTarget[key]
|
|
||||||
} else {
|
|
||||||
if (!currentTarget[key]) {
|
|
||||||
currentTarget[key] = {}
|
|
||||||
}
|
|
||||||
currentTarget = currentTarget[key]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return undefined
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// initialize dom tree and append to shadow root
|
// initialize dom tree and append to shadow root
|
||||||
this._initialize()
|
this._initialize()
|
||||||
|
@ -132,7 +163,7 @@ 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 => {
|
this._stateToElementsMap[keyPath].forEach((element) => {
|
||||||
updateQueue.add(element)
|
updateQueue.add(element)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -141,17 +172,27 @@ export default (options: ComponentOptions) => {
|
||||||
|
|
||||||
// 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) => {
|
||||||
if (binding.expr === keyPath || binding.expr.startsWith(keyPath + '.')) {
|
if (
|
||||||
this._updateTextNode(binding.node, binding.expr, binding.originalContent)
|
binding.expr === keyPath ||
|
||||||
|
binding.expr.startsWith(keyPath + '.')
|
||||||
|
) {
|
||||||
|
this._updateTextNode(
|
||||||
|
binding.node,
|
||||||
|
binding.expr,
|
||||||
|
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 => {
|
this._attributeBindings.forEach((binding) => {
|
||||||
if (binding.expr === keyPath || binding.expr.startsWith(keyPath + '.')) {
|
if (
|
||||||
|
binding.expr === 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))
|
||||||
|
@ -163,7 +204,7 @@ export default (options: ComponentOptions) => {
|
||||||
|
|
||||||
private _scheduleUpdate(elements: Set<HTMLElement>) {
|
private _scheduleUpdate(elements: Set<HTMLElement>) {
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
elements.forEach(element => {
|
elements.forEach((element) => {
|
||||||
this._updateElement(element)
|
this._updateElement(element)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -203,16 +244,21 @@ export default (options: ComponentOptions) => {
|
||||||
const walker = document.createTreeWalker(
|
const walker = document.createTreeWalker(
|
||||||
element,
|
element,
|
||||||
NodeFilter.SHOW_TEXT | NodeFilter.SHOW_ELEMENT,
|
NodeFilter.SHOW_TEXT | NodeFilter.SHOW_ELEMENT,
|
||||||
null
|
null,
|
||||||
)
|
)
|
||||||
|
|
||||||
// Store nodes and expressions that need to be updated
|
// Store nodes and expressions that need to be updated
|
||||||
const textBindings: Array<{ node: Text, expr: string, originalContent: string }> = []
|
const textBindings: Array<{
|
||||||
const ifDirectivesToProcess: Array<{ element: Element, expr: string }> = []
|
node: Text
|
||||||
|
expr: string
|
||||||
|
originalContent: string
|
||||||
|
}> = []
|
||||||
|
const ifDirectivesToProcess: Array<{ element: Element; expr: string }> =
|
||||||
|
[]
|
||||||
|
|
||||||
// Traverse the DOM tree
|
// Traverse the DOM tree
|
||||||
let currentNode: Node | null
|
let currentNode: Node | null
|
||||||
while (currentNode = walker.nextNode()) {
|
while ((currentNode = walker.nextNode())) {
|
||||||
// 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 || ''
|
||||||
|
@ -226,7 +272,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 => {
|
matches.forEach((match) => {
|
||||||
// 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()
|
||||||
|
|
||||||
|
@ -240,7 +286,9 @@ export default (options: ComponentOptions) => {
|
||||||
if (!this._stateToElementsMap[expr])
|
if (!this._stateToElementsMap[expr])
|
||||||
this._stateToElementsMap[expr] = new Set()
|
this._stateToElementsMap[expr] = new Set()
|
||||||
|
|
||||||
this._stateToElementsMap[expr].add(textNode as unknown as HTMLElement)
|
this._stateToElementsMap[expr].add(
|
||||||
|
textNode as unknown as HTMLElement,
|
||||||
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -248,13 +296,12 @@ export default (options: ComponentOptions) => {
|
||||||
|
|
||||||
// Handle element nodes (can extend to handle attribute bindings, etc.)
|
// Handle element nodes (can extend to handle attribute bindings, etc.)
|
||||||
else if (currentNode.nodeType === Node.ELEMENT_NODE) {
|
else if (currentNode.nodeType === Node.ELEMENT_NODE) {
|
||||||
|
|
||||||
const currentElementNode = currentNode as Element // Renamed to avoid conflict with outer 'element'
|
const currentElementNode = currentNode as Element // Renamed to avoid conflict with outer 'element'
|
||||||
|
|
||||||
// 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 => {
|
Array.from(currentElementNode.attributes).forEach((attr) => {
|
||||||
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()
|
||||||
|
@ -263,13 +310,20 @@ export default (options: ComponentOptions) => {
|
||||||
currentElementNode.removeAttribute(attr.name)
|
currentElementNode.removeAttribute(attr.name)
|
||||||
|
|
||||||
// Set up attribute binding
|
// Set up attribute binding
|
||||||
this._setupAttributeBinding(currentElementNode, attrName, expr, attr.value)
|
this._setupAttributeBinding(
|
||||||
|
currentElementNode,
|
||||||
|
attrName,
|
||||||
|
expr,
|
||||||
|
attr.value,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// Process @event bindings, such as @click="handleClick"
|
// Process @event bindings, such as @click="handleClick"
|
||||||
const eventBindings = Array.from(currentElementNode.attributes).filter(attr => attr.name.startsWith('@'))
|
const eventBindings = Array.from(
|
||||||
eventBindings.forEach(attr => {
|
currentElementNode.attributes,
|
||||||
|
).filter((attr) => attr.name.startsWith('@'))
|
||||||
|
eventBindings.forEach((attr) => {
|
||||||
const eventName = attr.name.substring(1) // Remove '@'
|
const eventName = attr.name.substring(1) // Remove '@'
|
||||||
const handlerValue = attr.value.trim()
|
const handlerValue = attr.value.trim()
|
||||||
|
|
||||||
|
@ -279,22 +333,42 @@ export default (options: ComponentOptions) => {
|
||||||
// Handle different types of event handlers
|
// Handle different types of event handlers
|
||||||
if (handlerValue.includes('=>')) {
|
if (handlerValue.includes('=>')) {
|
||||||
// Handle arrow function: @click="e => setState('count', count + 1)"
|
// Handle arrow function: @click="e => setState('count', count + 1)"
|
||||||
this._setupArrowFunctionHandler(currentElementNode, eventName, handlerValue)
|
this._setupArrowFunctionHandler(
|
||||||
} else if (handlerValue.includes('(') && handlerValue.includes(')')) {
|
currentElementNode,
|
||||||
|
eventName,
|
||||||
|
handlerValue,
|
||||||
|
)
|
||||||
|
} else if (
|
||||||
|
handlerValue.includes('(') &&
|
||||||
|
handlerValue.includes(')')
|
||||||
|
) {
|
||||||
// Handle function call: @click="increment(5)"
|
// Handle function call: @click="increment(5)"
|
||||||
this._setupFunctionCallHandler(currentElementNode, eventName, handlerValue)
|
this._setupFunctionCallHandler(
|
||||||
|
currentElementNode,
|
||||||
|
eventName,
|
||||||
|
handlerValue,
|
||||||
|
)
|
||||||
} else if (typeof (this as any)[handlerValue] === 'function') {
|
} else if (typeof (this as any)[handlerValue] === 'function') {
|
||||||
// Handle method reference: @click="handleClick"
|
// Handle method reference: @click="handleClick"
|
||||||
currentElementNode.addEventListener(eventName, (this as any)[handlerValue].bind(this))
|
currentElementNode.addEventListener(
|
||||||
|
eventName,
|
||||||
|
(this as any)[handlerValue].bind(this),
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
// Handle simple expression: @click="count++" or @input="name = $event.target.value"
|
// Handle simple expression: @click="count++" or @input="name = $event.target.value"
|
||||||
this._setupExpressionHandler(currentElementNode, eventName, handlerValue)
|
this._setupExpressionHandler(
|
||||||
|
currentElementNode,
|
||||||
|
eventName,
|
||||||
|
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(currentElementNode.attributes).filter(attr => attr.name.startsWith('%'))
|
const macroBindings = Array.from(
|
||||||
macroBindings.forEach(attr => {
|
currentElementNode.attributes,
|
||||||
|
).filter((attr) => attr.name.startsWith('%'))
|
||||||
|
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()
|
||||||
|
|
||||||
|
@ -302,19 +376,16 @@ export default (options: ComponentOptions) => {
|
||||||
currentElementNode.removeAttribute(attr.name)
|
currentElementNode.removeAttribute(attr.name)
|
||||||
|
|
||||||
// Handle different types of macros
|
// Handle different types of macros
|
||||||
if (macroName === 'connect') // Handle state connection: %connect="stateName"
|
if (macroName === 'connect')
|
||||||
|
// Handle state connection: %connect="stateName"
|
||||||
this._setupTwoWayBinding(currentElementNode, expr)
|
this._setupTwoWayBinding(currentElementNode, expr)
|
||||||
else if (macroName === 'if')
|
else if (macroName === 'if')
|
||||||
ifDirectivesToProcess.push({ element: currentElementNode, expr })
|
ifDirectivesToProcess.push({ element: currentElementNode, expr })
|
||||||
else if (macroName === 'for')
|
else if (macroName === 'for')
|
||||||
this._setupListRendering(currentElementNode, expr)
|
this._setupListRendering(currentElementNode, expr)
|
||||||
else if (macroName === 'key')
|
else if (macroName === 'key') return
|
||||||
return
|
else console.warn(`Unknown macro: %${macroName}`)
|
||||||
else
|
|
||||||
console.warn(`Unknown macro: %${macroName}`)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -336,7 +407,9 @@ export default (options: ComponentOptions) => {
|
||||||
if (value !== undefined)
|
if (value !== undefined)
|
||||||
element.setAttribute('data-laterano-connect', String(value))
|
element.setAttribute('data-laterano-connect', String(value))
|
||||||
else
|
else
|
||||||
console.error(`State \`${expr}\` not found in the component state. Although Laterano will try to work with it, it may has potentially unexpected behavior.`)
|
console.error(
|
||||||
|
`State \`${expr}\` not found in the component state. Although Laterano will try to work with it, it may has potentially unexpected behavior.`,
|
||||||
|
)
|
||||||
|
|
||||||
// Add event listener for input events
|
// Add event listener for input events
|
||||||
element.addEventListener('input', (event: Event) => {
|
element.addEventListener('input', (event: Event) => {
|
||||||
|
@ -359,20 +432,19 @@ export default (options: ComponentOptions) => {
|
||||||
|
|
||||||
// Handle condition rendering (%if macro)
|
// Handle condition rendering (%if macro)
|
||||||
private _setupConditionRendering(element: Element, expr: string) {
|
private _setupConditionRendering(element: Element, expr: string) {
|
||||||
|
|
||||||
const placeholder = document.createComment(` %if: ${expr} `)
|
const placeholder = document.createComment(` %if: ${expr} `)
|
||||||
element.parentNode?.insertBefore(placeholder, element)
|
element.parentNode?.insertBefore(placeholder, element)
|
||||||
|
|
||||||
this._conditionalElements.set(element, {
|
this._conditionalElements.set(element, {
|
||||||
expr,
|
expr,
|
||||||
placeholder,
|
placeholder,
|
||||||
isPresent: true
|
isPresent: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
this._evaluateIfCondition(element, expr)
|
this._evaluateIfCondition(element, expr)
|
||||||
|
|
||||||
const statePaths = this._extractStatePathsFromExpression(expr)
|
const statePaths = this._extractStatePathsFromExpression(expr)
|
||||||
statePaths.forEach(path => {
|
statePaths.forEach((path) => {
|
||||||
if (!this._stateToElementsMap[path]) {
|
if (!this._stateToElementsMap[path]) {
|
||||||
this._stateToElementsMap[path] = new Set()
|
this._stateToElementsMap[path] = new Set()
|
||||||
}
|
}
|
||||||
|
@ -383,7 +455,9 @@ export default (options: ComponentOptions) => {
|
||||||
// Handle list rendering (%for macro)
|
// Handle list rendering (%for macro)
|
||||||
private _setupListRendering(element: Element, expr: string) {
|
private _setupListRendering(element: Element, expr: string) {
|
||||||
// Parse the expression (e.g., "item in items" or "(item, index) in items")
|
// Parse the expression (e.g., "item in items" or "(item, index) in items")
|
||||||
const match = expr.match(/(?:\(([^,]+),\s*([^)]+)\)|([^,\s]+))\s+in\s+(.+)/)
|
const match = expr.match(
|
||||||
|
/(?:\(([^,]+),\s*([^)]+)\)|([^,\s]+))\s+in\s+(.+)/,
|
||||||
|
)
|
||||||
if (!match) {
|
if (!match) {
|
||||||
console.error(`Invalid %for expression: ${expr}`)
|
console.error(`Invalid %for expression: ${expr}`)
|
||||||
return
|
return
|
||||||
|
@ -404,9 +478,9 @@ export default (options: ComponentOptions) => {
|
||||||
|
|
||||||
// Store current rendered items
|
// Store current rendered items
|
||||||
const renderedItems: Array<{
|
const renderedItems: Array<{
|
||||||
element: Element,
|
element: Element
|
||||||
key: any,
|
key: any
|
||||||
data: any,
|
data: any
|
||||||
index: number
|
index: number
|
||||||
}> = []
|
}> = []
|
||||||
|
|
||||||
|
@ -414,7 +488,9 @@ export default (options: ComponentOptions) => {
|
||||||
const updateList = () => {
|
const updateList = () => {
|
||||||
const collection = this._evaluateExpression(collectionExpr)
|
const collection = this._evaluateExpression(collectionExpr)
|
||||||
if (!collection || !Array.isArray(collection)) {
|
if (!collection || !Array.isArray(collection)) {
|
||||||
console.warn(`Collection "${collectionExpr}" is not an array or does not exist`)
|
console.warn(
|
||||||
|
`Collection "${collectionExpr}" is not an array or does not exist`,
|
||||||
|
)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -425,19 +501,22 @@ export default (options: ComponentOptions) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Detach all currently rendered DOM items managed by this instance.
|
// Detach all currently rendered DOM items managed by this instance.
|
||||||
renderedItems.forEach(item => {
|
renderedItems.forEach((item) => {
|
||||||
if (item.element.parentNode === parentNode) {
|
if (item.element.parentNode === parentNode) {
|
||||||
parentNode.removeChild(item.element);
|
parentNode.removeChild(item.element)
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
|
|
||||||
// Get key attribute if available
|
// Get key attribute if available
|
||||||
const keyAttr = template.getAttribute('%key')
|
const keyAttr = template.getAttribute('%key')
|
||||||
if (!keyAttr) console.warn(`%key attribute not found in the template, which is not a recommended practice.`)
|
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
|
// Store a map of existing items by key for reuse
|
||||||
const existingElementsByKey = new Map()
|
const existingElementsByKey = new Map()
|
||||||
renderedItems.forEach(item => {
|
renderedItems.forEach((item) => {
|
||||||
if (item.key !== undefined) {
|
if (item.key !== undefined) {
|
||||||
existingElementsByKey.set(item.key, item)
|
existingElementsByKey.set(item.key, item)
|
||||||
}
|
}
|
||||||
|
@ -452,7 +531,15 @@ export default (options: ComponentOptions) => {
|
||||||
// Create or update items in the list
|
// Create or update items in the list
|
||||||
collection.forEach((item, index) => {
|
collection.forEach((item, index) => {
|
||||||
// Determine the key for this item
|
// Determine the key for this item
|
||||||
const key = keyAttr ? this._evaluateExpressionWithItemContext(keyAttr ?? '', item, index, itemVar, indexVar ? indexVar : undefined) : index
|
const key = keyAttr
|
||||||
|
? this._evaluateExpressionWithItemContext(
|
||||||
|
keyAttr ?? '',
|
||||||
|
item,
|
||||||
|
index,
|
||||||
|
itemVar,
|
||||||
|
indexVar ? indexVar : undefined,
|
||||||
|
)
|
||||||
|
: index
|
||||||
|
|
||||||
// Check if we can reuse an existing element
|
// Check if we can reuse an existing element
|
||||||
const existingItem = existingElementsByKey.get(key)
|
const existingItem = existingElementsByKey.get(key)
|
||||||
|
@ -472,19 +559,21 @@ export default (options: ComponentOptions) => {
|
||||||
element: itemElement,
|
element: itemElement,
|
||||||
key,
|
key,
|
||||||
data: item,
|
data: item,
|
||||||
index
|
index,
|
||||||
})
|
})
|
||||||
|
|
||||||
// Create item context for this item
|
// Create item context for this item
|
||||||
const itemContext = {
|
const itemContext = {
|
||||||
[itemVar]: item
|
[itemVar]: item,
|
||||||
}
|
}
|
||||||
if (indexVar)
|
if (indexVar) itemContext[indexVar] = index
|
||||||
itemContext[indexVar] = index
|
|
||||||
|
|
||||||
// insert %key attribute, which dynamically bind the key
|
// insert %key attribute, which dynamically bind the key
|
||||||
if (keyAttr) {
|
if (keyAttr) {
|
||||||
const keyValue = this._evaluateExpressionWithItemContext(keyAttr, itemContext)
|
const keyValue = this._evaluateExpressionWithItemContext(
|
||||||
|
keyAttr,
|
||||||
|
itemContext,
|
||||||
|
)
|
||||||
itemElement.setAttribute('data-laterano-key', String(keyValue))
|
itemElement.setAttribute('data-laterano-key', String(keyValue))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -503,7 +592,7 @@ export default (options: ComponentOptions) => {
|
||||||
placeholder.parentNode?.insertBefore(fragment, placeholder.nextSibling)
|
placeholder.parentNode?.insertBefore(fragment, placeholder.nextSibling)
|
||||||
|
|
||||||
// Remove any remaining unused items
|
// Remove any remaining unused items
|
||||||
existingElementsByKey.forEach(item => {
|
existingElementsByKey.forEach((item) => {
|
||||||
if (item.element.parentNode) {
|
if (item.element.parentNode) {
|
||||||
item.element.parentNode.removeChild(item.element)
|
item.element.parentNode.removeChild(item.element)
|
||||||
}
|
}
|
||||||
|
@ -519,7 +608,9 @@ export default (options: ComponentOptions) => {
|
||||||
}
|
}
|
||||||
// Using a unique identifier for this list rendering instance
|
// Using a unique identifier for this list rendering instance
|
||||||
const listVirtualElement = document.createElement('div')
|
const listVirtualElement = document.createElement('div')
|
||||||
this._stateToElementsMap[collectionExpr].add(listVirtualElement as HTMLElement)
|
this._stateToElementsMap[collectionExpr].add(
|
||||||
|
listVirtualElement as HTMLElement,
|
||||||
|
)
|
||||||
|
|
||||||
// Add listener for state changes
|
// Add listener for state changes
|
||||||
this._statesListeners[collectionExpr] = () => {
|
this._statesListeners[collectionExpr] = () => {
|
||||||
|
@ -528,9 +619,12 @@ export default (options: ComponentOptions) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Recursively process the element and its children, applying the item context
|
// Recursively process the element and its children, applying the item context
|
||||||
private _processElementWithItemContext(element: Element, itemContext: Record<string, any>) {
|
private _processElementWithItemContext(
|
||||||
|
element: Element,
|
||||||
|
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) => {
|
||||||
|
@ -538,28 +632,37 @@ export default (options: ComponentOptions) => {
|
||||||
const textContent = node.textContent || ''
|
const textContent = node.textContent || ''
|
||||||
if (textContent.includes('{{')) {
|
if (textContent.includes('{{')) {
|
||||||
const textNode = node as Text
|
const textNode = node as Text
|
||||||
const updatedContent = textContent.replace(/\{\{\s*([^}]+)\s*\}\}/g, (match, expr) => {
|
const updatedContent = textContent.replace(
|
||||||
const value = this._evaluateExpressionWithItemContext(expr.trim(), itemContext)
|
/\{\{\s*([^}]+)\s*\}\}/g,
|
||||||
return value !== undefined ? String(value) : ''
|
(match, expr) => {
|
||||||
})
|
const value = this._evaluateExpressionWithItemContext(
|
||||||
|
expr.trim(),
|
||||||
|
itemContext,
|
||||||
|
)
|
||||||
|
return value !== undefined ? String(value) : ''
|
||||||
|
},
|
||||||
|
)
|
||||||
textNode.textContent = updatedContent
|
textNode.textContent = updatedContent
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process the text nodes of the element itself
|
// Process the text nodes of the element itself
|
||||||
Array.from(element.childNodes).forEach(node => {
|
Array.from(element.childNodes).forEach((node) => {
|
||||||
if (node.nodeType === Node.TEXT_NODE) {
|
if (node.nodeType === Node.TEXT_NODE) {
|
||||||
processTextNodes(node)
|
processTextNodes(node)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// 3. Process attribute bindings (:attr)
|
// 3. Process attribute bindings (:attr)
|
||||||
Array.from(element.attributes).forEach(attr => {
|
Array.from(element.attributes).forEach((attr) => {
|
||||||
if (attr.name.startsWith(':')) {
|
if (attr.name.startsWith(':')) {
|
||||||
const attrName = attr.name.substring(1)
|
const attrName = attr.name.substring(1)
|
||||||
const expr = attr.value.trim()
|
const expr = attr.value.trim()
|
||||||
const value = this._evaluateExpressionWithItemContext(expr, itemContext)
|
const value = this._evaluateExpressionWithItemContext(
|
||||||
|
expr,
|
||||||
|
itemContext,
|
||||||
|
)
|
||||||
|
|
||||||
if (value !== undefined) {
|
if (value !== undefined) {
|
||||||
element.setAttribute(attrName, String(value))
|
element.setAttribute(attrName, String(value))
|
||||||
|
@ -571,7 +674,7 @@ export default (options: ComponentOptions) => {
|
||||||
})
|
})
|
||||||
|
|
||||||
// 4. Process event bindings (@event)
|
// 4. Process event bindings (@event)
|
||||||
Array.from(element.attributes).forEach(attr => {
|
Array.from(element.attributes).forEach((attr) => {
|
||||||
if (attr.name.startsWith('@')) {
|
if (attr.name.startsWith('@')) {
|
||||||
const eventName = attr.name.substring(1)
|
const eventName = attr.name.substring(1)
|
||||||
const handlerValue = attr.value.trim()
|
const handlerValue = attr.value.trim()
|
||||||
|
@ -587,14 +690,17 @@ export default (options: ComponentOptions) => {
|
||||||
...this._createHandlerContext(event, element),
|
...this._createHandlerContext(event, element),
|
||||||
...itemContext,
|
...itemContext,
|
||||||
$event: event,
|
$event: event,
|
||||||
$el: element
|
$el: element,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Execute the expression
|
// Execute the expression
|
||||||
const fnStr = `with(this) { ${handlerValue} }`
|
const fnStr = `with(this) { ${handlerValue} }`
|
||||||
new Function(fnStr).call(mergedContext)
|
new Function(fnStr).call(mergedContext)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(`Error executing event handler with item context: ${handlerValue}`, err)
|
console.error(
|
||||||
|
`Error executing event handler with item context: ${handlerValue}`,
|
||||||
|
err,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -604,7 +710,7 @@ export default (options: ComponentOptions) => {
|
||||||
let isConditional = false
|
let isConditional = false
|
||||||
let shouldDisplay = true
|
let shouldDisplay = true
|
||||||
|
|
||||||
Array.from(element.attributes).forEach(attr => {
|
Array.from(element.attributes).forEach((attr) => {
|
||||||
if (attr.name === '%if') {
|
if (attr.name === '%if') {
|
||||||
isConditional = true
|
isConditional = true
|
||||||
const expr = attr.value.trim()
|
const expr = attr.value.trim()
|
||||||
|
@ -613,12 +719,14 @@ export default (options: ComponentOptions) => {
|
||||||
element.removeAttribute(attr.name)
|
element.removeAttribute(attr.name)
|
||||||
|
|
||||||
// Calculate the condition
|
// Calculate the condition
|
||||||
const result = this._evaluateExpressionWithItemContext(expr, itemContext)
|
const result = this._evaluateExpressionWithItemContext(
|
||||||
|
expr,
|
||||||
|
itemContext,
|
||||||
|
)
|
||||||
shouldDisplay = Boolean(result)
|
shouldDisplay = Boolean(result)
|
||||||
|
|
||||||
// Apply the condition (in the list item context, we use display style to simplify)
|
// Apply the condition (in the list item context, we use display style to simplify)
|
||||||
if (!shouldDisplay)
|
if (!shouldDisplay) (element as HTMLElement).style.display = 'none'
|
||||||
(element as HTMLElement).style.display = 'none'
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -630,7 +738,7 @@ export default (options: ComponentOptions) => {
|
||||||
// 6. Process nested list rendering (%for)
|
// 6. Process nested list rendering (%for)
|
||||||
let hasForDirective = false
|
let hasForDirective = false
|
||||||
|
|
||||||
Array.from(element.attributes).forEach(attr => {
|
Array.from(element.attributes).forEach((attr) => {
|
||||||
if (attr.name === '%for') {
|
if (attr.name === '%for') {
|
||||||
hasForDirective = true
|
hasForDirective = true
|
||||||
const forExpr = attr.value.trim()
|
const forExpr = attr.value.trim()
|
||||||
|
@ -650,16 +758,22 @@ export default (options: ComponentOptions) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
// 7. Recursively process all child elements
|
// 7. Recursively process all child elements
|
||||||
Array.from(element.children).forEach(child => {
|
Array.from(element.children).forEach((child) => {
|
||||||
this._processElementWithItemContext(child, itemContext)
|
this._processElementWithItemContext(child, itemContext)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set up nested list rendering
|
// Set up nested list rendering
|
||||||
private _setupNestedListRendering(element: Element, expr: string, parentItemContext: Record<string, any>) {
|
private _setupNestedListRendering(
|
||||||
|
element: Element,
|
||||||
|
expr: string,
|
||||||
|
parentItemContext: Record<string, any>,
|
||||||
|
) {
|
||||||
// Similar to _setupListRendering, but applies to nested situations
|
// Similar to _setupListRendering, but applies to nested situations
|
||||||
// Parse the expression (e.g., "subItem in item.subItems")
|
// Parse the expression (e.g., "subItem in item.subItems")
|
||||||
const match = expr.match(/(?:\(([^,]+),\s*([^)]+)\)|([^,\s]+))\s+in\s+(.+)/)
|
const match = expr.match(
|
||||||
|
/(?:\(([^,]+),\s*([^)]+)\)|([^,\s]+))\s+in\s+(.+)/,
|
||||||
|
)
|
||||||
if (!match) {
|
if (!match) {
|
||||||
console.error(`Invalid nested %for expression: ${expr}`)
|
console.error(`Invalid nested %for expression: ${expr}`)
|
||||||
return
|
return
|
||||||
|
@ -671,10 +785,15 @@ export default (options: ComponentOptions) => {
|
||||||
const collectionExpr = match[4].trim()
|
const collectionExpr = match[4].trim()
|
||||||
|
|
||||||
// Evaluate the collection expression, using the parent item context
|
// Evaluate the collection expression, using the parent item context
|
||||||
const collection = this._evaluateExpressionWithItemContext(collectionExpr, parentItemContext)
|
const collection = this._evaluateExpressionWithItemContext(
|
||||||
|
collectionExpr,
|
||||||
|
parentItemContext,
|
||||||
|
)
|
||||||
|
|
||||||
if (!collection || !Array.isArray(collection)) {
|
if (!collection || !Array.isArray(collection)) {
|
||||||
console.warn(`Nested collection "${collectionExpr}" is not an array or does not exist`)
|
console.warn(
|
||||||
|
`Nested collection "${collectionExpr}" is not an array or does not exist`,
|
||||||
|
)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -693,7 +812,7 @@ export default (options: ComponentOptions) => {
|
||||||
// Create a nested item context, merging the parent context
|
// Create a nested item context, merging the parent context
|
||||||
const nestedItemContext = {
|
const nestedItemContext = {
|
||||||
...parentItemContext,
|
...parentItemContext,
|
||||||
[itemVar]: item
|
[itemVar]: item,
|
||||||
}
|
}
|
||||||
|
|
||||||
if (indexVar) {
|
if (indexVar) {
|
||||||
|
@ -705,14 +824,23 @@ export default (options: ComponentOptions) => {
|
||||||
|
|
||||||
// TODO: detect list items existed inside the view, use replace instead of remove and re-add,
|
// TODO: detect list items existed inside the view, use replace instead of remove and re-add,
|
||||||
// to improve performance
|
// to improve performance
|
||||||
|
|
||||||
// Insert the item element into the DOM
|
// Insert the item element into the DOM
|
||||||
placeholder.parentNode?.insertBefore(itemElement, placeholder.nextSibling)
|
placeholder.parentNode?.insertBefore(
|
||||||
|
itemElement,
|
||||||
|
placeholder.nextSibling,
|
||||||
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Evaluate expressions using the item context
|
// Evaluate expressions using the item context
|
||||||
private _evaluateExpressionWithItemContext(expression: string, itemContext: Record<string, any>, index?: number, itemVar?: string, indexVar?: string): any {
|
private _evaluateExpressionWithItemContext(
|
||||||
|
expression: string,
|
||||||
|
itemContext: Record<string, any>,
|
||||||
|
index?: number,
|
||||||
|
itemVar?: string,
|
||||||
|
indexVar?: string,
|
||||||
|
): any {
|
||||||
try {
|
try {
|
||||||
// Check if the expression directly references the item variable
|
// Check if the expression directly references the item variable
|
||||||
if (itemVar && expression === itemVar) {
|
if (itemVar && expression === itemVar) {
|
||||||
|
@ -751,7 +879,10 @@ export default (options: ComponentOptions) => {
|
||||||
const func = new Function(...contextKeys, `return ${expression}`)
|
const func = new Function(...contextKeys, `return ${expression}`)
|
||||||
return func(...contextValues)
|
return func(...contextValues)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Error evaluating expression with item context: ${expression}`, error)
|
console.error(
|
||||||
|
`Error evaluating expression with item context: ${expression}`,
|
||||||
|
error,
|
||||||
|
)
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -765,10 +896,14 @@ export default (options: ComponentOptions) => {
|
||||||
const shouldShow = Boolean(result)
|
const shouldShow = Boolean(result)
|
||||||
|
|
||||||
if (shouldShow !== info.isPresent) {
|
if (shouldShow !== info.isPresent) {
|
||||||
if (shouldShow) // Insert the element back into the DOM
|
if (shouldShow)
|
||||||
info.placeholder.parentNode?.insertBefore(element, info.placeholder.nextSibling)
|
// Insert the element back into the DOM
|
||||||
else // Remove the element from the DOM
|
info.placeholder.parentNode?.insertBefore(
|
||||||
element.parentNode?.removeChild(element)
|
element,
|
||||||
|
info.placeholder.nextSibling,
|
||||||
|
)
|
||||||
|
// Remove the element from the DOM
|
||||||
|
else element.parentNode?.removeChild(element)
|
||||||
|
|
||||||
// Update the state
|
// Update the state
|
||||||
info.isPresent = shouldShow
|
info.isPresent = shouldShow
|
||||||
|
@ -789,7 +924,9 @@ export default (options: ComponentOptions) => {
|
||||||
const func = new Function(...stateKeys, `return ${expression}`)
|
const func = new Function(...stateKeys, `return ${expression}`)
|
||||||
const execRes = func(...stateValues)
|
const execRes = func(...stateValues)
|
||||||
if (typeof execRes !== 'boolean')
|
if (typeof execRes !== 'boolean')
|
||||||
throw new Error(`The expression "${expression}" must return a boolean value.`)
|
throw new Error(
|
||||||
|
`The expression "${expression}" must return a boolean value.`,
|
||||||
|
)
|
||||||
return execRes
|
return execRes
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Error evaluating expression: ${expression}`, error)
|
console.error(`Error evaluating expression: ${expression}`, error)
|
||||||
|
@ -799,13 +936,18 @@ export default (options: ComponentOptions) => {
|
||||||
|
|
||||||
private _extractStatePathsFromExpression(expression: string): string[] {
|
private _extractStatePathsFromExpression(expression: string): string[] {
|
||||||
const matches = expression.match(/[a-zA-Z_$][a-zA-Z0-9_$]*/g) || []
|
const matches = expression.match(/[a-zA-Z_$][a-zA-Z0-9_$]*/g) || []
|
||||||
return matches.filter(match =>
|
return matches.filter(
|
||||||
!['true', 'false', 'null', 'undefined', 'this'].includes(match)
|
(match) =>
|
||||||
|
!['true', 'false', 'null', 'undefined', 'this'].includes(match),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle arrow function
|
// Handle arrow function
|
||||||
private _setupArrowFunctionHandler(element: Element, eventName: string, handlerValue: string) {
|
private _setupArrowFunctionHandler(
|
||||||
|
element: Element,
|
||||||
|
eventName: string,
|
||||||
|
handlerValue: string,
|
||||||
|
) {
|
||||||
element.addEventListener(eventName, (event: Event) => {
|
element.addEventListener(eventName, (event: Event) => {
|
||||||
try {
|
try {
|
||||||
// Arrow function parsing
|
// Arrow function parsing
|
||||||
|
@ -860,7 +1002,10 @@ export default (options: ComponentOptions) => {
|
||||||
handlerFn.apply(context, [event])
|
handlerFn.apply(context, [event])
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(`Error executing arrow function handler: ${handlerValue}`, err)
|
console.error(
|
||||||
|
`Error executing arrow function handler: ${handlerValue}`,
|
||||||
|
err,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -881,21 +1026,30 @@ export default (options: ComponentOptions) => {
|
||||||
$el: element,
|
$el: element,
|
||||||
this: this, // Provide reference to the component instance
|
this: this, // Provide reference to the component instance
|
||||||
setState: this.setState.bind(this),
|
setState: this.setState.bind(this),
|
||||||
getState: this.getState.bind(this)
|
getState: this.getState.bind(this),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add all methods of the component
|
// Add all methods of the component
|
||||||
Object.getOwnPropertyNames(Object.getPrototypeOf(this)).forEach(name => {
|
Object.getOwnPropertyNames(Object.getPrototypeOf(this)).forEach(
|
||||||
if (typeof (this as any)[name] === 'function' && name !== 'constructor') {
|
(name) => {
|
||||||
context[name] = (this as any)[name].bind(this)
|
if (
|
||||||
}
|
typeof (this as any)[name] === 'function' &&
|
||||||
})
|
name !== 'constructor'
|
||||||
|
) {
|
||||||
|
context[name] = (this as any)[name].bind(this)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
return context
|
return context
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle function call, such as @click="increment(5)"
|
// Handle function call, such as @click="increment(5)"
|
||||||
private _setupFunctionCallHandler(element: Element, eventName: string, handlerValue: string) {
|
private _setupFunctionCallHandler(
|
||||||
|
element: Element,
|
||||||
|
eventName: string,
|
||||||
|
handlerValue: string,
|
||||||
|
) {
|
||||||
element.addEventListener(eventName, (event: Event) => {
|
element.addEventListener(eventName, (event: Event) => {
|
||||||
try {
|
try {
|
||||||
// Create context object
|
// Create context object
|
||||||
|
@ -910,13 +1064,20 @@ export default (options: ComponentOptions) => {
|
||||||
|
|
||||||
new Function(fnStr).call(context)
|
new Function(fnStr).call(context)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(`Error executing function call handler: ${handlerValue}`, err)
|
console.error(
|
||||||
|
`Error executing function call handler: ${handlerValue}`,
|
||||||
|
err,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle simple expression, such as @click="count++" or @input="name = $event.target.value"
|
// Handle simple expression, such as @click="count++" or @input="name = $event.target.value"
|
||||||
private _setupExpressionHandler(element: Element, eventName: string, handlerValue: string) {
|
private _setupExpressionHandler(
|
||||||
|
element: Element,
|
||||||
|
eventName: string,
|
||||||
|
handlerValue: string,
|
||||||
|
) {
|
||||||
element.addEventListener(eventName, (event: Event) => {
|
element.addEventListener(eventName, (event: Event) => {
|
||||||
try {
|
try {
|
||||||
// Create context object
|
// Create context object
|
||||||
|
@ -935,7 +1096,10 @@ export default (options: ComponentOptions) => {
|
||||||
// If the expression returns a value, it can be used for two-way binding
|
// If the expression returns a value, it can be used for two-way binding
|
||||||
return result
|
return result
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(`Error executing expression handler: ${handlerValue}`, err)
|
console.error(
|
||||||
|
`Error executing expression handler: ${handlerValue}`,
|
||||||
|
err,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -959,7 +1123,12 @@ export default (options: ComponentOptions) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set up attribute binding
|
// Set up attribute binding
|
||||||
private _setupAttributeBinding(element: Element, attrName: string, expr: string, template: string) {
|
private _setupAttributeBinding(
|
||||||
|
element: Element,
|
||||||
|
attrName: string,
|
||||||
|
expr: string,
|
||||||
|
template: string,
|
||||||
|
) {
|
||||||
// Initialize attribute value
|
// Initialize attribute value
|
||||||
const value = this._getNestedState(expr)
|
const value = this._getNestedState(expr)
|
||||||
|
|
||||||
|
@ -977,7 +1146,7 @@ export default (options: ComponentOptions) => {
|
||||||
element,
|
element,
|
||||||
attrName,
|
attrName,
|
||||||
expr,
|
expr,
|
||||||
template
|
template,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1009,7 +1178,11 @@ export default (options: ComponentOptions) => {
|
||||||
return ['data-attribute']
|
return ['data-attribute']
|
||||||
}
|
}
|
||||||
|
|
||||||
attributeChangedCallback(attrName: string, oldValue: string, newValue: string) {
|
attributeChangedCallback(
|
||||||
|
attrName: string,
|
||||||
|
oldValue: string,
|
||||||
|
newValue: string,
|
||||||
|
) {
|
||||||
if (onAttributeChanged) onAttributeChanged(attrName, oldValue, newValue)
|
if (onAttributeChanged) onAttributeChanged(attrName, oldValue, newValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1032,10 +1205,9 @@ export default (options: ComponentOptions) => {
|
||||||
|
|
||||||
// function trigger
|
// function trigger
|
||||||
triggerFunc(eventName: string, ...args: any[]) {
|
triggerFunc(eventName: string, ...args: any[]) {
|
||||||
if (funcs && funcs[eventName])
|
if (funcs && funcs[eventName]) funcs[eventName].call(this, ...args)
|
||||||
funcs[eventName].call(this, ...args)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
customElements.define(tag, CustomElementImpl)
|
customElements.define(tag, CustomElementImpl)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user