Skip to content

Commit a9d4843

Browse files
committed
[add] Render methods for JSX runtime
1 parent 7bf06f4 commit a9d4843

File tree

10 files changed

+171
-21
lines changed

10 files changed

+171
-21
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,6 @@ package-lock.json
33
yarn.lock
44
.parcel-cache/
55
dist/
6+
/jsx-runtime.*
67
docs/
78
.vscode/settings.json

ReadMe.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,33 @@ const newVNode = new DOMRenderer().patch(
3636
console.log(newVNode);
3737
```
3838

39+
### TypeScript
40+
41+
#### `tsconfig.json`
42+
43+
```json
44+
{
45+
"compilerOptions": {
46+
"jsx": "react-jsx",
47+
"jsxImportSource": "dom-renderer"
48+
}
49+
}
50+
```
51+
52+
#### `index.tsx`
53+
54+
```tsx
55+
import { DOMRenderer } from 'dom-renderer';
56+
57+
const newVNode = new DOMRenderer().renderer(
58+
<a href="https://idea2.app/" style={{ color: 'red' }}>
59+
idea2app
60+
</a>
61+
);
62+
63+
console.log(newVNode);
64+
```
65+
3966
## Original
4067

4168
### Inspiration

package.json

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "dom-renderer",
3-
"version": "2.0.0-alpha.1",
3+
"version": "2.0.0-beta.6",
44
"license": "LGPL-3.0-or-later",
55
"author": "[email protected]",
66
"description": "A light-weight DOM Renderer supports Web components standard & TypeScript language",
@@ -26,6 +26,7 @@
2626
"main": "dist/index.js",
2727
"dependencies": {
2828
"@swc/helpers": "~0.4.14",
29+
"tslib": "^2.6.1",
2930
"web-utility": "^4.1.0"
3031
},
3132
"devDependencies": {
@@ -60,14 +61,17 @@
6061
},
6162
"browserslist": "> 0.5%, last 2 versions, not dead, IE 11",
6263
"targets": {
64+
"types": false,
6365
"main": {
6466
"optimize": true
6567
}
6668
},
6769
"scripts": {
6870
"prepare": "husky install",
6971
"test": "lint-staged && jest",
70-
"build": "rm -rf dist/ docs/ && typedoc source/ && parcel build",
72+
"pack-jsx": "tsc -p tsconfig.json && mv dist/jsx-runtime.* . && rm dist/*.js",
73+
"parcel": "npm run pack-jsx && parcel build",
74+
"build": "rm -rf dist/ docs/ && typedoc source/ && npm run parcel",
7175
"start": "typedoc source/ && open-cli docs/index.html",
7276
"prepublishOnly": "npm test && npm run build"
7377
}

pnpm-lock.yaml

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

source/DOMRenderer.ts

Lines changed: 53 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,15 @@
1-
import { diffKeys, DiffStatus, IndexKey } from 'web-utility';
2-
3-
export type DataObject = Record<string, any>;
4-
5-
export interface VNode {
6-
key?: IndexKey;
7-
text?: string;
8-
tagName?: string;
9-
props?: DataObject;
10-
style?: DataObject;
11-
children?: VNode[];
12-
node?: Node;
13-
}
1+
import { diffKeys, DiffStatus } from 'web-utility';
2+
3+
import { DataObject, VNode } from './VDOM';
144

155
export class DOMRenderer {
166
propsMap: DataObject = { className: 'class', htmlFor: 'for' };
177

8+
attrsMap = Object.fromEntries(
9+
Object.entries(this.propsMap).map(item => item.reverse())
10+
);
11+
eventPattern = /^on\w+/;
12+
1813
protected keyOf = ({ key, text, props }: VNode, index?: number) =>
1914
key || props?.id || text || index;
2015

@@ -25,7 +20,8 @@ export class DOMRenderer {
2520
node: T,
2621
oldProps: DataObject = {},
2722
newProps: DataObject = {},
28-
onDelete?: (node: T, key: string) => any
23+
onDelete?: (node: T, key: string) => any,
24+
onAdd?: (node: T, key: string, value: any) => any
2925
) {
3026
const { group } = diffKeys(
3127
Object.keys(oldProps),
@@ -39,7 +35,8 @@ export class DOMRenderer {
3935
...(group[DiffStatus.New] || [])
4036
])
4137
if (oldProps[key] !== newProps[key])
42-
Reflect.set(node, key, newProps[key]);
38+
if (onAdd instanceof Function) onAdd(node, key, newProps[key]);
39+
else Reflect.set(node, key, newProps[key]);
4340
}
4441

4542
protected createNode(vNode: VNode) {
@@ -91,7 +88,13 @@ export class DOMRenderer {
9188
oldVNode.node as Element,
9289
oldVNode.props,
9390
newVNode.props,
94-
(node, key) => node.removeAttribute(this.propsMap[key] || key)
91+
(node, key) =>
92+
this.eventPattern.test(key)
93+
? (node[key.toLowerCase()] = null)
94+
: node.removeAttribute(this.propsMap[key] || key),
95+
(node, key, value) =>
96+
(node[this.eventPattern.test(key) ? key.toLowerCase() : key] =
97+
value)
9598
);
9699
this.updateProps(
97100
(oldVNode.node as HTMLElement).style,
@@ -108,4 +111,38 @@ export class DOMRenderer {
108111

109112
return newVNode;
110113
}
114+
115+
toVNode = (node: Node): VNode => {
116+
if (node instanceof Text) return { node, text: node.nodeValue };
117+
118+
if (!(node instanceof Element)) return;
119+
120+
const { tagName, attributes, style, childNodes } = node as HTMLElement;
121+
122+
const vNode: VNode = { node, tagName: tagName.toLowerCase() };
123+
124+
const props = Array.from(
125+
attributes,
126+
({ name, value }) =>
127+
name !== 'style' && [this.attrsMap[name] || name, value]
128+
).filter(Boolean);
129+
130+
if (props[0]) vNode.props = Object.fromEntries(props);
131+
132+
const styles = Array.from(style, key => [key, style[key]]);
133+
134+
if (styles[0]) vNode.style = Object.fromEntries(styles);
135+
136+
const children = Array.from(childNodes, this.toVNode).filter(Boolean);
137+
138+
if (children[0]) vNode.children = children;
139+
140+
return vNode;
141+
};
142+
143+
render(vNode: VNode, node: Element = document.body) {
144+
const root = this.toVNode(node);
145+
146+
return this.patch(root, { ...root, children: [vNode] });
147+
}
111148
}

source/VDOM.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { HTMLProps, IndexKey } from 'web-utility';
2+
3+
export type DataObject = Record<string, any>;
4+
5+
export interface VNode {
6+
key?: IndexKey;
7+
text?: string;
8+
tagName?: string;
9+
props?: DataObject;
10+
style?: DataObject;
11+
children?: VNode[];
12+
node?: Node;
13+
}
14+
15+
type HTMLTags = {
16+
[tagName in keyof HTMLElementTagNameMap]: HTMLProps<
17+
HTMLElementTagNameMap[tagName]
18+
>;
19+
} & {
20+
[tagName: string]: HTMLProps<HTMLElement>;
21+
};
22+
23+
declare global {
24+
namespace JSX {
25+
interface IntrinsicElements extends HTMLTags {}
26+
}
27+
}

source/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1+
export * from './VDOM';
12
export * from './DOMRenderer';

source/jsx-runtime.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { IndexKey } from 'web-utility';
2+
3+
import { DataObject, VNode } from './VDOM';
4+
5+
/**
6+
* @see {@link https://github.com/reactjs/rfcs/blob/createlement-rfc/text/0000-create-element-changes.md}
7+
* @see {@link https://babeljs.io/docs/babel-plugin-transform-react-jsx}
8+
*/
9+
export const jsx = (
10+
type: string | Function,
11+
{ style, children, ...props }: DataObject,
12+
key?: IndexKey
13+
): VNode =>
14+
typeof type === 'string'
15+
? {
16+
key,
17+
tagName: type,
18+
props,
19+
style,
20+
children: (children instanceof Array
21+
? children
22+
: children && [children]
23+
)
24+
?.map(node =>
25+
typeof node === 'string' ? { text: node } : node
26+
)
27+
.flat(Infinity)
28+
}
29+
: type({ style, children, ...props });
30+
31+
export const jsxs = jsx;

test/DOMRenderer.spec.ts

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { DOMRenderer } from '../source';
2+
import { jsx } from '../source/jsx-runtime';
23

34
describe('DOM Renderer', () => {
45
const renderer = new DOMRenderer(),
@@ -43,10 +44,25 @@ describe('DOM Renderer', () => {
4344
expect(document.body.innerHTML).toBe(
4445
'<a href="https://idea2.app/" style="color: red;">idea2app</a>'
4546
);
46-
console.log(newNode, root);
47-
4847
renderer.patch(newNode, root);
4948

5049
expect(document.body.innerHTML).toBe('');
5150
});
51+
52+
it('should transfer a DOM node to a Virtual DOM node', () => {
53+
expect(renderer.toVNode(document.body)).toEqual(root);
54+
});
55+
56+
it('should render JSX to DOM', () => {
57+
renderer.render(
58+
jsx('a', {
59+
href: 'https://idea2.app/',
60+
style: { color: 'red' },
61+
children: ['idea2app']
62+
})
63+
);
64+
expect(document.body.innerHTML).toBe(
65+
'<a href="https://idea2.app/" style="color: red;">idea2app</a>'
66+
);
67+
});
5268
});

tsconfig.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
{
22
"compilerOptions": {
33
"esModuleInterop": true,
4+
"importHelpers": true,
45
"lib": ["ES2023", "DOM"],
5-
"skipLibCheck": true
6+
"skipLibCheck": true,
7+
"declaration": true,
8+
"outDir": "dist"
69
},
710
"include": ["source/*.ts"],
811
"typedocOptions": {

0 commit comments

Comments
 (0)