The vanilla alternative for writing JSX-based React applications
git clone git://soophie.de/figure
1
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter&family=Noto+Sans">3
<div id="app"></div>5
<style>7
* {8
font-family: 'Noto Sans', sans-serif;9
}11
body {12
margin: 0;13
}15
</style>17
<script type="module">19
import {useState, createElement} from 'https://cdn.skypack.dev/react';20
import {render} from 'https://cdn.skypack.dev/react-dom';21
import {createTlx, css} from './lib.js';23
const tlx = createTlx(createElement);25
const Button = (props, $slot) => {26
const styles = {27
...css`28
min-width: 40px;29
height: 40px;30
border: none;31
outline: none;32
border: 1px solid #a3a3a3;33
background-color: ${props.disabled ? '#f5f5f5' : '#e5e5e5'};34
cursor: ${props.disabled ? 'default' : 'pointer'};35
font-size: 1rem;36
`,37
...props.styles38
};39
return tlx`40
<button41
disabled="${props.disabled}"42
style="${styles}"43
@click="${() => props.onClick()}"44
>45
${$slot}46
</button>47
`;48
}50
const Counter = (props) => {51
const [defaultValue, setDefaultValue] = useState(0);52
const { min = 0, max = 999, value = defaultValue, setValue = setDefaultValue } = props ?? {};53
const validate = (value) => {54
if(/^\d+$/.test(value) && parseInt(value) <= max) {55
setValue(parseInt(value));56
}57
}58
return tlx`59
<div style="display: inline-flex; width: 100%; border: 1px solid #a3a3a3; border-radius: 4px; overflow: hidden; height: 40px; box-sizing: border-box;">60
${Button({61
onClick: () => setValue(value - 1),62
styles: css`63
font-size: 1.2rem;64
border-width: 0px 1px 0px 0px;65
`,66
disabled: value <= min67
}, '-')}68
<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;">69
${Button({70
onClick: () => setValue(value + 1),71
styles: css`72
font-size: 1.2rem;73
border-width: 0px 0px 0px 1px;74
`,75
disabled: value >= max76
}, '+')}77
</div>78
`;79
}81
const App = () => {82
const [quantity, setQuantity] = useState(1);83
const isDisabled = quantity <= 0;84
return tlx`85
<div style="margin: 100px; display: flex; width: 150px; flex-direction: column; gap: 15px; align-items: flex-start;">86
${Counter({ value: quantity, setValue: setQuantity })}87
${Button({88
styles: css`89
background-color: ${isDisabled ? '#f5f5f5' : '#f97316'};90
border-color: ${isDisabled ? '#d4d4d4' : '#c2410c'};91
color: ${isDisabled ? '#a3a3a3' : '#ffffff'};92
border-radius: 4px;93
width: 100%;94
`,95
onClick: () => alert(`Added ${quantity} ${quantity > 1 ? 'items' : 'item'} to the cart`),96
disabled: isDisabled97
}, 'Add to cart')}98
</div>99
`;100
}102
const app = document.querySelector('#app');103
render(createElement(App), app);105
</script>
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, css} from '../tlx.js';9
const tlx = createTlx(createElement);11
const Range = ({index, value, setValue}) => {12
const style = css`13
.range input[type=range] {14
width: 100%;15
-webkit-appearance: none;16
}18
.range.v-${index}-${value} input[type=range]::-webkit-slider-runnable-track {19
background-color: rgb(${Array.from([0, 0, 0], (item, i) => i === index ? value : item).join(', ')});20
width: 100%;21
height: 12px;22
}24
.range input[type=range]::-webkit-slider-thumb {25
background-color: #CCC;26
width: 12px;27
height: 12px;28
-webkit-appearance: none;29
cursor: pointer;30
}31
`;33
return tlx`34
<div class="range v-${index}-${value}">35
${style}36
<input37
type="range"38
@change="${(event) => setValue(Number(event.target.value))}"39
min="0"40
max="255"41
value="${value}"42
>43
</div>44
`;45
}47
const Picker = () => {48
const [red, setRed] = useState(255);49
const [green, setGreen] = useState(65);50
const [blue, setBlue] = useState(155);52
const style = css`53
.picker {54
display: inline-flex;55
flex-direction: column;56
gap: 10px;57
padding: 50px;58
}60
.picker .preview {61
width: 200px;62
height: 200px;63
background-color: rgb(${red}, ${green}, ${blue});64
}66
.picker p {67
margin: 0px;68
text-align: center;69
font-family: monospace;70
font-size: 16px;71
}72
`;74
const hex = (dec) => dec.toString(16).toUpperCase().padStart(2, '0');76
return tlx`77
<div class="picker">78
${style}79
<div class="preview"></div>80
<p>rgb(${red}, ${green}, ${blue})</p>81
<p>#${hex(red)}${hex(green)}${hex(blue)}</p>82
${Range({ index: 0, value: red, setValue: setRed })}83
${Range({ index: 1, value: green, setValue: setGreen })}84
${Range({ index: 2, value: blue, setValue: setBlue })}85
</div>86
`;87
}89
const app = document.querySelector('#app');90
render(createElement(Picker), app);92
</script>
1
const parser = new DOMParser();2
let counter = 1000;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
}19
function render(node, refs) {20
if(node.nodeType === Node.TEXT_NODE) {21
return apply(node.textContent, refs);22
}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
}36
else {37
attributes[key] = refs[ref];38
}39
isDynamic = true;40
break;41
}42
}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
}72
return tlx;73
}75
export const css = (parts, ...props) => {76
const [css, refs] = ltr(parts, props);77
const styles = {};78
for(const item of css.split(';')) {79
if(item.trim().length === 0) {80
break;81
}82
let [key, value] = item.split(':');83
key = key.trim();84
let match = null;85
if((match = /-(\w)/.exec(key)) !== null) {86
const char = match[1];87
key = key.replace(`-${char}`, char.toUpperCase());88
}89
value = value.trim();90
styles[key] = apply(value, refs).join('');91
}92
return styles;93
}95
function apply(value, refs) {96
let values = [];97
const expr = /\$tlx-\d+/g;98
let match = null;99
let last = 0;100
while((match = expr.exec(value)) !== null) {101
const index = match.index;102
values.push(value.substring(last, index));103
values.push(refs[match[0]]);104
last = index + match[0].length;105
}106
values.push(value.substring(last));107
return values;108
}110
function ltr(parts, props) {111
let string = '';112
const refs = {};113
for(let i = 0; i < parts.length; i++) {114
string += parts[i];115
if(props[i] !== undefined) {116
const id = `$tlx-${counter}`;117
refs[id] = props[i];118
string += id ?? '';119
counter++;120
}121
}122
return [string.trim(), refs];123
}
1
const parser = new DOMParser();2
let counter = 1000;3
let tlx = null;5
export const createTlx = (createElement) => {6
function parse(parts, ...props) {7
const [html, refs] = ltr(parts, props);8
const dom = parser.parseFromString(html, 'text/html');9
const node = dom.body.childNodes[0] ?? dom.head.childNodes[0];10
const elements = render(node, refs);11
if(elements.length !== 1) {12
throw 'invalid VDOM structure!';13
}14
return elements[0];15
}17
function render(node, refs) {18
if(node.nodeType === Node.TEXT_NODE) {19
return apply(node.textContent, refs);20
}21
if(node.nodeType === Node.COMMENT_NODE) {22
return [];23
}24
const tag = node.tagName;25
const attributes = {};26
for(const attribute of node.attributes) {27
const key = attribute.name;28
const value = attribute.textContent;29
let isDynamic = false;30
for(const ref in refs) {31
if(value === ref) {32
let match = null;33
if((match = /^@(\w+)$/.exec(key)) !== null) {34
const event = match[1];35
attributes[`on${event.substr(0, 1).toUpperCase()}${event.substr(1)}`] = refs[ref];36
}37
else {38
attributes[key] = refs[ref];39
}40
isDynamic = true;41
break;42
}43
}44
if(!isDynamic) {45
if(key === 'style') {46
const styles = {};47
for(const item of value.split(';')) {48
if(item.trim().length === 0) {49
break;50
}51
let [key, value] = item.split(':');52
key = key.trim();53
let match = null;54
if((match = /-(\w)/.exec(key)) !== null) {55
const char = match[1];56
key = key.replace(`-${char}`, char.toUpperCase());57
}58
value = value.trim();59
styles[key] = apply(value, refs).join('');60
}61
attributes[key] = styles;62
}63
else {64
attributes[key] = apply(value, refs).join('');65
}66
}67
}68
const children = [];69
(node.childNodes ?? []).forEach((child) => children.push(...render(child, refs)));70
return [createElement(tag, attributes, ...children)];71
}73
tlx = parse;74
return tlx;75
}77
export const css = (parts, ...props) => {78
const [css, refs] = ltr(parts, props);79
return tlx`80
<style type="text/css">81
${apply(css, refs).join('')}82
</style>83
`;84
}86
export const style = (parts, ...props) => {87
const [css, refs] = ltr(parts, props);88
const styles = {};89
for(const item of css.split(';')) {90
if(item.trim().length === 0) {91
break;92
}93
let [key, value] = item.split(':');94
key = key.trim();95
let match = null;96
if((match = /-(\w)/.exec(key)) !== null) {97
const char = match[1];98
key = key.replace(`-${char}`, char.toUpperCase());99
}100
value = value.trim();101
styles[key] = apply(value, refs).join('');102
}103
return styles;104
}106
function apply(value, refs) {107
let values = [];108
const expr = /\$tlx-\d+/g;109
let match = null;110
let last = 0;111
while((match = expr.exec(value)) !== null) {112
const index = match.index;113
values.push(value.substring(last, index));114
let refValue = refs[match[0]];115
if(refValue instanceof Function) {116
refValue = refValue();117
}118
values.push(refValue);119
last = index + match[0].length;120
}121
values.push(value.substring(last));122
return values;123
}125
function ltr(parts, props) {126
let string = '';127
const refs = {};128
for(let i = 0; i < parts.length; i++) {129
string += parts[i];130
if(props[i] !== undefined) {131
const id = `$tlx-${counter}`;132
refs[id] = props[i];133
string += id ?? '';134
counter++;135
}136
}137
return [string.trim(), refs];138
}