import { CustomElement } from 'typed-custom-elements'; /** Describes {@link WebComponent} property */ export interface WebComponentProp { /** Property name */ name: string; /** Default value */ default?: string; } /** Basic custom element's definition options that should be passed to {@link WebComponent.define} */ export interface BasicWebComponentDefinitionOptions { /** Custom element tag name */ tag: string; /** * Where to define given element * * @default customElements */ registry?: CustomElementRegistry; /** * Whether to ignore an error that is thrown when an element is defined second time under a different tag. * * This option is intended for when a web component uses another web component which may not be defined before * its construction. * * @default false */ noRedefinitionError?: boolean; } /** * Definition options that should be used by concrete web components. Components must provide default values * if necessary. */ export interface WebComponentDefinitionOptions extends Partial { } /** * Current component state. Possible values: * * 1. `uninitialized` - Initialization has not yet run. * 1. `initialization` - Component is initializing its internal state. * 1. `attrs-first-change` - Component is calling {@link WebComponent.attributeChangedCallback} for the first time right * after initialization is completed. * 1. `normal` - Normal state. */ export type WebComponentState = "uninitialized" | "initialization" | "attrs-first-change" | "normal"; /** Resolved and normalized property config */ export interface ResolvedWebComponentProp extends Omit { } /** * ### Introduction * * This is the base web components class and the whole web components implementation. * * This is just a * [custom element](https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_custom_elements) * with a number of necessary features missing from the standard custom elements. * * ### Implementation goals * * This web components implementation was designed with the following goals in mind: * * 1. Improve upon custom elements: * 1. Implement necessary features that are missing from the standard custom elements. * 1. Maintain compliance with the custom elements specification. * 1. Maintain compatibility with other libraries and frameworks without additional setup. * 1. Keep implementation minimal and size small: * 1. Don't pollute application's bundle with the code that most likely won't be used. * 1. Don't implement features for the sake of having features. * 1. Achieve maximum performance. * * As you can see, this is **not** a framework. There are no quality-of-life abstractions and features found in major * frameworks like [Lit](https://lit.dev/). Ultimately, this is a bare minimum to ship off web components integration. * * ### When to use this * * When you are: * * - Occasionally using web components (or just `vite-awesome-svg-loader` alone). * - Building a wrapper around `vite-awesome-svg-loader`. * - Having other reasons to use a **minimal** web components implementation like this one. * * ### When to NOT use this * * When you are: * * - Planning or already using a dedicated library for managing web components. * * Just use that library. * * - Planning to build more web components. * * Consider using a dedicated library. It will pay off in the long run. * * ### Behavioral differences from custom elements * * 1. All attributes are observed automatically. No need for {@link WebComponent.observedAttributes}. * * 1. {@link WebComponent.attributeChangedCallback} is called when component has initialized and when any attribute's * value has **actually** changed. Setting same value doesn't count as a change. * * 1. Properties can be synchronized with attributes by defining them in {@link WebComponent.props}. * * ### Core principles * * 1. Use constructors to initialize state. But do **not** use properties defined in {@link WebComponent.props} * in a constructor because that will trigger an attribute setter which renders constructor invalid * (per custom elements specification). * * 1. Use {@link WebComponent.connectedCallback} to initialize markup. * * 1. Use {@link WebComponent.attributeChangedCallback} to track property/attribute changes. Do **not** define your own * getters and setters, they will be overridden. * * 1. Prefer inheritance over composition. Yes, this is an anti-pattern. Web components are actual elements. * The more components you nest, the larger and slower to process DOM becomes. If you want composition, either sacrifice * performance or use a specialized library/framework. * * 1. When using a web component inside another web component, call `define({ noRedefinitionError: true })` and * use a constructor instead of `document.createElement()`. * * Suppress a redefinition error because in most cases user will not specify different tag and would not expect * an error if they haven't defined a dependency component. * * Use a constructor because user still might decide to use a custom tag. * * Ultimately, user will get an error, if the definition is invalid. * * 1. If you've encountered a problem that must be resolved immediately by modifying the internals, * everything's here for you. Just use `// @ts-ignore` to suppress the errors. After the hotfix, please file an issue * and explain your case. * * ### Creating web components * * Let's create a simple `BusinessCard` web component: * * ```ts * class BusinessCard extends WebComponent implements CustomElement { * // Define class properties that will be synced with attributes * * static readonly props = ["personName", { name: "orgName", default: "Not affiliated" }]; * * // Declare properties for type-checking to work. You may actually define them, but they will be overridden * // with proper getters and setters, so don't create empty overhead. * * declare personName?: string; * declare orgName: string; * * // Add markup * * connectedCallback() { * super.connectedCallback(); * * if (this.children.length) { * return * } * * const personName = document.createElement("div"); * personName.className = "person-name"; * this.appendChild(personName); * * const orgName = document.createElement("div"); * orgName.className = "org-name"; * this.appendChild(orgName); * } * * // Track changes * * attributeChangedCallback(name: string, oldValue: string | null, newValue: string | null) { * switch (name) { * // props are tracked: * case "personName": * case "orgName": * this.getElementsByClassName(name)[0]!.innerText = newValue || "-"; * break; * * // Every other attribute is also tracked: * case "data-some-attr": * console.log(oldValue, newValue); * break; * } * } * * // Provide default tag for definition (optional, but recommended) * * define(options?: WebComponentDefinitionOptions) { * super.define({ ...options, tag: options.tag || "business-card" }); * } * } * * // Define a custom element * * BusinessCard.define(); * ``` * * ### Using web components * * Let's instantiate and mount a `BusinessCard` web component that we've created: * * ```ts * // Create a web component instance: * * const card = new BusinessCard(); * // or: * const card = document.createElement("business-card"); * * // Add component to the DOM: * * document.getElementById("card")!.appendChild(card); * * // Change properties: * * card.firstName = "John"; * // or: * card.setAttribute("first-name", "John"); // Notice that attribute is in snake-case while property is in camelCase * ``` * * ### Extending web components * * Let's create a `CoolBusinessCard` component by extending `BusinessCard` component: * * ```ts * class CoolBusinessCard extends BusinessCard implements CustomElement { * // Define new properties. Inherited properties will be defined and copied to "props" automatically. * // Note: it is impossible to redefine inherited properties because parent relies on their behavior. * * static readonly props = [ * { name: "coolness", default: "120%" }, * ]; * * declare coolness: string; * * // Provide default tag for definition * * define(options: WebComponentDefinitionOptions = {}) { * super.define({ ...options, tag: options.tag || "cool-business-card" }); * } * } * ``` */ export declare class WebComponent extends HTMLElement implements CustomElement { /** * Doesn't do anything. All attributes are already observed. This property will be deleted when custom element * will be defined. * * This property is recognized for compatibility reasons only. */ static observedAttributes?: string[]; /** * Properties that will be synced with attributes. * * Properties must be in `camelCase`. Synchronized attributes will be in `kebab-case`. * * To not confuse cases, prefer single-word names. */ static props: (string | WebComponentProp)[]; /** * Current component state. See {@link WebComponentState} for the details. */ protected _state: WebComponentState; /** * Initial property values. * * If element is created before it is defined, and user will set properties, these properties will be own properties * of the element. When element is upgraded, these properties are kept as own. * Children's constructors may override the base class's properties.` * * The initialization logic must re-apply user's values. To do so, we save them here in the constructor before they're * overridden. * * This property is deleted after initialization is complete. */ private _initialValues?; /** * Called when: * * 1. Component has just finished initialization. Then: * * 1. {@link _state} property will be `attrs-first-change`. * 1. `oldValue` argument will be `null`. * 1. `newValue` will have a default value or `null`, if there's no default. * 1. Thus, following may be true: `oldValue === newValue && newValue === null`. * * 1. **Any** of the element's attributes has **actually** changed (unlike default custom elements' behavior where * this method is called even if the same value has been set). Then: * * 1. {@link _state} property will be `normal`. * 1. `oldValue` and `newValue` will be always different. */ attributeChangedCallback?(...args: Parameters>): void; constructor(); /** * Initializes component state. * * Custom elements may not add attributes or children in a constructor, so separate initialization is required. * Thus, this method is called whenever an attribute is accessed or in a microtask after component's constructor * has run. * * Initialization is performed only once. */ protected _init(): void; /** * Called when the element is added to a document. * * If overridden, `super.connectedCallback()` must be the first statement. */ connectedCallback(): void; getAttribute(name: string): string | null; setAttribute(name: string, value: string): void; removeAttribute(name: string): void; toggleAttribute(name: string, force?: boolean): boolean; /** * A common attribute setter called by attributes manipulation methods (`setAttribute`, `removeAttribute`, etc) * and property setters * * @param name Attribute name * @param value Attribute value */ private _setAttr; /** * Returns a constructor of this class * * If you need to refer to your own class, type it like so: * * ```ts * class MyClass extends WebComponent { * declare protected _ctor: () => typeof MyClass * } * ``` * * @returns constructor of this class */ protected _ctor(): typeof WebComponent; /** @returns Current web component's metadata */ private _getMeta; /** * Defines a custom element for this web component. * * Won't throw an error if called multiple times with the same tag. * * Will throw an error (unless suppressed via {@link BasicWebComponentDefinitionOptions.noRedefinitionError}) * if defined under a different tag. * * @param options Definition options */ static define(options: BasicWebComponentDefinitionOptions): void; /** Initializes class itself. Called by {@link define} and by the constructor. Initialization happens only once. */ protected static _initClass(): void; }