fix type issues in h() (#155600)

- improve regex with named capture groups
- drop $ in favor of inline id
- add tests

Co-authored-by: Henning Dieterichs <notify.henning.dieterichs@live.de>

Co-authored-by: Henning Dieterichs <notify.henning.dieterichs@live.de>
This commit is contained in:
João Moreno
2022-07-19 14:25:52 +02:00
committed by GitHub
parent 4b95a2d3ed
commit dc75592590
4 changed files with 230 additions and 58 deletions

View File

@@ -1738,56 +1738,81 @@ type HTMLElementAttributeKeys<T> = Partial<{ [K in keyof T]: T[K] extends Functi
type ElementAttributes<T> = HTMLElementAttributeKeys<T> & Record<string, any>;
type RemoveHTMLElement<T> = T extends HTMLElement ? never : T;
type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never;
type ArrayToObj<T extends any[]> = UnionToIntersection<RemoveHTMLElement<T[number]>>;
type ArrayToObj<T extends readonly any[]> = UnionToIntersection<RemoveHTMLElement<T[number]>>;
type HHTMLElementTagNameMap = HTMLElementTagNameMap & { '': HTMLDivElement };
type TagToElement<T> = T extends `.${string}`
? HTMLDivElement
: T extends `#${string}`
? HTMLDivElement
: T extends `${infer TStart}#${string}`
? TStart extends keyof HTMLElementTagNameMap
? HTMLElementTagNameMap[TStart]
type TagToElement<T> = T extends `${infer TStart}#${string}`
? TStart extends keyof HHTMLElementTagNameMap
? HHTMLElementTagNameMap[TStart]
: HTMLElement
: T extends `${infer TStart}.${string}`
? TStart extends keyof HTMLElementTagNameMap
? HTMLElementTagNameMap[TStart]
? TStart extends keyof HHTMLElementTagNameMap
? HHTMLElementTagNameMap[TStart]
: HTMLElement
: T extends keyof HTMLElementTagNameMap
? HTMLElementTagNameMap[T]
: HTMLElement;
type TagToElementAndId<TTag> = TTag extends `${infer TTag}@${infer TId}`
? { element: TagToElement<TTag>; id: TId }
: { element: TagToElement<TTag>; id: 'root' };
type TagToRecord<TTag> = TagToElementAndId<TTag> extends { element: infer TElement; id: infer TId }
? Record<(TId extends string ? TId : never) | 'root', TElement>
: never;
type Child = HTMLElement | string | Record<string, HTMLElement>;
type Children = []
| [Child]
| [Child, Child]
| [Child, Child, Child]
| [Child, Child, Child, Child]
| [Child, Child, Child, Child, Child]
| [Child, Child, Child, Child, Child, Child]
| [Child, Child, Child, Child, Child, Child, Child]
| [Child, Child, Child, Child, Child, Child, Child, Child]
| [Child, Child, Child, Child, Child, Child, Child, Child, Child]
| [Child, Child, Child, Child, Child, Child, Child, Child, Child, Child]
| [Child, Child, Child, Child, Child, Child, Child, Child, Child, Child, Child]
| [Child, Child, Child, Child, Child, Child, Child, Child, Child, Child, Child, Child]
| [Child, Child, Child, Child, Child, Child, Child, Child, Child, Child, Child, Child, Child]
| [Child, Child, Child, Child, Child, Child, Child, Child, Child, Child, Child, Child, Child, Child]
| [Child, Child, Child, Child, Child, Child, Child, Child, Child, Child, Child, Child, Child, Child, Child]
| [Child, Child, Child, Child, Child, Child, Child, Child, Child, Child, Child, Child, Child, Child, Child, Child];
const H_REGEX = /(?<tag>[\w\-]+)?(?:#(?<id>[\w\-]+))?(?<class>(?:\.(?:[\w\-]+))*)(?:@(?<name>(?:[\w\_])+))?/;
/**
* A helper function to create nested dom nodes.
*
*
* ```ts
* private readonly htmlElements = h('div.code-view', [
* h('div.title', { $: 'title' }),
* const elements = h('div.code-view', [
* h('div.title@title'),
* h('div.container', [
* h('div.gutter', { $: 'gutterDiv' }),
* h('div', { $: 'editor' }),
* h('div.gutter@gutterDiv'),
* h('div@editor'),
* ]),
* ]);
* private readonly editor = createEditor(this.htmlElements.editor);
* const editor = createEditor(elements.editor);
* ```
*/
export function h<TTag extends string>(
tag: TTag
): (Record<'root', TagToElement<TTag>>) extends infer Y ? { [TKey in keyof Y]: Y[TKey] } : never;
export function h<TTag extends string, TId extends string>(
tag: TTag,
attributes: { $: TId } & Partial<ElementAttributes<TagToElement<TTag>>>
): Record<TId | 'root', TagToElement<TTag>>;
export function h<TTag extends string, T extends (HTMLElement | string | Record<string, HTMLElement>)[]>(
tag: TTag,
children: T
): (ArrayToObj<T> & Record<'root', TagToElement<TTag>>) extends infer Y ? { [TKey in keyof Y]: Y[TKey] } : never;
export function h<TTag extends string>(tag: TTag, attributes: Partial<ElementAttributes<TagToElement<TTag>>>): Record<'root', TagToElement<TTag>>;
export function h<TTag extends string, TId extends string, T extends (HTMLElement | string | Record<string, HTMLElement>)[]>(
tag: TTag,
attributes: { $: TId } & Partial<ElementAttributes<TagToElement<TTag>>>,
children: T
): (ArrayToObj<T> & Record<TId, TagToElement<TTag>>) extends infer Y ? { [TKey in keyof Y]: Y[TKey] } : never;
export function h<TTag extends string>
(tag: TTag):
TagToRecord<TTag> extends infer Y ? { [TKey in keyof Y]: Y[TKey] } : never;
export function h<TTag extends string, T extends Children>
(tag: TTag, children: T):
(ArrayToObj<T> & TagToRecord<TTag>) extends infer Y ? { [TKey in keyof Y]: Y[TKey] } : never;
export function h<TTag extends string>
(tag: TTag, attributes: Partial<ElementAttributes<TagToElement<TTag>>>):
TagToRecord<TTag> extends infer Y ? { [TKey in keyof Y]: Y[TKey] } : never;
export function h<TTag extends string, T extends Children>
(tag: TTag, attributes: Partial<ElementAttributes<TagToElement<TTag>>>, children: T):
(ArrayToObj<T> & TagToRecord<TTag>) extends infer Y ? { [TKey in keyof Y]: Y[TKey] } : never;
export function h(tag: string, ...args: [] | [attributes: { $: string } & Partial<ElementAttributes<HTMLElement>> | Record<string, any>, children?: any[]] | [children: any[]]): Record<string, HTMLElement> {
let attributes: { $?: string } & Partial<ElementAttributes<HTMLElement>>;
let children: (Record<string, HTMLElement> | HTMLElement)[] | undefined;
@@ -1800,25 +1825,29 @@ export function h(tag: string, ...args: [] | [attributes: { $: string } & Partia
children = args[1];
}
const match = SELECTOR_REGEX.exec(tag);
const match = H_REGEX.exec(tag);
if (!match) {
if (!match || !match.groups) {
throw new Error('Bad use of h');
}
const tagName = match[1] || 'div';
const tagName = match.groups['tag'] || 'div';
const el = document.createElement(tagName);
if (match[3]) {
el.id = match[3];
if (match.groups['id']) {
el.id = match.groups['id'];
}
if (match[4]) {
el.className = match[4].replace(/\./g, ' ').trim();
if (match.groups['class']) {
el.className = match.groups['class'].replace(/\./g, ' ').trim();
}
const result: Record<string, HTMLElement> = {};
if (match.groups['name']) {
result[match.groups['name']] = el;
}
if (children) {
for (const c of children) {
if (c instanceof HTMLElement) {
@@ -1833,10 +1862,6 @@ export function h(tag: string, ...args: [] | [attributes: { $: string } & Partia
}
for (const [key, value] of Object.entries(attributes)) {
if (key === '$') {
result[value] = el;
continue;
}
if (key === 'style') {
for (const [cssKey, cssValue] of Object.entries(value)) {
el.style.setProperty(