export abstract class ExComponent extends HTMLElement { protected static html = `` static get observedAttributes() { const map: any = (this as any).exPropertyAttributeMapping if ( Array.isArray(map) ) { return map.map(x => x.attribute) } return [] } protected readonly shadow = this.attachShadow({ mode: 'open' }) private hadFirstRender = false private preFirstRenderDefaultAttributes: any = {} private rendersMap: any = {} private mountListeners: ((el: this) => unknown)[] = [] private didMount = false constructor() { super() this.setPropertyAttributeMappings() this.setPropertyRendersMappings() this.attachTemplate() this.setPropertyQueryMappings() } dispatchCustom(name: string, detail: any): boolean { return this.dispatchEvent( new CustomEvent(name, { detail }), ) } connectedCallback() { this.mount() this.didMount = true this.mountListeners.map(x => x(this)) this.render() } attachTemplate() { const template = document.createElement('template') template.innerHTML = ((this as any).constructor as typeof ExComponent).html this.shadow.appendChild(template.content.cloneNode(true)) } setPropertyQueryMappings(): void { const map: any = (this as any).constructor.exPropertyElementMapping if ( Array.isArray(map) ) { for ( const mapping of map ) { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore this[mapping.property] = this.shadow.querySelector(mapping.selector) } } } setPropertyRendersMappings(): void { const map: any = (this as any).constructor.exPropertyRendersMapping if ( Array.isArray(map) ) { for ( const mapping of map ) { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore delete this[mapping.property] Object.defineProperty(this, mapping.property, { get: () => this.rendersMap[mapping.property], set: value => { this.rendersMap[mapping.property] = value if ( this.hadFirstRender ) { this.render() } }, }) } } } setPropertyAttributeMappings(): void { const map: any = (this as any).constructor.exPropertyAttributeMapping if ( Array.isArray(map) ) { for ( const mapping of map ) { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore delete this[mapping.property] Object.defineProperty(this, mapping.property, { get: () => this.getAttribute(mapping.attribute), set: value => { if ( !this.hadFirstRender ) { this.preFirstRenderDefaultAttributes[mapping.attribute] = value return } this.setAttribute(mapping.attribute, value) }, }) } } } attributeChangedCallback(): void { this.render() } onMount(callback: (el: this) => unknown) { if ( this.didMount ) { callback(this) } else { this.mountListeners.push(callback) } } tap(callback: (el: this) => T): T { return callback(this) } // eslint-disable-next-line @typescript-eslint/no-empty-function mount(): void {} render(): void { if ( !this.hadFirstRender ) { for ( const prop of ((this as any).constructor as typeof ExComponent).observedAttributes ) { if ( !this.getAttribute(prop) ) { this.setAttribute(prop, this.preFirstRenderDefaultAttributes[prop]) } } } this.hadFirstRender = true } }