Build blazing-fast, reactive UIs in pure TypeScript/JavaScript – no virtual DOM.
Aberdeen offers a refreshingly simple approach to reactive UIs. Its core idea:
Use many small, anonymous functions for emitting DOM elements, and automatically rerun them when their underlying proxied data changes. This proxied data can be anything from simple values to complex, typed, and deeply nested data structures.
Now, let's dive into why this matters...
First, let's start with the obligatory reactive counter example. If you're reading this on the official website you should see a working demo below the code, and an 'edit' button in the top-right corner of the code, to play around.
import {$, proxy, ref} from 'aberdeen';
// Define some state as a proxied (observable) object
const state = proxy({question: "How many roads must a man walk down?", answer: 42});
$('h3', () => {
// This function reruns whenever the question or the answer changes
$(`:${state.question} ↪ ${state.answer || 'Blowing in the wind'}`)
});
// Two-way bind state.question to an <input>
$('input', {placeholder: 'Question', bind: ref(state, 'question')})
// Allow state.answer to be modified using both an <input> and buttons
$('div.row', {$marginTop: '1em'}, () => {
$('button:-', {click: () => state.answer--});
$('input', {type: 'number', bind: ref(state, 'answer')})
$('button:+', {click: () => state.answer++});
});
Okay, next up is a somewhat more complex app - a todo-list with the following behavior:
Pfew.. now let's look at the code:
import {$, proxy, onEach, insertCss, peek, observe, unproxy, ref} from "aberdeen";
import {grow, shrink} from "aberdeen/transitions";
// We'll use a simple class to store our data.
class TodoItem {
constructor(public label: string = '', public done: boolean = false) {}
toggle() { this.done = !this.done; }
}
// The top-level user interface.
function drawMain() {
// Add some initial items. We'll wrap a proxy() around it!
let items: TodoItem[] = proxy([
new TodoItem('Make todo-list demo', true),
new TodoItem('Learn Aberdeen', false),
]);
// Draw the list, ordered by label.
onEach(items, drawItem, item => item.label);
// Add item and delete checked buttons.
$('div.row', () => {
$('button:+', {
click: () => items.push(new TodoItem("")),
});
$('button.outline:Delete checked', {
click: () => {
for(let idx in items) {
if (items[idx].done) delete items[idx];
}
}
});
});
};
// Called for each todo list item.
function drawItem(item) {
// Items without a label open in editing state.
// Note that we're creating this proxy outside the `div.row` scope
// create below, so that it will persist when that state reruns.
let editing: {value: boolean} = proxy(item.label == '');
$('div.row', todoItemStyle, {create:grow, destroy: shrink}, () => {
// Conditionally add a class to `div.row`, based on item.done
$({".done": ref(item,'done')});
// The checkmark is hidden using CSS
$('div.checkmark:✅');
if (editing.value) {
// Label <input>. Save using enter or button.
function save() {
editing.value = false;
item.label = inputElement.value;
}
let inputElement = $('input', {
placeholder: 'Label',
value: item.label,
keydown: e => e.key==='Enter' && save(),
});
$('button.outline:Cancel', {click: () => editing.value = false});
$('button:Save', {click: save});
} else {
// Label as text.
$('p:' + item.label);
// Edit icon, if not done.
if (!item.done) {
$('a:Edit', {
click: e => {
editing.value = true;
e.stopPropagation(); // We don't want to toggle as well.
},
});
}
// Clicking a row toggles done.
$({click: () => item.done = !item.done, $cursor: 'pointer'});
}
});
}
// Insert some component-local CSS, specific for this demo.
const todoItemStyle = insertCss({
marginBottom: "0.5rem",
".checkmark": {
opacity: 0.2,
},
"&.done": {
textDecoration: "line-through",
".checkmark": {
opacity: 1,
},
},
});
// Go!
drawMain();
Some further examples:
And you may want to study the examples above, of course!