The vanilla alternative for writing JSX-based React applications
git clone git://soophie.de/figure
7
import figure from '...';9
// initialize figure10
const { dict } = figure({ createElement });10
const { dict } = figure(React.createElement);12
// add your components to a bundle13
const ElementBundle = {14
button: Button,15
};17
// add your bundles to the dictionary12
// add your component to the dictionary13
const html = dict({19
el: ElementBundle,14
el: {15
button: Button,16
},17
});19
function App() {
40
`;41
}46
render(createElement(App), document.querySelector('#root'));43
// render your App component as usual44
const root = document.querySelector('#root');45
ReactDOM.render(React.createElement(App), root);46
```
1
import { Options, Props, ReactElement, ReactFunction, Refs, Slices, Values, Dict } from './types.ts';1
import {2
Dict,3
Figure,4
Props,5
ReactElement,6
HtmlFunction,7
ReactFunction,8
CreateFunction,9
Refs,10
Slices,11
Values,12
} from './types.ts';3
export default function figure({ createElement }: Options) {14
/**15
* Initializes the figure utility.16
* @param {CreateFunction} create - The React createElement function.17
* @return {Figure} The util functions collected in an object.18
*/19
export default function figure(create: CreateFunction): Figure {21
// the parser for interpreting HTML22
const parser = new DOMParser();
28
* @param {Dict} dict - The dictionary for resolving React components.29
* @return {Function} The function for rendering HTML.30
*/15
function dict(dict?: Dict): (slices: Slices, ...values: Values) => ReactElement[] {31
function dict(dict?: Dict): HtmlFunction {33
/**34
* Converts the template literal HTML syntax into React elements.
38
*/39
function html(slices: Slices, ...values: Values): ReactElement[] {40
const [html, refs] = compose(slices, values);25
let dom;41
try {27
dom = parser.parseFromString(html, 'text/html');42
const dom = parser.parseFromString(html, 'text/html');43
// collect all nodes from head and body44
const nodes = [...dom.head.childNodes, ...dom.body.childNodes];45
return nodes.map((node) => render(node, refs, dict ?? {}));46
}47
catch (error) {48
console.error(error);49
throw 'Invalid DOM structure!';50
}33
// collect all nodes from head and body34
const nodes = [...dom.head.childNodes, ...dom.body.childNodes];35
return nodes.map((node) => render(node, refs, dict ?? {}));51
}53
return html;54
}56
/**42
* Creates a dynamic component with its own state.43
* Should be used if the component is statefull (contains hooks).44
*45
* @param {ReactFunction} element - The string containing references46
* @param {Props} props - The component properties47
* @param {ReactElement[]} children - The child elements48
* @return {ReactElement} The created React component49
*/50
function dyn(element: ReactFunction, props?: Props, children?: ReactElement[]): ReactElement {51
return createElement(element, props, children);52
}54
/**57
* Joins the template literal slices together and replaces the values with references.58
* The values are being mapped to there corresponding references and with the populated59
* HTML string returned.
72
for (let i = 0; i < slices.length; i++) {73
slice += slices[i];74
if (values[i] != null) {75
// create unique reference76
const uid = `$fig-${count++}`;77
refs[uid] = values[i];78
slice += uid ?? '';
91
function feed(slice: string, refs: Refs): ReactElement[] {92
const expr = /\$fig-\d+/g;93
const elements: ReactElement[] = [];91
let match = null;94
let match: RegExpExecArray | null = null;95
let last = 0;96
while ((match = expr.exec(slice)) !== null) {97
const index = match.index;98
const uid = match[0];96
elements.push(slice.substring(last, index));97
let value = refs[uid];98
if (value instanceof Function) {99
value = value();99
const before = slice.substring(last, index);100
// ignore empty strings101
if (before.length > 0) {102
elements.push(before);103
}104
const value = refs[uid];105
// ignore empty values106
if (value) {107
elements.push(value);108
}101
elements.push(value);109
last = index + uid.length;110
}104
elements.push(slice.substring(last));111
const after = slice.substring(last);112
// ignore empty strings113
if (after.length > 0) {114
elements.push(after);115
}116
return elements;117
}
147
// ignore empty attribute values148
continue;149
}139
let isDynamic = false;140
for (const ref in refs) {141
if (slice === ref) {142
let match = null;143
if ((match = /^on:(\w+)$/.exec(key)) !== null) {144
// add event to props145
const event = match[1];146
props[`on${event.substring(0, 1).toUpperCase()}${event.substring(1)}`] = refs[ref];147
}148
else {149
// add attribute to props150
props[key] = refs[ref];151
}152
isDynamic = true;153
break;154
}155
}156
if (!isDynamic) {157
if (key === 'style') {158
// convert style attribute into object format159
const styles: Record<string, string> = {};160
for (const item of slice.split(';')) {161
if (item.trim().length === 0) {162
break;163
}164
let [key, value] = item.split(':');165
key = key.trim();166
let match = null;167
if ((match = /-(\w)/.exec(key)) !== null) {168
const char = match[1];169
key = key.replace(`-${char}`, char.toUpperCase());170
}171
value = value.trim();172
styles[key] = feed(value, refs).join('');173
}174
props[key] = styles;175
}176
else {177
props[key] = feed(slice, refs).join('');178
}150
const values = feed(slice, refs);151
const value = values.length == 1 ? values[0] : values;152
let attr = key;153
const match = /^(\w+):(\w+)$/.exec(attr);154
if (match) {155
// camel case attribute name156
const [, pre, name] = match;157
attr = `${pre}${name.substring(0, 1).toUpperCase()}${name.substring(1)}`;158
}159
props[attr] = value instanceof Array ? value.join('') : value;160
}161
const children: ReactElement[] = [];162
// recursively render all child nodes
164
const domain = tag.split(':');165
// look up tag name in dictionary166
// deno-lint-ignore no-explicit-any187
const component: ReactFunction | undefined = domain.reduce((dict: any, level) => {188
return dict?.[level];167
const component: ReactFunction | null = domain.reduce((dict: any, level) => {168
return dict && dict[level] ? dict[level] : null;169
}, dict);170
// use React component or tag name191
return [createElement(component ?? tag, props, ...children)];171
return [create(component ?? tag, props, ...children)];172
}194
return { dict, dyn };174
return { dict, dyn: create };175
}
1
export type ReactElement = unknown;2
export type ReactFunction = (props: unknown) => ReactElement;3
export type CreateElement = (element: ReactFunction | string, props?: Props, ...children: ReactElement[]) => ReactElement;5
export type Options = { createElement: CreateElement };3
export type CreateFunction = (element: ReactFunction | string, props?: Props, ...children: ReactElement[]) => ReactElement;4
export type HtmlFunction = (slices: Slices, ...values: Values) => ReactElement[];5
export type Figure = { dict: (dict?: Dict) => HtmlFunction, dyn: CreateFunction };6
export type Refs = Record<string, unknown>;7
export type Props = Record<string, unknown>;8
export type Slices = TemplateStringsArray;