The vanilla alternative for writing JSX-based React applications
git clone git://soophie.de/figure
1
<div id="app"></div>3
<script type="module">5
import {useState, createElement} from 'https://cdn.skypack.dev/react';6
import {render} from 'https://cdn.skypack.dev/react-dom';7
import {createTlx} from './lib.js';9
const tlx = createTlx(createElement);11
const Counter = (props) => {12
const [defaultValue, setDefaultValue] = useState(0);13
const { min = 0, max = 999, value = defaultValue, setValue = setDefaultValue } = props ?? {};14
const validate = (value) => {15
if(/^\d+$/.test(value) && parseInt(value) <= max) {16
setValue(parseInt(value));17
}18
}19
return tlx`20
<div style="display: inline-flex; width: 100%; border: 1px solid #a3a3a3; border-radius: 4px; overflow: hidden; height: 40px; box-sizing: border-box;">21
${Button({22
text: '-',23
onClick: () => setValue(value - 1),24
styles: {25
fontSize: '1.2rem',26
borderWidth: '0px 1px 0px 0px'27
},28
disabled: value <= min29
})}30
<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;">31
${Button({32
text: '+',33
onClick: () => setValue(value + 1),34
styles: {35
fontSize: '1.2rem',36
borderWidth: '0px 0px 0px 1px'37
},38
disabled: value >= max39
})}40
</div>41
`;42
}44
const Button = (props) => {45
const styles = {46
minWidth: '40px',47
height: '40px',48
border: 'none',49
outline: 'none',50
border: '1px solid #a3a3a3',51
backgroundColor: props.disabled ? '#f5f5f5' : '#e5e5e5',52
cursor: props.disabled ? 'default' : 'pointer',53
fontSize: '1rem',54
fontFamily: 'Inter',55
...props.styles56
};57
return tlx`58
<button disabled="${props.disabled}" style="${styles}" @click="${() => props.onClick()}">${props.text}</button>59
`;60
}62
const App = () => {63
const [quantity, setQuantity] = useState(1);64
const isDisabled = quantity <= 0;65
return tlx`66
<div style="margin: 100px; display: flex; width: 150px; flex-direction: column; gap: 15px; align-items: flex-start;">67
${Counter({ value: quantity, setValue: setQuantity })}68
${Button({69
text: 'Add to cart',70
styles: {71
backgroundColor: isDisabled ? '#f5f5f5' : '#f97316',72
borderColor: isDisabled ? '#d4d4d4' : '#c2410c',73
color: isDisabled ? '#a3a3a3' : 'white',74
borderRadius: '4px',75
width: '100%'76
},77
onClick: () => alert(`Added ${quantity} ${quantity > 1 ? 'items' : 'item'} to the cart`),78
disabled: isDisabled79
})}80
</div>81
`;82
}84
const app = document.querySelector('#app');85
render(createElement(App), app);87
</script>
1
const parser = new DOMParser();2
let counter = 1000;4
function tlx(parts, ...props) {5
const [html, refs] = ltr(parts, props);6
const dom = parser.parseFromString(html, 'text/html');7
if(dom.body.childNodes.length !== 1) {8
throw 'invalid DOM structure!';9
}10
const node = dom.body.childNodes[0];11
const elements = render(node, refs);12
if(elements.length !== 1) {13
throw 'invalid VDOM structure!';4
export const createTlx = (createElement) => {5
function tlx(parts, ...props) {6
const [html, refs] = ltr(parts, props);7
const dom = parser.parseFromString(html, 'text/html');8
if(dom.body.childNodes.length !== 1) {9
throw 'invalid DOM structure!';10
}11
const node = dom.body.childNodes[0];12
const elements = render(node, refs);13
if(elements.length !== 1) {14
throw 'invalid VDOM structure!';15
}16
return elements[0];17
}15
return elements[0];16
}18
function render(node, refs) {19
if(node.nodeType === Node.TEXT_NODE) {20
return apply(node.textContent, refs);21
}22
const tag = node.tagName.toLowerCase();23
const attributes = {};24
for(const attribute of node.attributes) {25
const key = attribute.name;26
const value = attribute.textContent;27
let isDynamic = false;28
for(const ref in refs) {29
if(value === ref) {30
if((match = /^@(\w+)$/.exec(key)) !== null) {31
const event = match[1];32
attributes[`on${event.substr(0, 1).toUpperCase()}${event.substr(1)}`] = refs[ref];33
}34
else {35
attributes[key] = refs[ref];36
}37
isDynamic = true;38
break;39
}19
function render(node, refs) {20
if(node.nodeType === Node.TEXT_NODE) {21
return apply(node.textContent, refs);22
}41
if(!isDynamic) {42
if(key === 'style') {43
const styles = {};44
for(const item of value.split(';')) {45
if(item.trim().length === 0) {46
break;23
const tag = node.tagName.toLowerCase();24
const attributes = {};25
for(const attribute of node.attributes) {26
const key = attribute.name;27
const value = attribute.textContent;28
let isDynamic = false;29
for(const ref in refs) {30
if(value === ref) {31
let match = null;32
if((match = /^@(\w+)$/.exec(key)) !== null) {33
const event = match[1];34
attributes[`on${event.substr(0, 1).toUpperCase()}${event.substr(1)}`] = refs[ref];35
}48
let [key, value] = item.split(':');49
key = key.trim();50
if((match = /-(\w)/.exec(key)) !== null) {51
const char = match[1];52
key = key.replace(`-${char}`, char.toUpperCase());36
else {37
attributes[key] = refs[ref];38
}54
value = value.trim();55
styles[key] = apply(value, refs).join('');39
isDynamic = true;40
break;41
}57
attributes[key] = styles;42
}59
else {60
attributes[key] = apply(value, refs).join('');43
if(!isDynamic) {44
if(key === 'style') {45
const styles = {};46
for(const item of value.split(';')) {47
if(item.trim().length === 0) {48
break;49
}50
let [key, value] = item.split(':');51
key = key.trim();52
let match = null;53
if((match = /-(\w)/.exec(key)) !== null) {54
const char = match[1];55
key = key.replace(`-${char}`, char.toUpperCase());56
}57
value = value.trim();58
styles[key] = apply(value, refs).join('');59
}60
attributes[key] = styles;61
}62
else {63
attributes[key] = apply(value, refs).join('');64
}65
}66
}67
const children = [];68
(node.childNodes ?? []).forEach((child) => children.push(...render(child, refs)));69
return [createElement(tag, attributes, ...children)];70
}64
const children = [];65
(node.childNodes ?? []).forEach((child) => children.push(...render(child, refs)));66
return [React.createElement(tag, attributes, ...children)];67
}69
function apply(value, refs) {70
let values = [];71
const expr = /\$tlx-\d+/g;72
let match = null;73
let last = 0;74
while((match = expr.exec(value)) !== null) {75
const index = match.index;76
values.push(value.substring(last, index));77
values.push(refs[match[0]]);78
last = index + match[0].length;72
function apply(value, refs) {73
let values = [];74
const expr = /\$tlx-\d+/g;75
let match = null;76
let last = 0;77
while((match = expr.exec(value)) !== null) {78
const index = match.index;79
values.push(value.substring(last, index));80
values.push(refs[match[0]]);81
last = index + match[0].length;82
}83
values.push(value.substring(last));84
return values;85
}80
values.push(value.substring(last));81
return values;82
}84
function ltr(parts, props) {85
let string = '';86
const refs = {};87
for(let i = 0; i < parts.length; i++) {88
string += parts[i];89
if(props[i] !== undefined) {90
const id = `$tlx-${counter}`;91
refs[id] = props[i];92
string += id ?? '';93
counter++;87
function ltr(parts, props) {88
let string = '';89
const refs = {};90
for(let i = 0; i < parts.length; i++) {91
string += parts[i];92
if(props[i] !== undefined) {93
const id = `$tlx-${counter}`;94
refs[id] = props[i];95
string += id ?? '';96
counter++;97
}98
}99
return [string.trim(), refs];100
}96
return [string.trim(), refs];102
return tlx;103
}