-
-
Notifications
You must be signed in to change notification settings - Fork 832
Description
Prerequisites
- I have read the Contributing Guidelines.
- I agree to follow the Code of Conduct.
- I have searched for existing issues that already include this feature request, without success.
Describe the Feature Request
This feature would give type safety when using addEventListener or removeEventListener. This would improve the developer experience of TypeScript developers.
This would also make consuming Stencil components in an Angular project with or without using the Angular build target manageable with strictTemplates flag set to true.
Describe the Use Case
This feature would allow developers to use Stencil components in a TypeScript project where using addEventListener and removeEventListener would use proper types and would eliminate the need always to use type assertion, create custom types, or overloads to each component's html element type and define them ourselves in HTMLElementTagNameMap.
Consider the following:
import { Component, Event, EventEmitter, Prop, h } from '@stencil/core';
@Component(/* ... */)
export class MyComponent {
// Add an event `example`:
@Event() example!: EventEmitter<number>;
// ...
}// HTMLMyComponentElement
const myComponent = document.createElement('my-component');
myComponent.addEventListener('example', (event) => {
// Property 'detail' does not exist on type 'Event'.ts(2339)
console.log(event.detail);
});
This type for the event callback shouldn't be Event but instead MyComponentCustomEvent<number>.
This issue makes working with Stencil in an Angular project very difficult since there are less options and is unclear to influence this behavior in an Angular template:
Describe Preferred Solution
Stencil should provide types to Stencil component's html elements such that using addEventListener and removeEventListener have specific types instead of just Event, which is typically wrong since most events are likely a CustomEvent of some kind.
Since there are no overloads for each component's html element, they all fallback to HTMLElement for addEventListener and removeEventListener:
interface HTMLElement {
// ...
addEventListener<K extends keyof HTMLElementEventMap>(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void;
addEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void;
removeEventListener<K extends keyof HTMLElementEventMap>(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | EventListenerOptions): void;
removeEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions): void;
}If Stencil generated these overloads, there wouldn't be any extra steps needed to use addEventListener and removeEventListener:
/** demo/src/components.d.ts */
// ...
export interface MyComponentCustomEvent<T> extends CustomEvent<T> {
detail: T;
target: HTMLMyComponentElement;
}
declare global {
interface HTMLMyComponentElementEventMap {
example: number;
}
interface HTMLMyComponentElement extends Components.MyComponent, HTMLStencilElement {
addEventListener<K extends keyof HTMLMyComponentElementEventMap>(type: K, listener: (this: HTMLMyComponentElement, ev: MyComponentCustomEvent<HTMLMyComponentElementEventMap[K]>) => any, options?: boolean | AddEventListenerOptions): void;
addEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void;
removeEventListener<K extends keyof HTMLMyComponentElementEventMap>(type: K, listener: (this: HTMLMyComponentElement, ev: MyComponentCustomEvent<HTMLMyComponentElementEventMap[K]>) => any, options?: boolean | EventListenerOptions): void;
removeEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions): void;
}
var HTMLMyComponentElement: {
prototype: HTMLMyComponentElement;
new (): HTMLMyComponentElement;
};
interface HTMLElementTagNameMap {
"my-component": HTMLMyComponentElement;
}
}This would make dealing with events a lot easier in general:

Describe Alternatives
Creating a custom build target to create types that overload addEventListener and removeEventListener for each Stencil component's html element type. Or add types with overloads somewhere in your project when consuming your Stencil components. Or modifying your generated components.d.ts through some other kind of automation.
Example:
export interface MyComponentCustomEvent<T> extends CustomEvent<T> {
detail: T;
target: HTMLMyComponentElement;
}
declare global {
interface HTMLMyComponentElementEventMap {
example: number;
}
interface HTMLMyComponentElement extends Components.MyComponent, HTMLStencilElement {
addEventListener<K extends keyof HTMLMyComponentElementEventMap>(type: K, listener: (this: HTMLMyComponentElement, ev: MyComponentCustomEvent<HTMLMyComponentElementEventMap[K]>) => any, options?: boolean | AddEventListenerOptions): void;
addEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void;
removeEventListener<K extends keyof HTMLMyComponentElementEventMap>(type: K, listener: (this: HTMLMyComponentElement, ev: MyComponentCustomEvent<HTMLMyComponentElementEventMap[K]>) => any, options?: boolean | EventListenerOptions): void;
removeEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions): void;
}
var HTMLMyComponentElement: {
prototype: HTMLMyComponentElement;
new (): HTMLMyComponentElement;
};
interface HTMLElementTagNameMap {
"my-component": HTMLMyComponentElement;
}
}Another workaround is to use Another workaround to this issue is this custom output target I made: https://github.com/m-thompson-code/event-listener-types-output-target#event-listener-types-output-target to automate creating event listener types.
Related Code
- Generate a stencil component library:
npm init stencil
cd demoChoose component --> Project name: demo --> Confirm Y
- Add an event named
examplewith typeEventEmitter<number>toMyComponentfound atdemo/src/components/my-component/my-component.tsx:
// Add `Event` to import
import { Component, Event, EventEmitter, Prop, h } from '@stencil/core';
// ...
@Component(/* ... */)
export class MyComponent {
// Add an event `example`:
@Event() example!: EventEmitter<number>;
// ...
}- Build stencil library:
npm run build- Add the following to any TypeScript find such as
index.tsfound atdemo/src/index.ts:
// HTMLMyComponentElement
const myComponent = document.createElement('my-component');
// Add event listener
myComponent.addEventListener('example', (event) => {
// Property 'detail' does not exist on type 'Event'.ts(2339)
console.log(event.detail);// <-- Type error
});When trying to access event.detail, this leads to a type error:
Property 'detail' does not exist on type 'Event'.ts(2339)
Additional Information
This issue relates to stenciljs/output-targets#219 but is not the same issue. The difference is that this issue is about Stencil as custom elements and consuming them as custom elements with or without Angular and without using the Angular output target.
That being said, a solution would likely resolve the majority of the problems involving Outputs / Events with the Angular output target.
