aberdeen 0.5.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,177 +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 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("")),
146
86
  });
87
+ $('button.outline:Delete checked', {
88
+ click: () => {
89
+ for(let idx in items) {
90
+ if (items[idx].done) delete items[idx];
91
+ }
92
+ }
93
+ });
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
+ }
147
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
- ## Roadmap
177
+ ## News
169
178
 
170
- - [x] Support for (dis)appear transitions.
171
- - [x] A better alternative for scheduleTask.
172
- - [x] A simple router.
173
- - [x] Optimistic client-side predictions.
174
- - [x] Performance profiling and tuning regarding lists.
175
- - [x] Support for (component local) CSS
176
- - [ ] Architecture document.
177
- - [ ] 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.
@@ -163,7 +163,10 @@ export declare function proxy<T extends DatumType>(target: T): ValueRef<T extend
163
163
  * setTimeout(() => rawUser.name += '?', 2000);
164
164
  *
165
165
  * // Both userProxy and rawUser end up as `{name: 'Frank!?'}`
166
- * setTimeout(() => console.log('final values', userProxy, rawUser), 3000);
166
+ * setTimeout(() => {
167
+ * console.log('final proxied', userProxy)
168
+ * console.log('final unproxied', rawUser)
169
+ * }, 3000);
167
170
  * ```
168
171
  */
169
172
  export declare function unproxy<T>(target: T): T;
@@ -305,22 +308,32 @@ export declare function ref<T extends TargetType, K extends keyof T>(target: T,
305
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.
306
309
  * - `{element: Node}`: Add a pre-existing HTML `Node` to the *current* element.
307
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.
308
313
  *
309
314
  * @example Create Element
310
315
  * ```typescript
311
316
  * $('button.secondary.outline:Submit', {
312
- * disabled: true,
317
+ * disabled: false,
313
318
  * click: () => console.log('Clicked!'),
314
319
  * $color: 'red'
315
320
  * });
316
321
  * ```
317
322
  *
318
- * @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
319
332
  * ```typescript
320
333
  * const state = proxy({ count: 0 });
321
334
  * $('div', () => { // Outer element
322
335
  * // This scope re-renders when state.count changes
323
- * $('p:Count is ${state.count}`);
336
+ * $(`p:Count is ${state.count}`);
324
337
  * $('button:Increment', { click: () => state.count++ });
325
338
  * });
326
339
  * ```
@@ -345,7 +358,7 @@ export declare function ref<T extends TargetType, K extends keyof T>(target: T,
345
358
  * });
346
359
  * ```
347
360
  */
348
- 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;
349
362
  /**
350
363
  * Inserts CSS rules into the document, optionally scoping them with a unique class name.
351
364
  *
@@ -428,7 +441,7 @@ export declare function insertCss(style: object, global?: boolean): string;
428
441
  *
429
442
  * try {
430
443
  * // Attempt to show a custom message in the UI
431
- * $('div.error-display:Oops, something went wrong!');
444
+ * $('div.error-message:Oops, something went wrong!');
432
445
  * } catch (e) {
433
446
  * // Ignore errors during error handling itself
434
447
  * }
@@ -436,8 +449,20 @@ export declare function insertCss(style: object, global?: boolean): string;
436
449
  * return false; // Suppress default console log and DOM error message
437
450
  * });
438
451
  *
452
+ * // Styling for our custom error message
453
+ * insertCss({
454
+ * '.error-message': {
455
+ * backgroundColor: '#e31f00',
456
+ * display: 'inline-block',
457
+ * color: 'white',
458
+ * borderRadius: '3px',
459
+ * padding: '2px 4px',
460
+ * }
461
+ * }, true); // global style
462
+ *
439
463
  * // Cause an error within a render scope.
440
464
  * $('div.box', () => {
465
+ * // Will cause our error handler to insert an error message within the box
441
466
  * noSuchFunction();
442
467
  * })
443
468
  * ```
@@ -450,7 +475,7 @@ export declare function setErrorHandler(handler?: (error: Error) => boolean | un
450
475
  * call or a {@link $} element's render function).
451
476
  *
452
477
  * **Note:** While this provides access to the DOM element, directly manipulating it outside
453
- * 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 $}.
454
479
  *
455
480
  * @returns The current parent `Element` for DOM insertion.
456
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