NEWS: Welcome to my new homepage! <3

Added ESM support & example - 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 f4c5856f0d61c9e970c8fee39147804378234750
parent fe2376423c14f0e563699d40777fb09143bcdb1f
Author: typable <contact@typable.dev>
Date:   Thu, 15 Sep 2022 16:47:21 +0200

Added ESM support & example

Diffstat:
Aexample.html | 87+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mlib.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; }