From 4165f8d4afec9fe445d6f8b3277bd84009d39723 Mon Sep 17 00:00:00 2001 From: Sli Date: Thu, 17 Oct 2024 23:14:54 +0200 Subject: [PATCH] Add register decorator for web components and a better inheriting system for html elements --- core/static/webpack/ajax-select-index.ts | 13 +++-- core/static/webpack/easymde-index.ts | 12 ++--- core/static/webpack/utils/web-components.ts | 55 ++++++++++++++------- tsconfig.json | 1 + 4 files changed, 48 insertions(+), 33 deletions(-) diff --git a/core/static/webpack/ajax-select-index.ts b/core/static/webpack/ajax-select-index.ts index dc2389b5..e64df216 100644 --- a/core/static/webpack/ajax-select-index.ts +++ b/core/static/webpack/ajax-select-index.ts @@ -1,16 +1,17 @@ import "tom-select/dist/css/tom-select.css"; -import { InheritedComponent } from "#core:utils/web-components"; +import { inheritHtmlElement, registerComponent } from "#core:utils/web-components"; import TomSelect from "tom-select"; import type { TomItem, TomLoadCallback, TomOption } from "tom-select/dist/types/types"; import type { escape_html } from "tom-select/dist/types/utils"; import { type UserProfileSchema, userSearchUsers } from "#openapi"; -export class AjaxSelect extends InheritedComponent<"select"> { - widget: TomSelect; - filter?: (items: T[]) => T[]; +@registerComponent("ajax-select") +export class AjaxSelect extends inheritHtmlElement("select") { + public widget: TomSelect; + public filter?: (items: T[]) => T[]; constructor() { - super("select"); + super(); window.addEventListener("DOMContentLoaded", () => { this.loadTomSelect(); @@ -90,5 +91,3 @@ export class AjaxSelect extends InheritedComponent<"select"> { }); } } - -window.customElements.define("ajax-select", AjaxSelect); diff --git a/core/static/webpack/easymde-index.ts b/core/static/webpack/easymde-index.ts index 5bc8a370..04a33f17 100644 --- a/core/static/webpack/easymde-index.ts +++ b/core/static/webpack/easymde-index.ts @@ -1,7 +1,7 @@ // biome-ignore lint/correctness/noUndeclaredDependencies: shipped by easymde import "codemirror/lib/codemirror.css"; import "easymde/src/css/easymde.css"; -import { InheritedComponent } from "#core:utils/web-components"; +import { inheritHtmlElement, registerComponent } from "#core:utils/web-components"; // biome-ignore lint/correctness/noUndeclaredDependencies: Imported by EasyMDE import type CodeMirror from "codemirror"; // biome-ignore lint/style/useNamingConvention: This is how they called their namespace @@ -9,7 +9,7 @@ import EasyMDE from "easymde"; import { markdownRenderMarkdown } from "#openapi"; const loadEasyMde = (textarea: HTMLTextAreaElement) => { - const easyMde = new EasyMDE({ + new EasyMDE({ element: textarea, spellChecker: false, autoDownloadFontAwesome: false, @@ -183,12 +183,10 @@ const loadEasyMde = (textarea: HTMLTextAreaElement) => { } }; -class MarkdownInput extends InheritedComponent<"textarea"> { +@registerComponent("markdown-input") +class MarkdownInput extends inheritHtmlElement("textarea") { constructor() { - super("textarea"); - + super(); window.addEventListener("DOMContentLoaded", () => loadEasyMde(this.node)); } } - -window.customElements.define("markdown-input", MarkdownInput); diff --git a/core/static/webpack/utils/web-components.ts b/core/static/webpack/utils/web-components.ts index 82d039eb..2899a5af 100644 --- a/core/static/webpack/utils/web-components.ts +++ b/core/static/webpack/utils/web-components.ts @@ -1,3 +1,15 @@ +/** + * Class decorator to register components easily + * It's a wrapper around window.customElements.define + * What's nice about it is that you don't separate the component registration + * and the class definition + **/ +export function registerComponent(name: string, options?: ElementDefinitionOptions) { + return (component: CustomElementConstructor) => { + window.customElements.define(name, component, options); + }; +} + /** * Safari doesn't support inheriting from HTML tags on web components * The technique is to: @@ -6,28 +18,33 @@ * pass all attributes to the child component * store is at as `node` inside the parent * - * To use this, you must use the tag name twice, once for creating the class - * and the second time while calling super to pass it to the constructor + * Since we can't use the generic type to instantiate the node, we create a generator function + * + * ```js + * class MyClass extends inheritHtmlElement("select") { + * // do whatever + * } + * ``` **/ -export class InheritedComponent< - K extends keyof HTMLElementTagNameMap, -> extends HTMLElement { - node: HTMLElementTagNameMap[K]; +export function inheritHtmlElement(tagName: K) { + return class Inherited extends HTMLElement { + protected node: HTMLElementTagNameMap[K]; - constructor(tagName: K) { - super(); - this.node = document.createElement(tagName); - const attributes: Attr[] = []; // We need to make a copy to delete while iterating - for (const attr of this.attributes) { - if (attr.name in this.node) { - attributes.push(attr); + constructor() { + super(); + this.node = document.createElement(tagName); + const attributes: Attr[] = []; // We need to make a copy to delete while iterating + for (const attr of this.attributes) { + if (attr.name in this.node) { + attributes.push(attr); + } } - } - for (const attr of attributes) { - this.removeAttributeNode(attr); - this.node.setAttributeNode(attr); + for (const attr of attributes) { + this.removeAttributeNode(attr); + this.node.setAttributeNode(attr); + } + this.appendChild(this.node); } - this.appendChild(this.node); - } + }; } diff --git a/tsconfig.json b/tsconfig.json index 6bb7d717..deee110d 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -7,6 +7,7 @@ "target": "es6", "allowJs": true, "moduleResolution": "node", + "experimentalDecorators": true, "allowSyntheticDefaultImports": true, "esModuleInterop": true, "types": ["jquery", "alpinejs"],