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++});
});
What follows is a somewhat more complex app, Tic-tac-toe with undo history.
import {$, proxy, onEach, insertCss, observe} from "aberdeen";
// UI drawing functions.
function drawMain() {
// Define our state, wrapped by an observable proxy
const history = proxy({
boards: [[]], // eg. [[], [undefined, 'O', undefined, 'X'], ...]
current: 0, // indicates which of the boards is currently showing
});
$('main.row', () => {
$('div.box', () => drawBoard(history));
$('div.box', {$flex: 1}, () => {
drawStatusMessage(history);
drawTurns(history);
});
});
}
function drawBoard(history) {
$('div', boardStyle, () => {
for(let pos=0; pos<9; pos++) {
$('button.square', () => {
let marker = getBoard(history)[pos];
if (marker) {
$({ text: marker });
} else {
$({ click: () => markSquare(history, pos) });
}
});
}
})
}
function drawStatusMessage(history) {
$('h4', () => {
// Reruns whenever observable data read by calculateWinner or getCurrentMarker changes
const board = getBoard(history);
const winner = calculateWinner(board);
if (winner) {
$(`:Winner: ${winner}!`);
} else if (board.filter(square=>square).length === 9) {
$(`:It's a draw...`);
} else {
$(`:Current player: ${getCurrentMarker(board)}`);
}
});
}
function drawTurns(history) {
$('div:Select a turn:')
// Reactively iterate all (historic) board versions
onEach(history.boards, (_, index) => {
$('button', {
// A text node:
text: index,
// Conditional css class:
".outline": observe(() => history.current != index),
// Inline styles:
$marginRight: "0.5em",
$marginTop: "0.5em",
// Event listener:
click: () => history.current = index,
});
});
}
// Helper functions
function calculateWinner(board) {
const lines = [
[0, 1, 2], [3, 4, 5], [6, 7, 8], // horizontal
[0, 3, 6], [1, 4, 7], [2, 5, 8], // vertical
[0, 4, 8], [2, 4, 6] // diagonal
];
for (const [a, b, c] of lines) {
if (board[a] && board[a] === board[b] && board[a] === board[c]) {
return board[a];
}
}
}
function getCurrentMarker(board) {
return board.filter(v => v).length % 2 ? "O" : "X";
}
function getBoard(history) {
return history.boards[history.current];
}
function markSquare(history, position) {
const board = getBoard(history);
// Don't allow markers when we already have a winner
if (calculateWinner(board)) return;
// Copy the current board, and insert the marker into it
const newBoard = board.slice();
newBoard[position] = getCurrentMarker(board);
// Truncate any future states, and write a new future
history.current++;
history.boards.length = history.current;
history.boards.push(newBoard);
}
// Define component-local CSS, which we'll utilize in the drawBoard function.
// Of course, you can use any other styling solution instead, if you prefer.
const boardStyle = insertCss({
display: 'grid',
gap: '0.5em',
gridTemplateColumns: '1fr 1fr 1fr',
'> *': {
width: '2em',
height: '2em',
padding: 0,
},
});
// Fire it up! Mounts on document.body by default..
drawMain();
Some further examples:
And you may want to study the examples above, of course!