Lusito / tsx-dom

Lightweight DOM Libraries
https://lusito.github.io/tsx-dom/
MIT License
49 stars 17 forks source link

added support for webcomponents as jsx value-based elements #21

Closed SergheiGurgurov closed 4 months ago

SergheiGurgurov commented 7 months ago

TL;DR

i added support for native webcomponents inside tsx-dom, i made sure it passes all tests and linter control.

Description

Hi, i've been using tsx-dom to create some framework-agnostic reusable UI elements. i felt like a more complete support of webcomponents would be great for that task aswell so i added it in.

Behaviour

Before

//webcomponent
class MyCustomElement extends HTMLElement {
    ...
}

customElements.define("my-custom-element", MyCustomElement);

//needed declaration of intrinsic element to avoid typescript errors
declare global {
  namespace JSX {
    interface IntrinsicElements {
      "my-custom-element": any;
    }
  }
}

//can use webcomponent only by it's tagname
//cant pass complex data via attributes
//cant easily go to the class definition
const element = <my-custom-element example="value"></my-custom-element>

After

//web component
class MyHeader extends HTMLElement {
  props: { title: string };
  ....
}

customElements.define("my-header", MyHeader);

//can use webcomponent by className
//can pass complex data to contructor through props (with working types and intellisense)
//can easily got to the class definition
const myHeader = <MyHeader title={"Hello"}></MyHeader>;

Changes

the changes are really simple, the most important part is in the createElement function (and jsx-runtime counterpart)

Before

only accepts function components if className used as tagName this throws a runtime error


if (typeof tag === "function") return tag({ ...attrs, children });

After

i check if the function is a class constructor (new function in utils) and if it is a use the "new" keyword to instantiate the class which create the corrisponding HTMLElement. i use uppercase "Tag" beacuse your linter configuration wants it for constructor functions

if (typeof Tag === "function") {
  return isClassConstuctor(Tag) ? new Tag({ ...attrs, children }) : Tag({ ...attrs, children });
}
Lusito commented 5 months ago

Hi @SergheiGurgurov,

sorry for the delay. Life kept me very busy.

Thanks for the PR. I have some thoughts: In your PR, you pass props to the constructor of the custom element, but that's not how custom elements normally work, so you would have to make the props parameter of the constructor optional and then check if it exists, so that you can set all attributes on it. You'd also have to remember the children, so that you can add them within the connectedCallback. All of this needs to be done manually for each custom element.

What do you think about this instead (untested)?

function defineCustomElement<T extends Record<string, any>>(
    name: string,
    constructor: CustomElementConstructor,
    options?: ElementDefinitionOptions
) {
    customElements.define(name, constructor, options);
    return (props: T) => jsx(name, props);
}

const MyCustomElement = defineCustomElement<{ foo: string }>("my-custom-element", class extends HTMLElement {
    // ...
});

const element = <MyCustomElement foo="bar" />;

That way the custom element code is still written in the exact same way as originally intended. You would just have to call defineCusomElement instead of customElements.define.

From a typescript perspective, this works. I just have not tested it yet in runtime.

Lusito commented 4 months ago

I've just released a new version with a tested version of the above idea. Take a look at the documentation if you're interested: https://lusito.github.io/tsx-dom/tsx-dom/custom-elements.html

I'm closing this as solved differently. Thanks for the inspiration though :slightly_smiling_face: