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 CHANGED
@@ -1,181 +1,179 @@
1
- A TypeScript/JavaScript library for quickly building performant declarative user interfaces *without* the use of a virtual DOM.
1
+ # [Aberdeen](https://aberdeenjs.org/) [![](https://img.shields.io/badge/license-ISC-blue.svg)](https://github.com/vanviegen/aberdeen/blob/master/LICENSE.txt) [![](https://badge.fury.io/js/aberdeen.svg)](https://badge.fury.io/js/aberdeen) ![](https://img.shields.io/bundlejs/size/aberdeen) [![](https://img.shields.io/github/last-commit/vanviegen/aberdeen)](https://github.com/vanviegen/aberdeen)
2
2
 
3
- The key insight is the use of many small anonymous functions, that will automatically rerun when the underlying data changes. In order to trigger updates, that data should be encapsulated in any number of *proxied* JavaScript objects. They can hold anything, from simple values to complex, typed and deeply nested data structures, in which case user-interface functions can (automatically) subscribe to just the parts they depend upon.
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
- - It provides a flexible and simple to understand model for reactive user-interface building.
8
- - It allows you to express user-interfaces in plain JavaScript (or TypeScript) in an easy to read form, without (JSX-like) compilation steps.
9
- - It's fast, as it doesn't use a *virtual DOM* and only reruns small pieces of code, redrawing minimal pieces of the UI, in response to updated data.
10
- - It makes displaying and updating sorted lists very easy and very fast.
11
- - It's tiny, at about 5kb (minimized and gzipped) and without any run-time dependencies.
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 StackOver/AI answers.
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
- ## Example code
24
+ ## Examples
25
25
 
26
- To get a quick impression of what Aberdeen code looks like, below is a Tic-tac-toe app with undo history. If you're reading this on [the official website](https://vanviegen.github.io/aberdeen/README/) you should see a working demo below the code, and an 'edit' button in the top-right corner of the code, to play around.
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, onEach, insertCss, observe} from "aberdeen";
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
- function getCurrentMarker(board) {
47
- return board.filter(v => v).length % 2 ? "O" : "X";
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
- function getBoard(history) {
51
- return history.boards[history.current];
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
- function markSquare(history, position) {
55
- const board = getBoard(history);
39
+ // Two-way bind state.question to an <input>
40
+ $('input', {placeholder: 'Question', bind: ref(state, 'question')})
56
41
 
57
- // Don't allow markers when we already have a winner
58
- if (calculateWinner(board)) return;
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
- // Copy the current board, and insert the marker into it
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
- // Define component-local CSS, which we'll utilize in the drawBoard function.
71
- // Of course, you can use any other styling solution instead, if you prefer.
72
-
73
- const boardStyle = insertCss({
74
- display: 'grid',
75
- gap: '0.5em',
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
- // UI drawing functions.
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
- function drawStatusMessage(history) {
102
- $('h4', () => {
103
- // Reruns whenever observable data read by calculateWinner or getCurrentMarker changes
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
- function drawTurns(history) {
117
- $('div:Select a turn:')
118
- // Reactively iterate all (historic) board versions
119
- onEach(history.boards, (_, index) => {
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
- // Define our state, wrapped by an observable proxy
136
- const history = proxy({
137
- boards: [[]], // eg. [[], [undefined, 'O', undefined, 'X'], ...]
138
- current: 0, // indicates which of the boards is currently showing
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
- $('main.row', () => {
142
- $('div.box', () => drawBoard(history));
143
- $('div.box', {$flex: 1}, () => {
144
- drawStatusMessage(history);
145
- drawTurns(history);
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
- // Fire it up! Mounts on document.body by default..
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 example demo](https://vanviegen.github.io/aberdeen/examples/input/) - [Source](https://github.com/vanviegen/aberdeen/tree/master/examples/input)
158
- - [List example demo](https://vanviegen.github.io/aberdeen/examples/list/) - [Source](https://github.com/vanviegen/aberdeen/tree/master/examples/list)
159
- - [Routing example demo](https://vanviegen.github.io/aberdeen/examples/router/) - [Source](https://github.com/vanviegen/aberdeen/tree/master/examples/router)
160
- - [JS Framework Benchmark demo](https://vanviegen.github.io/aberdeen/examples/js-framework-benchmark/) - [Source](https://github.com/vanviegen/aberdeen/tree/master/examples/js-framework-benchmark)
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
- ## Documentation
172
+ - [Tutorial](https://aberdeenjs.org/Tutorial/)
173
+ - [Reference documentation](https://aberdeenjs.org/modules.html)
164
174
 
165
- - [Tutorial](https://vanviegen.github.io/aberdeen/Tutorial/)
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-5-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.
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.
@@ -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: true,
317
+ * disabled: false,
316
318
  * click: () => console.log('Clicked!'),
317
319
  * $color: 'red'
318
320
  * });
319
321
  * ```
320
322
  *
321
- * @example Nested Elements & Reactive Scope
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
- * $('p:Count is ${state.count}`);
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 declarative updates using {@link $}.
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
- const el = document.createElement(arg);
936
+ result = document.createElement(arg);
936
937
  if (classes)
937
- el.className = classes.replaceAll(".", " ");
938
+ result.className = classes.replaceAll(".", " ");
938
939
  if (text)
939
- el.textContent = text;
940
- addNode(el);
940
+ result.textContent = text;
941
+ addNode(result);
941
942
  if (!savedCurrentScope) {
942
943
  savedCurrentScope = currentScope;
943
944
  }
944
- let newScope = new ChainedScope(el, true);
945
- newScope.lastChild = el.lastChild || undefined;
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=DE860A8F16A3286C64756E2164756E21
1200
+ //# debugId=96472D32607348EB64756E2164756E21
1199
1201
  //# sourceMappingURL=aberdeen.js.map