Skip to content

Commit

Permalink
[add] Render methods for JSX runtime
Browse files Browse the repository at this point in the history
  • Loading branch information
TechQuery committed Aug 1, 2023
1 parent 7bf06f4 commit a9d4843
Show file tree
Hide file tree
Showing 10 changed files with 171 additions and 21 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@ package-lock.json
yarn.lock
.parcel-cache/
dist/
/jsx-runtime.*
docs/
.vscode/settings.json
27 changes: 27 additions & 0 deletions ReadMe.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,33 @@ const newVNode = new DOMRenderer().patch(
console.log(newVNode);
```

### TypeScript

#### `tsconfig.json`

```json
{
"compilerOptions": {
"jsx": "react-jsx",
"jsxImportSource": "dom-renderer"
}
}
```

#### `index.tsx`

```tsx
import { DOMRenderer } from 'dom-renderer';

const newVNode = new DOMRenderer().renderer(
<a href="https://idea2.app/" style={{ color: 'red' }}>
idea2app
</a>
);

console.log(newVNode);
```

## Original

### Inspiration
Expand Down
8 changes: 6 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "dom-renderer",
"version": "2.0.0-alpha.1",
"version": "2.0.0-beta.6",
"license": "LGPL-3.0-or-later",
"author": "shiy2008@gmail.com",
"description": "A light-weight DOM Renderer supports Web components standard & TypeScript language",
Expand All @@ -26,6 +26,7 @@
"main": "dist/index.js",
"dependencies": {
"@swc/helpers": "~0.4.14",
"tslib": "^2.6.1",
"web-utility": "^4.1.0"
},
"devDependencies": {
Expand Down Expand Up @@ -60,14 +61,17 @@
},
"browserslist": "> 0.5%, last 2 versions, not dead, IE 11",
"targets": {
"types": false,
"main": {
"optimize": true
}
},
"scripts": {
"prepare": "husky install",
"test": "lint-staged && jest",
"build": "rm -rf dist/ docs/ && typedoc source/ && parcel build",
"pack-jsx": "tsc -p tsconfig.json && mv dist/jsx-runtime.* . && rm dist/*.js",
"parcel": "npm run pack-jsx && parcel build",
"build": "rm -rf dist/ docs/ && typedoc source/ && npm run parcel",
"start": "typedoc source/ && open-cli docs/index.html",
"prepublishOnly": "npm test && npm run build"
}
Expand Down
3 changes: 3 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

69 changes: 53 additions & 16 deletions source/DOMRenderer.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,15 @@
import { diffKeys, DiffStatus, IndexKey } from 'web-utility';

export type DataObject = Record<string, any>;

export interface VNode {
key?: IndexKey;
text?: string;
tagName?: string;
props?: DataObject;
style?: DataObject;
children?: VNode[];
node?: Node;
}
import { diffKeys, DiffStatus } from 'web-utility';

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

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

attrsMap = Object.fromEntries(
Object.entries(this.propsMap).map(item => item.reverse())
);
eventPattern = /^on\w+/;

protected keyOf = ({ key, text, props }: VNode, index?: number) =>
key || props?.id || text || index;

Expand All @@ -25,7 +20,8 @@ export class DOMRenderer {
node: T,
oldProps: DataObject = {},
newProps: DataObject = {},
onDelete?: (node: T, key: string) => any
onDelete?: (node: T, key: string) => any,
onAdd?: (node: T, key: string, value: any) => any
) {
const { group } = diffKeys(
Object.keys(oldProps),
Expand All @@ -39,7 +35,8 @@ export class DOMRenderer {
...(group[DiffStatus.New] || [])
])
if (oldProps[key] !== newProps[key])
Reflect.set(node, key, newProps[key]);
if (onAdd instanceof Function) onAdd(node, key, newProps[key]);
else Reflect.set(node, key, newProps[key]);
}

protected createNode(vNode: VNode) {
Expand Down Expand Up @@ -91,7 +88,13 @@ export class DOMRenderer {
oldVNode.node as Element,
oldVNode.props,
newVNode.props,
(node, key) => node.removeAttribute(this.propsMap[key] || key)
(node, key) =>
this.eventPattern.test(key)
? (node[key.toLowerCase()] = null)
: node.removeAttribute(this.propsMap[key] || key),
(node, key, value) =>
(node[this.eventPattern.test(key) ? key.toLowerCase() : key] =
value)
);
this.updateProps(
(oldVNode.node as HTMLElement).style,
Expand All @@ -108,4 +111,38 @@ export class DOMRenderer {

return newVNode;
}

toVNode = (node: Node): VNode => {
if (node instanceof Text) return { node, text: node.nodeValue };

if (!(node instanceof Element)) return;

const { tagName, attributes, style, childNodes } = node as HTMLElement;

const vNode: VNode = { node, tagName: tagName.toLowerCase() };

const props = Array.from(
attributes,
({ name, value }) =>
name !== 'style' && [this.attrsMap[name] || name, value]
).filter(Boolean);

if (props[0]) vNode.props = Object.fromEntries(props);

const styles = Array.from(style, key => [key, style[key]]);

if (styles[0]) vNode.style = Object.fromEntries(styles);

const children = Array.from(childNodes, this.toVNode).filter(Boolean);

if (children[0]) vNode.children = children;

return vNode;
};

render(vNode: VNode, node: Element = document.body) {
const root = this.toVNode(node);

return this.patch(root, { ...root, children: [vNode] });
}
}
27 changes: 27 additions & 0 deletions source/VDOM.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { HTMLProps, IndexKey } from 'web-utility';

export type DataObject = Record<string, any>;

export interface VNode {
key?: IndexKey;
text?: string;
tagName?: string;
props?: DataObject;
style?: DataObject;
children?: VNode[];
node?: Node;
}

type HTMLTags = {
[tagName in keyof HTMLElementTagNameMap]: HTMLProps<
HTMLElementTagNameMap[tagName]
>;
} & {
[tagName: string]: HTMLProps<HTMLElement>;
};

declare global {
namespace JSX {
interface IntrinsicElements extends HTMLTags {}
}
}
1 change: 1 addition & 0 deletions source/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from './VDOM';
export * from './DOMRenderer';
31 changes: 31 additions & 0 deletions source/jsx-runtime.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { IndexKey } from 'web-utility';

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

/**
* @see {@link https://github.com/reactjs/rfcs/blob/createlement-rfc/text/0000-create-element-changes.md}
* @see {@link https://babeljs.io/docs/babel-plugin-transform-react-jsx}
*/
export const jsx = (
type: string | Function,
{ style, children, ...props }: DataObject,
key?: IndexKey
): VNode =>
typeof type === 'string'
? {
key,
tagName: type,
props,
style,
children: (children instanceof Array
? children
: children && [children]
)
?.map(node =>
typeof node === 'string' ? { text: node } : node
)
.flat(Infinity)
}
: type({ style, children, ...props });

export const jsxs = jsx;
20 changes: 18 additions & 2 deletions test/DOMRenderer.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { DOMRenderer } from '../source';
import { jsx } from '../source/jsx-runtime';

describe('DOM Renderer', () => {
const renderer = new DOMRenderer(),
Expand Down Expand Up @@ -43,10 +44,25 @@ describe('DOM Renderer', () => {
expect(document.body.innerHTML).toBe(
'<a href="https://idea2.app/" style="color: red;">idea2app</a>'
);
console.log(newNode, root);

renderer.patch(newNode, root);

expect(document.body.innerHTML).toBe('');
});

it('should transfer a DOM node to a Virtual DOM node', () => {
expect(renderer.toVNode(document.body)).toEqual(root);
});

it('should render JSX to DOM', () => {
renderer.render(
jsx('a', {
href: 'https://idea2.app/',
style: { color: 'red' },
children: ['idea2app']
})
);
expect(document.body.innerHTML).toBe(
'<a href="https://idea2.app/" style="color: red;">idea2app</a>'
);
});
});
5 changes: 4 additions & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
{
"compilerOptions": {
"esModuleInterop": true,
"importHelpers": true,
"lib": ["ES2023", "DOM"],
"skipLibCheck": true
"skipLibCheck": true,
"declaration": true,
"outDir": "dist"
},
"include": ["source/*.ts"],
"typedocOptions": {
Expand Down

0 comments on commit a9d4843

Please sign in to comment.