PoV:
You donโt want to use a frontend framework because you think itโs overkill.
You want to use Vanilla JS to manipulate the DOM,
but then you realize your code gets messy, filled with so many
document.createElement, element.setAttribute, & element.appendChild.
Youโre considering a minimal abstraction with an extremely small bundle size (1.7ย KB).
Combine the declarative React-like syntax with the flexibility of vanilla JS. ๐
<div id="example-render"></div>
<script>
const myElement = document.querySelector('#example-render');
$(myElement, { className: 'bg-yellow-300 rounded my-3 p-3' }, [
$('div', {}, 'Hello world!'),
$('ul', { className: 'list-disc pl-5' }, [
$('li', {}, 'Foo'),
$('li', {}, ['Bar ', $('b', { className: 'font-bold' }, 'Baz')]),
]),
]);
</script>
By default, there is no re-rendering at all.
You control which HTML elements should be re-rendered.
<div id="example-state"></div>
<script>
const MyComponent = (initialCounter = 1) => {
const counter = $.createState(initialCounter);
return $('div', { className: 'bg-orange-300 p-2 rounded flex gap-2' }, [
$('div',
{ ref: counter.ref }, // ๐ put ref in any element you want to re-render
['Count: ', counter.get]),
$('div',
// ๐ Use function for dynamic props
() => ({
ref: counter.ref,
className: ['size-3 rounded-full', counter.current % 2 ? 'bg-white' : 'bg-black'],
})
),
$('div',
{ className: 'ml-auto' },
'No re-rendered here'),
$('button',
{ onClick: () => counter.set((prev) => prev + 1) },
'Increment'
),
]);
};
$(
document.querySelector('#example-state'),
{ className: 'bg-yellow-300 rounded my-3 p-3 space-y-2' },
[
MyComponent, // A function (a.k.a component)
MyComponent(), // An element
MyComponent(30), // An element
],
);
</script>
Here is an example of a simple to-do app that covers both global & local state.
<div id="example-to-do"></div>
<script>
// Global state
const search = $.createState('');
const searchElement = $('input', {
placeholder: 'Search on both...',
className: 'p-2 rounded w-full',
onInput: (e) => search.set(e.target.value.toLowerCase()),
});
const ToDo = ({ heading, initialToDos }) => {
// Local state
const todos = $.createState(initialToDos);
return $('div', { className: 'flex-1 flex flex-col' }, [
$('h3', { className: 'text-lg font-bold' }, [heading, "'s To-Do:"]),
$(
'ul',
{
className: 'space-y-1 pb-3 pt-1 flex-1',
ref: [todos.ref, search.ref], // ๐ Re-render when todos or search changed
},
() => {
return todos.current.map((todo) => {
if (!todo.title.toLowerCase().includes(search.current)) return null;
return $('li', {}, [
$('div', { className: 'flex gap-1.5' }, [
$('button', {
className: 'px-2 bg-rose-300 rounded',
textContent: 'x',
onClick: () => todos.set((prev) => prev.filter((item) => item !== todo)),
}),
$('div', {}, todo.title),
]),
]);
});
}
),
AddToDo((newTodo) => todos.set((prev) => [...prev, newTodo])),
]);
};
const AddToDo = (onSubmit) => {
const ref = {};
return $(
'form',
{
className: 'bg-orange-300 p-1.5 rounded space-y-1.5 *:w-full *:rounded *:px-2.5 *:py-1',
onSubmit: (e) => {
e.preventDefault();
if (!ref.element.value) {
ref.element.focus();
return;
}
onSubmit({ title: ref.element.value });
e.target.reset();
if (searchElement.value) {
search.set('');
searchElement.value = '';
}
},
},
[
$('input', { ref, placeholder: 'Input title...', className: 'p-2 rounded' }),
$('button', { className: 'px-2 bg-emerald-200 rounded' }, ['Add']),
]
);
};
$(
document.querySelector('#example-to-do'),
{ className: 'p-2 bg-yellow-300 rounded' },
[
searchElement,
$('div', { className: 'flex gap-2 pt-3 items-stretch' }, [
ToDo({
heading: 'Foo',
initialToDos: [{ title: 'Buy milk' }, { title: 'Buy tomato' }, { title: 'Shear sheep' }, { title:'Catch fish' }],
}),
ToDo({
heading: 'Bar',
initialToDos: [{ title: 'Eat tomato' }, { title: 'Buy chocolate' }],
}),
]),
]
);
</script>
You can copy the code here:
https://github.com/afiiif/dom-dom-factory/blob/main/lib/dom.ts
Add this minified script ๐ Only 1.7 KB
{let v=(e,p={},u=[])=>{let y=e instanceof HTMLElement?e:document.createElement(e),m=(e="all")=>{var t="function"==typeof p?p():p;let r=new Map;if("all"===e||"props"===e)for(var[n,a]of Object.entries(t))if(void 0!==a)if("ref"===n){var i,o=(e="all")=>{if("all"===e||"props"===e){for(;0<y.attributes.length;)y.removeAttribute(y.attributes[0].name);r.forEach((e,t)=>{y.removeEventListener(t,e)})}"all"!==e&&"children"!==e||(y.innerHTML=""),m(e)};for(i of Array.isArray(a)?a:[a])"function"==typeof i?i({element:y,render:o}):i instanceof Map?i.set(y,o):(i.element=y,i.render=o)}else if(n.startsWith("on")){var s=n.substring(2).toLowerCase();y.addEventListener(s,a),r.set(s,a)}else if("className"===n)a&&(s=Array.isArray(a)?a:[a],y.className=s.filter(Boolean).join(" "));else if("style"===n)for(var[f,l]of Object.entries(a))void 0!==l&&(y.style[f]=l);else if("data"===n)for(var[c,d]of Object.entries(a))void 0!==d&&(y.dataset[c]=d);else n.startsWith("aria")?y.setAttribute(n,String(a)):y[n]=a;"all"!==e&&"children"!==e||v.append(y,u)};return m(),y};v.append=(e,t)=>{var r,n;for(r of Array.isArray(t)?t:[t])r instanceof HTMLElement?e.appendChild(r):"string"==typeof r||"number"==typeof r?e.appendChild(document.createTextNode(String(r))):"function"==typeof r&&((n=r())instanceof HTMLElement?e.appendChild(n):"string"==typeof n||"number"==typeof n?e.appendChild(document.createTextNode(String(n))):Array.isArray(n)&&v.append(e,n));return e},v.createRef=()=>{let e=new Map;return Object.assign(e,{render:t=>{e.forEach(e=>e(t))}})},v.createState=e=>{let t=v.createRef(),r={current:e,ref:t,get:()=>r.current,set:e=>{r.current="function"==typeof e?e(r.current):e,t.render()}};return r},window.$=v}