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:
M | README.md | | | 26 | ++++++++++++-------------- |
D | examples/counter.html | | | 49 | ------------------------------------------------- |
D | examples/picker.html | | | 91 | ------------------------------------------------------------------------------- |
M | lib.ts | | | 327 | +++++++++++++++++++++++++++++++++++++------------------------------------------ |
M | types.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;