Custom JSX Factory Function
Question
We usually use jsx in react projects. When we get used to the syntax of jsx, we may not be able to do without jsx. What if we want to use jsx in a native js project instead of react?
Solution
React officially has released a new jsx transform, stripped of jsx-runtime to parse jsx separately, can use jsx without react, and officially cooperate with babel and typescript, use babel or ts configuration to parse jsx, both are provided Concise configuration scheme. Or, we can also customize a jsx parsing function.
Option 1: Babel configuration
- Install the dependency package first
npm update @babel/core @babel/preset-react
- Then configure the
babel.config.json
file
{
"presets": [
["@babel/preset-react", {
"runtime": "automatic"
}]
]
}
Option 2: typescript configuration
To use jsx in ts, use tsx. Typescript 4.1 supports the tsconfig.json
configuration of React 17's jsx and jsxs factory functions
- Use
"jsx":"react-jsxdev"
in development environment - Use
"jsx":"react-jsx"
in production environment for example:
// ./src/tsconfig.json
{
"compilerOptions": {
"module": "esnext",
"target": "es2015",
"jsx": "react-jsx",
"strict": true
},
"include": [
"./**/*"
]
}
Option 3: Customize the tsx factory function
Customize a set of jsx parsing factory functions to understand the parsing process of jsx. The following demonstrates a ts version of the tsx parsing function.
- First define a
jsxFactory.ts
to define and export factory functions
// --- jsxFactory.ts ---
/* https://gist.github.com/borestad/eac42120613bc67a3714f115e8b485a7
* Custom jsx parser
* See: tsconfig.json
*
* {
* "jsx": "react",
* "jsxFactory": "h",
* "lib": [
* "es2017",
* "dom",
* "dom.iterable"
* ]
* }
*
*/
interface entityMapData {
[key: string]: string;
}
export const entityMap: entityMapData = {
"&": "amp",
"<": "lt",
">": "gt",
'"': "quot",
"'": "#39",
"/": "#x2F",
};
export const escapeHtml = (str: object[] | string) =>
String(str).replace(/[&<>"'\/\\]/g, (s) => `&${entityMap[s]};`);
// To keep some consistency with React DOM, lets use a mapper
// https://reactjs.org/docs/dom-elements.html
export const AttributeMapper = (val: string) =>
({
tabIndex: "tabindex",
className: "class",
readOnly: "readonly",
}[val] || val);
// tslint:disable-next-line:no-default-export
export function DOMcreateElement(
tag: Function | string,
attrs?: { [key: string]: any },
...children: (HTMLElement | string)[]
): HTMLElement {
attrs = attrs || {};
const stack: any[] = [...children];
// Support for components(ish)
if (typeof tag === "function") {
attrs.children = stack;
return tag(attrs);
}
const elm = document.createElement(tag);
// Add attributes
for (let [name, val] of Object.entries(attrs)) {
name = escapeHtml(AttributeMapper(name));
if (name.startsWith("on") && name.toLowerCase() in window) {
elm.addEventListener(name.toLowerCase().substr(2), val);
} else if (name === "ref") {
val(elm);
} else if (name === "style") {
Object.assign(elm.style, val);
} else if (val === true) {
elm.setAttribute(name, name);
} else if (val !== false && val != null) {
elm.setAttribute(name, escapeHtml(val));
} else if (val === false) {
elm.removeAttribute(name);
}
}
// Append children
while (stack.length) {
const child = stack.shift();
// Is child a leaf?
if (!Array.isArray(child)) {
elm.appendChild(
(child as HTMLElement).nodeType == null
? document.createTextNode(child.toString())
: child
);
} else {
stack.push(...child);
}
}
return elm;
}
export const DOMcreateFragment = (
attrs?: { [key: string]: any },
...children: (HTMLElement | string)[]
): (HTMLElement | string)[] => {
return children;
};
- Supporting factory function
d.ts
declaration file
// --- jsxFactory.d.ts ---
declare namespace JSX {
type Element = string;
interface IntrinsicElements {
[eleName: string]: any;
}
}
- Then add jsx configuration in
tsconfig.json
{
"compilerOptions":{
// ...Other configuration
"jsx": "preserve",
"jsxFactory": "DOMcreateElement",
"jsxFragmentFactory": "DOMcreateFragment",
}
}
For example, the following reference
{
"compilerOptions": {
"rootDir": "src",
"outDir": "lib",
"target": "ESNext",
"useDefineForClassFields": true,
"module": "ESNext",
"lib": ["ESNext", "dom", "dom.iterable"],
"moduleResolution": "Node",
"strict": true,
"sourceMap": true,
"resolveJsonModule": true,
"esModuleInterop": true,
"noEmit": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitReturns": true,
"declaration": true,
"declarationDir": "./lib",
"declarationMap": true,
"baseUrl": "./",
"jsx": "preserve",
"jsxFactory": "DOMcreateElement",
"jsxFragmentFactory": "DOMcreateFragment",
"allowJs": true
},
"include": ["./src"]
}
- Generally speaking, this can be used, just introduce
DOMcreateElement
in each file ending with.tsx
, but if you use esbuild, you can also automatically injectDOMcreateElement
andDOMcreateFragment
in the esbuild configuration
A reference esbuild configuration. In the following example, @/helper/jsxFactory
is the directory where jsxFactory.ts
is located
esbuild: {
jsxFactory: "DOMcreateElement",
jsxFragment: "DOMcreateFragment",
jsxInject: `import { DOMcreateElement, DOMcreateFragment } from '@/helper/jsxFactory';`,
}
Option 4: Concise jsx factory function
The simple version of jsx can be customized and extended according to this simple version
Version one
const appendChild = (parent, child) => {
if (Array.isArray(child))
child.forEach((nestedChild) => appendChild(parent, nestedChild));
else
parent.appendChild(child.nodeType ? child : document.createTextNode(child));
};
export const DOMcreateElement = (tag, props, ...children) => {
if (typeof tag === 'function') return tag(props, children);
const element = document.createElement(tag);
Object.entries(props || {}).forEach(([name, value]) => {
if (name.startsWith('on') && name.toLowerCase() in window) {
element.addEventListener(name.toLowerCase().substr(2), value);
} else {
element[name] = value;
// element.setAttribute(name, value.toString());
}
});
children.forEach((child) => {
appendChild(element, child);
});
return element;
};
export const DOMcreateFragment = (props, ...children) => {
return children;
};
Version two
/**
* A helper function that ensures we won't work with null values
* @param val
* @param fallback
*/
function nonNull(val, fallback) {
return Boolean(val) ? val : fallback;
}
/**
* How do we handle children. Children can either be:
* 1. Calls to DOMcreateElement, return a Node
* 2. Text content, returns a Text
* @param children
*/
function DOMparseChildren(children) {
return children.map((child) => {
if (typeof child === 'string') {
return document.createTextNode(child);
}
return child;
});
}
/**
* How do we handle regular nodes.
* 1. We create an element
* 2. We apply all properties from JSX to this DOM node
* 3. If available,we append all children.
* @param element
* @param properties
* @param children
*/
function DOMparseNode(element, properties, children) {
const el = document.createElement(element);
Object.keys(nonNull(properties, {})).forEach((key) => {
el[key] = properties[key];
});
DOMparseChildren(children).forEach((child) => {
el.appendChild(child);
});
return el;
}
/**
* Our entry function.
* 1. Is the element a function,than it's a functional component.
* We call this function (pass props and children of course)
* and return the result.We expect a return value of type Node
* 2. If the element is a string, we parse a regular node
* @param element
* @param properties
* @param children
*/
export function DOMcreateElement(element, properties, ...children) {
if (typeof element === 'function') {
return element({
...nonNull(properties, {}),
children,
});
}
return DOMparseNode(element, properties, children);
}
Comments