haunted icon indicating copy to clipboard operation
haunted copied to clipboard

CustomEvents and TypeScript

Open fgladisch opened this issue 5 years ago • 8 comments
trafficstars

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!

fgladisch avatar Mar 19 '20 09:03 fgladisch

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

matthewp avatar Mar 19 '20 22:03 matthewp

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'.

fgladisch avatar Mar 20 '20 10:03 fgladisch

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)

sopretty avatar Mar 26 '20 11:03 sopretty

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?

fegas avatar Jul 30 '20 17:07 fegas

Anything new about that?

christophediprima avatar Jan 04 '21 10:01 christophediprima

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...

RoXuS avatar Jan 19 '21 09:01 RoXuS

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.

joryphillips avatar Sep 29 '21 01:09 joryphillips

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 }
      })
    );
  };

joryphillips avatar Sep 29 '21 16:09 joryphillips