matthewp / haunted

React's Hooks API implemented for web components 👻

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

CustomEvents and TypeScript

fgladisch opened this issue · comments

Hi!

We want to use CustomEvents with Haunted in TypeScript, but ran into a problem when defining "this" for a function:

// What is "this" here?!
function List(this: any, { items }: { items: { id: string, name: string }[] }) {
  const handleListItemSelect = (id: string) => () => {
    this.dispatchEvent(
      new CustomEvent("item-change", {
        bubbles: true,
        composed: true,
        detail: { id }
      })
    );
  };

  return html`
    ${items.map(
      item => html`
        <div @click=${handleListItemSelect(item.id)}>${item.name}</div>
      `
    )}
  `;
}

Haunted does not seem to export anything we can replace "any" with. Any ideas?

Thanks!

I'm not a TS user but we have several around here, hopefully someone will chime in.

It's seems this has to be Element from lib.dom.d.ts. But TypeScript gives an error when defining the custom element:

customElements.define("clinq-list", component<Props>(List));
TS2345: Argument of type '(this: Element, { items }: Props) => TemplateResult' is not assignable to parameter of type 'Renderer<Props>'.
  The 'this' types of each signature are incompatible.
    Type 'unknown' is not assignable to type 'Element'.

Haunted & Ts versions: "haunted": "^4.7.0", "typescript": "^3.7.2",
Same problem here, when I try to define a custom element like this:

export const wcExample = component(Example, {
  useShadowDOM: false
});
customElements.define('example', wcExample);

I got this TS error

  Type 'Element' is missing the following properties from type 'HTMLElement': accessKey, accessKeyLabel, autocapitalize, dir, and 106 more.ts(2345)
commented

any clue on theright way to declare a component on TS?

i have this:

import { html } from 'haunted';
import { TemplateResult } from 'lit-html';
import scss from './dsTitle.scss';

enum DsType {
  t1 = "t1",
  t2 = "t2",
}

export interface Props extends HTMLElement {
  dsType: DsType;
}

export function dsTitle({dsType}: Props): TemplateResult {
  return html`
    <style>
      ${scss}
    </style>
    <header>
        
    </header>
  `;
}

and on the index.ts:

import { dsTitle, Props } from './dsTitle';
import { component } from 'haunted';
const options: any = { observedAttributes: ['ds-type'] };
window.customElements.define('ds-button', component<Props>(dsTitle, options));

how can avoid "any" on options? and, am i right on my TS definitions on haunted?

commented

Anything new about that?

customElements.define('ds-button', component<Props>(dsTitle, { observedAttributes: ['ds-type'] }));

OR

const options: Record<'observedAttributes', Array<keyof Props>> = { observedAttributes: ['ds-type'] };
customElements.define('ds-button', component<Props>(dsTitle, options));

edit: for camelCase attribute you have to add two types :

export interface Props extends HTMLElement {
  dsType: DsType;
  ds-type: DsType;
}

it is the only way that I found to work, if you have better idea...

I might have some semi-decent answers here (maybe).

First, to address the original custom event issue -- I don't see a need to pass in a context. this should simply refer to the Element that is created by List (assuming this is a custom element). I recreated the above scenario and this worked just fine and appropriately referred to the element that fired the custom event. It was heard by a parent element, too, that was using this.addEventListener. Maybe Haunted has changed since the issue was originally reported?

Second, for the more recent discussions about defining the custom element, I was able to give component() a generic that is an Intersection type to satisfy TypeScript and the overloads for Renderer and Creator, which I feel is a bit better than interface Props extends HTMLElement because it keeps the shape of the props object cleaner.

customElements.define('my-element', component<HTMLElement & Props>(MyElement));

or at worst,

customElements.define('my-element', component<HTMLElement & Props>(MyElement), { observedAttributes: ['foo-bar'] }));

#310 is a sloppy PR I made to illustrate this.

Follow-up: I didn't have "strict": true on in my tsconfig 🤦 and I now see why passing the context in is necessary 🤦

I solved by passing in this: unknown, which is what GenericRenderer expects, then casting this to HTMLElement in the places I needed it.

So in the original question it would be:

function List(this: unknown, { items }: { items: { id: string, name: string }[] }) {
  const handleListItemSelect = (id: string) => () => {
    (this as HTMLElement).dispatchEvent(
      new CustomEvent("item-change", {
        bubbles: true,
        composed: true,
        detail: { id }
      })
    );
  };