What if JSX just returned DOM elements?
export default function ClickMe() {
let i = 0;
const el = <button>Click me</button> as HTMLButtonElement;
el.onclick = (e) => {
el.textContent = `Clicked ${++i} times`;
};
return el;
}
Would they be reusable?
Could they keep their own state?
import ClickMe from "./sample1.js";
export default () => <>
<p><ClickMe /></p>
<p><ClickMe /></p>
<p><ClickMe /></p>
</>;
How would they work together?
Could they create an interactive DOM tree?
function TodoInput(attrs: { add: (v: string) => void }) {
const input = <input type='text' /> as HTMLInputElement;
input.placeholder = 'Add todo item...';
input.onkeydown = (e) => {
if (e.key === 'Enter') {
attrs.add(input.value);
input.value = '';
}
};
return input;
}
class TodoList {
ul = <ul class='todolist' /> as HTMLUListElement;
add(v: string) {
const item = <li>{v}</li> as HTMLLIElement;
item.onclick = () => item.remove();
this.ul.append(item);
}
}
export default () => {
const list = new TodoList();
list.add('foo');
list.add('bar');
return <>
<TodoInput add={(v) => list.add(v)} />
{list.ul}
</>;
};
How would they handle large data?
Could they be convenient without a virtual dom?
declare const data: Map<string, number>;
// Names of US citizens born in 1882 from ssa.gov
export default function FindNames() {
const status = <p class='status' /> as HTMLParagraphElement;
const results = <ul /> as HTMLUListElement;
const input = <input
type='text'
value='.?mary?'
autocomplete='new-password'
oninput={updateMatches}
/> as HTMLInputElement;
updateMatches();
function updateMatches() {
const regex = new RegExp(`(${input.value})`, 'gi');
const matched = ([...data.entries()]
.filter(([k]) => k.match(regex)));
const matches = (matched
.slice(0, 25)
.map(match => <Item regex={regex} match={match} />));
results.replaceChildren(...matches);
status.textContent = `${matched.length} / ${data.size}`;
}
return <div>{input}{status}{results}</div>;
}
function Item(attrs: { match: [string, number], regex: RegExp }) {
const [name, count] = attrs.match;
const total = <small style='color:#fff3'>({count})</small>;
return <li>
<span innerHTML={highlight(name, attrs.regex)} /> {total}
</li>;
}
function highlight(str: string, regex: RegExp) {
return str.replace(regex, '<span class="match">$1</span>');
}
How would they manage complex state?
How could parent components react to children?
export default () => <>
<TodoList />
</>;
function TodoList() {
const list = new List();
list.add('foo');
list.add('bar').toggle();
list.add('qux');
const input = <input type='text' /> as HTMLInputElement;
input.onkeydown = (e) => {
if (e.key === 'Enter' && input.value.trim().length > 0) {
list.add(input.value);
input.value = '';
}
};
return <div id='real-todolist'>
<div>{input}</div>
<div class='actions'>
<Counter list={list} />
<button onclick={() => list.clearDone()}>Clear</button>
<button onclick={() => list.invertAll()}><i>Invert</i></button>
</div>
{list.ul}
</div>;
}
class List extends EventTarget {
ul = <ul class='list' /> as HTMLUListElement;
items: Item[] = [];
itemUnlisteners = new Map<Item, () => void>();
add(text: string) {
const item = new Item(this, text);
this.items.push(item);
this.ul.append(item.li);
this.dispatchEvent(new Event('item-added'));
this.itemUnlisteners.set(item, listen(item, 'toggled', () => {
this.dispatchEvent(new Event('item-toggled'));
}));
return item;
}
rem(item: Item) {
const unlisten = this.itemUnlisteners.get(item)!;
this.itemUnlisteners.delete(item);
unlisten();
this.items = this.items.filter(it => it !== item);
this.dispatchEvent(new Event('item-removed'));
}
clearDone = () => this.doneItems().forEach(it => it.remove());
invertAll = () => this.items.forEach(it => it.toggle());
doneItems = () => this.items.filter(it => it.done);
}
class Item extends EventTarget {
done = false;
#checkbox = <input type='checkbox' /> as HTMLInputElement;
li;
constructor(private list: List, text: string) {
super();
this.li = (
<li class='item'>
{this.#checkbox}
<span onclick={() => this.toggle()}>{text}</span>
<button class='close' onclick={() => this.remove()}>✕</button>
</li> as HTMLLIElement
);
this.#checkbox.onclick = () => this.toggle();
}
remove() {
this.li.remove();
this.list.rem(this);
}
toggle() {
this.done = !this.done;
this.li.classList.toggle('done', this.done);
this.#checkbox.checked = this.done;
this.dispatchEvent(new Event('toggled'));
}
}
function Counter({ list }: { list: List }) {
const span = <span /> as HTMLSpanElement;
const updateText = () => {
const done = list.doneItems().length;
const total = list.items.length;
span.textContent = `Done: ${done}/${total}`;
};
updateText();
list.addEventListener('item-added', updateText);
list.addEventListener('item-removed', updateText);
list.addEventListener('item-toggled', updateText);
return span;
}
function listen(target: EventTarget, event: string, fn: () => void) {
target.addEventListener(event, fn);
return () => target.removeEventListener(event, fn);
}
The only runtime needed for this is @imlib/jsx-browser.js.
JSX is compiled by @swc/core to automatically import it.
The idea came out of my work on imlib.
Also check out the source to this page.