aberdeen 1.0.0 → 1.0.4
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/README.md +139 -141
- package/dist/aberdeen.d.ts +15 -5
- package/dist/aberdeen.js +9 -7
- package/dist/aberdeen.js.map +3 -3
- package/dist-min/aberdeen.js +3 -3
- package/dist-min/aberdeen.js.map +3 -3
- package/package.json +9 -6
- package/src/aberdeen.ts +23 -11
package/README.md
CHANGED
|
@@ -1,181 +1,179 @@
|
|
|
1
|
-
|
|
1
|
+
# [Aberdeen](https://aberdeenjs.org/) [](https://github.com/vanviegen/aberdeen/blob/master/LICENSE.txt) [](https://badge.fury.io/js/aberdeen)  [](https://github.com/vanviegen/aberdeen)
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Build blazing-fast, reactive UIs in pure TypeScript/JavaScript – no virtual DOM.
|
|
4
|
+
|
|
5
|
+
Aberdeen offers a refreshingly simple approach to reactive UIs. Its core idea:
|
|
6
|
+
|
|
7
|
+
> 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.
|
|
8
|
+
|
|
9
|
+
Now, let's dive into why this matters...
|
|
4
10
|
|
|
5
11
|
## Why use Aberdeen?
|
|
6
12
|
|
|
7
|
-
-
|
|
8
|
-
-
|
|
9
|
-
-
|
|
10
|
-
-
|
|
11
|
-
-
|
|
12
|
-
- It comes with batteries included:
|
|
13
|
-
- Client-side routing.
|
|
14
|
-
- Revertible patches, for optimistic user-interface updates.
|
|
15
|
-
- Component-local CSS generator.
|
|
16
|
-
- Helper functions for reactively working with data, such as for deriving, (multi)mapping, filtering, partitioning and counting.
|
|
17
|
-
- A couple of add/remove transition effects, to get you started.
|
|
13
|
+
- 🎩 **Simple:** Express UIs naturally in JavaScript/TypeScript, without build steps or JSX, and with a minimal amount of concepts you need to learn.
|
|
14
|
+
- ⏩ **Fast:** No virtual DOM. Aberdeen intelligently updates only the minimal, necessary parts of your UI when proxied data changes.
|
|
15
|
+
- 👥 **Awesome lists**: It's very easy and performant to reactively display data sorted by whatever you like.
|
|
16
|
+
- 🔬 **Tiny:** Around 5KB (minimized and gzipped) and with zero runtime dependencies.
|
|
17
|
+
- 🔋 **Batteries included**: Comes with client-side routing, revertible patches for optimistic user-interface updates, component-local CSS, helper functions for transforming reactive data (mapping, partitioning, filtering, etc) and hide/unhide transition effects. No bikeshedding required!
|
|
18
18
|
|
|
19
19
|
## Why *not* use Aberdeen?
|
|
20
20
|
|
|
21
|
-
- There are not many of us -Aberdeen developers- yet, so don't expect terribly helpful
|
|
22
|
-
- You'd have to code things yourself, instead of duct-taping together a gazillion React ecosystem libraries.
|
|
21
|
+
- 🤷 **Lack of community:** There are not many of us -Aberdeen developers- yet, so don't expect terribly helpful Stack Overflow/AI answers.
|
|
22
|
+
- 📚 **Lack of ecosystem:** You'd have to code things yourself, instead of duct-taping together a gazillion React ecosystem libraries.
|
|
23
23
|
|
|
24
|
-
##
|
|
24
|
+
## Examples
|
|
25
25
|
|
|
26
|
-
|
|
26
|
+
First, let's start with the obligatory reactive counter example. If you're reading this on [the official website](https://aberdeenjs.org) you should see a working demo below the code, and an 'edit' button in the top-right corner of the code, to play around.
|
|
27
27
|
|
|
28
28
|
```javascript
|
|
29
|
-
import {$, proxy,
|
|
30
|
-
|
|
31
|
-
// Helper functions
|
|
32
|
-
|
|
33
|
-
function calculateWinner(board) {
|
|
34
|
-
const lines = [
|
|
35
|
-
[0, 1, 2], [3, 4, 5], [6, 7, 8], // horizontal
|
|
36
|
-
[0, 3, 6], [1, 4, 7], [2, 5, 8], // vertical
|
|
37
|
-
[0, 4, 8], [2, 4, 6] // diagonal
|
|
38
|
-
];
|
|
39
|
-
for (const [a, b, c] of lines) {
|
|
40
|
-
if (board[a] && board[a] === board[b] && board[a] === board[c]) {
|
|
41
|
-
return board[a];
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
}
|
|
29
|
+
import {$, proxy, ref} from 'aberdeen';
|
|
45
30
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
}
|
|
31
|
+
// Define some state as a proxied (observable) object
|
|
32
|
+
const state = proxy({question: "How many roads must a man walk down?", answer: 42});
|
|
49
33
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
}
|
|
34
|
+
$('h3', () => {
|
|
35
|
+
// This function reruns whenever the question or the answer changes
|
|
36
|
+
$(`:${state.question} ↪ ${state.answer || 'Blowing in the wind'}`)
|
|
37
|
+
});
|
|
53
38
|
|
|
54
|
-
|
|
55
|
-
|
|
39
|
+
// Two-way bind state.question to an <input>
|
|
40
|
+
$('input', {placeholder: 'Question', bind: ref(state, 'question')})
|
|
56
41
|
|
|
57
|
-
|
|
58
|
-
|
|
42
|
+
// Allow state.answer to be modified using both an <input> and buttons
|
|
43
|
+
$('div.row', {$marginTop: '1em'}, () => {
|
|
44
|
+
$('button:-', {click: () => state.answer--});
|
|
45
|
+
$('input', {type: 'number', bind: ref(state, 'answer')})
|
|
46
|
+
$('button:+', {click: () => state.answer++});
|
|
47
|
+
});
|
|
48
|
+
```
|
|
59
49
|
|
|
60
|
-
|
|
61
|
-
const newBoard = board.slice();
|
|
62
|
-
newBoard[position] = getCurrentMarker(board);
|
|
63
|
-
|
|
64
|
-
// Truncate any future states, and write a new future
|
|
65
|
-
history.current++;
|
|
66
|
-
history.boards.length = history.current;
|
|
67
|
-
history.boards.push(newBoard);
|
|
68
|
-
}
|
|
50
|
+
Okay, next up is a somewhat more complex app - a todo-list with the following behavior:
|
|
69
51
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
gridTemplateColumns: '1fr 1fr 1fr',
|
|
77
|
-
'> *': {
|
|
78
|
-
width: '2em',
|
|
79
|
-
height: '2em',
|
|
80
|
-
padding: 0,
|
|
81
|
-
},
|
|
82
|
-
});
|
|
52
|
+
- New items open in an 'editing state'.
|
|
53
|
+
- Items that are in 'editing state' show a text input, a save button and a cancel button. Done status cannot be toggled while editing.
|
|
54
|
+
- Pressing one of the buttons, or pressing enter will transition from 'editing state' to 'viewing state', saving the new label text unless cancel was pressed.
|
|
55
|
+
- In 'viewing state', the label is shown as non-editable. There's an 'Edit' link, that will transition the item to 'editing state'. Clicking anywhere else will toggle the done status.
|
|
56
|
+
- The list of items is sorted alphabetically by label. Items move when 'save' changes their label.
|
|
57
|
+
- Items that are created, moved or deleted are grown and shrink as appropriate.
|
|
83
58
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
function drawBoard(history) {
|
|
87
|
-
$('div', boardStyle, () => {
|
|
88
|
-
for(let pos=0; pos<9; pos++) {
|
|
89
|
-
$('button.square', () => {
|
|
90
|
-
let marker = getBoard(history)[pos];
|
|
91
|
-
if (marker) {
|
|
92
|
-
$({ text: marker });
|
|
93
|
-
} else {
|
|
94
|
-
$({ click: () => markSquare(history, pos) });
|
|
95
|
-
}
|
|
96
|
-
});
|
|
97
|
-
}
|
|
98
|
-
})
|
|
99
|
-
}
|
|
59
|
+
Pfew.. now let's look at the code:
|
|
100
60
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
const board = getBoard(history);
|
|
105
|
-
const winner = calculateWinner(board);
|
|
106
|
-
if (winner) {
|
|
107
|
-
$(`:Winner: ${winner}!`);
|
|
108
|
-
} else if (board.filter(square=>square).length === 9) {
|
|
109
|
-
$(`:It's a draw...`);
|
|
110
|
-
} else {
|
|
111
|
-
$(`:Current player: ${getCurrentMarker(board)}`);
|
|
112
|
-
}
|
|
113
|
-
});
|
|
114
|
-
}
|
|
61
|
+
```typescript
|
|
62
|
+
import {$, proxy, onEach, insertCss, peek, observe, unproxy, ref} from "aberdeen";
|
|
63
|
+
import {grow, shrink} from "aberdeen/transitions";
|
|
115
64
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
$('button', {
|
|
121
|
-
// A text node:
|
|
122
|
-
text: index,
|
|
123
|
-
// Conditional css class:
|
|
124
|
-
".outline": observe(() => history.current != index),
|
|
125
|
-
// Inline styles:
|
|
126
|
-
$marginRight: "0.5em",
|
|
127
|
-
$marginTop: "0.5em",
|
|
128
|
-
// Event listener:
|
|
129
|
-
click: () => history.current = index,
|
|
130
|
-
});
|
|
131
|
-
});
|
|
65
|
+
// We'll use a simple class to store our data.
|
|
66
|
+
class TodoItem {
|
|
67
|
+
constructor(public label: string = '', public done: boolean = false) {}
|
|
68
|
+
toggle() { this.done = !this.done; }
|
|
132
69
|
}
|
|
133
70
|
|
|
71
|
+
// The top-level user interface.
|
|
134
72
|
function drawMain() {
|
|
135
|
-
//
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
73
|
+
// Add some initial items. We'll wrap a proxy() around it!
|
|
74
|
+
let items: TodoItem[] = proxy([
|
|
75
|
+
new TodoItem('Make todo-list demo', true),
|
|
76
|
+
new TodoItem('Learn Aberdeen', false),
|
|
77
|
+
]);
|
|
78
|
+
|
|
79
|
+
// Draw the list, ordered by label.
|
|
80
|
+
onEach(items, drawItem, item => item.label);
|
|
140
81
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
$('
|
|
144
|
-
|
|
145
|
-
|
|
82
|
+
// Add item and delete checked buttons.
|
|
83
|
+
$('div.row', () => {
|
|
84
|
+
$('button:+', {
|
|
85
|
+
click: () => items.push(new TodoItem("")),
|
|
86
|
+
});
|
|
87
|
+
$('button.outline:Delete checked', {
|
|
88
|
+
click: () => {
|
|
89
|
+
for(let idx in items) {
|
|
90
|
+
if (items[idx].done) delete items[idx];
|
|
91
|
+
}
|
|
92
|
+
}
|
|
146
93
|
});
|
|
147
94
|
});
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
// Called for each todo list item.
|
|
98
|
+
function drawItem(item) {
|
|
99
|
+
// Items without a label open in editing state.
|
|
100
|
+
// Note that we're creating this proxy outside the `div.row` scope
|
|
101
|
+
// create below, so that it will persist when that state reruns.
|
|
102
|
+
let editing: {value: boolean} = proxy(item.label == '');
|
|
103
|
+
|
|
104
|
+
$('div.row', todoItemStyle, {create:grow, destroy: shrink}, () => {
|
|
105
|
+
// Conditionally add a class to `div.row`, based on item.done
|
|
106
|
+
$({".done": ref(item,'done')});
|
|
107
|
+
|
|
108
|
+
// The checkmark is hidden using CSS
|
|
109
|
+
$('div.checkmark:✅');
|
|
110
|
+
|
|
111
|
+
if (editing.value) {
|
|
112
|
+
// Label <input>. Save using enter or button.
|
|
113
|
+
function save() {
|
|
114
|
+
editing.value = false;
|
|
115
|
+
item.label = inputElement.value;
|
|
116
|
+
}
|
|
117
|
+
let inputElement = $('input', {
|
|
118
|
+
placeholder: 'Label',
|
|
119
|
+
value: item.label,
|
|
120
|
+
keydown: e => e.key==='Enter' && save(),
|
|
121
|
+
});
|
|
122
|
+
$('button.outline:Cancel', {click: () => editing.value = false});
|
|
123
|
+
$('button:Save', {click: save});
|
|
124
|
+
} else {
|
|
125
|
+
// Label as text.
|
|
126
|
+
$('p:' + item.label);
|
|
127
|
+
|
|
128
|
+
// Edit icon, if not done.
|
|
129
|
+
if (!item.done) {
|
|
130
|
+
$('a:Edit', {
|
|
131
|
+
click: e => {
|
|
132
|
+
editing.value = true;
|
|
133
|
+
e.stopPropagation(); // We don't want to toggle as well.
|
|
134
|
+
},
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Clicking a row toggles done.
|
|
139
|
+
$({click: () => item.done = !item.done, $cursor: 'pointer'});
|
|
140
|
+
}
|
|
141
|
+
});
|
|
148
142
|
}
|
|
149
143
|
|
|
150
|
-
//
|
|
144
|
+
// Insert some component-local CSS, specific for this demo.
|
|
145
|
+
const todoItemStyle = insertCss({
|
|
146
|
+
marginBottom: "0.5rem",
|
|
147
|
+
".checkmark": {
|
|
148
|
+
opacity: 0.2,
|
|
149
|
+
},
|
|
150
|
+
"&.done": {
|
|
151
|
+
textDecoration: "line-through",
|
|
152
|
+
".checkmark": {
|
|
153
|
+
opacity: 1,
|
|
154
|
+
},
|
|
155
|
+
},
|
|
156
|
+
});
|
|
151
157
|
|
|
158
|
+
// Go!
|
|
152
159
|
drawMain();
|
|
153
160
|
```
|
|
154
161
|
|
|
155
162
|
Some further examples:
|
|
156
163
|
|
|
157
|
-
- [Input
|
|
158
|
-
- [
|
|
159
|
-
- [
|
|
160
|
-
- [
|
|
164
|
+
- [Input demo](https://aberdeenjs.org/examples/input/) - [Source](https://github.com/vanviegen/aberdeen/tree/master/examples/input)
|
|
165
|
+
- [Tic Tac Toe demo](https://aberdeenjs.org/examples/tictactoe/) - [Source](https://github.com/vanviegen/aberdeen/tree/master/examples/tictactoe)
|
|
166
|
+
- [List demo](https://aberdeenjs.org/examples/list/) - [Source](https://github.com/vanviegen/aberdeen/tree/master/examples/list)
|
|
167
|
+
- [Routing demo](https://aberdeenjs.org/examples/router/) - [Source](https://github.com/vanviegen/aberdeen/tree/master/examples/router)
|
|
168
|
+
- [JS Framework Benchmark demo](https://aberdeenjs.org/examples/js-framework-benchmark/) - [Source](https://github.com/vanviegen/aberdeen/tree/master/examples/js-framework-benchmark)
|
|
161
169
|
|
|
170
|
+
## Learning Aberdeen
|
|
162
171
|
|
|
163
|
-
|
|
172
|
+
- [Tutorial](https://aberdeenjs.org/Tutorial/)
|
|
173
|
+
- [Reference documentation](https://aberdeenjs.org/modules.html)
|
|
164
174
|
|
|
165
|
-
|
|
166
|
-
- [Reference documentation](https://vanviegen.github.io/aberdeen/modules.html)
|
|
175
|
+
And you may want to study the examples above, of course!
|
|
167
176
|
|
|
168
177
|
## News
|
|
169
178
|
|
|
170
|
-
- **2025-
|
|
171
|
-
|
|
172
|
-
## Roadmap
|
|
173
|
-
|
|
174
|
-
- [x] Support for (dis)appear transitions.
|
|
175
|
-
- [x] A better alternative for scheduleTask.
|
|
176
|
-
- [x] A simple router.
|
|
177
|
-
- [x] Optimistic client-side predictions.
|
|
178
|
-
- [x] Performance profiling and tuning regarding lists.
|
|
179
|
-
- [x] Support for (component local) CSS
|
|
180
|
-
- [ ] Architecture document.
|
|
181
|
-
- [ ] SVG support.
|
|
179
|
+
- **2025-05-07**: After five years of working on this library on and off, I'm finally happy with its API and the developer experience it offers. I'm calling it 1.0! To celebrate, I've created some pretty fancy (if I may say so myself) interactive documentation and a tutorial.
|
package/dist/aberdeen.d.ts
CHANGED
|
@@ -308,22 +308,32 @@ export declare function ref<T extends TargetType, K extends keyof T>(target: T,
|
|
|
308
308
|
* - `{html: string}`: Add the value as HTML to the *current* element. This should only be used in exceptional situations. And of course, beware of XSS.
|
|
309
309
|
* - `{element: Node}`: Add a pre-existing HTML `Node` to the *current* element.
|
|
310
310
|
*
|
|
311
|
+
* @returns The most inner DOM element that was created (not counting text nodes nor elements created by content functions),
|
|
312
|
+
* or undefined if no elements were created.
|
|
311
313
|
*
|
|
312
314
|
* @example Create Element
|
|
313
315
|
* ```typescript
|
|
314
316
|
* $('button.secondary.outline:Submit', {
|
|
315
|
-
* disabled:
|
|
317
|
+
* disabled: false,
|
|
316
318
|
* click: () => console.log('Clicked!'),
|
|
317
319
|
* $color: 'red'
|
|
318
320
|
* });
|
|
319
321
|
* ```
|
|
320
322
|
*
|
|
321
|
-
* @example Nested Elements
|
|
323
|
+
* @example Create Nested Elements
|
|
324
|
+
* ```typescript
|
|
325
|
+
* let inputElement: Element = $('label:Click me', 'input', {type: 'checkbox'});
|
|
326
|
+
* // You should usually not touch raw DOM elements, unless when integrating
|
|
327
|
+
* // with non-Aberdeen code.
|
|
328
|
+
* console.log('DOM element:', inputElement);
|
|
329
|
+
* ```
|
|
330
|
+
*
|
|
331
|
+
* @example Content Functions & Reactive Scope
|
|
322
332
|
* ```typescript
|
|
323
333
|
* const state = proxy({ count: 0 });
|
|
324
334
|
* $('div', () => { // Outer element
|
|
325
335
|
* // This scope re-renders when state.count changes
|
|
326
|
-
* $(
|
|
336
|
+
* $(`p:Count is ${state.count}`);
|
|
327
337
|
* $('button:Increment', { click: () => state.count++ });
|
|
328
338
|
* });
|
|
329
339
|
* ```
|
|
@@ -348,7 +358,7 @@ export declare function ref<T extends TargetType, K extends keyof T>(target: T,
|
|
|
348
358
|
* });
|
|
349
359
|
* ```
|
|
350
360
|
*/
|
|
351
|
-
export declare function $(...args: (string | null | undefined | false | (() => void) | Record<string, any>)[]): void;
|
|
361
|
+
export declare function $(...args: (string | null | undefined | false | (() => void) | Record<string, any>)[]): void | Element;
|
|
352
362
|
/**
|
|
353
363
|
* Inserts CSS rules into the document, optionally scoping them with a unique class name.
|
|
354
364
|
*
|
|
@@ -465,7 +475,7 @@ export declare function setErrorHandler(handler?: (error: Error) => boolean | un
|
|
|
465
475
|
* call or a {@link $} element's render function).
|
|
466
476
|
*
|
|
467
477
|
* **Note:** While this provides access to the DOM element, directly manipulating it outside
|
|
468
|
-
* of Aberdeen's control is generally discouraged. Prefer
|
|
478
|
+
* of Aberdeen's control is generally discouraged. Prefer reactive updates using {@link $}.
|
|
469
479
|
*
|
|
470
480
|
* @returns The current parent `Element` for DOM insertion.
|
|
471
481
|
*
|
package/dist/aberdeen.js
CHANGED
|
@@ -903,6 +903,7 @@ var SPECIAL_PROPS = {
|
|
|
903
903
|
function $(...args) {
|
|
904
904
|
let savedCurrentScope;
|
|
905
905
|
let err;
|
|
906
|
+
let result;
|
|
906
907
|
for (let arg of args) {
|
|
907
908
|
if (arg == null || arg === false)
|
|
908
909
|
continue;
|
|
@@ -932,17 +933,17 @@ function $(...args) {
|
|
|
932
933
|
err = `Tag '${arg}' cannot contain space`;
|
|
933
934
|
break;
|
|
934
935
|
} else {
|
|
935
|
-
|
|
936
|
+
result = document.createElement(arg);
|
|
936
937
|
if (classes)
|
|
937
|
-
|
|
938
|
+
result.className = classes.replaceAll(".", " ");
|
|
938
939
|
if (text)
|
|
939
|
-
|
|
940
|
-
addNode(
|
|
940
|
+
result.textContent = text;
|
|
941
|
+
addNode(result);
|
|
941
942
|
if (!savedCurrentScope) {
|
|
942
943
|
savedCurrentScope = currentScope;
|
|
943
944
|
}
|
|
944
|
-
let newScope = new ChainedScope(
|
|
945
|
-
newScope.lastChild =
|
|
945
|
+
let newScope = new ChainedScope(result, true);
|
|
946
|
+
newScope.lastChild = result.lastChild || undefined;
|
|
946
947
|
if (topRedrawScope === currentScope)
|
|
947
948
|
topRedrawScope = newScope;
|
|
948
949
|
currentScope = newScope;
|
|
@@ -968,6 +969,7 @@ function $(...args) {
|
|
|
968
969
|
}
|
|
969
970
|
if (err)
|
|
970
971
|
throw new Error(err);
|
|
972
|
+
return result;
|
|
971
973
|
}
|
|
972
974
|
var cssCount = 0;
|
|
973
975
|
function insertCss(style, global = false) {
|
|
@@ -1195,5 +1197,5 @@ export {
|
|
|
1195
1197
|
$
|
|
1196
1198
|
};
|
|
1197
1199
|
|
|
1198
|
-
//# debugId=
|
|
1200
|
+
//# debugId=96472D32607348EB64756E2164756E21
|
|
1199
1201
|
//# sourceMappingURL=aberdeen.js.map
|