commit f4c5856f0d61c9e970c8fee39147804378234750
parent fe2376423c14f0e563699d40777fb09143bcdb1f
Author: typable <contact@typable.dev>
Date: Thu, 15 Sep 2022 16:47:21 +0200
Added ESM support & example
Diffstat:
A | example.html | | | 87 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
M | lib.js | | | 162 | +++++++++++++++++++++++++++++++++++++++++-------------------------------------- |
2 files changed, 171 insertions(+), 78 deletions(-)
diff --git a/example.html b/example.html
@@ -0,0 +1,87 @@
+<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 {createTlx} from './lib.js';
+
+ const tlx = createTlx(createElement);
+
+ const Counter = (props) => {
+ const [defaultValue, setDefaultValue] = useState(0);
+ const { min = 0, max = 999, value = defaultValue, setValue = setDefaultValue } = props ?? {};
+ const validate = (value) => {
+ if(/^\d+$/.test(value) && parseInt(value) <= max) {
+ setValue(parseInt(value));
+ }
+ }
+ return tlx`
+ <div style="display: inline-flex; width: 100%; border: 1px solid #a3a3a3; border-radius: 4px; overflow: hidden; height: 40px; box-sizing: border-box;">
+ ${Button({
+ text: '-',
+ onClick: () => setValue(value - 1),
+ styles: {
+ fontSize: '1.2rem',
+ borderWidth: '0px 1px 0px 0px'
+ },
+ disabled: value <= min
+ })}
+ <input type="text" @input="${(event) => validate(event.target.value)}" value="${value}" style="font-size: 1rem; text-align: center; border: none; flex: 1; min-width: 0; height: 40px; outline: none;">
+ ${Button({
+ text: '+',
+ onClick: () => setValue(value + 1),
+ styles: {
+ fontSize: '1.2rem',
+ borderWidth: '0px 0px 0px 1px'
+ },
+ disabled: value >= max
+ })}
+ </div>
+ `;
+ }
+
+ const Button = (props) => {
+ const styles = {
+ minWidth: '40px',
+ height: '40px',
+ border: 'none',
+ outline: 'none',
+ border: '1px solid #a3a3a3',
+ backgroundColor: props.disabled ? '#f5f5f5' : '#e5e5e5',
+ cursor: props.disabled ? 'default' : 'pointer',
+ fontSize: '1rem',
+ fontFamily: 'Inter',
+ ...props.styles
+ };
+ return tlx`
+ <button disabled="${props.disabled}" style="${styles}" @click="${() => props.onClick()}">${props.text}</button>
+ `;
+ }
+
+ const App = () => {
+ const [quantity, setQuantity] = useState(1);
+ const isDisabled = quantity <= 0;
+ return tlx`
+ <div style="margin: 100px; display: flex; width: 150px; flex-direction: column; gap: 15px; align-items: flex-start;">
+ ${Counter({ value: quantity, setValue: setQuantity })}
+ ${Button({
+ text: 'Add to cart',
+ styles: {
+ backgroundColor: isDisabled ? '#f5f5f5' : '#f97316',
+ borderColor: isDisabled ? '#d4d4d4' : '#c2410c',
+ color: isDisabled ? '#a3a3a3' : 'white',
+ borderRadius: '4px',
+ width: '100%'
+ },
+ onClick: () => alert(`Added ${quantity} ${quantity > 1 ? 'items' : 'item'} to the cart`),
+ disabled: isDisabled
+ })}
+ </div>
+ `;
+ }
+
+ const app = document.querySelector('#app');
+ render(createElement(App), app);
+
+</script>
diff --git a/lib.js b/lib.js
@@ -1,97 +1,103 @@
const parser = new DOMParser();
let counter = 1000;
-function tlx(parts, ...props) {
- const [html, refs] = ltr(parts, props);
- const dom = parser.parseFromString(html, 'text/html');
- if(dom.body.childNodes.length !== 1) {
- throw 'invalid DOM structure!';
- }
- const node = dom.body.childNodes[0];
- const elements = render(node, refs);
- if(elements.length !== 1) {
- throw 'invalid VDOM structure!';
+export const createTlx = (createElement) => {
+ function tlx(parts, ...props) {
+ const [html, refs] = ltr(parts, props);
+ const dom = parser.parseFromString(html, 'text/html');
+ if(dom.body.childNodes.length !== 1) {
+ throw 'invalid DOM structure!';
+ }
+ const node = dom.body.childNodes[0];
+ const elements = render(node, refs);
+ if(elements.length !== 1) {
+ throw 'invalid VDOM structure!';
+ }
+ return elements[0];
}
- return elements[0];
-}
-function render(node, refs) {
- if(node.nodeType === Node.TEXT_NODE) {
- return apply(node.textContent, refs);
- }
- const tag = node.tagName.toLowerCase();
- const attributes = {};
- for(const attribute of node.attributes) {
- const key = attribute.name;
- const value = attribute.textContent;
- let isDynamic = false;
- for(const ref in refs) {
- if(value === ref) {
- if((match = /^@(\w+)$/.exec(key)) !== null) {
- const event = match[1];
- attributes[`on${event.substr(0, 1).toUpperCase()}${event.substr(1)}`] = refs[ref];
- }
- else {
- attributes[key] = refs[ref];
- }
- isDynamic = true;
- break;
- }
+ function render(node, refs) {
+ if(node.nodeType === Node.TEXT_NODE) {
+ return apply(node.textContent, refs);
}
- if(!isDynamic) {
- if(key === 'style') {
- const styles = {};
- for(const item of value.split(';')) {
- if(item.trim().length === 0) {
- break;
+ const tag = node.tagName.toLowerCase();
+ const attributes = {};
+ for(const attribute of node.attributes) {
+ const key = attribute.name;
+ const value = attribute.textContent;
+ let isDynamic = false;
+ for(const ref in refs) {
+ if(value === ref) {
+ let match = null;
+ if((match = /^@(\w+)$/.exec(key)) !== null) {
+ const event = match[1];
+ attributes[`on${event.substr(0, 1).toUpperCase()}${event.substr(1)}`] = refs[ref];
}
- let [key, value] = item.split(':');
- key = key.trim();
- if((match = /-(\w)/.exec(key)) !== null) {
- const char = match[1];
- key = key.replace(`-${char}`, char.toUpperCase());
+ else {
+ attributes[key] = refs[ref];
}
- value = value.trim();
- styles[key] = apply(value, refs).join('');
+ isDynamic = true;
+ break;
}
- attributes[key] = styles;
}
- else {
- attributes[key] = apply(value, refs).join('');
+ if(!isDynamic) {
+ if(key === 'style') {
+ const styles = {};
+ for(const item of value.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] = apply(value, refs).join('');
+ }
+ attributes[key] = styles;
+ }
+ else {
+ attributes[key] = apply(value, refs).join('');
+ }
}
}
+ const children = [];
+ (node.childNodes ?? []).forEach((child) => children.push(...render(child, refs)));
+ return [createElement(tag, attributes, ...children)];
}
- const children = [];
- (node.childNodes ?? []).forEach((child) => children.push(...render(child, refs)));
- return [React.createElement(tag, attributes, ...children)];
-}
-function apply(value, refs) {
- let values = [];
- const expr = /\$tlx-\d+/g;
- let match = null;
- let last = 0;
- while((match = expr.exec(value)) !== null) {
- const index = match.index;
- values.push(value.substring(last, index));
- values.push(refs[match[0]]);
- last = index + match[0].length;
+ function apply(value, refs) {
+ let values = [];
+ const expr = /\$tlx-\d+/g;
+ let match = null;
+ let last = 0;
+ while((match = expr.exec(value)) !== null) {
+ const index = match.index;
+ values.push(value.substring(last, index));
+ values.push(refs[match[0]]);
+ last = index + match[0].length;
+ }
+ values.push(value.substring(last));
+ return values;
}
- values.push(value.substring(last));
- return values;
-}
-function ltr(parts, props) {
- let string = '';
- const refs = {};
- for(let i = 0; i < parts.length; i++) {
- string += parts[i];
- if(props[i] !== undefined) {
- const id = `$tlx-${counter}`;
- refs[id] = props[i];
- string += id ?? '';
- counter++;
+ function ltr(parts, props) {
+ let string = '';
+ const refs = {};
+ for(let i = 0; i < parts.length; i++) {
+ string += parts[i];
+ if(props[i] !== undefined) {
+ const id = `$tlx-${counter}`;
+ refs[id] = props[i];
+ string += id ?? '';
+ counter++;
+ }
}
+ return [string.trim(), refs];
}
- return [string.trim(), refs];
+
+ return tlx;
}