Skip to content

Commit 5293b6d

Browse files
committed
[add] Web components extending & Static MarkUp rendering
[optimize] DataSet & ARIA attributes setting [optimize] Logic, Type & Document of JSX runtime
1 parent c99a169 commit 5293b6d

File tree

7 files changed

+132
-33
lines changed

7 files changed

+132
-33
lines changed

ReadMe.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ console.log(newVNode);
5656
```tsx
5757
import { DOMRenderer } from 'dom-renderer';
5858

59-
const newVNode = new DOMRenderer().renderer(
59+
const newVNode = new DOMRenderer().render(
6060
<a href="https://idea2.app/" style={{ color: 'red' }}>
6161
idea2app
6262
</a>

package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "dom-renderer",
3-
"version": "2.0.0-rc.3",
3+
"version": "2.0.0-rc.4",
44
"license": "LGPL-3.0-or-later",
55
"author": "[email protected]",
66
"description": "A light-weight DOM Renderer supports Web components standard & TypeScript language",
@@ -59,8 +59,8 @@
5959
"prepare": "husky install",
6060
"test": "lint-staged && jest",
6161
"parcel": "tsc -p tsconfig.json && mv dist/jsx-runtime.* . && mv dist/dist/* dist/ && rm -rf dist/dist",
62-
"build": "rm -rf dist/ docs/ && typedoc source/dist/ && npm run parcel",
63-
"start": "typedoc source/ && open-cli docs/index.html",
62+
"build": "rm -rf dist/ docs/ && typedoc && npm run parcel",
63+
"start": "typedoc && open-cli docs/index.html",
6464
"prepublishOnly": "npm test && npm run build"
6565
}
6666
}

source/dist/DOMRenderer.ts

Lines changed: 41 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,32 @@
1-
import { diffKeys, DiffStatus } from 'web-utility';
1+
import {
2+
diffKeys,
3+
DiffStatus,
4+
elementTypeOf,
5+
isDOMReadOnly,
6+
templateOf,
7+
toCamelCase,
8+
toHyphenCase
9+
} from 'web-utility';
210

311
import { DataObject, VDOMNode, VNode } from './VDOM';
412

513
export class DOMRenderer {
6-
eventPattern = /^on\w+/;
14+
eventPattern = /^on[A-Z]/;
15+
ariaPattern = /^aira[A-Z]/;
716

817
protected keyOf = ({ key, text, props, selector }: VNode, index?: number) =>
918
key || props?.id || text || (selector && selector + index);
1019

1120
protected vNodeOf = (list: VNode[], key: string) =>
1221
list.find((vNode, index) => this.keyOf(vNode, index) + '' === key);
1322

23+
protected propsKeyOf = (key: string) =>
24+
key.startsWith('aria-')
25+
? toCamelCase(key)
26+
: this.eventPattern.test(key)
27+
? key.toLowerCase()
28+
: key;
29+
1430
protected updateProps<T extends DataObject>(
1531
node: T,
1632
oldProps: DataObject = {},
@@ -39,7 +55,7 @@ export class DOMRenderer {
3955
return (vNode.node = document.createTextNode(vNode.text));
4056

4157
vNode.node = vNode.tagName
42-
? document.createElement(vNode.tagName)
58+
? document.createElement(vNode.tagName, { is: vNode.is })
4359
: document.createDocumentFragment();
4460

4561
return this.patch({ tagName: vNode.tagName, node: vNode.node }, vNode)
@@ -72,17 +88,27 @@ export class DOMRenderer {
7288
}
7389

7490
patch(oldVNode: VNode, newVNode: VNode): VNode {
91+
const { tagName } = oldVNode;
92+
const isXML = templateOf(tagName) && elementTypeOf(tagName) === 'xml';
93+
7594
this.updateProps(
7695
oldVNode.node as Element,
7796
oldVNode.props,
7897
newVNode.props,
7998
(node, key) =>
8099
this.eventPattern.test(key)
81100
? (node[key.toLowerCase()] = null)
82-
: node.removeAttribute(VDOMNode.propsMap[key] || key),
83-
(node, key, value) =>
84-
(node[this.eventPattern.test(key) ? key.toLowerCase() : key] =
85-
value)
101+
: node.removeAttribute(
102+
this.ariaPattern.test(key)
103+
? toHyphenCase(key)
104+
: VDOMNode.propsMap[key] || key
105+
),
106+
(node, key, value) => {
107+
// @ts-ignore
108+
if (isXML || key.includes('-') || isDOMReadOnly(tagName, key))
109+
node.setAttribute(key, value);
110+
else node[this.propsKeyOf(key)] = value;
111+
}
86112
);
87113
this.updateProps(
88114
(oldVNode.node as HTMLElement).style,
@@ -105,4 +131,12 @@ export class DOMRenderer {
105131

106132
return this.patch(root, { ...root, children: [vNode] });
107133
}
134+
135+
renderToStaticMarkup(tree: VNode) {
136+
const { body } = document.implementation.createHTMLDocument();
137+
138+
this.render(tree, body);
139+
140+
return body.innerHTML;
141+
}
108142
}

source/dist/VDOM.ts

Lines changed: 20 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ export interface VNode {
77
text?: string;
88
selector?: string;
99
tagName?: string;
10+
is?: string;
1011
props?: DataObject;
1112
style?: DataObject;
1213
children?: VNode[];
@@ -16,18 +17,24 @@ export interface VNode {
1617
export class VDOMNode implements VNode {
1718
text?: string;
1819
tagName?: string;
20+
is?: string;
1921
props?: DataObject;
2022
style?: DataObject;
2123
children?: VNode[];
2224

23-
static selectorOf = (tagName: string, className?: string) =>
24-
tagName.toLowerCase() +
25-
(className ? `.${className.trim().replace(/\s+/, '.')}` : '');
25+
static selectorOf = (tagName: string, is?: string, className?: string) =>
26+
[
27+
tagName.toLowerCase(),
28+
className && `.${className.trim().replace(/\s+/, '.')}`,
29+
is && `[is="${is}"]`
30+
]
31+
.filter(Boolean)
32+
.join('');
2633

2734
get selector() {
28-
const { tagName, props } = this;
35+
const { tagName, is, props } = this;
2936

30-
return tagName && VDOMNode.selectorOf(tagName, props?.className);
37+
return tagName && VDOMNode.selectorOf(tagName, is, props?.className);
3138
}
3239

3340
static propsMap: DataObject = { className: 'class', htmlFor: 'for' };
@@ -46,6 +53,7 @@ export class VDOMNode implements VNode {
4653
const { tagName, attributes, style, childNodes } = node as HTMLElement;
4754

4855
this.tagName = tagName.toLowerCase();
56+
this.is = node.getAttribute('is');
4957

5058
const props = Array.from(
5159
attributes,
@@ -65,16 +73,14 @@ export class VDOMNode implements VNode {
6573
}
6674
}
6775

68-
type HTMLTags = {
69-
[tagName in keyof HTMLElementTagNameMap]: HTMLProps<
70-
HTMLElementTagNameMap[tagName]
71-
>;
72-
} & {
73-
[tagName: string]: HTMLProps<HTMLElement>;
74-
};
75-
7676
declare global {
7777
namespace JSX {
78-
interface IntrinsicElements extends HTMLTags {}
78+
type Element = VNode;
79+
80+
type IntrinsicElements = {
81+
[tagName in keyof HTMLElementTagNameMap]: HTMLProps<
82+
HTMLElementTagNameMap[tagName]
83+
>;
84+
};
7985
}
8086
}

source/jsx-runtime.ts

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,25 +8,32 @@ import { DataObject, VDOMNode, VNode } from './dist/VDOM';
88
*/
99
export function jsx(
1010
type: string | Function,
11-
{ style, children, ...props }: DataObject,
11+
{ is, style, children, ...props }: DataObject,
1212
key?: IndexKey
1313
): VNode {
1414
if (typeof type === 'function' && isHTMLElementClass(type))
1515
type = tagNameOf(type);
1616

17+
children = (
18+
children instanceof Array ? children : children && [children]
19+
)?.map(node =>
20+
node instanceof Object
21+
? node
22+
: node === 0 || node
23+
? { text: node + '' }
24+
: { text: '' }
25+
);
1726
return typeof type === 'string'
1827
? {
1928
key,
20-
selector: VDOMNode.selectorOf(type, props.className),
29+
selector: VDOMNode.selectorOf(type, is, props.className),
2130
tagName: type,
31+
is,
2232
props,
2333
style,
24-
children: (children instanceof Array
25-
? children
26-
: children && [children]
27-
)?.map(node => (typeof node === 'string' ? { text: node } : node))
34+
children
2835
}
29-
: type({ style, children, ...props });
36+
: type({ is, style, children, ...props });
3037
}
3138

3239
export const jsxs = jsx;

test/jsx-runtime.spec.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,4 +43,55 @@ describe('JSX runtime', () => {
4343

4444
expect(document.body.innerHTML).toBe('<my-tag></my-tag>');
4545
});
46+
47+
it('should render an HTML tag extended by a Web components class', () => {
48+
class MyDiv extends HTMLDivElement {}
49+
50+
customElements.define('my-div', MyDiv, { extends: 'div' });
51+
52+
renderer.render(jsx('div', { is: 'my-div' }));
53+
54+
expect(document.body.innerHTML).toBe('<div is="my-div"></div>');
55+
});
56+
57+
it('should ignore Empty values except 0', () => {
58+
renderer.render(
59+
jsx(Fragment, { children: [0, false, null, undefined, NaN] })
60+
);
61+
expect(document.body.innerHTML).toBe('0');
62+
});
63+
64+
it('should render Non-empty Primitive values', () => {
65+
renderer.render(jsx(Fragment, { children: [1, true] }));
66+
67+
expect(document.body.innerHTML).toBe('1true');
68+
});
69+
70+
it('should render DataSet & ARIA attributes', () => {
71+
renderer.render(
72+
jsx('div', { 'data-id': 'idea2app', 'aria-label': 'idea2app' })
73+
);
74+
expect(document.body.innerHTML).toBe(
75+
'<div data-id="idea2app" aria-label="idea2app"></div>'
76+
);
77+
// To Do: https://github.com/jsdom/jsdom/issues/3323
78+
79+
// renderer.render(jsx('div', { ariaLabel: 'fCC' }));
80+
81+
// expect(document.body.innerHTML).toBe('<div aria-label="fCC"></div>');
82+
});
83+
84+
it('should call Event handlers', () => {
85+
const onClick = jest.fn();
86+
87+
renderer.render(jsx('i', { onClick }));
88+
89+
document.querySelector('i')?.click();
90+
91+
expect(onClick).toBeCalledTimes(1);
92+
});
93+
94+
it('should render to a Static String', () => {
95+
expect(renderer.renderToStaticMarkup(jsx('i', {}))).toBe('<i></i>');
96+
});
4697
});

tsconfig.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@
1212
"name": "DOM Renderer",
1313
"excludeExternals": true,
1414
"excludePrivate": true,
15-
"readme": "./ReadMe.md",
15+
"readme": "ReadMe.md",
16+
"entryPoints": ["source/dist/index.ts", "source/jsx-runtime.ts"],
1617
"plugin": ["typedoc-plugin-mdn-links"]
1718
}
1819
}

0 commit comments

Comments
 (0)