aberdeen 1.5.0 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/aberdeen.d.ts +71 -9
- package/dist/aberdeen.js +47 -24
- package/dist/aberdeen.js.map +3 -3
- package/dist/route.js +4 -4
- package/dist/route.js.map +3 -3
- package/dist-min/aberdeen.js +7 -7
- package/dist-min/aberdeen.js.map +3 -3
- package/dist-min/route.js.map +2 -2
- package/package.json +5 -2
- package/skill/SKILL.md +579 -204
- package/skill/aberdeen.md +2322 -0
- package/skill/dispatcher.md +126 -0
- package/skill/prediction.md +73 -0
- package/skill/route.md +249 -0
- package/skill/transitions.md +59 -0
- package/src/aberdeen.ts +121 -35
- package/src/route.ts +3 -3
- package/skill/references/prediction.md +0 -45
- package/skill/references/routing.md +0 -81
- package/skill/references/transitions.md +0 -52
package/skill/SKILL.md
CHANGED
|
@@ -1,290 +1,665 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: aberdeen
|
|
3
|
-
description: Expert guidance for building reactive UIs with the Aberdeen library. Covers element creation
|
|
3
|
+
description: Expert guidance for building reactive UIs with the Aberdeen library. Covers element creation, reactive state management, efficient list rendering, CSS integration, routing, transitions, and optimistic updates.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
Aberdeen is a reactive UI library using fine-grained reactivity via JavaScript Proxies. No virtual DOM, no build step required.
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
# Guidance for AI Assistants
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
10
|
+
1. **Use string syntax by default** - `$('div.box#Hello')` is more concise than object syntax
|
|
11
|
+
2. **Never concatenate user data** - Use `$('input value=', data)` not `$('input value=${data}')`
|
|
12
|
+
3. **Pass observables directly** - Use `text=', ref(obj, 'key')` to avoid parent scope subscriptions
|
|
13
|
+
4. **Use `onEach` for lists** - Never iterate proxy arrays with `for`/`map` in render functions
|
|
14
|
+
5. **Class instances are great** - Better than plain objects for typed, structured state
|
|
15
|
+
6. **CSS shortcuts** - Use `@3`, `@4` for spacing (1rem, 2rem), `@primary` for colors
|
|
16
|
+
7. **Minimal scopes** - Smaller reactive scopes = fewer DOM updates
|
|
17
|
+
|
|
18
|
+
# Obtaining info
|
|
19
|
+
|
|
20
|
+
The complete tutorial follows below. For detailed API reference open these files within the skill directory:
|
|
21
|
+
|
|
22
|
+
- **[aberdeen](aberdeen.md)** - Core: `$`, `proxy`, `onEach`, `ref`, `derive`, `map`, `multiMap`, `partition`, `count`, `isEmpty`, `peek`, `dump`, `clean`, `insertCss`, `insertGlobalCss`, `mount`, `runQueue`, `darkMode`
|
|
23
|
+
- **[route](route.md)** - Routing: `current`, `go`, `push`, `back`, `up`, `persistScroll`
|
|
24
|
+
- **[dispatcher](dispatcher.md)** - Path matching: `Dispatcher`, `matchRest`, `matchFailed`
|
|
25
|
+
- **[transitions](transitions.md)** - Animations: `grow`, `shrink`
|
|
26
|
+
- **[prediction](prediction.md)** - Optimistic UI: `applyPrediction`, `applyCanon`
|
|
27
|
+
|
|
28
|
+
# Tutorial
|
|
29
|
+
|
|
30
|
+
## Creating elements
|
|
31
|
+
|
|
32
|
+
This is a complete Aberdeen application:
|
|
33
|
+
|
|
34
|
+
```javascript
|
|
35
|
+
import {$} from 'aberdeen';
|
|
36
|
+
$('h3#Hello world');
|
|
15
37
|
```
|
|
16
38
|
|
|
17
|
-
|
|
18
|
-
```typescript
|
|
19
|
-
// Tag, classes, text content
|
|
20
|
-
$('div.container.active#Hello World');
|
|
39
|
+
It adds a `<h3>Hello world</h3>` element to the `<body>` (which is the default mount point).
|
|
21
40
|
|
|
22
|
-
|
|
23
|
-
$('div.wrapper mt:@3 span.icon');
|
|
41
|
+
The {@link aberdeen.$} function accepts various forms of arguments, which can be combined.
|
|
24
42
|
|
|
25
|
-
|
|
26
|
-
|
|
43
|
+
When a string is passed:
|
|
44
|
+
- The inital part (if any) is the name of the element to be created.
|
|
45
|
+
- One or multiple CSS classes can be added to the 'current' element, by prefixing them with a `.`.
|
|
46
|
+
- Content text can be added by prefixing it with a `#`.
|
|
27
47
|
|
|
28
|
-
|
|
29
|
-
$('input placeholder="Something containing spaces" value=', userInput);
|
|
30
|
-
$('button text=', `Count: ${state.count}`);
|
|
48
|
+
Instead of the `#` prefix for text content, you can also use the `text=` property, like this: `$('h3 text="Hello world"')`. The double quotes are needed here only because our text contains a space.
|
|
31
49
|
|
|
32
|
-
|
|
33
|
-
$('button text=Click click=', () => console.log('clicked'));
|
|
50
|
+
`$()` can accept multiple strings, so the following lines are equivalent:
|
|
34
51
|
|
|
35
|
-
|
|
36
|
-
$('
|
|
37
|
-
|
|
38
|
-
$('li#Item 2');
|
|
39
|
-
});
|
|
52
|
+
```javascript
|
|
53
|
+
$('button.outline.secondary#Pressing me does nothing!');
|
|
54
|
+
$('button', '.outline', '.secondary', '#Pressing me does nothing!');
|
|
40
55
|
```
|
|
41
56
|
|
|
42
|
-
|
|
43
|
-
```typescript
|
|
44
|
-
// WRONG - XSS risk and breaks on special chars
|
|
45
|
-
$(`input value=${userData}`);
|
|
57
|
+
Also, we can create multiple nested DOM elements in a single {@link aberdeen.$} invocation, *if* the parents need to have only a single child. For instance:
|
|
46
58
|
|
|
47
|
-
|
|
48
|
-
$('
|
|
59
|
+
```javascript
|
|
60
|
+
$('div.box', '#Text within the div element...', 'input');
|
|
49
61
|
```
|
|
50
62
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
63
|
+
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!
|
|
64
|
+
|
|
65
|
+
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:
|
|
66
|
+
|
|
67
|
+
```javascript
|
|
68
|
+
$('div.box id=cityContainer input value=London placeholder=City');
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
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).
|
|
72
|
+
|
|
73
|
+
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:
|
|
74
|
+
|
|
75
|
+
```javascript
|
|
76
|
+
$('div.box input value=Marshmallow input=', el => console.log(el.target.value));
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Note that the example is interactive - try typing something!
|
|
80
|
+
|
|
81
|
+
> **Note:** {@link aberdeen.$} 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.
|
|
82
|
+
|
|
83
|
+
## Inline styles
|
|
84
|
+
|
|
85
|
+
To set inline CSS styles on elements, use the `property:value` or `property:`, value syntax:
|
|
86
|
+
|
|
87
|
+
```javascript
|
|
88
|
+
$('div.box color:red backgroundColor:yellow#Styled text');
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## CSS shortcuts
|
|
92
|
+
|
|
93
|
+
Aberdeen provides shortcuts for commonly used CSS properties, making your code more concise.
|
|
94
|
+
|
|
95
|
+
### Property shortcuts
|
|
96
|
+
|
|
97
|
+
Common property names are automatically expanded:
|
|
98
|
+
|
|
99
|
+
| Shortcut | Expands to |
|
|
100
|
+
|----------|------------|
|
|
101
|
+
| `m`, `mt`, `mb`, `ml`, `mr` | `margin`, `marginTop`, `marginBottom`, `marginLeft`, `marginRight` |
|
|
102
|
+
| `mv`, `mh` | Vertical (top+bottom) or horizontal (left+right) margins |
|
|
103
|
+
| `p`, `pt`, `pb`, `pl`, `pr` | `padding`, `paddingTop`, `paddingBottom`, `paddingLeft`, `paddingRight` |
|
|
104
|
+
| `pv`, `ph` | Vertical or horizontal padding |
|
|
105
|
+
| `w`, `h` | `width`, `height` |
|
|
106
|
+
| `bg` | `background` |
|
|
107
|
+
| `fg` | `color` |
|
|
108
|
+
| `r` | `borderRadius` |
|
|
109
|
+
|
|
110
|
+
```javascript
|
|
111
|
+
$('div mv:10px ph:20px bg:lightblue r:10% #Styled box');
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### CSS variables
|
|
115
|
+
|
|
116
|
+
Values starting with `$` expand to native CSS custom properties via `var(--name)`. The {@link aberdeen.cssVars} object offers a convenient way of setting and updating CSS custom properties at the `:root` level.
|
|
117
|
+
|
|
118
|
+
When you add the first property to `cssVars`, Aberdeen automatically creates a reactive `<style>` tag in `<head>` containing the CSS custom property declarations.
|
|
119
|
+
|
|
120
|
+
```javascript
|
|
121
|
+
import { $, cssVars } from 'aberdeen';
|
|
60
122
|
|
|
61
|
-
### Object Syntax (alternative)
|
|
62
|
-
```typescript
|
|
63
|
-
// Equivalent to string syntax, useful for complex cases
|
|
64
|
-
// Note how the '$' prefix is used for CSS properties
|
|
65
|
-
$('input', { placeholder: 'Name', value: userData, $color: 'red' });
|
|
66
|
-
$('button', { click: handler, '.active': isActive });
|
|
67
|
-
```
|
|
68
|
-
|
|
69
|
-
### CSS Property Shortcuts
|
|
70
|
-
| Short | Full | Short | Full |
|
|
71
|
-
|-------|------|-------|------|
|
|
72
|
-
| `m` | margin | `p` | padding |
|
|
73
|
-
| `mt`,`mb`,`ml`,`mr` | marginTop/Bottom/Left/Right | `pt`,`pb`,`pl`,`pr` | paddingTop/... |
|
|
74
|
-
| `mv` | marginTop + marginBottom | `pv` | paddingTop + paddingBottom |
|
|
75
|
-
| `mh` | marginLeft + marginRight | `ph` | paddingLeft + paddingRight |
|
|
76
|
-
| `w` | width | `h` | height |
|
|
77
|
-
| `bg` | background | `fg` | color |
|
|
78
|
-
| `r` | borderRadius | | |
|
|
79
|
-
|
|
80
|
-
### CSS Variables (`@`)
|
|
81
|
-
Values starting with `@` expand to native CSS custom properties via `var(--name)`. Numeric keys are prefixed with `m` (e.g., `@3` → `var(--m3)`).
|
|
82
|
-
|
|
83
|
-
Predefined spacing scale:
|
|
84
|
-
| Var | CSS Output | Value |
|
|
85
|
-
|-----|------------|-------|
|
|
86
|
-
| `@1` | `var(--m1)` | 0.25rem |
|
|
87
|
-
| `@2` | `var(--m2)` | 0.5rem |
|
|
88
|
-
| `@3` | `var(--m3)` | 1rem |
|
|
89
|
-
| `@4` | `var(--m4)` | 2rem |
|
|
90
|
-
| `@5` | `var(--m5)` | 4rem |
|
|
91
|
-
| `@n` | `var(--mn)` | 2^(n-3) rem |
|
|
92
|
-
|
|
93
|
-
**Best practice:** Use `@3` and `@4` for most margins/paddings. For new projects, define color variables:
|
|
94
|
-
```typescript
|
|
95
123
|
cssVars.primary = '#3b82f6';
|
|
96
124
|
cssVars.danger = '#ef4444';
|
|
97
|
-
|
|
125
|
+
cssVars.textLight = '#f8fafc';
|
|
126
|
+
|
|
127
|
+
$('button bg:$primary fg:$textLight #Primary');
|
|
128
|
+
$('button bg:$danger fg:$textLight #Danger');
|
|
98
129
|
```
|
|
99
130
|
|
|
100
|
-
|
|
101
|
-
```typescript
|
|
102
|
-
// Objects (preserves type!)
|
|
103
|
-
const state = proxy({ name: 'Alice', count: 0 });
|
|
131
|
+
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 `cssVars` automatically propagate to all elements using those values.
|
|
104
132
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
flag.value = false;
|
|
133
|
+
#### Predefined spacing
|
|
134
|
+
You can optionally initialize `cssVars` with keys `1` through `12` mapping to an exponential `rem` scale using {@link aberdeen.setSpacingCssVars}. Since CSS custom property names can't start with a digit, numeric keys are prefixed with `m` (e.g., `$3` becomes `var(--m3)`):
|
|
108
135
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
constructor(public text: string, public done = false) {}
|
|
112
|
-
toggle() { this.done = !this.done; }
|
|
113
|
-
}
|
|
114
|
-
const todo: Todo = proxy(new Todo('Learn Aberdeen'));
|
|
115
|
-
todo.toggle(); // Reactive method call!
|
|
136
|
+
```javascript
|
|
137
|
+
import { setSpacingCssVars } from 'aberdeen';
|
|
116
138
|
|
|
117
|
-
//
|
|
118
|
-
|
|
119
|
-
items.push('new');
|
|
120
|
-
delete items[0];
|
|
139
|
+
setSpacingCssVars(); // Default: base=1, unit='rem'
|
|
140
|
+
// Or customize: setSpacingCssVars(16, 'px') or setSpacingCssVars(1, 'em')
|
|
121
141
|
```
|
|
122
142
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
1
|
|
126
|
-
2.
|
|
143
|
+
| Value | CSS Output | Result (default) |
|
|
144
|
+
|-------|------------|------------------|
|
|
145
|
+
| `$1` | `var(--m1)` | 0.25rem |
|
|
146
|
+
| `$2` | `var(--m2)` | 0.5rem |
|
|
147
|
+
| `$3` | `var(--m3)` | 1rem |
|
|
148
|
+
| `$4` | `var(--m4)` | 2rem |
|
|
149
|
+
| `$5` | `var(--m5)` | 4rem |
|
|
150
|
+
| ... | ... | 2^(n-3) rem |
|
|
151
|
+
|
|
152
|
+
```javascript
|
|
153
|
+
$('div mt:$3 ph:$4 #This text has 1rem top margin, 2rem left+right padding');
|
|
154
|
+
```
|
|
127
155
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
156
|
+
If you want different spacing, you can customize the base and unit when calling `setSpacingCssVars()`, or dynamically modify the values.
|
|
157
|
+
|
|
158
|
+
These shortcuts and variables are also available when using {@link aberdeen.insertCss}.
|
|
159
|
+
|
|
160
|
+
## Nesting content
|
|
161
|
+
Of course, putting everything in a single {@link aberdeen.$} 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.$}, like this:
|
|
162
|
+
|
|
163
|
+
```javascript
|
|
164
|
+
$('div.box.row id=cityContainer', () => {
|
|
165
|
+
$('input value=London placeholder=City');
|
|
166
|
+
$('button text=Confirm click=', () => alert("You got it!"));
|
|
132
167
|
});
|
|
133
168
|
```
|
|
134
169
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
170
|
+
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...
|
|
171
|
+
|
|
172
|
+
## Observable objects
|
|
173
|
+
Aberdeen's reactivity system is built around observable objects. These are created using the {@link aberdeen.proxy} function:
|
|
174
|
+
|
|
175
|
+
When you access properties of a proxied object within an observer function (the function passed to {@link aberdeen.$}), Aberdeen automatically tracks these dependencies. If the values change later, the observer function will re-run, updating only the affected parts of the DOM.
|
|
176
|
+
|
|
177
|
+
```javascript
|
|
178
|
+
import { $, proxy } from 'aberdeen';
|
|
179
|
+
|
|
180
|
+
const user = proxy({
|
|
181
|
+
name: 'Alice',
|
|
182
|
+
age: 28,
|
|
183
|
+
city: 'Aberdeen',
|
|
184
|
+
});
|
|
185
|
+
|
|
138
186
|
$('div', () => {
|
|
139
|
-
$(
|
|
140
|
-
$(
|
|
141
|
-
// Or
|
|
142
|
-
$('p#', ref(state, 'body')); // Two-way maps {body: x} to {value: x}, which will be read reactively by $
|
|
187
|
+
$(`h3#Hello, ${user.name}!`);
|
|
188
|
+
$(`p#You are ${user.age} years old.`);
|
|
143
189
|
});
|
|
190
|
+
|
|
191
|
+
setInterval(() => {
|
|
192
|
+
user.name = 'Bob';
|
|
193
|
+
user.age++;
|
|
194
|
+
}, 2000);
|
|
144
195
|
```
|
|
145
196
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
197
|
+
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.
|
|
198
|
+
|
|
199
|
+
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:
|
|
200
|
+
|
|
201
|
+
```javascript
|
|
202
|
+
const user = proxy({
|
|
203
|
+
name: 'Alice',
|
|
204
|
+
age: 28,
|
|
205
|
+
});
|
|
206
|
+
|
|
149
207
|
$('div', () => {
|
|
150
|
-
|
|
151
|
-
|
|
208
|
+
$(`h3`, () => {
|
|
209
|
+
console.log('Name draws:', user.name)
|
|
210
|
+
$(`#Hello, ${user.name}!`);
|
|
211
|
+
});
|
|
212
|
+
$(`p`, () => {
|
|
213
|
+
console.log('Age draws:', user.age)
|
|
214
|
+
$(`#You are ${user.age} years old.`);
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
setInterval(() => {
|
|
219
|
+
user.age++;
|
|
220
|
+
}, 2000);
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
Now, updating `user.name` would only cause the *Hello* text node to be replaced, leaving the `<div>`, `<h3>` and `<p>` elements as they were.
|
|
224
|
+
|
|
225
|
+
## Conditional rendering
|
|
226
|
+
|
|
227
|
+
Within an observer function (such as created by passing a function to {@link aberdeen.$}), you can use regular JavaScript logic. Like `if` and `else`, for instance:
|
|
228
|
+
|
|
229
|
+
```javascript
|
|
230
|
+
const user = proxy({
|
|
231
|
+
loggedIn: false
|
|
152
232
|
});
|
|
153
233
|
|
|
154
234
|
$('div', () => {
|
|
155
|
-
|
|
156
|
-
|
|
235
|
+
if (user.loggedIn) {
|
|
236
|
+
$('button.outline text=Logout click=', () => user.loggedIn = false);
|
|
237
|
+
} else {
|
|
238
|
+
$('button text=Login click=', () => user.loggedIn = true);
|
|
239
|
+
}
|
|
157
240
|
});
|
|
158
241
|
```
|
|
159
242
|
|
|
160
|
-
|
|
243
|
+
## Observable primitive values
|
|
161
244
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
```
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
245
|
+
The {@link aberdeen.proxy} method wraps an object in a JavaScript [Proxy](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/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.
|
|
246
|
+
|
|
247
|
+
```javascript
|
|
248
|
+
const cnt = proxy(42);
|
|
249
|
+
$('div.row', () => {
|
|
250
|
+
// This scope will not have to redraw
|
|
251
|
+
$('button text=- click=', () => cnt.value--);
|
|
252
|
+
$('div text=', cnt);
|
|
253
|
+
$('button text=+ click=', () => cnt.value++);
|
|
169
254
|
});
|
|
170
255
|
```
|
|
171
256
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
257
|
+
The reason the `div.row` scope doesn't redraw when `cnt.value` changes is that we're passing the entire `cnt` observable object to the `text:` property. Aberdeen then internally subscribes to `cnt.value` for just that text node, ensuring minimal updates.
|
|
258
|
+
|
|
259
|
+
If we would have done `$('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.
|
|
260
|
+
|
|
261
|
+
This also works for other properties, such as inline styles:
|
|
262
|
+
|
|
263
|
+
```javascript
|
|
264
|
+
import { $, proxy } from 'aberdeen';
|
|
265
|
+
|
|
266
|
+
const textColor = proxy('blue');
|
|
267
|
+
|
|
268
|
+
$('div.box color:', textColor, '#Click me to change color', 'click=', () => {
|
|
269
|
+
textColor.value = textColor.value === 'blue' ? 'red' : 'blue';
|
|
270
|
+
});
|
|
177
271
|
```
|
|
178
|
-
- Renders only changed items efficiently
|
|
179
|
-
- Sort key: `number | string | [number|string, ...]` or `undefined` to hide item
|
|
180
|
-
- Use `invertString(str)` for descending string sort
|
|
181
|
-
- Works on arrays, objects, and Maps
|
|
182
272
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
273
|
+
This way, when `textColor.value` changes, only the style is updated without recreating the element.
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
## Observable arrays
|
|
277
|
+
|
|
278
|
+
You can create observable arrays too. They work just like regular arrays, apart from being observable.
|
|
279
|
+
|
|
280
|
+
```javascript
|
|
281
|
+
const items = proxy([1, 2, 3]);
|
|
282
|
+
|
|
283
|
+
$('h3', () => {
|
|
284
|
+
// This subscribes to the length of the array and to the value at `items.length-1` in the array.
|
|
285
|
+
$('#Last item: '+items[items.length-1]);
|
|
286
|
+
})
|
|
287
|
+
|
|
288
|
+
$('ul', () => {
|
|
289
|
+
// This subscribes to the entire array, and thus redraws all <li>s when any item changes.
|
|
290
|
+
// In the next section, we'll learn about a better way.
|
|
291
|
+
for (const item of items) {
|
|
292
|
+
$(`li#Item ${item}`);
|
|
293
|
+
}
|
|
190
294
|
});
|
|
295
|
+
|
|
296
|
+
$('button text=Add click=', () => items.push(items.length+1));
|
|
191
297
|
```
|
|
192
298
|
|
|
193
|
-
##
|
|
299
|
+
## TypeScript and classes
|
|
300
|
+
|
|
301
|
+
Though this tutorial mostly uses plain JavaScript to explain the concepts, Aberdeen is written in and aimed towards TypeScript.
|
|
302
|
+
|
|
303
|
+
Class instances, like any other object, can be proxied to make them reactive.
|
|
194
304
|
|
|
195
|
-
### `derive()` - Derived primitives
|
|
196
305
|
```typescript
|
|
197
|
-
|
|
198
|
-
|
|
306
|
+
class Widget {
|
|
307
|
+
constructor(public name: string, public width: number, public height: number) {}
|
|
308
|
+
grow() { this.width *= 2; }
|
|
309
|
+
toString() { return `${this.name}Widget (${this.width}x${this.height})`; }
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
let graph: Widget = proxy(new Widget('Graph', 200, 100));
|
|
313
|
+
|
|
314
|
+
$('h3', () => $('#'+graph));
|
|
315
|
+
$('button text=Grow click=', () => graph.grow());
|
|
199
316
|
```
|
|
200
317
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
318
|
+
The type returned by {@link aberdeen.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).
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
## Efficient list rendering with onEach
|
|
322
|
+
For rendering lists efficiently, Aberdeen provides the {@link aberdeen.onEach} function. It takes three arguments:
|
|
323
|
+
1. The array to iterate over.
|
|
324
|
+
2. A render function that receives the item and its index.
|
|
325
|
+
3. 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.
|
|
326
|
+
|
|
327
|
+
```javascript
|
|
328
|
+
import { $, proxy, onEach } from 'aberdeen';
|
|
329
|
+
|
|
330
|
+
const items = proxy([]);
|
|
331
|
+
|
|
332
|
+
const randomInt = (max) => parseInt(Math.random() * max);
|
|
333
|
+
const randomWord = () => Math.random().toString(36).substring(2, 12).replace(/[0-9]+/g, '').replace(/^\w/, c => c.toUpperCase());
|
|
334
|
+
|
|
335
|
+
// Make random mutations
|
|
336
|
+
setInterval(() => {
|
|
337
|
+
if (randomInt(3)) items[randomInt(7)] = {label: randomWord(), prio: randomInt(4)};
|
|
338
|
+
else delete items[randomInt(7)];
|
|
339
|
+
}, 500);
|
|
340
|
+
|
|
341
|
+
$('div.row.wide height:250px', () => {
|
|
342
|
+
$('div.box#By index', () => {
|
|
343
|
+
onEach(items, (item, index) => {
|
|
344
|
+
// Called only for items that are created/updated
|
|
345
|
+
$(`li#${item.label} (prio ${item.prio})`)
|
|
346
|
+
});
|
|
347
|
+
})
|
|
348
|
+
$('div.box#By label', () => {
|
|
349
|
+
onEach(items, (item, index) => {
|
|
350
|
+
$(`li#${item.label} (prio ${item.prio})`)
|
|
351
|
+
}, item => item.label);
|
|
352
|
+
})
|
|
353
|
+
$('div.box#By desc prio, then label', () => {
|
|
354
|
+
onEach(items, (item, index) => {
|
|
355
|
+
$(`li#${item.label} (prio ${item.prio})`)
|
|
356
|
+
}, item => [-item.prio, item.label]);
|
|
357
|
+
})
|
|
358
|
+
})
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
We can also use {@link aberdeen.onEach} to reactively iterate over *objects*. In that case, the render and order functions receive `(value, key)` instead of `(value, index)` as their arguments.
|
|
205
362
|
|
|
206
|
-
|
|
207
|
-
|
|
363
|
+
```javascript
|
|
364
|
+
const pairs = proxy({A: 'Y', B: 'X',});
|
|
208
365
|
|
|
209
|
-
|
|
210
|
-
const names: string[] = map(users, u => u.active ? u.name : undefined);
|
|
366
|
+
const randomWord = () => Math.random().toString(36).substring(2, 12).replace(/[0-9]+/g, '').replace(/^\w/, c => c.toUpperCase());
|
|
211
367
|
|
|
212
|
-
|
|
213
|
-
const byId: Record<string, User> = multiMap(users, u => ({ [u.id]: u }));
|
|
368
|
+
$('button text="Add item" click=', () => pairs[randomWord()] = randomWord());
|
|
214
369
|
|
|
215
|
-
|
|
216
|
-
|
|
370
|
+
$('div.row.wide marginTop:1em', () => {
|
|
371
|
+
$('div.box#By key', () => {
|
|
372
|
+
onEach(pairs, (value, key) => {
|
|
373
|
+
$(`li#${key}: ${value}`)
|
|
374
|
+
});
|
|
375
|
+
})
|
|
376
|
+
$('div.box#By desc value', () => {
|
|
377
|
+
onEach(pairs, (value, key) => {
|
|
378
|
+
$(`li#${key}: ${value}`)
|
|
379
|
+
}, value => invertString(value));
|
|
380
|
+
})
|
|
381
|
+
})
|
|
217
382
|
```
|
|
218
383
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
384
|
+
Note the use of the provided {@link aberdeen.invertString} function to reverse-sort by a string value.
|
|
385
|
+
|
|
386
|
+
## Two-way binding
|
|
387
|
+
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.$}.
|
|
388
|
+
|
|
389
|
+
To bind to object properties not named .value (e.g., user.name), use {@link aberdeen.ref}. This creates a new observable proxy whose .value property directly maps to the specified property (e.g., name) on your original observable object (e.g., user).
|
|
390
|
+
|
|
391
|
+
```javascript
|
|
392
|
+
import { $, proxy, ref } from 'aberdeen';
|
|
393
|
+
|
|
394
|
+
const user = proxy({
|
|
395
|
+
name: 'Alice',
|
|
396
|
+
active: false
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
// Text input binding
|
|
400
|
+
$('input placeholder=Name bind=', ref(user, 'name'));
|
|
401
|
+
|
|
402
|
+
// Checkbox binding
|
|
403
|
+
$('label', () => {
|
|
404
|
+
$('input type=checkbox bind=', ref(user, 'active'));
|
|
405
|
+
}, '#Active');
|
|
406
|
+
|
|
407
|
+
// Display the current state
|
|
408
|
+
$('div.box', () => {
|
|
409
|
+
$(`p#Name: ${user.name} `, () => {
|
|
410
|
+
// Binding works both ways
|
|
411
|
+
$('button.outline.secondary#!', {
|
|
412
|
+
click: () => user.name += '!'
|
|
413
|
+
});
|
|
414
|
+
});
|
|
415
|
+
$(`p#Status: ${user.active ? 'Active' : 'Inactive'}`);
|
|
416
|
+
});
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
## CSS
|
|
420
|
+
Through the {@link aberdeen.insertCss} function, Aberdeen provides a way to create component-local CSS.
|
|
421
|
+
|
|
422
|
+
```javascript
|
|
423
|
+
import { $, proxy, insertCss } from 'aberdeen';
|
|
424
|
+
|
|
425
|
+
// Create a CSS class that can be applied to elements
|
|
426
|
+
const myBoxStyle = insertCss({
|
|
427
|
+
borderColor: '#6936cd',
|
|
428
|
+
backgroundColor: '#1b0447',
|
|
226
429
|
button: {
|
|
227
|
-
|
|
228
|
-
|
|
430
|
+
backgroundColor: '#6936cd',
|
|
431
|
+
border: 0,
|
|
432
|
+
transition: 'box-shadow 0.3s',
|
|
433
|
+
boxShadow: '0 0 4px #ff6a0044',
|
|
434
|
+
'&:hover': {
|
|
435
|
+
boxShadow: '0 0 16px #ff6a0088',
|
|
436
|
+
}
|
|
229
437
|
}
|
|
230
438
|
});
|
|
231
439
|
|
|
232
|
-
//
|
|
233
|
-
|
|
440
|
+
// myBoxStyle is now something like ".AbdStl1", the name for a generated CSS class.
|
|
441
|
+
// Here's how to use it:
|
|
442
|
+
$('div.box', myBoxStyle, 'button#Click me');
|
|
234
443
|
```
|
|
235
444
|
|
|
236
|
-
|
|
237
|
-
|
|
445
|
+
This allows you to create single-file components with advanced CSS rules. The {@link aberdeen.insertGlobalCss} function can be used to add CSS without a class prefix.
|
|
446
|
+
|
|
447
|
+
Both functions support the same CSS shortcuts and variables as inline styles (see above). For example:
|
|
448
|
+
|
|
449
|
+
```javascript
|
|
450
|
+
import { cssVars, insertGlobalCss } from 'aberdeen';
|
|
451
|
+
cssVars.boxBg = '#f0f0e0';
|
|
238
452
|
insertGlobalCss({
|
|
239
|
-
body: {
|
|
240
|
-
|
|
453
|
+
body: {
|
|
454
|
+
m: 0, // Using shortcut for margin
|
|
455
|
+
},
|
|
456
|
+
form: {
|
|
457
|
+
bg: "$boxBg", // Using background shortcut and CSS variable
|
|
458
|
+
mv: "$3", // Set vertical margin to predefined spacing value $3 (1rem)
|
|
459
|
+
}
|
|
241
460
|
});
|
|
242
461
|
```
|
|
243
462
|
|
|
244
|
-
CSS can
|
|
245
|
-
|
|
463
|
+
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.
|
|
464
|
+
|
|
465
|
+
## Transitions
|
|
466
|
+
Aberdeen allows you to easily apply transitions on element creation and element destruction:
|
|
467
|
+
|
|
468
|
+
```javascript
|
|
469
|
+
let titleStyle = insertCss({
|
|
470
|
+
transition: "all 1s ease-out",
|
|
471
|
+
transformOrigin: "top left",
|
|
472
|
+
"&.faded": {
|
|
473
|
+
opacity: 0,
|
|
474
|
+
},
|
|
475
|
+
"&.imploded": {
|
|
476
|
+
transform: "scale(0.1)",
|
|
477
|
+
},
|
|
478
|
+
"&.exploded": {
|
|
479
|
+
transform: "scale(5)",
|
|
480
|
+
},
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
const show = proxy(true);
|
|
484
|
+
$('label', () => {
|
|
485
|
+
$('input', {type: 'checkbox', bind: show});
|
|
486
|
+
$('#Show title');
|
|
487
|
+
});
|
|
246
488
|
$(() => {
|
|
247
|
-
|
|
489
|
+
if (!show.value) return;
|
|
490
|
+
$('h2#(Dis)appearing text', titleStyle, 'create=.faded.imploded destroy=.faded.exploded');
|
|
248
491
|
});
|
|
249
492
|
```
|
|
250
493
|
|
|
251
|
-
|
|
252
|
-
The
|
|
253
|
-
|
|
494
|
+
- 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.
|
|
495
|
+
- 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.
|
|
496
|
+
|
|
497
|
+
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):
|
|
498
|
+
|
|
499
|
+
```javascript
|
|
500
|
+
import { $, proxy, onEach } from 'aberdeen';
|
|
254
501
|
import { grow, shrink } from 'aberdeen/transitions';
|
|
255
502
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
503
|
+
const items = proxy([]);
|
|
504
|
+
|
|
505
|
+
const randomInt = (max) => parseInt(Math.random() * max);
|
|
506
|
+
const randomWord = () => Math.random().toString(36).substring(2, 12).replace(/[0-9]+/g, '').replace(/^\w/, c => c.toUpperCase());
|
|
507
|
+
|
|
508
|
+
// Make random mutations
|
|
509
|
+
setInterval(() => {
|
|
510
|
+
if (randomInt(3)) items[randomInt(7)] = {label: randomWord(), prio: randomInt(4)};
|
|
511
|
+
else delete items[randomInt(7)];
|
|
512
|
+
}, 500);
|
|
513
|
+
|
|
514
|
+
$('div.row.wide height:250px', () => {
|
|
515
|
+
$('div.box#By index', () => {
|
|
516
|
+
onEach(items, (item, index) => {
|
|
517
|
+
$(`li#${item.label} (prio ${item.prio})`, {create: grow, destroy: shrink})
|
|
518
|
+
});
|
|
519
|
+
})
|
|
520
|
+
$('div.box#By label', () => {
|
|
521
|
+
onEach(items, (item, index) => {
|
|
522
|
+
$(`li#${item.label} (prio ${item.prio})`, {create: grow, destroy: shrink})
|
|
523
|
+
}, item => item.label);
|
|
524
|
+
})
|
|
525
|
+
$('div.box#By desc prio, then label', () => {
|
|
526
|
+
onEach(items, (item, index) => {
|
|
527
|
+
$(`li#${item.label} (prio ${item.prio})`, {create: grow, destroy: shrink})
|
|
528
|
+
}, item => [-item.prio, item.label]);
|
|
529
|
+
})
|
|
530
|
+
});
|
|
531
|
+
```
|
|
532
|
+
|
|
533
|
+
## Advanced: Peeking without subscribing
|
|
534
|
+
|
|
535
|
+
Sometimes you need to read reactive data inside an observer scope without creating a subscription to that data. The {@link aberdeen.peek} function allows you to do this:
|
|
536
|
+
|
|
537
|
+
```javascript
|
|
538
|
+
import { $, proxy, peek } from 'aberdeen';
|
|
539
|
+
|
|
540
|
+
const data = proxy({ a: 1, b: 2 });
|
|
541
|
+
|
|
542
|
+
$(() => {
|
|
543
|
+
// This scope only re-runs when data.a changes
|
|
544
|
+
// Changes to data.b won't trigger a re-render
|
|
545
|
+
const b = peek(data, 'b');
|
|
546
|
+
console.log(`A is ${data.a}, B was ${b} when A changed.`);
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
$('button text="Change B" click=', () => data.b++); // Won't log
|
|
550
|
+
$('button text="Change A" click=', () => data.a++); // Will log
|
|
551
|
+
```
|
|
552
|
+
|
|
553
|
+
You can also pass a function to `peek()` to execute it without any subscriptions:
|
|
554
|
+
|
|
555
|
+
```javascript
|
|
556
|
+
const count = peek(() => data.a + data.b); // Reads both without subscribing
|
|
557
|
+
```
|
|
558
|
+
|
|
559
|
+
This can be useful to avoid rerenders (of even rerender loops) when you only need a point-in-time snapshot of some reactive data.
|
|
560
|
+
|
|
561
|
+
## Debugging with dump()
|
|
562
|
+
|
|
563
|
+
The {@link aberdeen.dump} function creates a live, interactive tree view of any data structure in the DOM. It's particularly useful for debugging reactive state:
|
|
564
|
+
|
|
565
|
+
```javascript
|
|
566
|
+
import { $, proxy, dump } from 'aberdeen';
|
|
567
|
+
|
|
568
|
+
const state = proxy({
|
|
569
|
+
user: { name: 'Frank', kids: 1 },
|
|
570
|
+
items: ['a', 'b']
|
|
259
571
|
});
|
|
260
572
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
//
|
|
573
|
+
$('h2#Live State Dump');
|
|
574
|
+
dump(state);
|
|
575
|
+
|
|
576
|
+
// The dump updates automatically as state changes
|
|
577
|
+
$('button text="Update state" click=', () => {
|
|
578
|
+
state.user.kids++;
|
|
579
|
+
state.items.push('new');
|
|
580
|
+
});
|
|
581
|
+
```
|
|
582
|
+
|
|
583
|
+
The 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.
|
|
584
|
+
|
|
585
|
+
## Derived values
|
|
586
|
+
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:
|
|
587
|
+
|
|
588
|
+
```javascript
|
|
589
|
+
// NOTE: See below for a better way.
|
|
590
|
+
const original = proxy(1);
|
|
591
|
+
const derived = proxy();
|
|
592
|
+
$(() => {
|
|
593
|
+
derived.value = original.value * 42;
|
|
594
|
+
});
|
|
595
|
+
|
|
596
|
+
$('h3 text=', derived);
|
|
597
|
+
$('button text=Increment click=', () => original.value++);
|
|
598
|
+
```
|
|
599
|
+
|
|
600
|
+
The {@link aberdeen.derive} function makes the above a little easier. It works just like passing a function to {@link aberdeen.$}, 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:
|
|
601
|
+
|
|
602
|
+
```javascript
|
|
603
|
+
const original = proxy(1);
|
|
604
|
+
const derived = derive(() => original.value * 42);
|
|
605
|
+
|
|
606
|
+
$('h3 text=', derived);
|
|
607
|
+
$('button text=Increment click=', () => original.value++);
|
|
608
|
+
```
|
|
609
|
+
|
|
610
|
+
For deriving values from (possibly large) arrays or objects, Aberdeen provides specialized functions that enable fast, incremental updates to derived data: {@link aberdeen.map} (each item becomes zero or one derived item), {@link aberdeen.multiMap} (each item becomes any number of derived items), {@link aberdeen.count} (reactively counts the number of object properties), {@link aberdeen.isEmpty} (true when the object/array has no items) and {@link aberdeen.partition} (sorts each item into one or more buckets). An example:
|
|
611
|
+
|
|
612
|
+
```javascript
|
|
613
|
+
import * as aberdeen from 'aberdeen';
|
|
614
|
+
const {$, proxy} = aberdeen;
|
|
615
|
+
|
|
616
|
+
// Create some random data
|
|
617
|
+
const people = proxy({});
|
|
618
|
+
const randomInt = (max) => parseInt(Math.random() * max);
|
|
619
|
+
setInterval(() => {
|
|
620
|
+
people[randomInt(250)] = {height: 150+randomInt(60), weight: 45+randomInt(90)};
|
|
621
|
+
}, 250);
|
|
622
|
+
|
|
623
|
+
// Do some mapping, counting and observing
|
|
624
|
+
const totalCount = aberdeen.count(people);
|
|
625
|
+
const bmis = aberdeen.map(people,
|
|
626
|
+
person => Math.round(person.weight / ((person.height/100) ** 2))
|
|
627
|
+
);
|
|
628
|
+
const overweightBmis = aberdeen.map(bmis, // Use map() as a filter
|
|
629
|
+
bmi => bmi > 25 ? bmi : undefined
|
|
630
|
+
);
|
|
631
|
+
const overweightCount = aberdeen.count(overweightBmis);
|
|
632
|
+
const message = aberdeen.derive(
|
|
633
|
+
() => `There are ${totalCount.value} people, of which ${overweightCount.value} are overweight.`
|
|
634
|
+
);
|
|
635
|
+
|
|
636
|
+
// Show the results
|
|
637
|
+
$('p text=', message);
|
|
638
|
+
$(() => {
|
|
639
|
+
// isEmpty only causes a re-run when the count changes between zero and non-zero
|
|
640
|
+
if (aberdeen.isEmpty(overweightBmis)) return;
|
|
641
|
+
$('p#These are their BMIs:', () => {
|
|
642
|
+
aberdeen.onEach(overweightBmis, bmi => $('# '+bmi), bmi => -bmi);
|
|
643
|
+
// Sort by descending BMI
|
|
644
|
+
});
|
|
645
|
+
})
|
|
265
646
|
```
|
|
266
647
|
|
|
267
|
-
|
|
648
|
+
## html-to-aberdeen
|
|
649
|
+
|
|
650
|
+
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:
|
|
268
651
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
```bash
|
|
272
|
-
echo '<div class="box"><p>Hello</p></div>' | npx html-to-aberdeen
|
|
273
|
-
# Output: $('div.box', () => { $('p#Hello'); });
|
|
652
|
+
```sh
|
|
653
|
+
npx html-to-aberdeen
|
|
274
654
|
```
|
|
275
655
|
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
656
|
+
It takes HTML on stdin (paste it and press `ctrl-d` for end-of-file), and outputs JavaScript on stdout.
|
|
657
|
+
|
|
658
|
+
> 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.
|
|
659
|
+
|
|
660
|
+
## Further reading
|
|
661
|
+
|
|
662
|
+
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:
|
|
280
663
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
2. **Use CSS variables:** Define `@primary`, `@secondary`, etc. in `cssVars` for colors.
|
|
284
|
-
3. **Use spacing scale:** Prefer `@3`, `@4` for margins/paddings over hardcoded values, for consistency and easy theming/scaling. Don't use when exact pixel values are needed.
|
|
285
|
-
4. **Minimize scope size:** Smaller reactive scopes = fewer DOM updates.
|
|
286
|
-
5. **Use `onEach` for lists:** Never iterate proxied arrays with `for`/`map` in render functions.
|
|
287
|
-
6. **Pass observables directly:** `$('span text=', observable)` not interpolation.
|
|
288
|
-
7. **Components are functions:** Just write functions that call `$`.
|
|
289
|
-
8. **Top-level CSS:** Call `insertCss` at module level, not in render functions.
|
|
290
|
-
9. **Dynamic values:** Always use `attr=', value` syntax for user data.
|
|
664
|
+
- [Reference documentation](https://aberdeenjs.org/modules.html)
|
|
665
|
+
- [Examples](https://aberdeenjs.org/#examples)
|