Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

improve the JSX type helpers, add a new dispatchEventWithCall(event) method #35

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
146 changes: 76 additions & 70 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -604,9 +604,6 @@ Modifying the very first example from above for TypeScript, it would look
like the following.

```tsx
/* @jsxImportSource solid-js */
// ^ Alternatively, configure this in tsconfig.json instead of per-file.

import {createSignal} from 'solid-js'
import {div} from '@lume/element/dist/type-helpers.js'

Expand Down Expand Up @@ -643,7 +640,6 @@ The main differences from plain JS are
> `<menu>` element then we need to use the `menu()` helper like follows.

```tsx
/* @jsxImportSource solid-js */
import {createSignal} from 'solid-js'
import {menu} from '@lume/element/dist/type-helpers.js'

Expand All @@ -663,7 +659,6 @@ type `HTMLDivElement` despite the fact that at runtime we will be have an
`HTMLMenuElement` instance.

```tsx
/* @jsxImportSource solid-js */
import {div, button} from '@lume/element/dist/type-helpers.js'

// GOOD.
Expand All @@ -680,8 +675,6 @@ following to have the proper types, but note that the following is also not type
safe:

```tsx
/* @jsxImportSource solid-js */

// GOOD.
const el = (<menu>...</menu>) as any as HTMLMenuElement

Expand All @@ -695,33 +688,41 @@ const el2 = (<menu>...</menu>) as any as HTMLDivElement
### With Solid JSX

To give your Custom Elements type checking for use with DOM APIs, and type
checking in Solid JSX, we can add the element type definition to `JSX.IntrinsicElements`:
checking in Solid JSX, we can add the element type definition to
`JSX.IntrinsicElements`. We can use the `ElementAttributes` helper to specify
which attributes/properties should be exposed in the JSX type:

```tsx
/* @jsxImportSource solid-js */

// We already use @jsxImportSource above, but if you need to reference JSX
// anywhere in non-JSX parts of the code, you also need to import it from
// solid-js:
import {Element, element, stringAttribute, numberAttribute, /*...,*/ JSX} from 'solid-js'

// Define the attributes that your element accepts
export interface CoolElementAttributes extends JSX.HTMLAttributes<CoolElement> {
'cool-type'?: 'beans' | 'hair'
'cool-factor'?: number
// ^ NOTE: These should be dash-case versions of your class's attribute properties.
}
import type {ElementAttributes} from '@lume/element'
import {Element, element, stringAttribute, numberAttribute} from '@lume/element'

// List the properties that should be picked from the class type for JSX props.
// Note! Make sure that the properties listed are either decorated with
// attribute decorators, or that they are on* event properties.
export type CoolElementAttributes = 'coolType' | 'coolFactor' | 'oncoolness'

@element('cool-element')
class CoolElement extends Element {
export class CoolElement extends Element {
@stringAttribute coolType: 'beans' | 'hair' = 'beans'
@numberAttribute coolFactor = 100
// ^ NOTE: These are the camelCase equivalents of the attributes defined above.

// Define the event prop by defining a method with the event name prefixed with 'on'.
oncoolness: ((event: SomeEvent) => void) | null = null

// This property will not appear in the JSX types because it is not listed in
// the CoolElementAttributes that are passed to ElementAttributes below.
notJsxProp = 123

// ... Define your class as described above. ...
}

export {CoolElement}
/** This an event that our element emits. */
class SomeEvent extends Event {
constructor() {
super('someevent', {...})
}
}

// Add your element to the list of known HTML elements. This makes it possible
// for browser APIs to have the expected return type. For example, the return
Expand All @@ -732,36 +733,14 @@ declare global {
}
}

// Also register the element name in the JSX types for TypeScript to recognize
// the element as a valid JSX tag.
declare module 'solid-js' {
namespace JSX {
interface IntrinsicElements {
'cool-element': CoolElementAttributes
}
}
}
```

> :bulb:**TIP:**
>
> To make code less redundant, use the `ElementAttributes` helper to
> pluck the types of properties directly from your custom element class for the
> attribute types:

```ts
import type {ElementAttributes} from '@lume/element'

// This definition is now shorter than before, automatically maps the property
// names to dash-case, and automatically picks up the property types from the
// class.
export type CoolElementAttributes = ElementAttributes<CoolElement, 'coolType' | 'coolFactor'>

// The same as before:
declare module 'solid-js' {
namespace JSX {
interface IntrinsicElements {
'cool-element': CoolElementAttributes
// This automatically maps the property names from camelCase to dash-case,
// automatically picks up the property types from the class, and also
// defines additional types for attr:, prop:, and bool: prefixed props.
'cool-element': ElementAttributes<CoolElement, CoolElementAttributes>
}
}
}
Expand All @@ -772,8 +751,11 @@ Now when you use `<cool-element>` in Solid JSX, it will be type checked:
```jsx
return (
<cool-element
cool-type={123} // Type error: number is not assignable to 'beans' | 'hair'
cool-factor={'foo'} // Type error: string is not assignable to number
// cool-type={123} // Type error: number is not assignable to 'beans' | 'hair'
// cool-factor={'foo'} // Type error: string is not assignable to number
cool-type="hair" // ok
cool-factor="200" // ok
oncoolness={() = console.log('someevent')} // ok
></cool-element>
)
```
Expand All @@ -794,16 +776,7 @@ Defining the types of custom elements for React JSX is similar as for Solid JSX
```

```ts
import type {HTMLAttributes} from 'react'

// Define the attributes that your element accepts, almost the same as before:
export interface CoolElementAttributes extends HTMLAttributes<CoolElement> {
coolType?: 'beans' | 'hair'
coolFactor?: number
// ^ NOTE: These are the names of the class's properties verbatim, not
// dash-cased as with Solid. React works differently than Solid's: it will
// map the exact prop name to the JS property.
}
import type {ReactElementAttributes} from '@lume/element/dist/react.js'

// Add your element to the list of known HTML elements, like before.
declare global {
Expand Down Expand Up @@ -832,14 +805,15 @@ declare global {
```ts
import type {ReactElementAttributes} from '@lume/element/dist/react'

// This definition is now shorter than before, and automatically maps the property names to dash-case.
export type CoolElementAttributes = ReactElementAttributes<CoolElement, 'coolType' | 'coolFactor'>
// ... same as before ...

// The same as before:
declare global {
declare module 'react' {
namespace JSX {
interface IntrinsicElements {
'cool-element': CoolElementAttributes
// Similar as before, with ReactElementAttributes instead of
// ElementAttributes, and props will remain camelCase, not mapped to
// dash-case:
'cool-element': ReactElementAttributes<CoolElement, CoolElementAttributes>
}
}
}
Expand All @@ -850,8 +824,11 @@ Now when you use `<cool-element>` in React JSX, it will be type checked:
```jsx
return (
<cool-element
coolType={123} // Type error: number is not assignable to 'beans' | 'hair'
coolFactor={'foo'} // Type error: string is not assignable to number
// coolType={123} // Type error: number is not assignable to 'beans' | 'hair'
// coolFactor={'foo'} // Type error: string is not assignable to number
coolType="hair" // ok
coolFactor="200" // ok
oncoolness={() = console.log('someevent')} // ok
></cool-element>
)
```
Expand All @@ -860,8 +837,7 @@ return (
> You may want to define React JSX types for your elements in separate files, and
> have only React users import those files if they need the types, and similar if you make
> JSX types for Vue, Svelte, etc (we don't have helpers for those other fameworks
> yet, but you can manually augment JSX as in the examples above on a
> per-framework basis, contributions welcome!).
> yet, but you can manually augment JSX in that case, contributions welcome!).

### With Preact JSX

Expand All @@ -884,6 +860,8 @@ layer:
}
```

The rest is the same.

## API

### `Element`
Expand Down Expand Up @@ -1664,6 +1642,34 @@ element's style sheet into the `ShadowRoot` conflicts with how DOM is created in
`ShadowRoot` content, or etc, then the user may want to place the stylesheet
somewhere else).

#### `dispatchEventWithCall(event)`

This is similar to `dispatchEvent()`, but useful for dispatching a non-builtin
event and causing any `on*` method for that event to also be called if it
exists.

With builtin events, for example, when the builtin `click` event is dispatched,
the element's `.onclick()` method is called automatically if it exists. Now we
can achieve the same behavior with custom events, so that for example
`dispatchEventWithCall(new Event('myevent'))` will also cause `.onmyevent()`
to be called if it exists.

Note, avoid calling this method with an event that is not a custom event, or
you'll trigger the respective builtin `on*` method twice.

```ts
import {element, Element} from '@lume/element'

@element('my-el')
class MyEl extends Element {
onfoo: ((event: Event) => void) | null = null
}

const el = new MyEl()
el.onfoo = () => console.log('foo')
el.dispatchEventWithCall(new Event('foo')) // logs "foo"
```

### Decorators

Using decorators (if available in your build, or natively in your JS engine)
Expand Down
76 changes: 69 additions & 7 deletions dist/LumeElement.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,21 @@ declare class LumeElement extends LumeElement_base {
disconnectedCallback(): void;
attributeChangedCallback?(name: string, oldVal: string | null, newVal: string | null): void;
adoptedCallback(): void;
/**
* This is similar to `dispatchEvent()`, but useful for dispatching a
* non-builtin event and causing any `on*` method for that event to also be
* called if it exists.
*
* With builtin events, for example, when the builtin `click` event is
* dispatched, the element's `.onclick()` method is called automatically if
* it exists. Now we can achieve the same behavior with custom events, so
* that for example `dispatchEventWithCall(new Event('myevent'))` will
* also cause `.onmyevent()` to be called if it exists.
*
* Note, avoid calling this method with an event that is not a custom event,
* or you'll trigger the respective builtin `on*` method twice.
*/
dispatchEventWithCall(event: Event): boolean;
}
export { LumeElement as Element };
export type AttributeHandlerMap = Record<string, AttributeHandler>;
Expand Down Expand Up @@ -204,23 +219,70 @@ type Template = TemplateContent | (() => TemplateContent);
* let coolEl = <cool-element foo={'foo'} bar={null} lorem-ipsum={456}></cool-element>
* ```
*/
export type ElementAttributes<ElementType extends HTMLElement, SelectedProperties extends keyof RemovePrefixes<RemoveAccessors<ElementType>, SetterTypePrefix>, AdditionalProperties extends object = {}> = Omit<JSX.HTMLAttributes<ElementType>, SelectedProperties | keyof AdditionalProperties | 'onerror'> & {
export type ElementAttributes<El, SelectedProperties extends keyof RemoveSetterPrefixes<RemoveAccessors<El>, SetterTypePrefix>, AdditionalProperties extends object = {}> = Omit<JSX.HTMLAttributes<El>, SelectedProperties | keyof AdditionalProperties | 'onerror'> & {
onerror?: ((error: ErrorEvent) => void) | null;
} & Partial<DashCasedProps<WithStringValues<Pick<RemovePrefixes<RemoveAccessors<ElementType>, SetterTypePrefix>, SelectedProperties>>>> & AdditionalProperties;
} & Partial<DashCasedProps<WithStringValues<NonNumberProps<NonBooleanProps<NonOnProps<El, SelectedProperties>>>>>> & Partial<AsValues<NonFunctionsOnly<EventProps<El, SelectedProperties>>, never>> & Partial<PrefixProps<'prop:', WithStringValues<NonNumberProps<NonBooleanProps<NonEventProps<El, SelectedProperties>>>>>> & Partial<PrefixProps<'attr:', DashCasedProps<AsStringValues<NonNumberProps<NonBooleanProps<NonEventProps<El, SelectedProperties>>>>>>> & Partial<DashCasedProps<WithBooleanStringValues<BooleanProps<Pick<RemapSetters<El>, SelectedProperties>>>> & PrefixProps<'bool:', DashCasedProps<AsBooleanValues<BooleanProps<Pick<RemapSetters<El>, SelectedProperties>>>>> & PrefixProps<'prop:', WithBooleanStringValues<BooleanProps<Pick<RemapSetters<El>, SelectedProperties>>>> & PrefixProps<'attr:', DashCasedProps<AsBooleanStringValues<BooleanProps<Pick<RemapSetters<El>, SelectedProperties>>>>>> & Partial<DashCasedProps<WithNumberStringValues<NumberProps<Pick<RemapSetters<El>, SelectedProperties>>>> & PrefixProps<'prop:', WithNumberStringValues<NumberProps<Pick<RemapSetters<El>, SelectedProperties>>>> & PrefixProps<'attr:', DashCasedProps<AsNumberStringValues<NumberProps<Pick<RemapSetters<El>, SelectedProperties>>>>>> & Partial<FunctionsOnly<EventProps<El, SelectedProperties>>> & Partial<AddDelimitersToEventKeys<FunctionsOnly<EventProps<El, SelectedProperties>>>> & AdditionalProperties;
type RemapSetters<El> = RemoveSetterPrefixes<RemoveAccessors<El>, SetterTypePrefix>;
type NonOnProps<El, K extends keyof RemapSetters<El>> = Pick<RemapSetters<El>, OmitFromUnion<K, EventKeys<K>>>;
export type NonEventProps<El, K extends keyof RemoveSetterPrefixes<RemoveAccessors<El>, SetterTypePrefix>> = NonOnProps<El, K> & NonFunctionsOnly<EventProps<El, K>>;
export type EventProps<T, Keys extends keyof T> = Pick<T, EventKeys<OmitFromUnion<Keys, symbol | number>>>;
export type NonBooleanProps<T> = Omit<T, keyof BooleanProps<T>>;
export type BooleanProps<T> = {
[K in keyof T as T[K] extends boolean | 'true' | 'false' ? K : never]: T[K];
};
export type NonNumberProps<T> = Omit<T, keyof NumberProps<T>>;
export type NumberProps<T> = {
[K in keyof T as T[K] extends number ? K : never]: T[K];
};
export type FunctionsOnly<T> = {
[K in keyof T as NonNullable<T[K]> extends (...args: any[]) => any ? K : never]: T[K];
};
export type NonFunctionsOnly<T> = {
[K in keyof T as ((...args: any[]) => any) extends NonNullable<T[K]> ? never : K]: T[K];
};
/**
* Make all non-string properties union with |string because they can all
* receive string values from string attributes like opacity="0.5" (those values
* are converted to the types of values they should be, f.e. reading a
* `@numberAttribute` property always returns a `number`)
*/
export type WithStringValues<Type extends object> = {
[Property in keyof Type]: PickFromUnion<Type[Property], string> extends never ? // if the type does not include a type assignable to string
Type[Property] | string : Type[Property];
export type WithStringValues<T extends object> = {
[K in keyof T]: PickFromUnion<T[K], string> extends never ? // if the type does not include a type assignable to string
T[K] | string : T[K];
};
export type WithBooleanStringValues<T extends object> = {
[K in keyof T]: T[K] | 'true' | 'false';
};
export type AsBooleanStringValues<T extends object> = {
[K in keyof T]: 'true' | 'false';
};
export type AsBooleanValues<T extends object> = {
[K in keyof T]: boolean;
};
export type WithNumberStringValues<T extends object> = {
[K in keyof T]: T[K] | `${number}`;
};
export type AsNumberStringValues<T extends object> = {
[K in keyof T]: `${number}`;
};
export type AsValues<T extends object, V> = {
[K in keyof T]: V;
};
type AsStringValues<T extends object> = {
[K in keyof T]: PickFromUnion<T[K], string> extends never ? string : T[K];
};
type StringKeysOnly<T extends PropertyKey> = OmitFromUnion<T, number | symbol>;
type OmitFromUnion<T, TypeToOmit> = T extends TypeToOmit ? never : T;
export type OmitFromUnion<T, TypeToOmit> = T extends TypeToOmit ? never : T;
type PickFromUnion<T, TypeToPick> = T extends TypeToPick ? T : never;
export type RemovePrefixes<T, Prefix extends string> = {
export type EventKeys<T extends string> = T extends `on${infer _}` ? T : never;
type AddDelimitersToEventKeys<T extends object> = {
[K in keyof T as K extends string ? AddDelimiters<K, ':'> : never]: T[K];
};
type AddDelimiters<T extends string, Delimiter extends string> = T extends `${'on'}${infer Right}` ? `${'on'}${Delimiter}${Right}` : T;
type PrefixProps<Prefix extends string, T> = {
[K in keyof T as K extends string ? `${Prefix}${K}` : K]: T[K];
};
export type RemoveSetterPrefixes<T, Prefix extends string> = {
[K in keyof T as K extends string ? RemovePrefix<K, Prefix> : K]: T[K];
};
type RemovePrefix<T extends string, Prefix extends string> = T extends `${Prefix}${infer Rest}` ? Rest : T;
Expand Down
Loading
Loading