161
src/compiler/codeshift.ts
Normal file
161
src/compiler/codeshift.ts
Normal file
@ -0,0 +1,161 @@
|
||||
import { ArrayExpression, Identifier, JSCodeshift, Literal, ObjectExpression, Property, Transform } from "jscodeshift"
|
||||
import pathUtils from 'path/posix'
|
||||
interface ObjectField {
|
||||
type: 'Object'
|
||||
items: Record<string, Field>
|
||||
item: ObjectExpression
|
||||
}
|
||||
|
||||
interface EndField {
|
||||
type: 'Literal'
|
||||
item: Literal
|
||||
}
|
||||
|
||||
interface ArrayField {
|
||||
type: 'Array'
|
||||
items: Array<Field>
|
||||
item: ArrayExpression
|
||||
}
|
||||
|
||||
type Field = ObjectField | EndField | ArrayField
|
||||
type Possible = ObjectExpression | ArrayExpression | Literal
|
||||
|
||||
function processItem(value: Possible): Field {
|
||||
|
||||
if (value.type === 'ObjectExpression') {
|
||||
return simplify(value)
|
||||
} else if (value.type === 'ArrayExpression') {
|
||||
const field: Field = {
|
||||
type: 'Array',
|
||||
items: [],
|
||||
item: value
|
||||
}
|
||||
value.elements.forEach((it) => {
|
||||
field.items.push(processItem(it as Possible))
|
||||
})
|
||||
return field
|
||||
} else {
|
||||
return {
|
||||
type: 'Literal',
|
||||
item: value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function simplify(base: ObjectExpression): ObjectField {
|
||||
const list: ObjectField['items'] = {}
|
||||
base.properties.forEach((it) => {
|
||||
const item = it as Property
|
||||
const key = (item.key as Identifier).name
|
||||
list[key] = processItem(item.value as Possible)
|
||||
})
|
||||
return {
|
||||
type: 'Object',
|
||||
items: list,
|
||||
item: base
|
||||
}
|
||||
}
|
||||
|
||||
function exists(path: ObjectExpression | ArrayExpression, key: string | number) {
|
||||
if (path.type === 'ObjectExpression') {
|
||||
path.properties.forEach((p) => {
|
||||
const prop = p as Property
|
||||
if ((prop.key as Identifier).name === (key + '')) {
|
||||
return true
|
||||
}
|
||||
})
|
||||
return false
|
||||
} else {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
function set(j: JSCodeshift, path: ObjectExpression | ArrayExpression, value: Possible, key: string | number, options?: { override?: boolean }) {
|
||||
|
||||
let exists = false
|
||||
if (path.type === 'ObjectExpression') {
|
||||
path.properties.forEach((p) => {
|
||||
const prop = p as Property
|
||||
if ((prop.key as Identifier).name === (key + '')) {
|
||||
exists = true
|
||||
if (!options?.override) {
|
||||
console.warn('Property already exist, add the option override to change it')
|
||||
return
|
||||
}
|
||||
prop.value = value
|
||||
}
|
||||
})
|
||||
if (exists) { return }
|
||||
|
||||
if (key.toString().includes('-')) {
|
||||
key = `'${key.toString()}'`
|
||||
}
|
||||
|
||||
path.properties.push(j.property('init', j.identifier(key + ''), value))
|
||||
} else {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
function remove(path: ObjectExpression | ArrayExpression, key: string | number) {
|
||||
if (path.type === 'ObjectExpression') {
|
||||
const index = path.properties.findIndex((p) => ((p as Property).key as Identifier).name === (key + ''))
|
||||
if (index === -1) {
|
||||
return
|
||||
}
|
||||
path.properties.splice(index)
|
||||
} else {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
function rename(parent: ObjectExpression, oldKey: string, newKey: string) {
|
||||
parent.properties.forEach((p) => {
|
||||
if (p.key.name === oldKey) {
|
||||
p.key.name = newKey
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Start editing here !
|
||||
*/
|
||||
module.exports = (file, api): Transformer => {
|
||||
const j = api.jscodeshift
|
||||
|
||||
const root = j(file.source)
|
||||
return root
|
||||
.find(j.ObjectExpression)
|
||||
.forEach((path, index) => {
|
||||
if (index !== 0) return
|
||||
const filename = pathUtils.basename(file.path, '.ts')
|
||||
let simplified = simplify(path.node)
|
||||
|
||||
rename(simplified.item, 'abbrevation', 'abbreviations')
|
||||
|
||||
// set(j, simplified.item, j.objectExpression([j.property('init', j.identifier('fr'), j.literal(abbr))]), 'abbrevation')
|
||||
// set(j, simplified.item, j.literal('a'), 's.official')
|
||||
|
||||
// Example remove field
|
||||
// remove(name.item as ObjectExpression, 'fr')
|
||||
|
||||
// Example Set/Add regulationMArk to cards
|
||||
// set(j, name.items.fr, j.literal('D'), 'regulationMark')
|
||||
// console.log(filename)
|
||||
const ids = [
|
||||
5, 6, 8, 11, 12, 13, 14, 17, 22, 23, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 37, 41, 43, 44, 45, 46, 49, 51, 54, 56, 57, 58, 59, 60, 64, 65, 70, 73, 75, 76, 78, 80, 82, 91, 92, 116, 117, 119, 128, 129
|
||||
]
|
||||
const id = parseInt(filename)
|
||||
const isHolo = ids.includes(id) || id >= 131
|
||||
const isNormal = !isHolo
|
||||
if (isHolo) {
|
||||
set(j, simplified.items.variants.item as ObjectExpression, j.literal(true), 'holo')
|
||||
set(j, simplified.items.variants.item as ObjectExpression, j.literal(false), 'normal')
|
||||
} else {
|
||||
remove(simplified.item, 'variants')
|
||||
}
|
||||
|
||||
})
|
||||
.toSource({ useTabs: true, lineTerminator: '\n' }).replace(/ /g, ' ')
|
||||
}
|
||||
module.exports.parser = 'ts'
|
220
src/compiler/index.ts
Normal file
220
src/compiler/index.ts
Normal file
@ -0,0 +1,220 @@
|
||||
import ts from 'typescript'
|
||||
import fs from 'fs'
|
||||
|
||||
const input = 'src/components/button/index.ts' // Input file path
|
||||
const output = 'src/components/button/Button.transformed.ts' // Output file path for transformed code
|
||||
|
||||
interface SimpleHTMLElement {
|
||||
name: string
|
||||
attrs?: Record<string, string | any>
|
||||
childs?: Array<SimpleHTMLElement | string>
|
||||
}
|
||||
|
||||
// Read input source code
|
||||
const src = fs.readFileSync(input, 'utf-8')
|
||||
|
||||
// Create a TypeScript source file from input source code string
|
||||
const sourceFile = ts.createSourceFile(input, src, ts.ScriptTarget.Latest, true)
|
||||
|
||||
function parseName(name: string, values: Array<ts.Expression>): string | ts.Expression {
|
||||
const match = /^\u0000(\d+)\u0000$/.exec(name)
|
||||
if (match) {
|
||||
const idx = Number(match[1])
|
||||
return values[idx]
|
||||
}
|
||||
return name
|
||||
}
|
||||
|
||||
function parseAttrs(attrString: string, values: Array<ts.Expression>): Record<string, string | ts.Expression> {
|
||||
const attrs: Record<string, string | ts.Expression> = {}
|
||||
console.log(attrString) // \u00001\u0000
|
||||
const parts = attrString.match(/(?:[^.*=]+=(?:"[^"]*"|'[^']*'|[^.*"']+))/g) ?? []
|
||||
for (const part of parts) {
|
||||
if (!part) {
|
||||
continue
|
||||
}
|
||||
const [key, raw] = part.trim().split('=') as [string, string]
|
||||
if (!raw) {
|
||||
continue
|
||||
}
|
||||
const val = raw.replace(/^['"]?|['"]?$/g, '')
|
||||
const match = /^\u0000(\d+)\u0000$/.exec(val)
|
||||
if (match) {
|
||||
const idx = Number(match[1])
|
||||
attrs[key] = values[idx]
|
||||
} else {
|
||||
attrs[key] = val
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return attrs
|
||||
}
|
||||
|
||||
/**
|
||||
* Naively parse a TemplateLiteral assumed to contain a simple html tagged template string.
|
||||
* Extracts:
|
||||
* - tagName (e.g. 'button')
|
||||
* - attrs as key-value pairs (only "class" attribute parsed here)
|
||||
* - children: a mix of static strings and interpolated expressions
|
||||
* @param template TemplateLiteral node from the AST (expects TemplateExpression)
|
||||
* @returns parsed SimpleHTMLElement parts or null if parsing fails
|
||||
*/
|
||||
function extractHtmlData(template: ts.TemplateLiteral): {
|
||||
tagName: string | ts.Expression
|
||||
attrs: Record<string, string | ts.Expression>
|
||||
children: (ts.Expression | ts.StringLiteral)[]
|
||||
} | null {
|
||||
// We only handle TemplateExpression (with interpolations) here
|
||||
if (!ts.isTemplateExpression(template)) return null
|
||||
|
||||
// Collect all template literal strings and expressions separately
|
||||
const fullStrings: string[] = [template.head.text]
|
||||
const exprs: ts.Expression[] = []
|
||||
|
||||
// Each template span has an expression and a literal text after it
|
||||
let idx = 0
|
||||
for (const span of template.templateSpans) {
|
||||
exprs.push(span.expression) // interpolation `${...}`
|
||||
fullStrings.push(`\u0000${idx++}\u0000`) // literal after interpolation
|
||||
fullStrings.push(span.literal.text) // literal after interpolation
|
||||
}
|
||||
|
||||
console.log('aaa', exprs, fullStrings.join('').replaceAll(/\s*\n\s*/g, '').trim())
|
||||
|
||||
// Join all literal parts to parse the opening tag naively with regex
|
||||
const fullHtml = fullStrings.join('').replaceAll(/\s*\n\s*/g, '').trim()
|
||||
const selfClose = /^<([a-zA-Z0-9-\u0000]+)([^>]*)\/>$/.exec(fullHtml)
|
||||
|
||||
if (selfClose) {
|
||||
const [, tagName, rawAttrs] = selfClose
|
||||
const attrs = parseAttrs(rawAttrs, exprs)
|
||||
return {
|
||||
tagName: parseName(tagName, exprs),
|
||||
attrs: Object.keys(attrs).length ? attrs : {},
|
||||
children: []
|
||||
}
|
||||
}
|
||||
|
||||
const fullMatch = /^<([a-zA-Z0-9-\u0000]+)([^>]*)>([\s\S]*)<\/.*>$/.exec(fullHtml)
|
||||
|
||||
if (!fullMatch) {
|
||||
throw new Error(`Invalid HTML: ${fullHtml}`)
|
||||
}
|
||||
|
||||
const [, tagName, rawAttrs, inner] = fullMatch
|
||||
const attrs = parseAttrs(rawAttrs, exprs)
|
||||
|
||||
const children: (ts.Expression | ts.StringLiteral)[] = []
|
||||
const startsWithShit = inner.startsWith('\u0000')
|
||||
inner.split(/\u0000/).forEach((part, i) => {
|
||||
if (i % 2 === (startsWithShit ? 1 : 0) && exprs[Number.parseInt(part)]) {
|
||||
children.push(exprs[Number.parseInt(part)])
|
||||
} else if (part) {
|
||||
children.push(ts.factory.createStringLiteral(part))
|
||||
}
|
||||
})
|
||||
|
||||
// // Build children array alternating strings and expressions from template parts
|
||||
// const children: (ts.Expression | ts.StringLiteral)[] = []
|
||||
// for (let i = 0; i < fullStrings.length; i++) {
|
||||
// console.log(fullStrings[i], exprs[i])
|
||||
// // Add string chunk if not empty
|
||||
// if (inner[i]) {
|
||||
// children.push(ts.factory.createStringLiteral(fullStrings[i]))
|
||||
// }
|
||||
// // Add expression if exists (one less than strings length)
|
||||
// if (exprs[i]) {
|
||||
// children.push(exprs[i])
|
||||
// }
|
||||
// }
|
||||
|
||||
return {
|
||||
tagName: parseName(tagName, exprs),
|
||||
attrs,
|
||||
children,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* TypeScript Transformer Factory
|
||||
* Transforms `html` tagged template literals into SimpleHTMLElement object literals.
|
||||
* Only handles simple cases with static tags and attributes and interpolated children.
|
||||
*/
|
||||
const transformer: ts.TransformerFactory<ts.SourceFile> = context => {
|
||||
return rootNode => {
|
||||
// Recursive AST visitor function
|
||||
function visit(node: ts.Node): ts.Node {
|
||||
// Check if node is a tagged template expression with tag name 'html'
|
||||
if (
|
||||
ts.isTaggedTemplateExpression(node) &&
|
||||
node.tag.getText() === 'html' &&
|
||||
ts.isTemplateExpression(node.template)
|
||||
) {
|
||||
// Extract html data from template literal
|
||||
const parsed = extractHtmlData(node.template)
|
||||
// console.log(parsed)
|
||||
if (!parsed) return node // fallback: no transform if parsing failed
|
||||
|
||||
// Create AST properties for the SimpleHTMLElement object literal
|
||||
// console.log(parsed.tagName)
|
||||
const props: ts.ObjectLiteralElementLike[] = [
|
||||
// name property: tag name as string literal
|
||||
ts.factory.createPropertyAssignment(
|
||||
'name',
|
||||
typeof parsed.tagName === 'string' ? ts.factory.createStringLiteral(parsed.tagName) : parsed.tagName
|
||||
),
|
||||
]
|
||||
|
||||
// Add attrs property if any attributes found
|
||||
if (Object.keys(parsed.attrs).length > 0) {
|
||||
props.push(
|
||||
ts.factory.createPropertyAssignment(
|
||||
'attrs',
|
||||
ts.factory.createObjectLiteralExpression(
|
||||
// Create key-value pairs for each attribute
|
||||
Object.entries(parsed.attrs).map(([k, v]) =>
|
||||
ts.factory.createPropertyAssignment(k, typeof v === 'string' ? ts.factory.createStringLiteral(v) : v)
|
||||
),
|
||||
true // multiline formatting
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
// Add childs property if any children (strings or expressions)
|
||||
if (parsed.children.length > 0) {
|
||||
props.push(
|
||||
ts.factory.createPropertyAssignment(
|
||||
'childs',
|
||||
ts.factory.createArrayLiteralExpression(parsed.children, true)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
// Return the object literal AST node that replaces the `html` tagged template call
|
||||
return ts.factory.createObjectLiteralExpression(props, true)
|
||||
}
|
||||
|
||||
// Recursively visit children nodes
|
||||
return ts.visitEachChild(node, visit, context)
|
||||
}
|
||||
|
||||
// Start AST traversal from root
|
||||
return ts.visitNode(rootNode, visit) as ts.SourceFile
|
||||
}
|
||||
}
|
||||
|
||||
// Run the transform on the source file with our custom transformer
|
||||
const result = ts.transform(sourceFile, [transformer])
|
||||
|
||||
// Prepare printer to output transformed AST back to TypeScript code string
|
||||
const printer = ts.createPrinter()
|
||||
|
||||
// Get transformed source code text
|
||||
const outputText = printer.printFile(result.transformed[0])
|
||||
|
||||
// Write the transformed source code to output file
|
||||
fs.writeFileSync(output, outputText)
|
||||
|
||||
console.log(`✅ Wrote transformed file to: ${output}`)
|
5
src/components/badge/Badge.astro
Normal file
5
src/components/badge/Badge.astro
Normal file
@ -0,0 +1,5 @@
|
||||
---
|
||||
import Item from '.'
|
||||
import AstroSSR from '../utils/AstroSSR.astro'
|
||||
---
|
||||
<AstroSSR component={Item} props={Astro.props} slots={Astro.slots} />
|
22
src/components/badge/index.ts
Normal file
22
src/components/badge/index.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import { Component, WebElement, type SimpleHTMLElement, html, cls, attribute } from '..'
|
||||
|
||||
@Component('bad-ge')
|
||||
export default class Badge extends WebElement {
|
||||
|
||||
@attribute()
|
||||
public badge?: WebElement
|
||||
|
||||
@attribute()
|
||||
public class?: string
|
||||
|
||||
public override async render(): Promise<SimpleHTMLElement> {
|
||||
return html`
|
||||
<div class=${cls(['px-4 py-1 text-center rounded-full relative flex', this.class])}>
|
||||
<div class="overflow-ellipsis w-full overflow-clip">
|
||||
${this.badge}
|
||||
<slot/>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
}
|
5
src/components/button/Button.astro
Normal file
5
src/components/button/Button.astro
Normal file
@ -0,0 +1,5 @@
|
||||
---
|
||||
import Btn from '.'
|
||||
import AstroSSR from '../utils/AstroSSR.astro'
|
||||
---
|
||||
<AstroSSR component={Btn} props={Astro.props} slots={Astro.slots} />
|
74
src/components/button/index.ts
Normal file
74
src/components/button/index.ts
Normal file
@ -0,0 +1,74 @@
|
||||
import { attribute, Component, WebElement, cls, html } from '..'
|
||||
|
||||
@Component('butt-on')
|
||||
export default class Button extends WebElement {
|
||||
@attribute('boolean')
|
||||
public block?: boolean
|
||||
|
||||
@attribute()
|
||||
public iconLeft?: any
|
||||
|
||||
@attribute()
|
||||
public iconRight?: any
|
||||
|
||||
@attribute('boolean')
|
||||
public outline?: boolean
|
||||
|
||||
@attribute('boolean')
|
||||
public outlineR?: boolean
|
||||
|
||||
@attribute('boolean')
|
||||
public outlineG?: boolean
|
||||
|
||||
@attribute('boolean')
|
||||
public ghost?: boolean
|
||||
|
||||
@attribute('boolean')
|
||||
public disabled?: boolean | undefined
|
||||
|
||||
@attribute()
|
||||
public name?: string
|
||||
|
||||
@attribute()
|
||||
public value?: string
|
||||
|
||||
@attribute()
|
||||
public tag?: string
|
||||
|
||||
@attribute()
|
||||
public enctype?: string
|
||||
|
||||
@attribute()
|
||||
public class?: string
|
||||
|
||||
@attribute()
|
||||
public href?: string
|
||||
|
||||
public override async render() {
|
||||
const classes = [
|
||||
'button',
|
||||
'no-link-style',
|
||||
'focus:ring',
|
||||
{ 'w-full': this.block },
|
||||
{ outline: this.outline },
|
||||
{ outlineR: this.outlineR && !this.disabled },
|
||||
{ outlineG: this.outlineG && !this.disabled },
|
||||
{ ghost: this.ghost },
|
||||
{ disabled: this.disabled },
|
||||
this.class,
|
||||
]
|
||||
const tag = this.tag ?? this.href ? 'a' : 'button'
|
||||
return html`
|
||||
<${tag} ${{ ...this.getProps() }} class="${cls(classes)}">
|
||||
${this.iconLeft}
|
||||
<slot />
|
||||
${this.iconRight}
|
||||
</${tag}>
|
||||
`
|
||||
}
|
||||
|
||||
private onClick = () => {
|
||||
if (this.disabled) { return }
|
||||
console.log('Button clicked')
|
||||
}
|
||||
}
|
7
src/components/index.ts
Normal file
7
src/components/index.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import WebElement from './utils/web-element'
|
||||
|
||||
export {
|
||||
WebElement
|
||||
}
|
||||
export * from './utils/decorators'
|
||||
export * from './utils/utils'
|
11
src/components/list.ts
Normal file
11
src/components/list.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import Button from './button'
|
||||
import Badge from './badge'
|
||||
|
||||
const components = [
|
||||
Button,
|
||||
Badge
|
||||
]
|
||||
|
||||
export function loadComponents() {
|
||||
return components
|
||||
}
|
32
src/components/utils/AstroSSR.astro
Normal file
32
src/components/utils/AstroSSR.astro
Normal file
@ -0,0 +1,32 @@
|
||||
---
|
||||
import { objectRemap, objectSize } from '@dzeio/object-util'
|
||||
import { html, type SimpleHTMLElement, type WebElement } from '..'
|
||||
|
||||
export interface Props {
|
||||
component: new () => WebElement
|
||||
props?: Record<string, any>
|
||||
slots?: typeof Astro.slots
|
||||
}
|
||||
|
||||
const omp = new Astro.props.component()
|
||||
const Tag = omp.getConfig().tag
|
||||
|
||||
// parse Astro slots
|
||||
const slots : Record<string, string | SimpleHTMLElement> = {}
|
||||
for (const slot of Object.keys(Astro.slots)) {
|
||||
slots[slot] = html(await Astro.slots.render(slot))
|
||||
}
|
||||
if (Astro.props.slots) {
|
||||
for (const slot of Object.keys(Astro.props.slots)) {
|
||||
slots[slot] = html(await Astro.props.slots.render(slot))
|
||||
}
|
||||
}
|
||||
|
||||
// render the component server-side
|
||||
omp.setProps(Astro.props.props)
|
||||
omp.setSlots(slots)
|
||||
const rendered = await omp
|
||||
.renderString()
|
||||
---
|
||||
|
||||
<Tag set:html={rendered.output} data-hydrate={rendered.needHydration ? true : undefined} {...omp.getProps()} data-slots={objectSize(slots) > 0 ? JSON.stringify(slots) : undefined} />
|
121
src/components/utils/decorators.ts
Normal file
121
src/components/utils/decorators.ts
Normal file
@ -0,0 +1,121 @@
|
||||
import { WebElement } from '..'
|
||||
|
||||
interface ComponentConfig {
|
||||
tag: `${string}-${string}`
|
||||
attrs?: Record<string, AttributeOptions>
|
||||
shadow?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Decorator to define a custom element.
|
||||
* @param tagOrConfig The tag name of the custom element. it MUST respect the formatting else the browser will not recognize it.
|
||||
*/
|
||||
export function Component(tagOrConfig: ComponentConfig['tag'] | Omit<ComponentConfig, 'attrs'>) {
|
||||
const conf = typeof tagOrConfig === 'string' ? { tag: tagOrConfig } : tagOrConfig
|
||||
return function(constructor: typeof WebElement) {
|
||||
|
||||
const child = class extends constructor {
|
||||
// public static override readonly tag = tag
|
||||
public constructor() {
|
||||
super()
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||
const proto = ((this as any).__proto__ as typeof WebElement)
|
||||
Object.assign(proto.__config, conf)
|
||||
proto.__config.tag = conf.tag
|
||||
const config = proto.__config
|
||||
for (const attr of Object.keys(config.attrs ?? {})) {
|
||||
const pouet = config.attrs![attr]
|
||||
// console.log(attr, pouet)
|
||||
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
|
||||
delete proto[attr as keyof typeof WebElement]
|
||||
lateAttribute(this, attr, pouet)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof customElements === 'undefined') {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
||||
return child as any
|
||||
}
|
||||
|
||||
if (!customElements.get(conf.tag)) {
|
||||
customElements.define(conf.tag, child)
|
||||
} else {
|
||||
throw new Error(`Custom element ${conf.tag} is already defined`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export interface AttributeOptions {
|
||||
type?: 'string' | 'number' | 'boolean'
|
||||
onChange?: (newValue: any, oldValue: any) => void
|
||||
}
|
||||
|
||||
export function lateAttribute(el: WebElement, name: string, options: AttributeOptions = {}) {
|
||||
if (!(el instanceof WebElement)) {
|
||||
return
|
||||
}
|
||||
|
||||
const privateKey = `__${name}`
|
||||
|
||||
|
||||
Object.defineProperty(el, name, {
|
||||
get(this: WebElement) {
|
||||
return this[privateKey as keyof WebElement]
|
||||
},
|
||||
set(this: WebElement, newVal) {
|
||||
const oldVal = this[privateKey as keyof WebElement]
|
||||
|
||||
const changed = newVal !== oldVal
|
||||
if (!changed) {
|
||||
return
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
|
||||
(this as any)[privateKey] = newVal
|
||||
|
||||
options.onChange?.(newVal, oldVal)
|
||||
|
||||
if (newVal == null || newVal === false) {
|
||||
this.removeAttribute(name)
|
||||
} else {
|
||||
this.setAttribute(name, String(newVal))
|
||||
}
|
||||
|
||||
// invalide current rendering
|
||||
this.invalidate()
|
||||
|
||||
// trigger new render
|
||||
void this.triggerRender()
|
||||
},
|
||||
configurable: true,
|
||||
enumerable: true,
|
||||
})
|
||||
}
|
||||
|
||||
type AttributeFunction = (proto: any, name: string) => void
|
||||
|
||||
/**
|
||||
* Decorator to define an attribute on a WebElement.
|
||||
*/
|
||||
export function attribute(): AttributeFunction
|
||||
/**
|
||||
* Decorator to define an attribute on a WebElement.
|
||||
* @param type The type of the attribute.
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/unified-signatures
|
||||
export function attribute(type: AttributeOptions['type']): AttributeFunction
|
||||
/**
|
||||
* Decorator to define an attribute on a WebElement.
|
||||
* @param options The options for the attribute.
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/unified-signatures
|
||||
export function attribute(options: AttributeOptions): AttributeFunction
|
||||
export function attribute(typeOrOptions?: AttributeOptions['type'] | AttributeOptions): AttributeFunction {
|
||||
return function(proto: typeof WebElement, name: string) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||
proto.__config ??= {} as unknown as typeof proto['__config']
|
||||
proto.__config.attrs ??= {}
|
||||
proto.__config.attrs[name] = typeof typeOrOptions === 'string' ? { type: typeOrOptions } : typeOrOptions ?? {}
|
||||
}
|
||||
}
|
291
src/components/utils/utils.ts
Normal file
291
src/components/utils/utils.ts
Normal file
@ -0,0 +1,291 @@
|
||||
/* eslint-disable max-depth */
|
||||
/* eslint-disable complexity */
|
||||
import { objectMap } from '@dzeio/object-util'
|
||||
import { WebElement } from '..'
|
||||
|
||||
export function HTMLtoString(tag: SimpleHTMLElement): string {
|
||||
// compile attributes (note: the attributes values are encoded to not break json)
|
||||
const attrs = objectMap(tag.attrs ?? {}, (value, key) => ` ${key}="${value.replaceAll('"', '"')}"`).join('')
|
||||
// console.log('a', tag)
|
||||
// console.log('a', tag.childs)
|
||||
let childs = ''
|
||||
if (tag.childs) {
|
||||
console.log('na', tag, tag.childs)
|
||||
childs = tag.childs.map((it) => typeof it === 'string' ? it : HTMLtoString(it)).join('')
|
||||
}
|
||||
|
||||
return `<${tag.name}${attrs}>${childs}</${tag.name}>`
|
||||
}
|
||||
|
||||
export function HTMLToElement(tag: SimpleHTMLElement): HTMLElement {
|
||||
// create root element
|
||||
const el = document.createElement(tag.name)
|
||||
|
||||
// apply attributes
|
||||
if (tag.attrs) {
|
||||
for (const [k, v] of Object.entries(tag.attrs)) {
|
||||
el.setAttribute(k, v)
|
||||
}
|
||||
}
|
||||
|
||||
// apply event handlers
|
||||
if (tag.events) {
|
||||
for (const [evt, handler] of Object.entries(tag.events)) {
|
||||
el.addEventListener(evt, handler)
|
||||
}
|
||||
}
|
||||
|
||||
// apply childs
|
||||
if (tag.childs) {
|
||||
for (const child of tag.childs) {
|
||||
if (typeof child === 'string') {
|
||||
el.appendChild(document.createTextNode(child))
|
||||
} else {
|
||||
el.appendChild(HTMLToElement(child))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return el
|
||||
}
|
||||
|
||||
|
||||
export interface SimpleHTMLElement {
|
||||
name: string
|
||||
attrs?: Record<string, string>
|
||||
events?: Record<string, EventListener>
|
||||
childs?: Array<SimpleHTMLElement | string>
|
||||
}
|
||||
|
||||
export function hasEvents(el: SimpleHTMLElement): boolean {
|
||||
if (Object.keys(el.events ?? {}).length > 0) {
|
||||
return true
|
||||
}
|
||||
if (el.childs) {
|
||||
for (const child of el.childs) {
|
||||
if (typeof child === 'string') {
|
||||
continue
|
||||
}
|
||||
if (hasEvents(child)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
function parseAttrs(attrString: string, values: Array<unknown>): { attrs: Record<string, string>, events: Record<string, EventListener> } {
|
||||
const attrs: Record<string, string> = {}
|
||||
const events: Record<string, EventListener> = {}
|
||||
const parts = attrString.match(/(?:[^.*=]+=(?:"[^"]*"|'[^']*'|[^.*"']+))/g) ?? []
|
||||
for (const part of parts) {
|
||||
if (!part) {
|
||||
continue
|
||||
}
|
||||
const [key, raw] = part.trim().split('=') as [string, string]
|
||||
if (!raw) {
|
||||
continue
|
||||
}
|
||||
const val = raw.replace(/^['"]?|['"]?$/g, '')
|
||||
const match = /^\u0000(\d+)\u0000$/.exec(val)
|
||||
if (match) {
|
||||
const idx = Number(match[1])
|
||||
const value = values[idx]
|
||||
if (key.startsWith('on') && typeof value === 'function') {
|
||||
events[key.slice(2)] = value as EventListener
|
||||
} else if (typeof value === 'string') {
|
||||
attrs[key] = value
|
||||
}
|
||||
} else {
|
||||
attrs[key] = val
|
||||
}
|
||||
}
|
||||
return { attrs, events }
|
||||
}
|
||||
|
||||
// TODO: rework
|
||||
function parseElement(fragment: string, values: Array<unknown> = []): SimpleHTMLElement | string {
|
||||
fragment = fragment.trim()
|
||||
if (!fragment.startsWith('<')) { return fragment }
|
||||
|
||||
// self-closing tag
|
||||
const selfClose = /^<([a-zA-Z0-9_-]+)([^>]*)\/>$/.exec(fragment)
|
||||
if (selfClose) {
|
||||
const [, tagName, rawAttrs] = selfClose
|
||||
const { attrs, events } = parseAttrs(rawAttrs, values)
|
||||
return {
|
||||
name: tagName,
|
||||
attrs: Object.keys(attrs).length ? attrs : undefined,
|
||||
events: Object.keys(events).length ? events : undefined,
|
||||
}
|
||||
}
|
||||
|
||||
// regular tag
|
||||
const fullMatch = /^<([a-zA-Z0-9_-]+)([^>]*)>([\s\S]*)<\/\1>$/.exec(fragment)
|
||||
if (!fullMatch) { return fragment }
|
||||
|
||||
const [, tagName, rawAttrs, inner] = fullMatch
|
||||
const { attrs, events } = parseAttrs(rawAttrs, values)
|
||||
const childs: Array<SimpleHTMLElement | string> = []
|
||||
|
||||
let rest = inner.trim()
|
||||
|
||||
while (rest) {
|
||||
// interpolation placeholder
|
||||
const interp = /^\u0000(\d+)\u0000/.exec(rest)
|
||||
if (interp) {
|
||||
const idx = Number(interp[1])
|
||||
const val = values[idx]
|
||||
if (val instanceof WebElement) {
|
||||
// childs.push(val.render())
|
||||
} else if (typeof val === 'object' && val !== null && 'name' in val) { childs.push(val as SimpleHTMLElement) }
|
||||
else { childs.push(String(val)) }
|
||||
rest = rest.slice(interp[0].length).trim()
|
||||
continue
|
||||
}
|
||||
|
||||
if (rest.startsWith('<')) {
|
||||
const tagMatch = /^<([a-zA-Z0-9_-]+)/.exec(rest)
|
||||
if (!tagMatch) { break }
|
||||
const childTag = tagMatch[1]
|
||||
let depth = 0
|
||||
let i = 0
|
||||
for (; i < rest.length; i++) {
|
||||
if (rest.startsWith(`<${childTag}`, i)) { depth++ }
|
||||
else if (rest.startsWith(`</${childTag}>`, i)) {
|
||||
depth--
|
||||
if (depth === 0) {
|
||||
i += (`</${childTag}>`).length
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
const chunk = rest.slice(0, i)
|
||||
childs.push(parseElement(chunk, values) as SimpleHTMLElement)
|
||||
rest = rest.slice(i).trim()
|
||||
} else {
|
||||
const nextIndices = [rest.indexOf('<'), rest.indexOf('\u0000')].filter((i) => i >= 0)
|
||||
const idx = nextIndices.length ? Math.min(...nextIndices) : -1
|
||||
const text = idx >= 0 ? rest.slice(0, idx) : rest
|
||||
childs.push(text.trim())
|
||||
rest = idx >= 0 ? rest.slice(idx).trim() : ''
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
name: tagName,
|
||||
attrs: Object.keys(attrs).length ? attrs : undefined,
|
||||
events: Object.keys(events).length ? events : undefined,
|
||||
childs: childs.length ? childs : undefined,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a tagged template literal into a SimpleHTMLElement.
|
||||
*/
|
||||
export function html(str: string): SimpleHTMLElement
|
||||
export function html(strings: TemplateStringsArray, ...values: Array<string | number | undefined | boolean | object | null | WebElement>): SimpleHTMLElement
|
||||
export function html(strings: TemplateStringsArray | string, ...values: Array<unknown>): SimpleHTMLElement {
|
||||
// input as a raw string, limited parsing.
|
||||
if (typeof strings === 'string') {
|
||||
return parseElement(strings.replaceAll('"', '"')) as SimpleHTMLElement
|
||||
}
|
||||
|
||||
// the new built string
|
||||
let full = ''
|
||||
|
||||
// indexes of values that are already parsed into the `full` string
|
||||
for (let idx = 0; idx < strings.length; idx++) {
|
||||
full += strings[idx]!
|
||||
if (idx < values.length) {
|
||||
const value = values[idx]
|
||||
switch (typeof value) {
|
||||
case 'undefined': {
|
||||
break
|
||||
}
|
||||
// if the value is a string, add it to the full string and mark it for removal
|
||||
case 'string': {
|
||||
full += values[idx] as string
|
||||
break
|
||||
}
|
||||
case 'object': {
|
||||
if (value === null) {
|
||||
break
|
||||
}
|
||||
if (value instanceof WebElement) {
|
||||
|
||||
} else {
|
||||
const attrs = Object.entries(value as Record<string, unknown>)
|
||||
.filter(([, value]) => typeof value !== 'undefined' && value !== null)
|
||||
.map(([key, value]) => `${key}="${(value as string).replace('"', '"')}"`)
|
||||
.join(' ')
|
||||
full += attrs
|
||||
break
|
||||
}
|
||||
}
|
||||
default: {
|
||||
// add a placeholder for the value to be parsed later
|
||||
full += `\u0000${idx}\u0000`
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// parse & return :D
|
||||
return parseElement(full.replaceAll('"', '"'), values) as SimpleHTMLElement
|
||||
}
|
||||
|
||||
type ClassList = Array<string | Record<string, any>>
|
||||
|
||||
/**
|
||||
* Simple helper function to create a string with class names.
|
||||
* @param items - Array of class names or objects with class names as keys and boolean values as values.
|
||||
* @returns A string with the class names separated by spaces.
|
||||
*/
|
||||
export function cls(items: string | ClassList): string
|
||||
/**
|
||||
* Simple helper function to create a string with class names.
|
||||
* @param items - Array of class names or objects with class names as keys and boolean values as values.
|
||||
* @returns A string with the class names separated by spaces.
|
||||
*/
|
||||
export function cls(...items: ClassList | Array<ClassList>): string
|
||||
/**
|
||||
* Simple helper function to create a string with class names.
|
||||
* @param items - Array of class names or objects with class names as keys and boolean values as values.
|
||||
* @returns A string with the class names separated by spaces.
|
||||
*/
|
||||
export function cls(...items: Array<string | undefined | null | Record<string, any>>): string {
|
||||
if (items.length === 1 && typeof items[0] === 'string') {
|
||||
return items[0]
|
||||
} else if (items.length === 1 && Array.isArray(items[0])) {
|
||||
items = items[0] as ClassList
|
||||
}
|
||||
|
||||
return items.map((item) => {
|
||||
if (typeof item === 'undefined' || item === null) {
|
||||
return null
|
||||
}
|
||||
if (typeof item === 'string') {
|
||||
return item
|
||||
}
|
||||
return Object.keys(item).filter((key) => item[key]).join(' ')
|
||||
}).filter((it) => !!it).join(' ')
|
||||
}
|
||||
|
||||
export function attrs(string: Record<string, any>): string {
|
||||
return ''
|
||||
}
|
||||
|
||||
export function getContext(): 'browser' | 'node' {
|
||||
if (typeof document !== 'undefined') {
|
||||
return 'browser'
|
||||
}
|
||||
return 'node'
|
||||
}
|
||||
|
||||
|
||||
export function assert(bool: any, message?: string): asserts bool {
|
||||
if (!bool) {
|
||||
throw new Error(message ?? 'Assertion failed')
|
||||
}
|
||||
}
|
287
src/components/utils/web-element.ts
Normal file
287
src/components/utils/web-element.ts
Normal file
@ -0,0 +1,287 @@
|
||||
/* eslint-disable max-classes-per-file */
|
||||
|
||||
import { objectLoop } from '@dzeio/object-util'
|
||||
|
||||
import { assert, type AttributeOptions, html, type SimpleHTMLElement, HTMLToElement, HTMLtoString, getContext, hasEvents } from '..'
|
||||
|
||||
// Polyfill HTMLElement on server to allow SSR rendering
|
||||
let localHTMLElement: typeof HTMLElement
|
||||
if (typeof HTMLElement !== 'undefined') {
|
||||
localHTMLElement = HTMLElement
|
||||
} else {
|
||||
// @ts-expect-error polyfill for SSR
|
||||
localHTMLElement = class HTMLElement {
|
||||
public attachShadow(_options: ShadowRootInit): ShadowRoot {
|
||||
return undefined as unknown as ShadowRoot
|
||||
}
|
||||
public setAttribute(_name: string, _value: string) {
|
||||
// super.setAttribute(name, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper around HTMLElement that provides a simple way to create custom elements.
|
||||
*/
|
||||
export default class WebElement extends localHTMLElement {
|
||||
public static readonly tag: string = 'web-element'
|
||||
|
||||
public static __config: {
|
||||
tag: string
|
||||
attrs?: Record<string, AttributeOptions>
|
||||
} = {
|
||||
tag: 'web-element'
|
||||
}
|
||||
|
||||
/**
|
||||
* childs of the current element that will be rendered inside `<slot />` elements
|
||||
*/
|
||||
public childs: Record<string, SimpleHTMLElement | string> = {}
|
||||
|
||||
/**
|
||||
* Indicates whether the element has been mounted in the DOM.
|
||||
*/
|
||||
private mounted = false
|
||||
|
||||
/**
|
||||
* Indicates whether the element needs to be rendered.
|
||||
*/
|
||||
private needRender = true
|
||||
|
||||
/**
|
||||
* The last rendered element for caching purposes.
|
||||
*/
|
||||
private lastRender: SimpleHTMLElement | undefined
|
||||
|
||||
public constructor() {
|
||||
super()
|
||||
|
||||
// attach to shadow root
|
||||
// this.attachShadow({ mode: 'open' })
|
||||
// this.connectedCallback()
|
||||
}
|
||||
|
||||
public static async ref(props?: any): Promise<SimpleHTMLElement> {
|
||||
const conf = this.__config
|
||||
const self = new this()
|
||||
.setProps(props)
|
||||
return html`<${conf.tag}>${(await self.renderString()).output}</${conf.tag}>`
|
||||
}
|
||||
|
||||
public getConfig(): typeof WebElement['__config'] {
|
||||
return this.__proto__.__config
|
||||
}
|
||||
/**
|
||||
* Function run when the element is connected to the DOM.
|
||||
*/
|
||||
public connectedCallback() {
|
||||
// setup slots
|
||||
if (this.dataset.slots) {
|
||||
this.setSlots(JSON.parse(this.dataset.slots) as Record<string, SimpleHTMLElement>)
|
||||
this.removeAttribute('data-slots')
|
||||
}
|
||||
|
||||
// move the element inside of the shadow root
|
||||
// this.shadowRoot!.innerHTML = this.innerHTML
|
||||
// this.innerHTML = ''
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||
const attrSettings = ((this as any).__proto__ as typeof WebElement).__config.attrs
|
||||
|
||||
for (const key of Object.keys(attrSettings)) {
|
||||
if (this.hasAttribute(key)) {
|
||||
this.setProps({[key]: this.getAttribute(key)})
|
||||
}
|
||||
}
|
||||
|
||||
// setup attributes observer
|
||||
const observer = new MutationObserver((mutations) => {
|
||||
|
||||
mutations.forEach((mutation) => {
|
||||
const attr = mutation.attributeName
|
||||
if (mutation.type === 'attributes' && attr) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||
const attrSettings = ((this as any).__proto__ as typeof WebElement).__config.attrs?.[attr]
|
||||
if (!attrSettings) {
|
||||
return
|
||||
}
|
||||
|
||||
const type = attrSettings.type ?? 'string'
|
||||
|
||||
switch (type) {
|
||||
case 'boolean': {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||
(this as any)[attr] = this.hasAttribute(attr) ? this.getAttribute(attr) !== 'false' : false
|
||||
break
|
||||
}
|
||||
case 'number': {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||
(this as any)[attr] = this.hasAttribute(attr) ? parseInt(this.getAttribute(attr) ?? '0', 10) : undefined
|
||||
break
|
||||
}
|
||||
default: {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||
(this as any)[attr] = this.getAttribute(attr)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
observer.observe(this, {
|
||||
attributes: true
|
||||
})
|
||||
|
||||
// trigger frontend hydration
|
||||
if (this.dataset.hydrate === 'true') {
|
||||
this.removeAttribute('data-hydrate')
|
||||
void this.triggerRender()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the component is mounted in the DOM
|
||||
*/
|
||||
public didMount() { /** child to implement */}
|
||||
|
||||
/**
|
||||
* Called when the component was updated from an attribute change
|
||||
*/
|
||||
public didUpdate() { /** child to implement */}
|
||||
|
||||
/**
|
||||
* Render the component, this MUST be stateless and return the result of the HTML `html` helper
|
||||
*
|
||||
* note: if a parent changes, this render function won't be run again
|
||||
*/
|
||||
public async render(): Promise<SimpleHTMLElement> {
|
||||
return html`<div />`
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidate the component, this will trigger a re-render on the next call to `triggerRender`
|
||||
*/
|
||||
public invalidate() {
|
||||
this.needRender = true
|
||||
}
|
||||
|
||||
public setSlots(name: string, value: SimpleHTMLElement | string | null): this
|
||||
public setSlots(slots: Record<string, SimpleHTMLElement | string | null>): this
|
||||
public setSlots(slots: Record<string, SimpleHTMLElement | string | null> | string, element?: SimpleHTMLElement | string | null) {
|
||||
if (typeof slots === 'string') {
|
||||
slots = { [slots]: element as SimpleHTMLElement | string | null }
|
||||
}
|
||||
objectLoop(slots, (value, key) => {
|
||||
if (value === null) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
|
||||
delete this.childs[key]
|
||||
} else {
|
||||
this.childs[key] = value
|
||||
}
|
||||
})
|
||||
return this
|
||||
}
|
||||
|
||||
public getProps(): Record<string, any> {
|
||||
const props: Record<string, any> = {}
|
||||
for (const key of Object.keys(this.getConfig().attrs ?? {})) {
|
||||
props[key] = this[key]
|
||||
}
|
||||
return props
|
||||
}
|
||||
|
||||
public setProps(props?: Partial<Record<string, any>>) {
|
||||
if (!props) {return this}
|
||||
objectLoop(props, (value, key) => {
|
||||
this[key] = value
|
||||
})
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the component to a string.
|
||||
*
|
||||
* warning: events are excluded from the output.
|
||||
*/
|
||||
public async renderString(): Promise<{output: string, needHydration: boolean}> {
|
||||
const res = await this.renderOrCache()
|
||||
|
||||
// SSR: manually replace <slot> tags
|
||||
let out = HTMLtoString(res)
|
||||
|
||||
for (const [name, el] of Object.entries(this.childs)) {
|
||||
let regex: RegExp
|
||||
if (name === 'default') {
|
||||
regex = /<slot(\s+name=["']?default["']?)?\s*\/?>.*?<\/slot>|<slot(\s+name=["']?default["']?)?\s*\/?>/i
|
||||
} else {
|
||||
regex = new RegExp(
|
||||
`<slot\\s+name=["']?${name}["']?\\s*\\/?>.*?<\\/slot>|<slot\\s+name=["']?${name}["']?\\s*\\/?>`,
|
||||
'i'
|
||||
)
|
||||
}
|
||||
|
||||
out = out.replace(regex, typeof el === 'string' ? el : HTMLtoString(el))
|
||||
}
|
||||
|
||||
return { output: out, needHydration: hasEvents(res) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the component to an HTMLElement for the browser.
|
||||
*
|
||||
* @throws {Error} if run outside the browser
|
||||
*/
|
||||
public async renderHTML(): Promise<HTMLElement> {
|
||||
assert(getContext() === 'browser', 'renderHTML can only be run inside the browser')
|
||||
|
||||
const rendered = await this.renderOrCache()
|
||||
|
||||
const root = HTMLToElement(rendered)
|
||||
for (const [name, el] of Object.entries(this.childs)) {
|
||||
let slot: HTMLSlotElement | undefined | null
|
||||
if (name === 'default') {
|
||||
slot = root.querySelector('slot')
|
||||
} else {
|
||||
slot = root.querySelector<HTMLSlotElement>(`slot[name="${name}"]`)
|
||||
}
|
||||
if (slot) {
|
||||
slot.replaceWith(typeof el === 'string' ? el : HTMLToElement(el))
|
||||
}
|
||||
}
|
||||
|
||||
return root
|
||||
}
|
||||
|
||||
/**
|
||||
* Tell the component to render itself.
|
||||
*
|
||||
* *works only in browser*
|
||||
*/
|
||||
public async triggerRender() {
|
||||
// ignore automatics renders inside if we are not in the server
|
||||
if (getContext() !== 'browser' /* || !this.shadowRoot*/) {
|
||||
return
|
||||
}
|
||||
|
||||
const rendered = await this.renderHTML()
|
||||
|
||||
this.innerHTML = ''
|
||||
this.appendChild(rendered)
|
||||
|
||||
if (!this.mounted) {
|
||||
this.mounted = true
|
||||
this.didMount()
|
||||
} else {
|
||||
this.didUpdate()
|
||||
}
|
||||
}
|
||||
|
||||
private async renderOrCache(): Promise<SimpleHTMLElement> {
|
||||
if (this.needRender || !this.lastRender) {
|
||||
this.lastRender = await this.render()
|
||||
}
|
||||
return this.lastRender
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user