Tutorial
title: Tutorial
Section titled “title: Tutorial”Tutorial
Section titled “Tutorial”Creating elements
Section titled “Creating elements”This is a complete Aberdeen application:
import A from 'aberdeen';A('h3#Hello world');It adds a <h3>Hello world</h3> element to the <body> (which is the default mount point).
The {@link aberdeen.A} function accepts various forms of arguments, which can be combined.
When a string is passed:
- The inital part (if any) is the name of the element to be created.
- One or multiple CSS classes can be added to the ‘current’ element, by prefixing them with a
.. - Content text can be added by prefixing it with a
#.
Instead of the # prefix for text content, you can also use the text= property, like this: A('h3 text="Hello world"'). The double quotes are needed here only because our text contains a space.
For simple formatting, use rich= which supports *italic*, **bold**, `code`, and [links](https://github.com/vanviegen/aberdeen/blob/master/url):
A('p rich="This is *italic*, **bold**, and `code` with a [link](https://github.com/vanviegen/aberdeen/blob/master/path)."');A() can accept multiple strings, so the following lines are equivalent:
A('button.outline.secondary#Pressing me does nothing!');A('button', '.outline', '.secondary', '#Pressing me does nothing!');Also, we can create multiple nested DOM elements in a single {@link aberdeen.A} invocation, if the parents need to have only a single child. For instance:
A('div.box', '#Text within the div element...', 'input');Note that you can play around, modifying any example while seeing its live result by pressing the Edit button that appears when hovering over an example!
In order to pass in additional properties and attributes to the ‘current’ DOM element, we can use the key=value or key=, value syntax. So to extend the above example:
A('div.box id=cityContainer input value=London placeholder=City');Note that value doesn’t become an HTML attribute. This (together with selectedIndex) is one of two special cases, where Aberdeen applies it as a DOM property instead, in order to preserve the variable type (as attributes can only be strings).
When a value ends with =, the next argument is used as its value. This is used for dynamic values and event listeners. So to always log the current input value to the console you can do:
A('div.box input value=Marshmallow input=', el => console.log(el.target.value));Note that the example is interactive - try typing something!
Note: {@link aberdeen.A} also accepts object syntax as an alternative to strings (see the API reference), but the string syntax shown here is more concise and is recommended for most use cases.
Inline styles
Section titled “Inline styles”To set inline CSS styles on elements, use the property:value (short form) or property: value containing spaces; (long form) syntax:
A('p color:red padding:8px background-color:#a882 border: 2px solid #a884; #Styled text');Property shortcuts
Section titled “Property shortcuts”Aberdeen provides shortcuts for commonly used CSS properties, making your code more concise.
| Shortcut | Expands to |
|---|---|
m, mt, mb, ml, mr | margin, margin-top, margin-bottom, margin-left, margin-right |
mv, mh | Vertical (top+bottom) or horizontal (left+right) margins |
p, pt, pb, pl, pr | padding, padding-top, padding-bottom, padding-left, padding-right |
pv, ph | Vertical or horizontal padding |
w, h | width, height |
bg | background |
fg | color |
r | border-radius |
A('div mv:10px ph:20px bg:lightblue r:10% #Styled box');CSS variables
Section titled “CSS variables”Values starting with $ expand to native CSS custom properties via var(--name). The {@link aberdeen.cssVars | A.cssVars} object offers a convenient way of setting and updating CSS custom properties at the :root level.
When you add the first property to A.cssVars, Aberdeen automatically creates a reactive <style> tag in <head> containing the CSS custom property declarations.
import A from 'aberdeen';
A.cssVars.primary = '#3b82f6';A.cssVars.danger = '#ef4444';A.cssVars.textLight = '#f8fafc';
A('button bg:$primary fg:$textLight #Primary');A('button bg:$danger fg:$textLight #Danger');The above generates CSS like background: var(--primary) and automatically injects a :root style defining the actual values. Since this uses native CSS custom properties, changes to A.cssVars automatically propagate to all elements using those values.
Spacing variables
Section titled “Spacing variables”You can optionally initialize A.cssVars with keys 1 through 12 mapping to an exponential rem scale using {@link aberdeen.setSpacingCssVars | A.setSpacingCssVars}. Since CSS custom property names can’t start with a digit, numeric keys are prefixed with m (e.g., $3 becomes var(--m3)):
import A from 'aberdeen';
A.setSpacingCssVars(); // Default: base=1, unit='rem'// Or customize: A.setSpacingCssVars(16, 'px') or A.setSpacingCssVars(1, 'em')| Value | CSS Output | Result (default) |
|---|---|---|
$1 | var(--m1) | 0.25rem |
$2 | var(--m2) | 0.5rem |
$3 | var(--m3) | 1rem |
$4 | var(--m4) | 2rem |
$5 | var(--m5) | 4rem |
| … | … | 2^(n-3) rem |
A('div mt:$3 ph:$4 #This text has 1rem top margin, 2rem left+right padding');If you want different spacing, you can customize the base and unit when calling A.setSpacingCssVars(), or dynamically modify the values.
These shortcuts and variables are also available when using {@link aberdeen.insertCss | A.insertCss}.
Nesting content
Section titled “Nesting content”Of course, putting everything in a single {@link aberdeen.A} call will get messy soon, and you’ll often want to nest more than one child within a parent. To do that, you can pass in a content function to {@link aberdeen.A}, like this:
A('div.box.row id=cityContainer', () => { A('input value=London placeholder=City'); A('button text=Confirm click=', () => alert("You got it!"));});Why are we passing in a function instead of just, say, an array of children? I’m glad you asked! :-) For each such function Aberdeen will create an observer, which will play a major part in what comes next…
Observable objects
Section titled “Observable objects”Aberdeen’s reactivity system is built around observable objects. These are created using the {@link aberdeen.proxy | A.proxy} function:
By convention variables that hold proxied values are prefixed with $ so reactive reads stand out.
When you access properties of a proxied object within an observer function (the function passed to {@link aberdeen.A}), Aberdeen automatically tracks these dependencies. If the values change later, the observer function will re-run, updating only the affected parts of the DOM.
import A from 'aberdeen';
const $user = A.proxy({ name: 'Alice', age: 28, city: 'Aberdeen',});
A('div', () => { A(`h3#Hello, ${$user.name}!`); A(`p#You are ${$user.age} years old.`);});
setInterval(() => { $user.name = 'Bob'; $user.age++;}, 2000);As the content function of our div is subscribed to both $user.name and $user.age, modifying either of these would trigger a re-run of that function, first undoing any side-effects (most notably: inserting DOM elements) of the earlier run. If, however $user.city is changed, no re-run would be triggered as the function is not subscribed to that property.
So if either property changes, both the <h3> and <p> are recreated as the inner most observer function tracking the changes is re-run. If you want to redraw on an even granular level, you can of course:
const $user = A.proxy({ name: 'Alice', age: 28,});
A('div', () => { A(`h3`, () => { console.log('Name draws:', $user.name) A(`#Hello, ${$user.name}!`); }); A(`p`, () => { console.log('Age draws:', $user.age) A(`#You are ${$user.age} years old.`); });});
setInterval(() => { $user.age++;}, 2000);Now, updating $user.name would only cause the Hello text node to be replaced, leaving the <div>, <h3> and <p> elements as they were.
Conditional rendering
Section titled “Conditional rendering”Within an observer function (such as created by passing a function to {@link aberdeen.A}), you can use regular JavaScript logic. Like if and else, for instance:
const $user = A.proxy({ loggedIn: false});
A('div', () => { if ($user.loggedIn) { A('button.outline text=Logout click=', () => $user.loggedIn = false); } else { A('button text=Login click=', () => $user.loggedIn = true); }});Observable primitive values
Section titled “Observable primitive values”The {@link aberdeen.proxy | A.proxy} method wraps an object in a JavaScript Proxy. As this doesn’t work for primitive values (like numbers, strings and booleans), the method will create an object in order to make it observable. The observable value is made available as its .value property.
const $count = A.proxy(42);A('div.row', () => { // This scope will not have to redraw A('button text=- click=', () => $count.value--); A('div text=', $count); A('button text=+ click=', () => $count.value++);});The reason the div.row scope doesn’t redraw when $count.value changes is that we’re passing the entire $count observable object to the text: property. Aberdeen then internally subscribes to $count.value for just that text node, ensuring minimal updates.
If we would have done A('div', {text: $count.value}); instead, we would have subscribed to $count.value within the div.row scope, meaning we’d be redrawing the two buttons and the div every time the count changes.
This also works for other properties, such as inline styles:
import A from 'aberdeen';
const $textColor = A.proxy('blue');
A('div.box color:', $textColor, '#Click me to change color', 'click=', () => { $textColor.value = $textColor.value === 'blue' ? 'red' : 'blue';});This way, when $textColor.value changes, only the style is updated without recreating the element.
Observable arrays and sets
Section titled “Observable arrays and sets”You can create observable arrays too. They work just like regular arrays, apart from being observable.
const $items = A.proxy([1, 2, 3]);
A('h3', () => { // This subscribes to the length of the array and to the value at `$items.length-1` in the array. A('#Last item: '+$items[$items.length-1]);})
A('ul', () => { // This subscribes to the entire array, and thus redraws all <li>s when any item changes. // In the next section, we'll learn about a better way. for (const item of $items) { A(`li#Item ${item}`); }});
A('button text=Add click=', () => $items.push($items.length+1));Observable Sets work too. They preserve normal Set semantics, including .size. When you iterate them with A.onEach(), by default they are sorted by value (or an error is thrown if the value is not a number, string or an array of those).
const $tags = A.proxy(new Set(['ui', 'tiny']));
A('div', () => { A(`#Tag count: ${$tags.size}`);});
A('ul', () => { A.onEach($tags, tag => { // Ordered by tag A(`li#${tag}`); });});
A('button text=Add fast click=', () => $tags.add('fast'));TypeScript and classes
Section titled “TypeScript and classes”Though this tutorial mostly uses plain JavaScript to explain the concepts, Aberdeen is written in and aimed towards TypeScript.
Class instances, like any other object, can be proxied to make them reactive.
class Widget { constructor(public name: string, public width: number, public height: number) {} grow() { this.width *= 2; } toString() { return `${this.name}Widget (${this.width}x${this.height})`; }}
let $graph: Widget = A.proxy(new Widget('Graph', 200, 100));
A('h3', () => A('#'+$graph));A('button text=Grow click=', () => $graph.grow());The type returned by {@link aberdeen.proxy | A.proxy} matches the input type, meaning the type system does not distinguish proxied and unproxied objects. That makes sense, as they have the exact same methods and properties (though proxied objects may have additional side effects).
Efficient list rendering with A.onEach
Section titled “Efficient list rendering with A.onEach”For rendering lists efficiently, Aberdeen provides the {@link aberdeen.onEach | A.onEach} function. It takes three arguments:
- The array to iterate over.
- A render function that receives the item and its index.
- An optional order function, that returns the value by which the item is to be sorted. By default, the output is sorted by array index.
import A from 'aberdeen';
const $items = A.proxy([]);
const randomInt = (max) => parseInt(Math.random() * max);const randomWord = () => Math.random().toString(36).substring(2, 12).replace(/[0-9]+/g, '').replace(/^\w/, c => c.toUpperCase());
// Make random mutationssetInterval(() => { if (randomInt(3)) $items[randomInt(7)] = {label: randomWord(), prio: randomInt(4)}; else delete $items[randomInt(7)];}, 500);
A('div.row.wide height:250px', () => { A('div.box#By index', () => { A.onEach($items, ($item, index) => { // Called only for items that are created/updated A(`li#${$item.label} (prio ${$item.prio})`) }); }) A('div.box#By label', () => { A.onEach($items, ($item, index) => { A(`li#${$item.label} (prio ${$item.prio})`) }, $item => $item.label); }) A('div.box#By desc prio, then label', () => { A.onEach($items, ($item, index) => { A(`li#${$item.label} (prio ${$item.prio})`) }, $item => [-$item.prio, $item.label]); })})We can also use {@link aberdeen.onEach | A.onEach} to reactively iterate over objects, arrays, Maps and Sets. For objects and Maps, the render and order functions receive (value, key) instead of (value, index). For Sets, they receive only the value. By default, Sets are ordered by that value, which only works for numbers, strings and arrays of those, so Sets of objects need an explicit order function.
const $pairs = A.proxy({A: 'Y', B: 'X',});
const randomWord = () => Math.random().toString(36).substring(2, 12).replace(/[0-9]+/g, '').replace(/^\w/, c => c.toUpperCase());
A('button text="Add item" click=', () => $pairs[randomWord()] = randomWord());
A('div.row.wide margin-top:1em', () => { A('div.box#By key', () => { A.onEach($pairs, (value, key) => { A(`li#${key}: ${value}`) }); }) A('div.box#By desc value', () => { A.onEach($pairs, (value, key) => { A(`li#${key}: ${value}`) }, value => A.invertString(value)); })})Note the use of the provided {@link aberdeen.invertString | A.invertString} function to reverse-sort by a string value.
Two-way binding
Section titled “Two-way binding”Aberdeen makes it easy to create two-way bindings between form elements (the various <input> types, <textarea> and <select>) and your data, by passing an observable object with a .value as bind: property to {@link aberdeen.A}.
To bind to object properties not named .value (e.g., $user.name), use {@link aberdeen.ref | A.ref}. This creates a new observable A.proxy whose .value property directly maps to the specified property (e.g., name) on your original observable object (e.g., $user).
import A from 'aberdeen';
const $user = A.proxy({ name: 'Alice', active: false});
// Text input bindingA('input placeholder=Name bind=', A.ref($user, 'name'));
// Checkbox bindingA('label', () => { A('input type=checkbox bind=', A.ref($user, 'active'));}, '#Active');
// Display the current stateA('div.box', () => { A(`p#Name: ${$user.name} `, () => { // Binding works both ways A('button.outline.secondary#!', { click: () => $user.name += '!' }); }); A(`p#Status: ${$user.active ? 'Active' : 'Inactive'}`);});Through the {@link aberdeen.insertCss | A.insertCss} function, Aberdeen provides a way to create component-local CSS.
For simple single-element styles, you can pass a string directly:
import A from 'aberdeen';
const simpleCard = A.insertCss("bg:#f0f0f0 p:$3 r:8px");A('div', simpleCard, '#Card content');For more complex styles with nested selectors, pass an object where each key is a selector and each value is a style string using the same property:value syntax as inline styles:
import A from 'aberdeen';
// Create a CSS class that can be applied to elementsconst myBoxStyle = A.insertCss({ "&": "border-color:#6936cd background-color:#1b0447", "button": "background-color:#6936cd border:0 transition: box-shadow 0.3s; box-shadow: 0 0 4px #ff6a0044;", "button:hover": "box-shadow: 0 0 16px #ff6a0088;", "@media (max-width: 600px)": "p:$1", // Media query is scoped to myBoxStyle as well});
// myBoxStyle is now something like ".AbdStl1", the name for a generated CSS class.// Here's how to use it:A('div.box', myBoxStyle, 'button#Click me');The "&" selector refers to the element with the generated class itself. Child selectors like "button" are scoped to descendants of that element, while pseudo-selectors like "&:hover" apply to the element itself.
This allows you to create single-file components with advanced CSS rules. The {@link aberdeen.insertGlobalCss | A.insertGlobalCss} function can be used to add CSS without a class prefix - it accepts the same string or object syntax.
Both functions support the same CSS shortcuts and variables as inline styles (see above). For example:
import A from 'aberdeen';A.cssVars.boxBg = '#f0f0e0';A.insertGlobalCss({ "body": "m:0", // Using shortcut for margin "form": "bg:$boxBg mv:$3" // Using background shortcut, CSS variable, and spacing value});Of course, if you dislike JavaScript-based CSS and/or prefer to use some other way to style your components, you can just ignore this Aberdeen function.
Transitions
Section titled “Transitions”Aberdeen allows you to easily apply transitions on element creation and element destruction:
let titleStyle = A.insertCss({ "&": "transition: all 1s ease-out; transform-origin: left center;", "&.faded": "opacity:0", "&.imploded": "transform:scale(0.1)", "&.exploded": "transform:scale(5)"});
const $show = A.proxy(true);A('label', () => { A('input type=checkbox bind=', $show); A('#Show title');});A(() => { if (!$show.value) return; A('h2#(Dis)appearing text', titleStyle, 'create=faded.imploded destroy=faded.exploded');});- The creation transition works by briefly adding the given CSS classes on element creation, and immediately removing them after the initial browser layout has taken place.
- The destruction transition works by delaying the removal of the element from the DOM by two seconds (currently hardcoded - should be enough for any reasonable transition), while adding the given CSS classes.
Though this approach is easy (you just need to provide some CSS), you may require more control over the specifics, for instance in order to animate the layout height (or width) taken by the element as well. (Note how the document height changes in the example above are rather ugly.) For this, create and destroy may be functions instead of CSS class names. For more control, create and destroy can also accept functions. While custom function details are beyond this tutorial, Aberdeen offers ready-made {@link transitions.grow} and {@link transitions.shrink} transition functions (which also serve as excellent examples for creating your own):
import A from 'aberdeen';import { grow, shrink } from 'aberdeen/transitions';
const $items = A.proxy([]);
const randomInt = (max) => parseInt(Math.random() * max);const randomWord = () => Math.random().toString(36).substring(2, 12).replace(/[0-9]+/g, '').replace(/^\w/, c => c.toUpperCase());
// Make random mutationssetInterval(() => { if (randomInt(3)) $items[randomInt(7)] = {label: randomWord(), prio: randomInt(4)}; else delete $items[randomInt(7)];}, 500);
A('div.row.wide height:250px', () => { A('div.box#By index', () => { A.onEach($items, ($item, index) => { A(`li#${$item.label} (prio ${$item.prio})`, {create: grow, destroy: shrink}) }); }) A('div.box#By label', () => { A.onEach($items, ($item, index) => { A(`li#${$item.label} (prio ${$item.prio})`, {create: grow, destroy: shrink}) }, $item => $item.label); }) A('div.box#By desc prio, then label', () => { A.onEach($items, ($item, index) => { A(`li#${$item.label} (prio ${$item.prio})`, {create: grow, destroy: shrink}) }, $item => [-$item.prio, $item.label]); })});Advanced: Peeking without subscribing
Section titled “Advanced: Peeking without subscribing”Sometimes you need to read reactive data inside an observer scope without creating a subscription to that data. The {@link aberdeen.peek | A.peek} function allows you to do this:
import A from 'aberdeen';
const $data = A.proxy({ a: 1, b: 2 });
A(() => { // This scope only re-runs when $data.a changes // Changes to $data.b won't trigger a re-render A(`h2#a == ${$data.a} && b == ${A.peek($data, 'b')}`);});
A(`button text="a++ (will update)" click=`, () => $data.a++);A(`button ml:1rem text="b++ (won't update)" click=`, () => $data.b++);You can also pass a function to A.peek() to execute it without any subscriptions:
const $a = A.proxy(42);const $b = A.proxy(7);const sum = A.peek(() => $a.value + $b.value); // Reads both without subscribingA('#Sum is: '+sum);setInterval(() => $a.value++, 1000); // Won't updateThis can be useful to avoid rerenders (of even rerender loops) when you only need a point-in-time snapshot of some reactive data.
Derived values
Section titled “Derived values”An observer scope doesn’t need to create DOM elements. It may also perform other side effects, such as modifying other observable objects. For instance:
// NOTE: See below for a better way.const $original = A.proxy(1);const $derived = A.proxy();A(() => { $derived.value = $original.value * 42;});
A('h3 text=', $derived);A('button text=Increment click=', () => $original.value++);The {@link aberdeen.derive | A.derive} function makes the above a little easier. It works just like passing a function to {@link aberdeen.A}, creating an observer, the only difference being that the value returned by the function is reactively assigned to the value property of the observable object returned by derive. So the above could also be written as:
const $original = A.proxy(1);const $derived = A.derive(() => $original.value * 42);
A('h3 text=', $derived);A('button text=Increment click=', () => $original.value++);For deriving values from (possibly large) arrays, objects, Maps or Sets, Aberdeen provides specialized functions that enable fast, incremental updates to derived data: {@link aberdeen.map | A.map} (each item becomes zero or one derived item), {@link aberdeen.multiMap | A.multiMap} (each item becomes any number of derived items), {@link aberdeen.count | A.count} (reactively counts the number of object properties or collection items), {@link aberdeen.isEmpty | A.isEmpty} (true when the object/array/Map/Set has no items) and {@link aberdeen.partition | A.partition} (sorts each item into one or more buckets). An example:
import A from 'aberdeen';
// Create some random dataconst $people = A.proxy({});const randomInt = (max) => parseInt(Math.random() * max);setInterval(() => { $people[randomInt(250)] = {height: 150+randomInt(60), weight: 45+randomInt(90)};}, 250);
// Do some mapping, counting and observingconst $totalCount = A.count($people);const $bmis = A.map($people, $person => Math.round($person.weight / (($person.height/100) ** 2)));const $overweightBmis = A.map($bmis, // Use A.map() as a filter bmi => bmi > 25 ? bmi : undefined);const $overweightCount = A.count($overweightBmis);const $message = A.derive( () => `There are ${$totalCount.value} people, of which ${$overweightCount.value} are overweight.`);
// Show the resultsA('p text=', $message);A(() => { // isEmpty only causes a re-run when the count changes between zero and non-zero if (A.isEmpty($overweightBmis)) return; A('p#These are their BMIs:', () => { A.onEach($overweightBmis, bmi => A('# '+bmi), bmi => -bmi); // Sort by descending BMI });})UI Components
Section titled “UI Components”UI Components in Aberdeen are just functions, named draw<Something> by convention, that use {@link aberdeen.A} to create some DOM structure. They can accept arguments, return (proxied) values and create local (proxied) state just like any other function.
function drawCounter(initialValue = 0) { const $count = A.proxy(initialValue); A('div.row', () => { A('button text=- click=', () => $count.value--); A('div text=', $count); A('button text=+ click=', () => $count.value++); }); return $count; // Return the reactive count value for external use}// Create multiple independent instances:drawCounter();const $second = drawCounter(42);A('input value=', $second); // Bind the second counter to an input fieldDebugging with A.dump()
Section titled “Debugging with A.dump()”The {@link aberdeen.dump | A.dump} function creates a live, interactive tree view of any data structure in the DOM. It’s particularly useful for debugging reactive state:
import A from 'aberdeen';
const $state = A.proxy({ user: { name: 'Frank', kids: 1 }, items: ['a', 'b']});
A('h2#Live State Dump');A.dump($state);
// The A.dump updates automatically as $state changesA('button text="Update state" click=', () => { $state.user.kids++; $state.items.push('new');});The A.dump renders recursively using <ul> and <li> elements, showing all properties and their values. It updates reactively when any proxied data changes. It is intended for debugging, though with some CSS styling you may find it useful in some simple real-world scenarios as well.
html-to-aberdeen
Section titled “html-to-aberdeen”Sometimes, you want to just paste a largish block of HTML into your application (and then maybe modify it to bind some actual data). Having to translate HTML to $ calls manually is little fun, so there’s a tool for that:
npx html-to-aberdeenIt takes HTML on stdin (paste it and press ctrl-d for end-of-file), and outputs JavaScript on stdout.
Caveat: This tool has been vibe coded (thanks Claude!) with very little code review. As it doesn’t use the filesystem nor the network, I’d say it’s safe to use though! :-) Also, it happens to work pretty well.
Routing
Section titled “Routing”Aberdeen provides an optional built-in router via the {@link route} module. The router is reactive and integrates seamlessly with browser history.
The {@link route.current} object is an observable that reflects the current URL:
import A from 'aberdeen';import * as route from 'aberdeen/route';
A(() => { A(`p#Path string: ${route.current.path}`); // eg "/example/123" A(`p#Path segments: ${JSON.stringify(route.current.p)}`); // eg ["example", "123"]});To navigate programmatically, use {@link route.go}:
import A from 'aberdeen';import * as route from 'aberdeen/route';console.log('pn', location.protocol, location.host, location.hostname, location.pathname);
A('button#Go to settings', { click: () => route.go('/settings')});
// Or using path segmentsA('button ml:1rem #Go to user 123', { click: () => route.go({p: ['users', 123]})});For convenience, you can call {@link route.interceptLinks} once to automatically convert clicks on local <a> tags into Aberdeen routing, so you can use regular anchor tags without manual click handlers. Example: A('a href=/settings text=Settings').
import A from 'aberdeen';import * as route from 'aberdeen/route';
route.interceptLinks(); // Just once on startup:
A('a role=button href=/settings #Go to settings')The {@link route.push} function is useful for overlays that should be closeable with browser back:
import A from 'aberdeen';import * as route from 'aberdeen/route';
A('button#Open modal', { click: () => route.push({state: {modal: 'settings'}})});
A(() => { if (!route.current.state.modal) return; A('div.modal-overlay', { click: () => route.back({state: {modal: undefined}}) }, () => { A('div.modal#Modal content here'); });});Optionally, you can use the {@link dispatcher.Dispatcher} class for declarative routing. It allows you to register route patterns with associated handler functions, which are invoked when the current route matches the pattern. It can match typed parameters and rest parameters.
Prediction
Section titled “Prediction”When building interactive applications with client-server communication, Aberdeen’s prediction system allows for optimistic UI updates. The {@link prediction.applyPrediction} function records changes to any proxied objects made within its callback. These changes are treated as predictions that may later be confirmed or reverted based on server responses. When a server response arrives, the {@link prediction.applyCanon} function applies authoritative changes from the server, reverting any conflicting predictions while attempting to reapply non-conflicting ones.
Full Example: Multi-page App
Section titled “Full Example: Multi-page App”Here’s a complete example (a contact manager) demonstrating routing, state management, CSS, dark mode, and dynamic content:
import A from 'aberdeen';import * as route from 'aberdeen/route';import { Dispatcher } from 'aberdeen/dispatcher';import { grow, shrink } from 'aberdeen/transitions';
class Contact { constructor( public id: number, public firstName: string, public lastName: string, public email: string, public phone: string ) {}}
// Enable link interception for SPA navigationroute.interceptLinks();
// Initialize $1-$12 CSS variables for consistent spacing ($2=0.5rem, $3=1rem, $4=2rem, etc.)A.setSpacingCssVars();
// Reactive theme based on system preferenceA(() => { A.cssVars.primary = '#2563eb'; A.cssVars.bg = A.darkMode() ? '#0f172a' : '#ffffff'; A.cssVars.fg = A.darkMode() ? '#e2e8f0' : '#1e293b'; A.cssVars.cardBg = A.darkMode() ? '#1e293b' : '#f8fafc'; A.cssVars.border = A.darkMode() ? '#334155' : '#e2e8f0';});
// Global styles for semantic HTML elements that apply everywhereA.insertGlobalCss({ "*": "m:0 p:0", "body": "bg:$bg fg:$fg font-family: system-ui, sans-serif;", "a": "color:$primary text-decoration:none", "a:hover": "text-decoration:underline", "a[role=button]": "bg:$primary fg:white r:8px p:$2",});
// Application stateconst $contacts = A.proxy([ new Contact(1, 'Emma', 'Wilson', 'emma.wilson@email.com', '555-0101'), new Contact(2, 'James', 'Anderson', 'j.anderson@email.com', '555-0102'), new Contact(3, 'Sofia', 'Martinez', 'sofia.m@email.com', '555-0103'), new Contact(4, 'Liam', 'Brown', 'liam.brown@email.com', '555-0104')]);
// Router setupconst dispatcher = new Dispatcher();dispatcher.addRoute(drawHome);dispatcher.addRoute('contacts', drawContactList);dispatcher.addRoute('contacts', Number, drawContactDetail);
// Main appA('div.app', () => { A('nav display:flex gap:$3 p:$3 border-bottom: 1px solid $border;', () => { A('a href=/ text=Home font-weight:', route.current.p.length === 0 ? 'bold' : 'normal'); A('a href=/contacts text=Contacts font-weight:', route.current.p[0] === 'contacts' ? 'bold' : 'normal'); }); A('main p:$3', () => dispatcher.dispatch(route.current.p));});
function drawHome() { A('h1#Contact Manager'); A('p#A modern contact list with search, sort, and dark mode support.');}
// Contact card stylesconst cardStyle = A.insertCss({ "&": "bg:$cardBg border: 1px solid $border; r:8px p:$3 mv:$2 display:block transition: transform 0.2s;", "&:hover": "transform:translateX(4px)", "a&": "color:inherit;",});
const filterStyle = A.insertCss({ "&": "display:flex gap:$3 mv:$3", "> *": "p:$2 r:4px bg:$bg fg:$fg border: 1px solid $border;",});
function drawContactList() { A('h1#Contacts');
// Search and sort controls A('div', filterStyle, () => { A('input flex:1 placeholder="Search contacts..." bind=', A.ref(route.current.search, 'q')); A('select bind=', A.ref(route.current.search, 'sort'), () => { A('option value=firstName #First Name'); A('option value=lastName #Last Name'); A('option value=email #Email'); }); });
// Contact list A('div', () => { const sortBy = route.current.search.sort || 'firstName';
const $filtered = A.map($contacts, $contact => { const query = route.current.search.q; if (query) { const info = `${$contact.firstName} ${$contact.lastName} ${$contact.email}`; if (!info.toLowerCase().includes(query.toLowerCase())) return; // Skip! } return $contact; });
A.onEach($filtered, $contact => { A('a', cardStyle, 'create=', grow, 'destroy=', shrink, `href=/contacts/${$contact.id}`, () => { A('h2', () => { A('span font-weight:normal text=', $contact.firstName+" "); A('span text=', $contact.lastName); }); A('div text=', $contact.email); }); }, $contact => $contact[sortBy].toLowerCase());
A(`a role=button mt:$3 text="Add new contact" href=/contacts/${$contacts.length}`); });}
// Detail form stylesconst detailStyle = A.insertCss({ "&": "bg:$cardBg border: 1px solid $border; r:8px p:$4 max-width:600px", "label": "display:block font-weight:600 mt:$3 mb:$2", "input": "w:100% p:$2 r:4px border: 1px solid $border; bg:$bg fg:$fg"});
function drawContactDetail(id: number) { $contacts[id] ||= new Contact(id, '', '', '', ''); const $contact = $contacts[id];
A('a role=button href=/contacts #← Back');
A('div mt:$3', detailStyle, () => { A('h2 mb:$2 text=', A.ref($contact, 'firstName'), 'text=', ' ', 'text=', A.ref($contact, 'lastName')); A('label text="First Name" input bind=', A.ref($contact, 'firstName')); A('label text="Last Name" input bind=', A.ref($contact, 'lastName')); A('label text="Email" input type=email bind=', A.ref($contact, 'email')); A('label text="Phone" input type=tel bind=', A.ref($contact, 'phone')); });}Further reading
Section titled “Further reading”If you’ve understood all/most of the above, you should be ready to get going with Aberdeen! You may also find these links helpful: