aberdeen 1.3.1 → 1.4.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aberdeen",
3
- "version": "1.3.1",
3
+ "version": "1.4.0",
4
4
  "author": "Frank van Viegen",
5
5
  "main": "dist-min/aberdeen.js",
6
6
  "devDependencies": {
package/src/aberdeen.ts CHANGED
@@ -189,6 +189,19 @@ abstract class Scope implements QueueRunner {
189
189
  // }
190
190
  }
191
191
 
192
+ /**
193
+ * Execute a function once, after all currently scheduled jobs are completed.
194
+ */
195
+ class DelayedOneTimeRunner implements QueueRunner {
196
+ prio: number = --lastPrio;
197
+ [ptr: ReverseSortedSetPointer]: this;
198
+ constructor(
199
+ public queueRun: () => void
200
+ ) {
201
+ queue(this);
202
+ }
203
+ }
204
+
192
205
  /**
193
206
  * All Scopes that can hold nodes and subscopes, including `SimpleScope` and `OnEachItemScope`
194
207
  * but *not* `OnEachScope`, are `ContentScope`s.
@@ -272,6 +285,8 @@ class ChainedScope extends ContentScope {
272
285
  // If `currentScope` is not actually a ChainedScope, prevSibling will be undefined, as intended
273
286
  this.prevSibling = currentScope.getChildPrevSibling();
274
287
  currentScope.lastChild = this;
288
+ } else {
289
+ this.prevSibling = el.lastChild || undefined;
275
290
  }
276
291
 
277
292
  // We're always adding ourselve as a cleaner, in order to run our own cleaners
@@ -1688,6 +1703,26 @@ export const NO_COPY = Symbol("NO_COPY");
1688
1703
  // Promises break when proxied, so we'll just mark them as NO_COPY
1689
1704
  (Promise.prototype as any)[NO_COPY] = true;
1690
1705
 
1706
+ /**
1707
+ * CSS variable lookup table for the `@` value prefix.
1708
+ *
1709
+ * When a CSS value starts with `@`, the rest is used as a key to look up the actual value.
1710
+ * Pre-initialized with keys '1'-'12' mapping to an exponential rem scale (e.g., @1=0.25rem, @3=1rem).
1711
+ *
1712
+ * @example
1713
+ * ```typescript
1714
+ * cssVars.primary = '#3b82f6';
1715
+ * cssVars[3] = '16px'; // Override @3 to be 16px instead of 1rem
1716
+ * $('p color:@primary'); // Sets color to #3b82f6
1717
+ * $('div mt:@3'); // Sets margin-top to 16px
1718
+ * ```
1719
+ */
1720
+ export const cssVars: Record<string, string> = optProxy({});
1721
+
1722
+ for (let i = 1; i <= 12; i++) {
1723
+ cssVars[i] = 2 ** (i - 3) + "rem";
1724
+ }
1725
+
1691
1726
  /**
1692
1727
  * Clone an (optionally proxied) object or array.
1693
1728
  *
@@ -1806,8 +1841,10 @@ function applyBind(el: HTMLInputElement, target: any) {
1806
1841
  onProxyChange = () => {
1807
1842
  el.value = target.value;
1808
1843
  // biome-ignore lint/suspicious/noDoubleEquals: it's fine for numbers to be casts to strings here
1809
- if (el.tagName === "SELECT" && el.value != target.value)
1810
- throw new Error(`SELECT has no '${target.value}' OPTION (yet)`);
1844
+ if (el.tagName === "SELECT" && el.value != target.value) {
1845
+ // Presumable, OPTIONs haven't been created yet. Try again after all currently queued work has been done.
1846
+ new DelayedOneTimeRunner(() => el.value = target.value);
1847
+ }
1811
1848
  };
1812
1849
  }
1813
1850
  derive(onProxyChange);
@@ -2047,7 +2084,7 @@ function findFirst(str: string, chars: string, startPos: number): number {
2047
2084
  let cssCount = 0;
2048
2085
 
2049
2086
  /**
2050
- * Inserts CSS rules into the document, optionally scoping them with a unique class name.
2087
+ * Inserts CSS rules into the document, scoping them with a unique class name.
2051
2088
  *
2052
2089
  * Takes a JavaScript object representation of CSS rules. camelCased property keys are
2053
2090
  * converted to kebab-case (e.g., `fontSize` becomes `font-size`).
@@ -2058,11 +2095,9 @@ let cssCount = 0;
2058
2095
  * - In case a selector contains a `&`, that character will be replaced by the parent selector.
2059
2096
  * - Selectors will be split on `,` characters, each combining with the parent selector with *or* semantics.
2060
2097
  * - Selector starting with `'@'` define at-rules like media queries. They may be nested within regular selectors.
2061
- * @param global - If `true`, styles are inserted globally without prefixing.
2062
- * If `false` (default), all selectors are prefixed with a unique generated
2063
- * class name (e.g., `.AbdStl1`) to scope the styles.
2064
- * @returns The unique class name prefix used for scoping (e.g., `.AbdStl1`), or an empty string
2065
- * if `global` was `true`. Use this prefix with {@link $} to apply the styles.
2098
+ * @param global - @deprecated Use {@link insertGlobalCss} instead.
2099
+ * @returns The unique class name prefix used for scoping (e.g., `.AbdStl1`). Use this
2100
+ * prefix with {@link $} to apply the styles.
2066
2101
  *
2067
2102
  * @example Scoped Styles
2068
2103
  * ```typescript
@@ -2087,29 +2122,61 @@ let cssCount = 0;
2087
2122
  * $('div.child-element#Child'); // .AbdStl1 .child-element rule applies
2088
2123
  * });
2089
2124
  * ```
2090
- *
2125
+ */
2126
+ export function insertCss(style: object, global = false): string {
2127
+ const prefix = global ? "" : `.AbdStl${++cssCount}`;
2128
+ const css = styleToCss(style, prefix);
2129
+ if (css) $(`style#${css}`);
2130
+ return prefix;
2131
+ }
2132
+
2133
+ /**
2134
+ * Inserts CSS rules globally.
2135
+ *
2136
+ * Works exactly like {@link insertCss}, but without prefixing selectors with a unique class name.
2137
+ *
2091
2138
  * @example Global Styles
2092
2139
  * ```typescript
2093
- * insertCss({
2140
+ * insertGlobalCss({
2094
2141
  * '*': {
2095
2142
  * fontFamily: 'monospace',
2143
+ * m: 0, // Using shortcut for margin
2096
2144
  * },
2097
2145
  * 'a': {
2098
2146
  * textDecoration: 'none',
2099
- * color: "#107ab0",
2147
+ * fg: "@primary", // Using foreground shortcut and CSS variable
2100
2148
  * }
2101
- * }, true); // Pass true for global
2149
+ * });
2102
2150
  *
2103
2151
  * $('a#Styled link');
2104
2152
  * ```
2105
2153
  */
2106
- export function insertCss(style: object, global = false): string {
2107
- const prefix = global ? "" : `.AbdStl${++cssCount}`;
2108
- const css = styleToCss(style, prefix);
2109
- if (css) $(`style#${css}`);
2110
- return prefix;
2154
+ export function insertGlobalCss(style: object): string {
2155
+ return insertCss(style, true);
2111
2156
  }
2112
2157
 
2158
+ const CSS_SHORT: Record<string, string | string[]> = {
2159
+ m: "margin",
2160
+ mt: "marginTop",
2161
+ mb: "marginBottom",
2162
+ ml: "marginLeft",
2163
+ mr: "marginRight",
2164
+ mh: ["marginLeft", "marginRight"],
2165
+ mv: ["marginTop", "marginBottom"],
2166
+ p: "padding",
2167
+ pt: "paddingTop",
2168
+ pb: "paddingBottom",
2169
+ pl: "paddingLeft",
2170
+ pr: "paddingRight",
2171
+ ph: ["paddingLeft", "paddingRight"],
2172
+ pv: ["paddingTop", "paddingBottom"],
2173
+ w: "width",
2174
+ h: "height",
2175
+ bg: "background",
2176
+ fg: "color",
2177
+ r: "borderRadius",
2178
+ };
2179
+
2113
2180
  function styleToCss(style: object, prefix: string): string {
2114
2181
  let props = "";
2115
2182
  let rules = "";
@@ -2127,7 +2194,11 @@ function styleToCss(style: object, prefix: string): string {
2127
2194
  );
2128
2195
  }
2129
2196
  } else {
2130
- props += `${k.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`)}:${v};`;
2197
+ const val = v == null || v === false ? "" : typeof v === 'string' ? (v[0] === '@' ? (cssVars as any)[v.substring(1)] || "" : v) : String(v);
2198
+ const expanded = CSS_SHORT[k] || k;
2199
+ for (const prop of (Array.isArray(expanded) ? expanded : [expanded])) {
2200
+ props += `${prop.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`)}:${val};`;
2201
+ }
2131
2202
  }
2132
2203
  }
2133
2204
  }
@@ -2150,10 +2221,15 @@ function applyArg(el: Element, key: string, value: any) {
2150
2221
  if (value) el.classList.add(...classes);
2151
2222
  else el.classList.remove(...classes);
2152
2223
  } else if (key[0] === "$") {
2153
- // Style
2154
- const name = key.substring(1);
2155
- if (value == null || value === false) (el as any).style[name] = "";
2156
- else (el as any).style[name] = `${value}`;
2224
+ // Style (with shortcuts)
2225
+ key = key.substring(1);
2226
+ const val = value == null || value === false ? "" : typeof value === 'string' ? (value[0] === '@' ? (cssVars as any)[value.substring(1)] || "" : value) : String(value);
2227
+ const expanded = CSS_SHORT[key] || key;
2228
+ if (typeof expanded === "string") {
2229
+ (el as any).style[expanded] = val;
2230
+ } else {
2231
+ for (const prop of expanded) (el as any).style[prop] = val;
2232
+ }
2157
2233
  } else if (value == null) {
2158
2234
  // Value left empty
2159
2235
  // Do nothing
package/README.md.bak DELETED
@@ -1,212 +0,0 @@
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
-
3
- Build fast reactive UIs in pure TypeScript/JavaScript without a virtual DOM.
4
-
5
- Aberdeen's approach is refreshingly simple:
6
-
7
- > Use many small anonymous functions for emitting DOM elements, and automatically rerun them when their underlying data changes. JavaScript `Proxy` is used to track reads and updates to this data, which can consist of anything, from simple values to complex, typed, and deeply nested data structures.
8
-
9
- ## Why use Aberdeen?
10
-
11
- - 🎩 **Simple:** Express UIs naturally in JavaScript/TypeScript, without build steps or JSX, and with a minimal amount of concepts you need to learn.
12
- - ⏩ **Fast:** No virtual DOM. Aberdeen intelligently updates only the minimal, necessary parts of your UI when proxied data changes.
13
- - 👥 **Awesome lists**: It's very easy and performant to reactively display data sorted by whatever you like.
14
- - 🔬 **Tiny:** Around 6KB (minimized and gzipped) for the core system. Zero runtime dependencies.
15
- - 🔋 **Batteries included**: Comes with browser history management, routing, revertible patches for optimistic user-interface updates, component-local CSS, SVG support, helper functions for transforming reactive data (mapping, partitioning, filtering, etc) and hide/unhide transition effects. No bikeshedding required!
16
-
17
- ## Why *not* use Aberdeen?
18
-
19
- - 🤷 **Lack of community:** There are not many of us -Aberdeen developers- yet, so don't expect terribly helpful Stack Overflow/AI answers.
20
- - 📚 **Lack of ecosystem:** You'd have to code things yourself, instead of duct-taping together a gazillion React ecosystem libraries.
21
-
22
- ## Examples
23
-
24
- 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.
25
-
26
- ```javascript
27
- import A from 'aberdeen';
28
-
29
- // Define some state as a proxied (observable) object
30
- const state = A.proxy({question: "How many roads must a man walk down?", answer: 42});
31
-
32
- A`h3`(() => {
33
- // This function reruns whenever the question or the answer changes
34
- A`"${state.question} ↪ ${state.answer || 'Blowing in the wind'}"`;
35
- });
36
-
37
- // Two-way bind state.question to an <input>
38
- A`input placeholder=Question bind!${A.at(state, 'question')}`;
39
-
40
- // Allow state.answer to be modified using both an <input> and buttons
41
- A`div.row`(() => {
42
- A`button click=${() => state.answer--} "-"`;
43
- A`input type=number bind!${A.at(state, 'answer')}`;
44
- A`button click=${() => state.answer++} "+"`;
45
- });
46
- ```
47
-
48
- Okay, next up is a somewhat more complex app - a todo-list with the following behavior:
49
-
50
- - New items open in an 'editing state'.
51
- - Items that are in 'editing state' show a text input, a save button and a cancel button. Done status cannot be toggled while editing.
52
- - 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.
53
- - 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.
54
- - The list of items is sorted alphabetically by label. Items move when 'save' changes their label.
55
- - Items that are created, moved or deleted grow and shrink as appropriate.
56
-
57
- Pfew.. now let's look at the code:
58
-
59
- ```typescript
60
- import A from "aberdeen";
61
- import {grow, shrink} from "aberdeen/transitions";
62
-
63
- // We'll use a simple class to store our data.
64
- class TodoItem {
65
- constructor(public label: string = '', public done: boolean = false) {}
66
- toggle() { this.done = !this.done; }
67
- }
68
-
69
- // The top-level user interface.
70
- function drawMain() {
71
- // Add some initial items. We'll wrap a proxy() around it!
72
- let items: TodoItem[] = A.proxy([
73
- new TodoItem('Make todo-list demo', true),
74
- new TodoItem('Learn Aberdeen', false),
75
- ]);
76
-
77
- // Draw the list, ordered by label.
78
- A.onEach(items, drawItem, item => item.label);
79
-
80
- // Add item and delete checked buttons.
81
- A`div.row`(() => {
82
- A`button click=${() => items.push(new TodoItem(""))} "+"`;
83
- A`button.outline click=${() => {
84
- for(let idx in items) {
85
- if (items[idx].done) delete items[idx];
86
- }
87
- }} "Delete checked"`;
88
- });
89
- };
90
-
91
- // Called for each todo list item.
92
- function drawItem(item) {
93
- // Items without a label open in editing state.
94
- // Note that we're creating this proxy outside the `div.row` scope
95
- // create below, so that it will persist when that state reruns.
96
- let editing: {value: boolean} = A.proxy(item.label == '');
97
-
98
- A`div.row.${todoItemStyle} create!${grow} destroy!${shrink}`(() => {
99
- // Conditionally add a class to `div.row`, based on item.done
100
- A`if!${A.at(item,'done')} .done`;
101
-
102
- // The checkmark is hidden using CSS
103
- A`div.checkmark "✅"`;
104
-
105
- if (editing.value) {
106
- // Label <input>. Save using enter or button.
107
- function save() {
108
- editing.value = false;
109
- item.label = inputElement.value;
110
- }
111
- let inputElement = A`input placeholder=Label value~${item.label} keydown=${e => e.key==='Enter' && save()}`;
112
- A`button.outline click=${() => editing.value = false} "Cancel"`;
113
- A`button click=${save} "Save"`;
114
- } else {
115
- // Label as text.
116
- A`p "${item.label}"`;
117
-
118
- // Edit icon, if not done.
119
- if (!item.done) {
120
- A`a click=${e => {
121
- editing.value = true;
122
- e.stopPropagation(); // We don't want to toggle as well.
123
- }} "Edit"`;
124
- }
125
-
126
- // Clicking a row toggles done.
127
- A`click=${() => item.done = !item.done} cursor:pointer`;
128
- }
129
- });
130
- }
131
-
132
- // Insert some component-local CSS, specific for this demo.
133
- const todoItemStyle = A.insertCss({
134
- marginBottom: "0.5rem",
135
- ".checkmark": {
136
- opacity: 0.2,
137
- },
138
- "&.done": {
139
- textDecoration: "line-through",
140
- ".checkmark": {
141
- opacity: 1,
142
- },
143
- },
144
- });
145
-
146
- // Go!
147
- drawMain();
148
- ```
149
-
150
- Some further examples:
151
-
152
- - [Input demo](https://aberdeenjs.org/examples/input/) - [Source](https://github.com/vanviegen/aberdeen/tree/master/examples/input)
153
- - [Tic Tac Toe demo](https://aberdeenjs.org/examples/tictactoe/) - [Source](https://github.com/vanviegen/aberdeen/tree/master/examples/tictactoe)
154
- - [List demo](https://aberdeenjs.org/examples/list/) - [Source](https://github.com/vanviegen/aberdeen/tree/master/examples/list)
155
- - [Routing demo](https://aberdeenjs.org/examples/router/) - [Source](https://github.com/vanviegen/aberdeen/tree/master/examples/router)
156
- - [JS Framework Benchmark demo](https://aberdeenjs.org/examples/js-framework-benchmark/) - [Source](https://github.com/vanviegen/aberdeen/tree/master/examples/js-framework-benchmark)
157
-
158
- ## Learning Aberdeen
159
-
160
- - [Tutorial](https://aberdeenjs.org/Tutorial/)
161
- - [Reference documentation](https://aberdeenjs.org/modules.html)
162
-
163
- And you may want to study the examples above, of course!
164
-
165
- ## Changelog
166
-
167
- ### 1.2.0 (2025-09-27)
168
-
169
- **Enhancements:**
170
- - The `A` function now supports a concise template literal syntax for creating elements, setting attributes and properties, adding classes, etc. See the documentation for details.
171
- - The `A.proxy()` function can now accept `Promise`s, which will return an observable object with properties for `busy` status, `error` (if any), and the resolved `value`. This makes it easier to call async functions from within UI code.
172
-
173
- **Breaking changes:**
174
- - When a UI render function returns a `Promise`, that will now be reported as an error. Async render functions are fundamentally incompatible with Aberdeen's reactive model, so it's helpful to point that out. Use the new `A.proxy()` async support instead.
175
- - Setting attributes versus properties is no longer automatically inferred. Use `name=value` for attributes and `name~value` for properties.
176
- - Special attributes (like `bind`, `create`, `destroy`, `if`, `else`, `end`, and `html`) now use a different syntax: `name!value` or just `name!`.
177
- - Conditional rendering has been completely redesigned, now using `if!`, `else!`, and `end!` special attributes with different semantics.
178
-
179
- ### 1.1.0 (2025-09-12)
180
-
181
- This major release aims to reduce surprises in our API, aligning more closely with regular JavaScript semantics (for better or worse).
182
-
183
- **Breaking changes:**
184
-
185
- - Functions that iterate objects (like `A.onEach` and `A.map`) will now only work on *own* properties of the object, ignoring those in the prototype chain. The new behavior should be more consistent and faster.
186
- - These iteration function now properly distinguish between `undefined` and *empty*. Previously, object/array/map items with `undefined` values were considered non-existent. The new behavior (though arguably confusing) is more consistent with regular JavaScript semantics.
187
- - The `A.copy` function no longer ..
188
- - Supports `SHALLOW` and `MERGE` flags. The latter has been replaced by a dedicated `A.merge` function. The former turned out not to be particularly useful.
189
- - Has weird special cases that would allow copying objects into maps and merging objects into arrays.
190
- - Copies properties from the prototype chain of objects. Only *own* properties are copied now. As the prototype link itself *is* copied over, this should actually result in copies being *more* similar to the original.
191
- - The `observe` function has been renamed to `A.derive` to better reflect its purpose and match terminology used in other reactive programming libraries.
192
- - The `A.route` API brings some significant changes. Modifying the `A.route` observable (which should now be accessed as `A.route.current`) will now always result in changing the current browser history item (URL and state, using `replaceState`), instead of using a heuristic to figure out what you probably want. Dedicated functions have been added for navigating to a new URL (`A.go`), back to a previous URL (`A.back`), and for going up in the route hierarchy (`A.up`).
193
- - The concept of immediate observers (through the `immediateObserve` function) no longer exists. It caused unexpected behavior (for instance due to the fact that an array `pop()` in JavaScript is implemented as a delete followed by a length change, so happens in two steps that would each call immediate observers). The reason it existed was mostly to enable a pre-1.0 version of the `A.route` API. It turned out to be a mistake.
194
-
195
- **Enhancements:**
196
-
197
- - The `A.peek` function can now also accept an object and a key as argument (e.g. `A.peek(obj, 'myKey')`). It does the same as `A.peek(() => obj.myKey)`, but more concise and faster.
198
- - The `A.copy` and `A.merge` functions now ..
199
- - Accept an optional `dstKey` argument, allowing you to assign to a specific key with `A.copy` semantics, and without subscribing to the key.
200
- - Return a boolean indicating whether any changes were made.
201
- - Are faster.
202
- - A new `A.dispatcher` module has been added. It provides a simple and type-safe way to match URL paths to handler functions, and extract parameters from the path. You can still use your own routing solution if you prefer, of course.
203
- - The `A.route` module now also has tests, making the whole project now fully covered by tests.
204
-
205
- **Fixes:**
206
-
207
- - Browser-back behavior in the `A.route` module had some reliability issues after page reloads.
208
- - The `A.copy` and `A.clone` function created Maps and Arrays with the wrong internal type. So `instanceof Array` would say yes, while `Array.isArray` would say no. JavaScript is weird.
209
-
210
- ### 1.0.0 (2025-05-07)
211
-
212
- 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.