NEWS: Welcome to my new homepage! <3

feat: Optimized render function - figure - Unnamed repository; edit this file 'description' to name the repository.

figure

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README | LICENSE

commit 84c42a421c58de3cc26f9a55cf8ea6a19bd71b81
parent ae669c8f9b17a7e1b7cc975ec599b48ff1c53b74
Author: typable <contact@typable.dev>
Date:   Fri,  7 Apr 2023 17:03:46 +0200

feat: Optimized render function

Diffstat:
MREADME.md | 17++++++++---------
Mlib.ts | 124+++++++++++++++++++++++++++++++++----------------------------------------------
Mtypes.ts | 6+++---
3 files changed, 63 insertions(+), 84 deletions(-)

diff --git a/README.md b/README.md @@ -7,16 +7,13 @@ Reactive template literals for React import figure from '...'; // initialize figure -const { dict } = figure({ createElement }); +const { dict } = figure(React.createElement); -// add your components to a bundle -const ElementBundle = { - button: Button, -}; - -// add your bundles to the dictionary +// add your component to the dictionary const html = dict({ - el: ElementBundle, + el: { + button: Button, + }, }); function App() { @@ -43,5 +40,7 @@ function Button() { `; } -render(createElement(App), document.querySelector('#root')); +// render your App component as usual +const root = document.querySelector('#root'); +ReactDOM.render(React.createElement(App), root); ``` diff --git a/lib.ts b/lib.ts @@ -1,6 +1,22 @@ -import { Options, Props, ReactElement, ReactFunction, Refs, Slices, Values, Dict } from './types.ts'; +import { + Dict, + Figure, + Props, + ReactElement, + HtmlFunction, + ReactFunction, + CreateFunction, + Refs, + Slices, + Values, +} from './types.ts'; -export default function figure({ createElement }: Options) { +/** + * Initializes the figure utility. + * @param {CreateFunction} create - The React createElement function. + * @return {Figure} The util functions collected in an object. + */ +export default function figure(create: CreateFunction): Figure { // the parser for interpreting HTML const parser = new DOMParser(); @@ -12,7 +28,7 @@ export default function figure({ createElement }: Options) { * @param {Dict} dict - The dictionary for resolving React components. * @return {Function} The function for rendering HTML. */ - function dict(dict?: Dict): (slices: Slices, ...values: Values) => ReactElement[] { + function dict(dict?: Dict): HtmlFunction { /** * Converts the template literal HTML syntax into React elements. @@ -22,36 +38,22 @@ export default function figure({ createElement }: Options) { */ function html(slices: Slices, ...values: Values): ReactElement[] { const [html, refs] = compose(slices, values); - let dom; try { - dom = parser.parseFromString(html, 'text/html'); + const dom = parser.parseFromString(html, 'text/html'); + // collect all nodes from head and body + const nodes = [...dom.head.childNodes, ...dom.body.childNodes]; + return nodes.map((node) => render(node, refs, dict ?? {})); } catch (error) { console.error(error); throw 'Invalid DOM structure!'; } - // collect all nodes from head and body - const nodes = [...dom.head.childNodes, ...dom.body.childNodes]; - return nodes.map((node) => render(node, refs, dict ?? {})); } return html; } /** - * Creates a dynamic component with its own state. - * Should be used if the component is statefull (contains hooks). - * - * @param {ReactFunction} element - The string containing references - * @param {Props} props - The component properties - * @param {ReactElement[]} children - The child elements - * @return {ReactElement} The created React component - */ - function dyn(element: ReactFunction, props?: Props, children?: ReactElement[]): ReactElement { - return createElement(element, props, children); - } - - /** * Joins the template literal slices together and replaces the values with references. * The values are being mapped to there corresponding references and with the populated * HTML string returned. @@ -70,6 +72,7 @@ export default function figure({ createElement }: Options) { for (let i = 0; i < slices.length; i++) { slice += slices[i]; if (values[i] != null) { + // create unique reference const uid = `$fig-${count++}`; refs[uid] = values[i]; slice += uid ?? ''; @@ -88,20 +91,28 @@ export default function figure({ createElement }: Options) { function feed(slice: string, refs: Refs): ReactElement[] { const expr = /\$fig-\d+/g; const elements: ReactElement[] = []; - let match = null; + let match: RegExpExecArray | null = null; let last = 0; while ((match = expr.exec(slice)) !== null) { const index = match.index; const uid = match[0]; - elements.push(slice.substring(last, index)); - let value = refs[uid]; - if (value instanceof Function) { - value = value(); + const before = slice.substring(last, index); + // ignore empty strings + if (before.length > 0) { + elements.push(before); + } + const value = refs[uid]; + // ignore empty values + if (value) { + elements.push(value); } - elements.push(value); last = index + uid.length; } - elements.push(slice.substring(last)); + const after = slice.substring(last); + // ignore empty strings + if (after.length > 0) { + elements.push(after); + } return elements; } @@ -136,47 +147,16 @@ export default function figure({ createElement }: Options) { // ignore empty attribute values continue; } - let isDynamic = false; - for (const ref in refs) { - if (slice === ref) { - let match = null; - if ((match = /^on:(\w+)$/.exec(key)) !== null) { - // add event to props - const event = match[1]; - props[`on${event.substring(0, 1).toUpperCase()}${event.substring(1)}`] = refs[ref]; - } - else { - // add attribute to props - props[key] = refs[ref]; - } - isDynamic = true; - break; - } - } - if (!isDynamic) { - if (key === 'style') { - // convert style attribute into object format - const styles: Record<string, string> = {}; - for (const item of slice.split(';')) { - if (item.trim().length === 0) { - break; - } - let [key, value] = item.split(':'); - key = key.trim(); - let match = null; - if ((match = /-(\w)/.exec(key)) !== null) { - const char = match[1]; - key = key.replace(`-${char}`, char.toUpperCase()); - } - value = value.trim(); - styles[key] = feed(value, refs).join(''); - } - props[key] = styles; - } - else { - props[key] = feed(slice, refs).join(''); - } + const values = feed(slice, refs); + const value = values.length == 1 ? values[0] : values; + let attr = key; + const match = /^(\w+):(\w+)$/.exec(attr); + if (match) { + // camel case attribute name + const [, pre, name] = match; + attr = `${pre}${name.substring(0, 1).toUpperCase()}${name.substring(1)}`; } + props[attr] = value instanceof Array ? value.join('') : value; } const children: ReactElement[] = []; // recursively render all child nodes @@ -184,12 +164,12 @@ export default function figure({ createElement }: Options) { const domain = tag.split(':'); // look up tag name in dictionary // deno-lint-ignore no-explicit-any - const component: ReactFunction | undefined = domain.reduce((dict: any, level) => { - return dict?.[level]; + const component: ReactFunction | null = domain.reduce((dict: any, level) => { + return dict && dict[level] ? dict[level] : null; }, dict); // use React component or tag name - return [createElement(component ?? tag, props, ...children)]; + return [create(component ?? tag, props, ...children)]; } - return { dict, dyn }; + return { dict, dyn: create }; } diff --git a/types.ts b/types.ts @@ -1,8 +1,8 @@ export type ReactElement = unknown; export type ReactFunction = (props: unknown) => ReactElement; -export type CreateElement = (element: ReactFunction | string, props?: Props, ...children: ReactElement[]) => ReactElement; - -export type Options = { createElement: CreateElement }; +export type CreateFunction = (element: ReactFunction | string, props?: Props, ...children: ReactElement[]) => ReactElement; +export type HtmlFunction = (slices: Slices, ...values: Values) => ReactElement[]; +export type Figure = { dict: (dict?: Dict) => HtmlFunction, dyn: CreateFunction }; export type Refs = Record<string, unknown>; export type Props = Record<string, unknown>; export type Slices = TemplateStringsArray;