Using web-components within Preact and typescript
Asked Answered
S

4

14

I'm using custom-elements aka web-components within Preact. The problem is that Typescript complains about elements not being defined in JSX.IntrinsicElements - in this case a check-box element:

<div className={styles.option}>
    <check-box checked={this.prefersDarkTheme} ref={this.svgOptions.darkTheme}/>
    <p>Dark theme</p>
</div>

Error message (path omitted):

ERROR in MyComponent.tsx
[tsl] ERROR in MyComponent.tsx(50,29)
      TS2339: Property 'check-box' does not exist on type 'JSX.IntrinsicElements'.

I came across the following, unfortunately not working, possible solutions:

  1. https://mcmap.net/q/156632/-typescript-complains-property-does-not-exist-on-type-39-jsx-intrinsicelements-39-when-using-react-createclass - It's an answer not really realted to the question but it covered my problem

I've tried adding the following to my typings.d.ts file:

import * as Preact from 'preact';

declare global {
    namespace JSX {
        interface IntrinsicElements {
            'check-box': any; // The 'any' just for testing purposes
        }
    }
}

My IDE grayed out the import part and IntrinsicElements which means it's not used (?!) and it didn't worked anyway. I'm still getting the same error message.

  1. https://mcmap.net/q/829596/-typescript-error-quot-property-does-not-exist-on-type-39-jsx-intrinsicelements-39-quot-when-using-native-web-component - Also for react, I've tried to "convert" it to preact and I got the same results as for 1.

I've even found a file maintained by google in the squoosh project where they did the following to "polyfill" the support:

In the same folder as the component a missing-types.d.ts file with the following content, basically the same setup I have but with a index.ts file instead of check-bock.ts and they're using an older TS version v3.5.3:

declare namespace JSX {
  interface IntrinsicElements {
    'range-input': HTMLAttributes;
  }
}

I'm assuming their build didn't fail so how does it work and how do I properly define custom-elements to use them within preact / react components?

I'm currently using [email protected] and [email protected].

Speedboat answered 3/4, 2020 at 15:39 Comment(0)
S
7

Okay I managed to solve it using module augmentation:

declare module 'preact/src/jsx' {
    namespace JSXInternal {

        // We're extending the IntrinsicElements interface which holds a kv-list of
        // available html-tags.
        interface IntrinsicElements {
            'check-box': unknown;
        }
    }
}

Using the HTMLAttributes interface we can tell JSX which attributes are available for our custom-element:

// Your .ts file, e.g. index.ts
declare module 'preact/src/jsx' {
    namespace JSXInternal {
        import HTMLAttributes = JSXInternal.HTMLAttributes;

        interface IntrinsicElements {
            'check-box': HTMLAttributes<CheckBoxElement>;
        }
    }
}

// This interface describes our custom element, holding all its
// available attributes. This should be placed within a .d.ts file.
declare interface CheckBoxElement extends HTMLElement {
    checked: boolean;
}
Speedboat answered 4/4, 2020 at 17:41 Comment(2)
This is the one, future self.Leucomaine
Wow, yep, here I am again. Wish I could upvote this answer multiple times.Leucomaine
E
7

Here are the correct attributes to use, otherwise you will get an error when passing key in for example.

declare global {
  namespace JSX {
    interface IntrinsicElements {
      'xx-element1': React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement>; // Normal web component
      'xx-element2': React.DetailedHTMLProps<React.HTMLAttributes<HTMLInputElement>, HTMLInputElement>; // Web component extended from input
    }
  }
}
Electrometer answered 16/7, 2020 at 12:8 Comment(0)
L
6

With typescript 4.2.3 and preact 10.5.13, here is what works to define a custom tag name with attributes:

declare module 'preact' {
    namespace JSX {
        interface IntrinsicElements {
            'overlay-trigger': OverlayTriggerAttributes;
        }
    }
}

interface OverlayTriggerAttributes extends preact.JSX.HTMLAttributes<HTMLElement> {
    placement?: string;
}

Differences:

  • The module is 'preact' (must be quoted).
  • The namespace is JSX.
  • The IntrinsicElements value type is an interface that extends HTMLAttributes.
  • It extends HTMLAttributes via the name preact.JSX.HTMLAttributes.
  • It supplies the base element type as HTMLElement to populate the eventTarget type in props/attrs like event listeners. You could also put SVGElement if applicable.
Leucomaine answered 19/4, 2021 at 17:44 Comment(1)
This works for me with TypeScript 4.3.5 and Preact 10.5.15, with the caveat that I also needed to import 'preact' before my declaration.Caffrey
O
1

There is a better way to do this without manually binding events.

You can use @lit-labs/react's createComponent to wrap web component to React Component.

import * as React from "react";

import { createComponent } from "@lit-labs/react";
import Slrange from "@shoelace-style/shoelace/dist/components/range/range.js";

export const Range = createComponent(React, "sl-range", Slrange,{
    change: "sl-change" // map web component event to react event
});

import { Range } from "./SlRange";

export default function App() {
  return (
    <div className="App">
      <Range max={6} min={2} step={2}></Range>
    </div>
  );
}

Edit react-web-component

Oldenburg answered 22/4, 2021 at 7:44 Comment(2)
It is still a web component under the hood just abstactedOldenburg
I like this but it's not for PreactEmlen

© 2022 - 2024 — McMap. All rights reserved.