add web components
This commit is contained in:
parent
e036589813
commit
d49f083a3e
7 changed files with 199 additions and 2 deletions
|
|
@ -18,6 +18,5 @@ export default tseslint.config(
|
|||
}
|
||||
},
|
||||
...tseslint.configs.strict,
|
||||
...tseslint.configs.stylistic,
|
||||
jsdoc.configs['flat/recommended-typescript']
|
||||
);
|
||||
30
src/abstractComponent.ts
Normal file
30
src/abstractComponent.ts
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
import { ComponentParams, CUSTOM_COMPONENT_CONFIG, LinkedProperty } from "./component.type";
|
||||
|
||||
/** Basic class used to declare web components */
|
||||
export class AbstractComponent extends HTMLElement {
|
||||
|
||||
/** List of all the class members linked to a dom element */
|
||||
public __linkedProperties?: LinkedProperty[]
|
||||
/** All the component's params */
|
||||
public [CUSTOM_COMPONENT_CONFIG]?: ComponentParams;
|
||||
|
||||
public connectedCallback(): void {
|
||||
this.init();
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
public disconnectedCallback(): void { }
|
||||
|
||||
/**
|
||||
* Function called in the component's life cycle
|
||||
* Used to init the DOM and the sub components
|
||||
*/
|
||||
public init(): void { }
|
||||
|
||||
/**
|
||||
* Function called in the component's life cycle
|
||||
* Used to refresh the datas, and re render the component
|
||||
* Please be conscious that if you clean THIS as a dom element, the template won't be re rendered again
|
||||
*/
|
||||
public refresh(): void { }
|
||||
}
|
||||
108
src/component.tools.ts
Normal file
108
src/component.tools.ts
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
import { AbstractComponent } from "./abstractComponent";
|
||||
import { ComponentParams, CUSTOM_COMPONENT_CONFIG } from "./component.type";
|
||||
import { findRefElement, parseModuleToHtml } from "./dom.tools";
|
||||
|
||||
// Declare a unique key to store the flag on the prototype.
|
||||
// This symbol is used to uniquely identify and access the DOM link flag,
|
||||
// ensuring there are no conflicts with other properties on the prototype.
|
||||
export const REQUEST_DOM_LINK_KEY = Symbol("requestDomLink");
|
||||
|
||||
interface ConstructorFunction<T> {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
new(...args: any[]): T
|
||||
}
|
||||
|
||||
/**
|
||||
* Define a custom web component using a decorator.
|
||||
* The decorator configures the component's template, styles, and any additional
|
||||
* properties, as well as handling Shadow DOM attachment if specified.
|
||||
* @param p @see {@link ComponentParams}
|
||||
* @returns a constructor factory
|
||||
*/
|
||||
export function CustomComponent<T extends ConstructorFunction<AbstractComponent>>(p: ComponentParams): (constructor: T) => void {
|
||||
return (constructor: T): void => {
|
||||
|
||||
// Create a template element to contain the component's HTML structure, parsed for use.
|
||||
const templateElement = document.createElement('template');
|
||||
templateElement.innerHTML = parseModuleToHtml(p.template);
|
||||
|
||||
// Attach component configuration as a non-writable property on the prototype
|
||||
Object.defineProperty(constructor.prototype, CUSTOM_COMPONENT_CONFIG, {
|
||||
value: p
|
||||
});
|
||||
|
||||
// Save the original connectedCallback, if it exists, to allow for extension.
|
||||
const originalConnectedCallback = constructor.prototype.connectedCallback;
|
||||
|
||||
// Define a custom connectedCallback for the component
|
||||
constructor.prototype.connectedCallback = function (this: AbstractComponent) {
|
||||
|
||||
// If Shadow DOM is specified, attach it and add the template to it
|
||||
if (p.attachToShadow) {
|
||||
// Determine Shadow DOM mode, defaulting to 'open' if boolean true is given.
|
||||
const mode: ShadowRootMode = typeof p.attachToShadow === "boolean" ? "open" : p.attachToShadow;
|
||||
|
||||
// Attach a Shadow DOM if not already attached
|
||||
if (!this.shadowRoot) {
|
||||
this.attachShadow({ mode });
|
||||
}
|
||||
|
||||
// Append the template content to the Shadow DOM
|
||||
if (this.shadowRoot) {
|
||||
this.shadowRoot.appendChild(templateElement.content.cloneNode(true));
|
||||
}
|
||||
} else {
|
||||
// If not using Shadow DOM, insert template content directly into Light DOM
|
||||
this.innerHTML = "";
|
||||
this.appendChild(templateElement.content.cloneNode(true));
|
||||
}
|
||||
|
||||
// Apply any specified CSS classes to the component root element
|
||||
this.classList.add(...p.cssClasses);
|
||||
|
||||
// Process and link properties specified in __linkedProperties to their DOM elements
|
||||
for (const requested of this.__linkedProperties ?? []) {
|
||||
const elt = findRefElement(requested.ref);
|
||||
if (elt == null) {
|
||||
throw new Error(`ref ${requested.ref} not found`);
|
||||
}
|
||||
// Attach the found element as a property on the component instance
|
||||
Object.defineProperty(this, requested.property, { value: elt });
|
||||
}
|
||||
|
||||
// Call the original connectedCallback if it exists
|
||||
if (originalConnectedCallback) {
|
||||
originalConnectedCallback.call(this);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
// Register the custom element with the specified tag name
|
||||
window.customElements.define(p.tagName, constructor);
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Decorator that flags the class member as a link to a dom element
|
||||
* @param ref
|
||||
*/
|
||||
export function LinkToDom(ref: string) {
|
||||
return function <T extends AbstractComponent>(target: T, key: string | symbol): void {
|
||||
// Si target est indéfini, renvoyer un avertissement
|
||||
if (!target) {
|
||||
throw new Error(`LinkToDom: 'target' is undefined for property ${String(key)}`);
|
||||
}
|
||||
|
||||
// Ajouter un flag `request` pour marquer cette propriété comme liée au DOM
|
||||
if (!target.__linkedProperties) {
|
||||
target.__linkedProperties = [];
|
||||
}
|
||||
|
||||
// Ajouter la propriété à la liste des propriétés liées
|
||||
target.__linkedProperties.push({
|
||||
property: String(key),
|
||||
ref
|
||||
});
|
||||
}
|
||||
}
|
||||
28
src/component.type.ts
Normal file
28
src/component.type.ts
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
|
||||
export const CUSTOM_COMPONENT_CONFIG = "__customComponentConfig"
|
||||
|
||||
/** Abstract component params*/
|
||||
export interface ComponentParams {
|
||||
/**
|
||||
* A tag name that will be used to register the component as a custom element
|
||||
* Should always follow the following format "my-name"
|
||||
* ex: "custom-menu-component"
|
||||
*/
|
||||
tagName: string;
|
||||
/** HTML Template */
|
||||
template?: string;
|
||||
/** Specific style of the component */
|
||||
style?: string;
|
||||
/** Css classes to append */
|
||||
cssClasses?: string[];
|
||||
/** Define if the component should be atatched to the shadow dom */
|
||||
attachToShadow?: boolean | ShadowRootMode
|
||||
}
|
||||
|
||||
/** Map a property key to a html element by it's ref attribute */
|
||||
export interface LinkedProperty {
|
||||
/** The ref attribute value */
|
||||
ref: string;
|
||||
/** The object's property */
|
||||
property: string;
|
||||
}
|
||||
25
src/dom.tools.ts
Normal file
25
src/dom.tools.ts
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
|
||||
/** The attriute used to bind dom to component's members */
|
||||
const REF_ATTRIBUTE = "ref"
|
||||
|
||||
/**
|
||||
* Find an element with the attribute ref= in the given parent (or the entire body)
|
||||
* @param ref the ref attribute's value of the target dom element
|
||||
* @param parent the container used to search the element. document.body by default
|
||||
* @returns the founded html element or undefined
|
||||
*/
|
||||
export function findRefElement(ref: string, parent?: HTMLElement): HTMLElement | undefined {
|
||||
const parentElement = parent ?? document.body;
|
||||
const elt = parentElement.querySelector(`[${REF_ATTRIBUTE}=${ref}]`);
|
||||
|
||||
return elt == null ? undefined : elt as HTMLElement
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the value of ComponentParams.template into a string if it was fetched with required function
|
||||
* @param mod the template value
|
||||
* @returns the template as a string
|
||||
*/
|
||||
export function parseModuleToHtml(mod: any): string {
|
||||
return "default" in mod ? mod.default as string : "";
|
||||
}
|
||||
2
src/dom.types.ts
Normal file
2
src/dom.types.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
/** Anything that could be append in html*/
|
||||
export type AcceptedHTMLContent = HTMLElement | string
|
||||
|
|
@ -1 +1,6 @@
|
|||
console.log("hello world")
|
||||
export { AbstractComponent } from "./abstractComponent";
|
||||
export { CustomComponent, LinkToDom } from "./component.tools";
|
||||
export { ComponentParams, LinkedProperty } from "./component.type";
|
||||
export { findRefElement } from "./dom.tools";
|
||||
export { AcceptedHTMLContent } from "./dom.types";
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue