export type Attributes = Omit<
Partial,
'style'
> & {
style?: string;
};
export const jsxAsString = (
tag: T,
props: Attributes | null,
...children: (string | HTMLElement)[]
): string => {
const attributes = props
? Object.entries(props)
.map(([key, value]) => {
if (value === true) return key;
if (value === false || value === null || value === undefined)
return '';
// Convert camelCase to kebab-case for attributes
const attrName = key.replace(/([A-Z])/g, '-$1').toLowerCase();
return `${attrName}="${String(value).replace(/"/g, '"')}"`;
})
.filter(Boolean)
.join(' ')
: '';
const attributesString = attributes ? ` ${attributes}` : '';
const childrenString = children.join('');
// Self-closing tags
const selfClosing = [
'area',
'base',
'br',
'col',
'embed',
'hr',
'img',
'input',
'link',
'meta',
'param',
'source',
'track',
'wbr',
];
if (selfClosing.includes(tag)) {
return `<${tag}${attributesString} />`;
}
return `<${tag}${attributesString}>${childrenString}${tag}>`;
};
export const jsxAsElement = (
tag: T,
props: Attributes | null,
...children: (string | HTMLElement)[]
) => {
const element = document.createElement(tag);
Object.entries(props || {}).forEach(([key, value]) => {
const castKey = key as keyof HTMLElementTagNameMap[T];
if (typeof castKey === 'string' && castKey.includes('data-')) {
element.dataset[castKey.replace('data-', '')] = value as string;
} else if (
typeof castKey === 'string' &&
![
'className',
'style',
'onload',
'onerror',
'onclick',
'onchange',
].includes(castKey)
) {
// Use setAttributeNS for custom elements to preserve unknown attributes (and maintain casing)
element.setAttributeNS(null, key, value as string);
} else {
// Since we're replacing the style attribute this complains heaps
// @ts-ignore
element[castKey] = value;
}
});
if (Array.isArray(children)) {
children.forEach((child) => {
element.append(child);
});
} else {
element.append(children);
}
return element;
};