NEWS: Welcome to my new homepage! <3

Reorganized code and removed css, and style 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 b38eec5eb647373a391c87bdc426fddf2c49441c
parent 2a128d5c6ee64e437271446a0751af72ab311130
Author: typable <contact@typable.dev>
Date:   Thu,  6 Apr 2023 11:51:58 +0200

Reorganized code and removed css, and style function

Diffstat:
MREADME.md | 26++++++++++++--------------
Dexamples/counter.html | 49-------------------------------------------------
Dexamples/picker.html | 91-------------------------------------------------------------------------------
Mlib.ts | 327+++++++++++++++++++++++++++++++++++++------------------------------------------
Mtypes.ts | 2--
5 files changed, 165 insertions(+), 330 deletions(-)

diff --git a/README.md b/README.md @@ -4,26 +4,24 @@ Reactive template literals for React ### Example ```javascript -import {figure, html, css} from '...'; +import figure from '...'; -figure({ createElement }); +const { html, dyn } = figure({ createElement }); -const Counter = () => { - const [count, setCount] = useState(0); +const global = createContext({}); - const style = css` - .counter { ... } - `; +function App() { + const [name, setName] = useState('world'); + + const context = {}; return html` - <div class="counter"> - ${style} - <button @click="${() => setCount(count - 1)}">-</button> - <p>${count}</p> - <button @click="${() => setCount(count + 1)}">+</button> - </div> + ${dyn(global.Provider, { value: context }), html` + <h1>Hello ${name}!</h1> + <p>Some description text.</p> + `} `; } -render(createElement(Counter), document.querySelector('#app')); +render(createElement(App), document.querySelector('#root')); ``` diff --git a/examples/counter.html b/examples/counter.html @@ -1,49 +0,0 @@ -<div id="app"></div> - -<script type="module"> - - import {useState, createElement} from 'https://cdn.skypack.dev/react'; - import {render} from 'https://cdn.skypack.dev/react-dom'; - import {figure, html, css} from '../lib.js'; - - const Counter = () => { - const [count, setCount] = useState(0); - - const style = css` - .counter { - display: inline-flex; - align-items: center; - gap: 10px; - padding: 50px; - } - - .counter p { - width: 30px; - margin: 0px; - text-align: center; - font-family: monospace; - font-size: 16px; - } - - .counter button { - width: 32px; - height: 32px; - cursor: pointer; - } - `; - - return html` - <div class="counter"> - ${style} - <button @click="${() => setCount(count - 1)}">-</button> - <p>${count}</p> - <button @click="${() => setCount(count + 1)}">+</button> - </div> - `; - } - - const app = document.querySelector('#app'); - figure({ createElement }); - render(createElement(Counter), app); - -</script> diff --git a/examples/picker.html b/examples/picker.html @@ -1,91 +0,0 @@ -<div id="app"></div> - -<script type="module"> - - import {useState, createElement} from 'https://cdn.skypack.dev/react'; - import {render} from 'https://cdn.skypack.dev/react-dom'; - import {figure, html, css} from '../lib.js'; - - const Range = ({index, value, setValue}) => { - const style = css` - .range input[type=range] { - width: 100%; - -webkit-appearance: none; - } - - .range.v-${index}-${value} input[type=range]::-webkit-slider-runnable-track { - background-color: rgb(${Array.from([0, 0, 0], (item, i) => i === index ? value : item).join(', ')}); - width: 100%; - height: 12px; - } - - .range input[type=range]::-webkit-slider-thumb { - background-color: #CCC; - width: 12px; - height: 12px; - -webkit-appearance: none; - cursor: pointer; - } - `; - - return html` - <div class="range v-${index}-${value}"> - ${style} - <input - type="range" - @change="${(event) => setValue(Number(event.target.value))}" - min="0" - max="255" - value="${value}" - > - </div> - `; - } - - const Picker = () => { - const [red, setRed] = useState(255); - const [green, setGreen] = useState(65); - const [blue, setBlue] = useState(155); - - const style = css` - .picker { - display: inline-flex; - flex-direction: column; - gap: 10px; - padding: 50px; - } - - .picker .preview { - width: 200px; - height: 200px; - background-color: rgb(${red}, ${green}, ${blue}); - } - - .picker p { - margin: 0px; - text-align: center; - font-family: monospace; - font-size: 16px; - } - `; - - const hex = (dec) => dec.toString(16).toUpperCase().padStart(2, '0'); - - return html` - <div class="picker"> - ${style} - <div class="preview"></div> - <p>rgb(${red}, ${green}, ${blue})</p> - <p>#${hex(red)}${hex(green)}${hex(blue)}</p> - ${Range({ index: 0, value: red, setValue: setRed })} - ${Range({ index: 1, value: green, setValue: setGreen })} - ${Range({ index: 2, value: blue, setValue: setBlue })} - </div> - `; - } - - const app = document.querySelector('#app'); - figure({ createElement }); - render(createElement(Picker), app); - -</script> diff --git a/lib.ts b/lib.ts @@ -1,199 +1,178 @@ -import { CreateElement, Option, Options, Props, ReactElement, ReactFunction, Refs, Slices, Values } from './types.ts'; +import { Options, Props, ReactElement, ReactFunction, Refs, Slices, Values } from './types.ts'; -const parser = new DOMParser(); -let count = 1000; -let create: Option<CreateElement> = null; +export default function figure({ createElement }: Options) { -function figure({ createElement }: Options): void { - create = createElement; -} - -function html(slices: Slices, ...values: Values): ReactElement { - const elements = parse(slices, ...values); - if (elements.length === 0) { - throw 'No DOM element was returned!'; - } - if (elements.length > 1) { - console.warn('Only one DOM element can be returned!'); - } - return elements[0]; -} + // the parser for interpreting HTML + const parser = new DOMParser(); + // the counter for creating unique references + let count = 0; -function parse(slices: Slices, ...values: Values): ReactElement[] { - const [html, refs] = compose(slices, values); - let dom; - try { - dom = parser.parseFromString(html, 'text/html'); - } - catch (error) { - console.error(error); - throw 'Invalid DOM structure!'; + /** + * Converts the template literal HTML syntax into React elements. + * @param {Slices} slices - The template literal slices + * @param {Values} values - The template literal values + * @return {ReactElement[]} The converted HTML as React elements. + */ + function html(slices: Slices, ...values: Values): ReactElement[] { + const [html, refs] = compose(slices, values); + let dom; + try { + dom = parser.parseFromString(html, 'text/html'); + } + 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)); } - const nodes = [...dom.head.childNodes, ...dom.body.childNodes]; - return nodes.map((node) => render(node, refs)); -} -/** - * 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. - * - * @param {string[]} slices - The template literal slices - * @param {any[]} values - The template literal values - * @return {any[]} The joined HTML string and the values mapped to there references. - */ -function compose(slices: Slices, values: Values): [string, Refs] { - if (slices == null) { - // handles dyn function without body - return ['', {}]; - } - const refs: Refs = {}; - let slice = ''; - for (let i = 0; i < slices.length; i++) { - slice += slices[i]; - if (values[i] != null) { - const uid = `$seg-${count++}`; - refs[uid] = values[i]; - slice += uid ?? ''; + /** + * 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. + * + * @param {Slices} slices - The template literal slices + * @param {Values} values - The template literal values + * @return {[string, Refs]} The joined HTML string and the values mapped to there references. + */ + function compose(slices: Slices, values: Values): [string, Refs] { + if (slices == null) { + // handle dyn function without body + return ['', {}]; } + const refs: Refs = {}; + let slice = ''; + for (let i = 0; i < slices.length; i++) { + slice += slices[i]; + if (values[i] != null) { + const uid = `$fig-${count++}`; + refs[uid] = values[i]; + slice += uid ?? ''; + } + } + return [slice.trim(), refs]; } - return [slice.trim(), refs]; -} -/** - * Injects the values into the corresponding reference locations of the string. - * - * @param {string} slice - The string containing references - * @param {any[]} refs - The values mapped to there references - * @return {any[]} The string populated with the passed values - */ -function feed(slice: string, refs: Refs): ReactElement[] { - const expr = /\$seg-\d+/g; - const elements: ReactElement[] = []; - let match = 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(); + /** + * Injects the values into the corresponding reference locations of the string. + * + * @param {string} slice - The string containing references + * @param {Refs} refs - The values mapped to there references + * @return {ReactElement[]} The string populated with the passed values + */ + function feed(slice: string, refs: Refs): ReactElement[] { + const expr = /\$fig-\d+/g; + const elements: ReactElement[] = []; + let match = 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(); + } + elements.push(value); + last = index + uid.length; } - elements.push(value); - last = index + uid.length; + elements.push(slice.substring(last)); + return elements; } - elements.push(slice.substring(last)); - return elements; -} -function dyn(element: ReactFunction, props: Props): (slices: Slices, ...values: Values) => ReactElement { - return (slices: Slices, ...values: Values) => { - if (create == null) { - throw 'Invalid state! Figure was not initialized!'; + /** + * Converts a HTML node into a React element. + * + * @param {Node} node - The HTML node + * @param {Refs} refs - The values mapped to there references + * @return {ReactElement[]} The converted HTML node as React element + */ + function render(node: Node, refs: Refs): ReactElement[] { + if (node.nodeType === Node.TEXT_NODE) { + const text = node as Text; + if (text.textContent == null) { + // ignore empty text nodes + return []; + } + return feed(text.textContent, refs); } - return create(element, props, ...parse(slices, ...values)); - }; -} - -function render(node: Node, refs: Refs): ReactElement[] { - if (node.nodeType === Node.TEXT_NODE) { - const text = node as Text; - if (text.textContent == null) { + if (node.nodeType === Node.COMMENT_NODE) { + // ignore comments return []; } - return feed(text.textContent, refs); - } - if (node.nodeType === Node.COMMENT_NODE) { - return []; - } - const element = node as HTMLElement; - const tag = element.tagName; - const attributes: Props = {}; - for (const attribute of element.attributes) { - const key = attribute.name; - const slice = attribute.textContent; - if (slice == null) { - continue; - } - let isDynamic = false; - for (const ref in refs) { - if (slice === ref) { - let match = null; - if ((match = /^@(\w+)$/.exec(key)) !== null) { - const event = match[1]; - attributes[`on${event.substring(0, 1).toUpperCase()}${event.substring(1)}`] = refs[ref]; - } - else if ((match = /^\[(\w+)\]$/.exec(key)) !== null) { - const property = match[1]; - attributes[property] = refs[ref]; - } - else { - attributes[key] = refs[ref]; - } - isDynamic = true; - break; + const element = node as HTMLElement; + const tag = element.tagName; + const props: Props = {}; + // iterate over each attribute and add it to the props + for (const attribute of element.attributes) { + const key = attribute.name; + const slice = attribute.textContent; + if (slice == null) { + // ignore empty attribute values + continue; } - } - if (!isDynamic) { - if (key === 'style') { - 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 isDynamic = false; + for (const ref in refs) { + if (slice === ref) { let match = null; - if ((match = /-(\w)/.exec(key)) !== null) { - const char = match[1]; - key = key.replace(`-${char}`, char.toUpperCase()); + 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]; } - value = value.trim(); - styles[key] = feed(value, refs).join(''); + isDynamic = true; + break; } - attributes[key] = styles; } - else { - attributes[key] = feed(slice, refs).join(''); + 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 children: ReactElement[] = []; + // recursively render all child nodes + (node.childNodes ?? []).forEach((child) => children.push(...render(child, refs))); + return [createElement(tag, props, ...children)]; } - const children: ReactElement[] = []; - (node.childNodes ?? []).forEach((child) => children.push(...render(child, refs))); - if (create == null) { - throw 'Invalid state! Figure was not initialized!'; - } - return [create(tag, attributes, ...children)]; -} - -function css(slices: Slices, ...values: Values): ReactElement { - const [css, refs] = compose(slices, values); - return html` - <style type="text/css"> - ${feed(css, refs).join('')} - </style> - `; -} -function style(slices: Slices, ...values: Values): Record<string, string> { - const [css, refs] = compose(slices, values); - const styles: Record<string, string> = {}; - for (const item of css.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(''); + /** + * 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); } - return styles; -} -export { figure, html, dyn, css, style }; + return { html, dyn }; +} diff --git a/types.ts b/types.ts @@ -1,5 +1,3 @@ -export type Option<T> = T | null | undefined; - export type ReactElement = unknown; export type ReactFunction = unknown; export type CreateElement = (element: ReactFunction, props?: Props, ...children: ReactElement[]) => ReactElement;