aberdeen 1.1.0 → 1.3.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/src/aberdeen.ts CHANGED
@@ -138,7 +138,7 @@ function partToStr(part: number | string): string {
138
138
  * ]);
139
139
  *
140
140
  * onEach(users, (user) => {
141
- * $(`p:${user.name}: ${user.score}`);
141
+ * $(`p#${user.name}: ${user.score}`);
142
142
  * }, (user) => invertString(user.name)); // Reverse alphabetic order
143
143
  * ```
144
144
  *
@@ -166,7 +166,9 @@ abstract class Scope implements QueueRunner {
166
166
 
167
167
  [ptr: ReverseSortedSetPointer]: this;
168
168
 
169
- abstract onChange(index: any): void;
169
+ onChange(index: any): void {
170
+ queue(this);
171
+ }
170
172
  abstract queueRun(): void;
171
173
 
172
174
  abstract getLastNode(): Node | undefined;
@@ -196,8 +198,8 @@ abstract class ContentScope extends Scope {
196
198
  // be for child scopes, subscriptions as well as `clean(..)` hooks.
197
199
  cleaners: Array<{ delete: (scope: Scope) => void } | (() => void)>;
198
200
 
199
- // Whether this scope is within an SVG namespace context
200
- inSvgNamespace: boolean = false;
201
+ abstract svg: boolean;
202
+ abstract el: Element;
201
203
 
202
204
  constructor(
203
205
  cleaners: Array<{ delete: (scope: Scope) => void } | (() => void)> = [],
@@ -211,8 +213,6 @@ abstract class ContentScope extends Scope {
211
213
  // Should be subclassed in most cases..
212
214
  redraw() {}
213
215
 
214
- abstract parentElement: Element;
215
-
216
216
  getLastNode(): Node | undefined {
217
217
  return findLastNodeInPrevSiblings(this.lastChild);
218
218
  }
@@ -260,16 +260,15 @@ class ChainedScope extends ContentScope {
260
260
 
261
261
  constructor(
262
262
  // The parent DOM element we'll add our child nodes to.
263
- public parentElement: Element,
263
+ public el: Element,
264
+ // Whether this scope is within an SVG namespace context
265
+ public svg: boolean,
264
266
  // When true, we share our 'cleaners' list with the parent scope.
265
267
  useParentCleaners = false,
266
268
  ) {
267
269
  super(useParentCleaners ? currentScope.cleaners : []);
268
270
 
269
- // Inherit SVG namespace state from current scope
270
- this.inSvgNamespace = currentScope.inSvgNamespace;
271
-
272
- if (parentElement === currentScope.parentElement) {
271
+ if (el === currentScope.el) {
273
272
  // If `currentScope` is not actually a ChainedScope, prevSibling will be undefined, as intended
274
273
  this.prevSibling = currentScope.getChildPrevSibling();
275
274
  currentScope.lastChild = this;
@@ -300,12 +299,13 @@ class ChainedScope extends ContentScope {
300
299
  */
301
300
  class RegularScope extends ChainedScope {
302
301
  constructor(
303
- parentElement: Element,
302
+ el: Element,
303
+ svg: boolean,
304
304
  // The function that will be reactively called. Elements it creates using `$` are
305
305
  // added to the appropriate position within `parentElement`.
306
306
  public renderer: () => any,
307
307
  ) {
308
- super(parentElement);
308
+ super(el, svg);
309
309
 
310
310
  // Do the initial run
311
311
  this.redraw();
@@ -325,23 +325,23 @@ class RegularScope extends ChainedScope {
325
325
  }
326
326
 
327
327
  class RootScope extends ContentScope {
328
- parentElement = document.body;
328
+ el = document.body;
329
+ svg = false;
329
330
  getPrecedingNode(): Node | undefined {
330
331
  return undefined;
331
332
  }
332
333
  }
333
334
 
334
335
  class MountScope extends ContentScope {
336
+ svg: boolean;
335
337
  constructor(
336
338
  // The parent DOM element we'll add our child nodes to
337
- public parentElement: Element,
339
+ public el: Element,
338
340
  // The function that
339
341
  public renderer: () => any,
340
342
  ) {
341
343
  super();
342
-
343
- // Inherit SVG namespace state from current scope
344
- this.inSvgNamespace = currentScope.inSvgNamespace;
344
+ this.svg = el.namespaceURI === 'http://www.w3.org/2000/svg';
345
345
 
346
346
  this.redraw();
347
347
  currentScope.cleaners.push(this);
@@ -408,11 +408,9 @@ class ResultScope<T> extends ChainedScope {
408
408
  public result: ValueRef<T> = optProxy({ value: undefined });
409
409
 
410
410
  constructor(
411
- parentElement: Element,
412
411
  public renderer: () => T,
413
412
  ) {
414
- super(parentElement);
415
-
413
+ super(currentScope.el, currentScope.svg);
416
414
  this.redraw();
417
415
  }
418
416
 
@@ -435,18 +433,19 @@ class ResultScope<T> extends ChainedScope {
435
433
  */
436
434
 
437
435
  class SetArgScope extends ChainedScope {
436
+ public svg = false;
438
437
  constructor(
439
- parentElement: Element,
440
- public key: string,
441
- public target: { value: any },
438
+ el: Element,
439
+ private key: string,
440
+ private target: { value: any },
442
441
  ) {
443
- super(parentElement);
442
+ super(el, el.namespaceURI === 'http://www.w3.org/2000/svg');
444
443
  this.redraw();
445
444
  }
446
445
  redraw() {
447
446
  const savedScope = currentScope;
448
447
  currentScope = this;
449
- applyArg(this.key, this.target.value);
448
+ applyArg(this.el, this.key, this.target.value);
450
449
  currentScope = savedScope;
451
450
  }
452
451
  }
@@ -454,7 +453,7 @@ class SetArgScope extends ChainedScope {
454
453
  /** @internal */
455
454
  class OnEachScope extends Scope {
456
455
  // biome-ignore lint/correctness/noInvalidUseBeforeDeclaration: circular, as currentScope is initialized with a Scope
457
- parentElement: Element = currentScope.parentElement;
456
+ parentElement: Element = currentScope.el;
458
457
  prevSibling: Node | Scope | undefined;
459
458
 
460
459
  /** The data structure we are iterating */
@@ -556,7 +555,8 @@ class OnEachScope extends Scope {
556
555
  /** @internal */
557
556
  class OnEachItemScope extends ContentScope {
558
557
  sortKey: string | number | undefined; // When undefined, this scope is currently not showing in the list
559
- public parentElement: Element;
558
+ public el: Element;
559
+ public svg: boolean;
560
560
 
561
561
  constructor(
562
562
  public parent: OnEachScope,
@@ -564,10 +564,10 @@ class OnEachItemScope extends ContentScope {
564
564
  topRedraw: boolean,
565
565
  ) {
566
566
  super();
567
- this.parentElement = parent.parentElement;
567
+ this.el = parent.parentElement;
568
568
 
569
569
  // Inherit SVG namespace state from current scope
570
- this.inSvgNamespace = currentScope.inSvgNamespace;
570
+ this.svg = currentScope.svg;
571
571
 
572
572
  this.parent.byIndex.set(this.itemIndex, this);
573
573
 
@@ -711,8 +711,12 @@ class OnEachItemScope extends ContentScope {
711
711
  }
712
712
  }
713
713
 
714
- function addNode(node: Node) {
715
- const parentEl = currentScope.parentElement;
714
+ function addNode(el: Element, node: Node) {
715
+ if (el !== currentScope.el) {
716
+ el.appendChild(node);
717
+ return;
718
+ }
719
+ const parentEl = currentScope.el;
716
720
  const prevNode = currentScope.getInsertAfterNode();
717
721
  parentEl.insertBefore(
718
722
  node,
@@ -842,7 +846,7 @@ export function onEach<K extends string | number | symbol, T>(
842
846
  * const items = proxy(['apple', 'banana', 'cherry']);
843
847
  *
844
848
  * // Basic iteration
845
- * onEach(items, (item, index) => $(`li:${item} (#${index})`));
849
+ * onEach(items, (item, index) => $(`li#${item} (#${index})`));
846
850
  *
847
851
  * // Add a new item - the list updates automatically
848
852
  * setTimeout(() => items.push('durian'), 2000);
@@ -861,7 +865,7 @@ export function onEach<K extends string | number | symbol, T>(
861
865
  *
862
866
  * // Sort by name alphabetically
863
867
  * onEach(users, (user) => {
864
- * $(`p:${user.name} (id=${user.id})`);
868
+ * $(`p#${user.name} (id=${user.id})`);
865
869
  * }, (user) => [user.group, user.name]); // Sort by group, and within each group sort by name
866
870
  * ```
867
871
  *
@@ -873,8 +877,8 @@ export function onEach<K extends string | number | symbol, T>(
873
877
  * $('dl', () => {
874
878
  * onEach(config, (value, key) => {
875
879
  * if (key === 'showTips') return; // Don't render this one
876
- * $('dt:'+key);
877
- * $('dd:'+value);
880
+ * $('dt#'+key);
881
+ * $('dd#'+value);
878
882
  * });
879
883
  * });
880
884
  *
@@ -921,9 +925,9 @@ const EMPTY = Symbol("empty");
921
925
  * // Reactively display a message if the items array is empty
922
926
  * $('div', () => {
923
927
  * if (isEmpty(items)) {
924
- * $('p', 'i:No items yet!');
928
+ * $('p', 'i#No items yet!');
925
929
  * } else {
926
- * onEach(items, item=>$('p:'+item));
930
+ * onEach(items, item=>$('p#'+item));
927
931
  * }
928
932
  * });
929
933
  *
@@ -1056,7 +1060,7 @@ const objectHandler: ProxyHandler<any> = {
1056
1060
  return true;
1057
1061
  },
1058
1062
  deleteProperty(target: any, prop: any) {
1059
- const old = target.hasOwnProperty(prop) ? target[prop] : undefined;
1063
+ const old = target.hasOwnProperty(prop) ? target[prop] : EMPTY;
1060
1064
  delete target[prop];
1061
1065
  emit(target, prop, EMPTY, old);
1062
1066
  return true;
@@ -1268,7 +1272,8 @@ function optProxy(value: any): any {
1268
1272
  if (
1269
1273
  typeof value !== "object" ||
1270
1274
  !value ||
1271
- value[TARGET_SYMBOL] !== undefined
1275
+ value[TARGET_SYMBOL] !== undefined ||
1276
+ NO_COPY in value
1272
1277
  ) {
1273
1278
  return value;
1274
1279
  }
@@ -1289,6 +1294,25 @@ function optProxy(value: any): any {
1289
1294
  return proxied;
1290
1295
  }
1291
1296
 
1297
+ /**
1298
+ * When `proxy` is called with a Promise, the returned object has this shape.
1299
+ */
1300
+ export interface PromiseProxy<T> {
1301
+ /**
1302
+ * True if the promise is still pending, false if it has resolved or rejected.
1303
+ */
1304
+ busy: boolean;
1305
+ /**
1306
+ * If the promise has resolved, this contains the resolved value.
1307
+ */
1308
+ value?: T;
1309
+ /**
1310
+ * If the promise has rejected, this contains the rejection error.
1311
+ */
1312
+ error?: any;
1313
+ }
1314
+
1315
+ export function proxy<T extends any>(target: Promise<T>): PromiseProxy<T>;
1292
1316
  export function proxy<T extends any>(target: Array<T>): Array<T extends number ? number : T extends string ? string : T extends boolean ? boolean : T >;
1293
1317
  export function proxy<T extends object>(target: T): T;
1294
1318
  export function proxy<T extends any>(target: T): ValueRef<T extends number ? number : T extends string ? string : T extends boolean ? boolean : T>;
@@ -1304,6 +1328,10 @@ export function proxy<T extends any>(target: T): ValueRef<T extends number ? num
1304
1328
  * property access and mutations, but otherwise works like the underlying data.
1305
1329
  * - Primitives (string, number, boolean, null, undefined) are wrapped in an object
1306
1330
  * `{ value: T }` which is then proxied. Access the primitive via the `.value` property.
1331
+ * - Promises are represented by proxied objects `{ busy: boolean, value?: T, error?: any }`.
1332
+ * Initially, `busy` is `true`. When the promise resolves, `value` is set and `busy`
1333
+ * is set to `false`. If the promise is rejected, `error` is set and `busy` is also
1334
+ * set to `false`.
1307
1335
  *
1308
1336
  * Use {@link unproxy} to get the original underlying data back.
1309
1337
  *
@@ -1347,6 +1375,21 @@ export function proxy<T extends any>(target: T): ValueRef<T extends number ? num
1347
1375
  * ```
1348
1376
  */
1349
1377
  export function proxy(target: TargetType): TargetType {
1378
+ if (target instanceof Promise) {
1379
+ const result: PromiseProxy<any> = optProxy({
1380
+ busy: true,
1381
+ });
1382
+ target
1383
+ .then((value) => {
1384
+ result.value = value;
1385
+ result.busy = false;
1386
+ })
1387
+ .catch((err) => {
1388
+ result.error = err;
1389
+ result.busy = false;
1390
+ });
1391
+ return result;
1392
+ }
1350
1393
  return optProxy(
1351
1394
  typeof target === "object" && target !== null ? target : { value: target },
1352
1395
  );
@@ -1439,14 +1482,14 @@ export function copy<T extends object>(dst: T, src: T): boolean;
1439
1482
  export function copy<T extends object>(dst: T, dstKey: keyof T, src: T[typeof dstKey]): boolean;
1440
1483
  export function copy(a: any, b: any, c?: any): boolean {
1441
1484
  if (arguments.length > 2) return copySet(a, b, c, 0);
1442
- return copyRecursive(a, b, 0);
1485
+ return copyImpl(a, b, 0);
1443
1486
  }
1444
1487
 
1445
1488
  function copySet(dst: any, dstKey: any, src: any, flags: number): boolean {
1446
1489
  let dstVal = peek(dst, dstKey);
1447
1490
  if (src === dstVal) return false;
1448
1491
  if (typeof dstVal === "object" && dstVal && typeof src === "object" && src && dstVal.constructor === src.constructor) {
1449
- return copyRecursive(dstVal, src, flags);
1492
+ return copyImpl(dstVal, src, flags);
1450
1493
  }
1451
1494
  src = clone(src);
1452
1495
  if (dst instanceof Map) dst.set(dstKey, src);
@@ -1483,26 +1526,31 @@ export function merge<T extends object>(dst: T, value: Partial<T>): boolean;
1483
1526
  export function merge<T extends object>(dst: T, dstKey: keyof T, value: Partial<T[typeof dstKey]>): boolean;
1484
1527
  export function merge(a: any, b: any, c?: any) {
1485
1528
  if (arguments.length > 2) return copySet(a, b, c, MERGE);
1486
- return copyRecursive(a, b, MERGE);
1529
+ return copyImpl(a, b, MERGE);
1487
1530
  }
1488
1531
 
1489
- // The dst and src parameters must be objects. Will throw a friendly message if they're not both the same type.
1490
- function copyRecursive<T extends object>(dst: T, src: T, flags: number): boolean {
1532
+ function copyImpl(dst: any, src: any, flags: number): boolean {
1491
1533
  // We never want to subscribe to reads we do to the target (to find changes). So we'll
1492
1534
  // take the unproxied version and `emit` updates ourselve.
1493
- let unproxied = (dst as any)[TARGET_SYMBOL] as T;
1535
+ let unproxied = (dst as any)[TARGET_SYMBOL];
1494
1536
  if (unproxied) {
1495
1537
  dst = unproxied;
1496
1538
  flags |= COPY_EMIT;
1497
1539
  }
1498
1540
  // For performance, we'll work on the unproxied `src` and manually subscribe to changes.
1499
- unproxied = (src as any)[TARGET_SYMBOL] as T;
1541
+ unproxied = (src as any)[TARGET_SYMBOL];
1500
1542
  if (unproxied) {
1501
1543
  src = unproxied;
1502
1544
  // If we're not in peek mode, we'll manually subscribe to all source reads.
1503
1545
  if (currentScope !== ROOT_SCOPE && !peeking) flags |= COPY_SUBSCRIBE;
1504
1546
  }
1505
1547
 
1548
+ return copyRecursive(dst, src, flags);
1549
+ }
1550
+
1551
+ // The dst and src parameters must be objects. Will throw a friendly message if they're not both the same type.
1552
+ function copyRecursive<T extends object>(dst: T, src: T, flags: number): boolean {
1553
+
1506
1554
  if (flags & COPY_SUBSCRIBE) subscribe(src, ANY_SYMBOL);
1507
1555
  let changed = false;
1508
1556
 
@@ -1525,7 +1573,7 @@ function copyRecursive<T extends object>(dst: T, src: T, flags: number): boolean
1525
1573
  }
1526
1574
  else if (dstValue !== srcValue) {
1527
1575
  if (srcValue && typeof srcValue === "object") {
1528
- if (typeof dstValue === "object" && dstValue && srcValue.constructor === dstValue.constructor) {
1576
+ if (typeof dstValue === "object" && dstValue && srcValue.constructor === dstValue.constructor && !(NO_COPY in srcValue)) {
1529
1577
  changed = copyRecursive(dstValue, srcValue, flags) || changed;
1530
1578
  continue;
1531
1579
  }
@@ -1561,7 +1609,7 @@ function copyRecursive<T extends object>(dst: T, src: T, flags: number): boolean
1561
1609
  if (dstValue === undefined && !dst.has(key)) dstValue = EMPTY;
1562
1610
  if (dstValue !== srcValue) {
1563
1611
  if (srcValue && typeof srcValue === "object") {
1564
- if (typeof dstValue === "object" && dstValue && srcValue.constructor === dstValue.constructor) {
1612
+ if (typeof dstValue === "object" && dstValue && srcValue.constructor === dstValue.constructor && !(NO_COPY in srcValue)) {
1565
1613
  changed = copyRecursive(dstValue, srcValue, flags) || changed;
1566
1614
  continue;
1567
1615
  }
@@ -1594,7 +1642,7 @@ function copyRecursive<T extends object>(dst: T, src: T, flags: number): boolean
1594
1642
  const dstValue = dst.hasOwnProperty(key) ? dst[key] : EMPTY;
1595
1643
  if (dstValue !== srcValue) {
1596
1644
  if (srcValue && typeof srcValue === "object") {
1597
- if (typeof dstValue === "object" && dstValue && srcValue.constructor === dstValue.constructor) {
1645
+ if (typeof dstValue === "object" && dstValue && srcValue.constructor === dstValue.constructor && !(NO_COPY in srcValue)) {
1598
1646
  changed = copyRecursive(dstValue as typeof srcValue, srcValue, flags) || changed;
1599
1647
  continue;
1600
1648
  }
@@ -1630,6 +1678,16 @@ const MERGE = 1;
1630
1678
  const COPY_SUBSCRIBE = 32;
1631
1679
  const COPY_EMIT = 64;
1632
1680
 
1681
+ /**
1682
+ * A symbol that can be added to an object to prevent it from being cloned by {@link clone} or {@link copy}.
1683
+ * This is useful for objects that should be shared by reference. That also mean that their contents won't
1684
+ * be observed for changes.
1685
+ */
1686
+ export const NO_COPY = Symbol("NO_COPY");
1687
+
1688
+ // Promises break when proxied, so we'll just mark them as NO_COPY
1689
+ (Promise.prototype as any)[NO_COPY] = true;
1690
+
1633
1691
  /**
1634
1692
  * Clone an (optionally proxied) object or array.
1635
1693
  *
@@ -1638,11 +1696,12 @@ const COPY_EMIT = 64;
1638
1696
  * @returns A new unproxied array or object (of the same type as `src`), containing a deep copy of `src`.
1639
1697
  */
1640
1698
  export function clone<T extends object>(src: T): T {
1699
+ if (NO_COPY in src) return src;
1641
1700
  // Create an empty object of the same type
1642
1701
  const copied = Array.isArray(src) ? [] : src instanceof Map ? new Map() : Object.create(Object.getPrototypeOf(src));
1643
1702
  // Copy all properties to it. This doesn't need to emit anything, and because
1644
1703
  // the destination is an empty object, we can just MERGE, which is a bit faster.
1645
- copyRecursive(copied, src, MERGE);
1704
+ copyImpl(copied, src, MERGE);
1646
1705
  return copied;
1647
1706
  }
1648
1707
 
@@ -1695,7 +1754,7 @@ const refHandler: ProxyHandler<RefTarget> = {
1695
1754
  * });
1696
1755
  *
1697
1756
  * // Usage as a dynamic property, causes a TextNode with just the name to be created and live-updated
1698
- * $('p:Selected color: ', {
1757
+ * $('p#Selected color: ', {
1699
1758
  * text: ref(formData, 'color'),
1700
1759
  * $color: ref(formData, 'color')
1701
1760
  * });
@@ -1758,9 +1817,8 @@ function applyBind(el: HTMLInputElement, target: any) {
1758
1817
  });
1759
1818
  }
1760
1819
 
1761
- const SPECIAL_PROPS: { [key: string]: (value: any) => void } = {
1762
- create: (value: any) => {
1763
- const el = currentScope.parentElement;
1820
+ const SPECIAL_PROPS: { [key: string]: (el: Element, value: any) => void } = {
1821
+ create: (el: Element, value: any) => {
1764
1822
  if (currentScope !== topRedrawScope) return;
1765
1823
  if (typeof value === "function") {
1766
1824
  value(el);
@@ -1774,20 +1832,19 @@ const SPECIAL_PROPS: { [key: string]: (value: any) => void } = {
1774
1832
  })();
1775
1833
  }
1776
1834
  },
1777
- destroy: (value: any) => {
1778
- const el = currentScope.parentElement;
1835
+ destroy: (el: Element, value: any) => {
1779
1836
  onDestroyMap.set(el, value);
1780
1837
  },
1781
- html: (value: any) => {
1838
+ html: (el: Element, value: any) => {
1782
1839
  const tmpParent = document.createElement(
1783
- currentScope.parentElement.tagName,
1840
+ currentScope.el.tagName,
1784
1841
  );
1785
1842
  tmpParent.innerHTML = `${value}`;
1786
- while (tmpParent.firstChild) addNode(tmpParent.firstChild);
1843
+ while (tmpParent.firstChild) addNode(el, tmpParent.firstChild);
1844
+ },
1845
+ text: (el: Element, value: any) => {
1846
+ addNode(el, document.createTextNode(value));
1787
1847
  },
1788
- text: (value: any) => {
1789
- addNode(document.createTextNode(value));
1790
- }
1791
1848
  };
1792
1849
 
1793
1850
  /**
@@ -1798,11 +1855,13 @@ const SPECIAL_PROPS: { [key: string]: (value: any) => void } = {
1798
1855
  * @param {...(string | function | object | false | undefined | null)} args - Any number of arguments can be given. How they're interpreted depends on their types:
1799
1856
  *
1800
1857
  * - `string`: Strings can be used to create and insert new elements, set classnames for the *current* element, and add text to the current element.
1801
- * The format of a string is: **tag**? (`.` **class**)* (':' **text**)?
1802
- * meaning it consists of...
1803
- * - An optional HTML **tag**, something like `h1`. If present, a DOM element of that tag is created, and that element will be the *current* element for the rest of this `$` function execution.
1804
- * - Any number of CSS classes prefixed by `.` characters. These classes will be added to the *current* element.
1805
- * - Optional content **text** prefixed by a `:` character, ranging til the end of the string. This will be added as a TextNode to the *current* element.
1858
+ * The format of a string is: (**tag** | `.` **class** | **key**=**val** | **key**="**long val**")* ('#' **text** | **key**=)?
1859
+ * So there can be:
1860
+ * - Any number of **tag** element, like `h1` or `div`. These elements are created, added to the *current* element, and become the new *current* element for the rest of this `$` function execution.
1861
+ * - Any number of CSS classes prefixed by `.` characters. These classes will be added to the *current* element. Optionally, CSS classes can be appended to a **tag** without a space. So both `div.myclass` and `div .myclass` are valid and do the same thing.
1862
+ * - Any number of key/value pairs with string values, like `placeholder="Your name"` or `data-id=123`. These will be handled according to the rules specified for `object`, below, but with the caveat that values can only be strings. The quotes around string values are optional, unless the value contains spaces. It's not possible to escape quotes within the value. If you want to do that, or if you have user-provided values, use the `object` syntax (see below) or end your string with `key=` followed by the data as a separate argument (see below).
1863
+ * - The string may end in a '#' followed by text, which will be added as a TextNode to the *current* element. The text ranges til the end of the string, and may contain any characters, including spaces and quotes.
1864
+ * - Alternatively, the string may end in a key followed by an '=' character, in which case the value is expected as a separate argument. The key/value pair is set according to the rules specified for `object` below. This is useful when the value is not a string or contains spaces or user data. Example: `$('button text="Click me" click=', () => alert('Clicked!'))` or `$('input.value=', someUserData, "placeholder=", "Type your stuff")`.
1806
1865
  * - `function`: When a function (without argument nor a return value) is passed in, it will be reactively executed in its own observer scope, preserving the *current element*. So any `$()` invocations within this function will create DOM elements with our *current* element as parent. If the function reads observable data, and that data is changed later on, the function we re-execute (after side effects, such as DOM modifications through `$`, have been cleaned - see also {@link clean}).
1807
1866
  * - `object`: When an object is passed in, its key-value pairs are used to modify the *current* element in the following ways...
1808
1867
  * - `{<attrName>: any}`: The common case is setting the value as an HTML attribute named key. So `{placeholder: "Your name"}` would add `placeholder="Your name"` to the current HTML element.
@@ -1817,7 +1876,7 @@ const SPECIAL_PROPS: { [key: string]: (value: any) => void } = {
1817
1876
  * - `{<any>: <obsvalue>}`: Create a new observer scope and read the `value` property of the given observable (proxy) variable from within it, and apply the contained value using any of the other rules in this list. Example:
1818
1877
  * ```typescript
1819
1878
  * const myColor = proxy('red');
1820
- * $('p:Test', {$color: myColor, click: () => myColor.value = 'yellow'})
1879
+ * $('p#Test', {$color: myColor, click: () => myColor.value = 'yellow'})
1821
1880
  * // Clicking the text will cause it to change color without recreating the <p> itself
1822
1881
  * ```
1823
1882
  * This is often used together with {@link ref}, in order to use properties other than `.value`.
@@ -1830,16 +1889,24 @@ const SPECIAL_PROPS: { [key: string]: (value: any) => void } = {
1830
1889
  *
1831
1890
  * @example Create Element
1832
1891
  * ```typescript
1833
- * $('button.secondary.outline:Submit', {
1892
+ * $('button.secondary.outline#Submit', {
1834
1893
  * disabled: false,
1835
1894
  * click: () => console.log('Clicked!'),
1836
1895
  * $color: 'red'
1837
1896
  * });
1838
1897
  * ```
1898
+ *
1899
+ * Which can also be written as:
1900
+ * ```typescript
1901
+ * $('button.secondary.outline text=Submit $color=red disabled=', false, 'click=', () => console.log('Clicked!'));
1902
+ * ```
1903
+ *
1904
+ * We want to set `disabled` as a property instead of an attribute, so we must use the `key=` syntax in order to provide
1905
+ * `false` as a boolean instead of a string.
1839
1906
  *
1840
1907
  * @example Create Nested Elements
1841
1908
  * ```typescript
1842
- * let inputElement: Element = $('label:Click me', 'input', {type: 'checkbox'});
1909
+ * let inputElement: Element = $('label#Click me', 'input', {type: 'checkbox'});
1843
1910
  * // You should usually not touch raw DOM elements, unless when integrating
1844
1911
  * // with non-Aberdeen code.
1845
1912
  * console.log('DOM element:', inputElement);
@@ -1850,8 +1917,8 @@ const SPECIAL_PROPS: { [key: string]: (value: any) => void } = {
1850
1917
  * const state = proxy({ count: 0 });
1851
1918
  * $('div', () => { // Outer element
1852
1919
  * // This scope re-renders when state.count changes
1853
- * $(`p:Count is ${state.count}`);
1854
- * $('button:Increment', { click: () => state.count++ });
1920
+ * $(`p#Count is ${state.count}`);
1921
+ * $('button#Increment', { click: () => state.count++ });
1855
1922
  * });
1856
1923
  * ```
1857
1924
  *
@@ -1860,17 +1927,17 @@ const SPECIAL_PROPS: { [key: string]: (value: any) => void } = {
1860
1927
  * const user = proxy({ name: '' });
1861
1928
  * $('input', { placeholder: 'Name', bind: ref(user, 'name') });
1862
1929
  * $('h3', () => { // Reactive scope
1863
- * $(`:Hello ${user.name || 'stranger'}`);
1930
+ * $(`#Hello ${user.name || 'stranger'}`);
1864
1931
  * });
1865
1932
  * ```
1866
1933
  *
1867
1934
  * @example Conditional Rendering
1868
1935
  * ```typescript
1869
1936
  * const show = proxy(false);
1870
- * $('button', { click: () => show.value = !show.value }, () => $(show.value ? ':Hide' : ':Show'));
1937
+ * $('button', { click: () => show.value = !show.value }, () => $(show.value ? '#Hide' : '#Show'));
1871
1938
  * $(() => { // Reactive scope
1872
1939
  * if (show.value) {
1873
- * $('p:Details are visible!');
1940
+ * $('p#Details are visible!');
1874
1941
  * }
1875
1942
  * });
1876
1943
  * ```
@@ -1886,96 +1953,104 @@ export function $(
1886
1953
  | Record<string, any>
1887
1954
  )[]
1888
1955
  ): undefined | Element {
1889
- let savedCurrentScope: undefined | ContentScope;
1890
- let err: undefined | string;
1891
- let result: undefined | Element;
1892
-
1893
- for (let arg of args) {
1894
- if (arg == null || arg === false) continue;
1895
- if (typeof arg === "string") {
1896
- let text: undefined | string;
1897
- let classes: undefined | string;
1898
- const textPos = arg.indexOf(":");
1899
- if (textPos >= 0) {
1900
- text = arg.substring(textPos + 1);
1901
- arg = arg.substring(0, textPos);
1902
- }
1903
- const classPos = arg.indexOf(".");
1904
- if (classPos >= 0) {
1905
- classes = arg.substring(classPos + 1);
1906
- arg = arg.substring(0, classPos);
1907
- }
1908
- if (arg === "") {
1909
- // Add text or classes to parent
1910
- if (text) addNode(document.createTextNode(text));
1911
- if (classes) {
1912
- const el = currentScope.parentElement;
1913
- el.classList.add(...classes.split("."));
1914
- if (!savedCurrentScope) {
1915
- clean(() => el.classList.remove(...classes.split(".")));
1956
+ let el: undefined | Element = currentScope.el;
1957
+ let svg: boolean = currentScope.svg
1958
+
1959
+ const argCount = args.length;
1960
+ for(let argIndex = 0; argIndex < argCount; argIndex++) {
1961
+ const arg = args[argIndex];
1962
+ if (arg == null || arg === false) {
1963
+ // Ignore
1964
+ } else if (typeof arg === "string") {
1965
+ let argLen = arg.length;
1966
+ let nextPos = 0;
1967
+ for(let pos=0; pos<argLen; pos=nextPos+1) {
1968
+ nextPos = findFirst(arg, " .=:#", pos);
1969
+ const next = arg[nextPos];
1970
+
1971
+ if (next === ":" || next === "=") {
1972
+ let key = arg.substring(pos, nextPos);
1973
+ if (next === ':') key = '$' + key; // Style prefix
1974
+ if (nextPos + 1 >= argLen) {
1975
+ applyArg(el, key, args[++argIndex]);
1976
+ break;
1977
+ }
1978
+ if (arg[nextPos+1] === '"') {
1979
+ const endIndex = findFirst(arg, '"', nextPos + 2);
1980
+ const value = arg.substring(nextPos+2, endIndex);
1981
+ applyArg(el, key, value);
1982
+ nextPos = endIndex;
1983
+ } else {
1984
+ const endIndex = findFirst(arg, " ", nextPos + 1);
1985
+ const value = arg.substring(nextPos + 1, endIndex);
1986
+ applyArg(el, key, value);
1987
+ nextPos = endIndex;
1916
1988
  }
1917
- }
1918
- } else if (arg.indexOf(" ") >= 0) {
1919
- err = `Tag '${arg}' cannot contain space`;
1920
- break;
1921
- } else {
1922
- // Determine which namespace to use for element creation
1923
- const useNamespace = currentScope.inSvgNamespace || arg === 'svg';
1924
- if (useNamespace) {
1925
- result = document.createElementNS('http://www.w3.org/2000/svg', arg);
1926
1989
  } else {
1927
- result = document.createElement(arg);
1928
- }
1929
-
1930
- if (classes) result.className = classes.replaceAll(".", " ");
1931
- if (text) result.textContent = text;
1932
- addNode(result);
1933
- if (!savedCurrentScope) {
1934
- savedCurrentScope = currentScope;
1935
- }
1936
- const newScope = new ChainedScope(result, true);
1937
-
1938
- // If we're creating an SVG element, set the SVG namespace flag for child scopes
1939
- if (arg === 'svg') {
1940
- newScope.inSvgNamespace = true;
1990
+ if (nextPos > pos) { // Up til this point if non-empty, is a tag
1991
+ const tag = arg.substring(pos, nextPos);
1992
+ // Determine which namespace to use for element creation
1993
+ svg ||= tag === 'svg';
1994
+ let newEl = svg ? document.createElementNS('http://www.w3.org/2000/svg', tag) : document.createElement(tag);
1995
+ addNode(el, newEl);
1996
+ el = newEl;
1997
+ }
1998
+
1999
+ if (next === "#") { // The rest of the string is text (or a text arg follows)
2000
+ const text = nextPos + 1 < argLen ? arg.substring(nextPos + 1) : args[++argIndex];
2001
+ applyArg(el, "text", text);
2002
+ break;
2003
+ }
2004
+
2005
+ if (next === ".") { // Class name
2006
+ let classEnd = findFirst(arg, " #=.", nextPos + 1);
2007
+ if (arg[classEnd] === '=' && classEnd + 1 >= argLen) {
2008
+ // Conditional class name. Pass to applyArg including the leading '.'
2009
+ applyArg(el, arg.substring(nextPos, classEnd), args[++argIndex]);
2010
+ nextPos = classEnd;
2011
+ } else {
2012
+ let className: any = arg.substring(nextPos + 1, classEnd);
2013
+ el.classList.add(className || args[++argIndex]);
2014
+ nextPos = classEnd - 1;
2015
+ }
2016
+ }
1941
2017
  }
1942
-
1943
- newScope.lastChild = result.lastChild || undefined;
1944
- if (topRedrawScope === currentScope) topRedrawScope = newScope;
1945
- currentScope = newScope;
1946
2018
  }
1947
2019
  } else if (typeof arg === "object") {
1948
2020
  if (arg.constructor !== Object) {
1949
2021
  if (arg instanceof Node) {
1950
- addNode(arg);
2022
+ addNode(el, arg);
1951
2023
  if (arg instanceof Element) {
1952
- // If it's an Element, it may contain children, so we make it the current scope
1953
- if (!savedCurrentScope) savedCurrentScope = currentScope;
1954
- currentScope = new ChainedScope(arg, true);
1955
- currentScope.lastChild = arg.lastChild || undefined;
2024
+ el = arg;
2025
+ svg = arg.namespaceURI === 'http://www.w3.org/2000/svg';
1956
2026
  }
1957
2027
  } else {
1958
- err = `Unexpected argument: ${arg}`;
1959
- break;
2028
+ throw new Error(`Unexpected argument: ${arg}`);
1960
2029
  }
1961
2030
  } else {
1962
2031
  for (const key of Object.keys(arg)) {
1963
- const val = arg[key];
1964
- applyArg(key, val);
2032
+ applyArg(el, key, arg[key]);
1965
2033
  }
1966
2034
  }
1967
2035
  } else if (typeof arg === "function") {
1968
- new RegularScope(currentScope.parentElement, arg);
2036
+ new RegularScope(el, svg, arg);
1969
2037
  } else {
1970
- err = `Unexpected argument: ${arg}`;
1971
- break;
2038
+ throw new Error(`Unexpected argument: ${arg}`);
1972
2039
  }
1973
2040
  }
1974
- if (savedCurrentScope) {
1975
- currentScope = savedCurrentScope;
2041
+ return el;
2042
+ }
2043
+
2044
+ function findFirst(str: string, chars: string, startPos: number): number {
2045
+ if (chars.length === 1) {
2046
+ const idx = str.indexOf(chars, startPos);
2047
+ return idx >= 0 ? idx : str.length;
1976
2048
  }
1977
- if (err) throw new Error(err);
1978
- return result;
2049
+ const strLen = str.length;
2050
+ for (let i = startPos; i < strLen; i++) {
2051
+ if (chars.indexOf(str[i]) >= 0) return i;
2052
+ }
2053
+ return strLen;
1979
2054
  }
1980
2055
 
1981
2056
  let cssCount = 0;
@@ -2017,8 +2092,8 @@ let cssCount = 0;
2017
2092
  *
2018
2093
  * // Apply the styles
2019
2094
  * $(scopeClass, () => { // Add class to the div
2020
- * $(`:Scoped content`);
2021
- * $('div.child-element:Child'); // .AbdStl1 .child-element rule applies
2095
+ * $(`#Scoped content`);
2096
+ * $('div.child-element#Child'); // .AbdStl1 .child-element rule applies
2022
2097
  * });
2023
2098
  * ```
2024
2099
  *
@@ -2034,13 +2109,13 @@ let cssCount = 0;
2034
2109
  * }
2035
2110
  * }, true); // Pass true for global
2036
2111
  *
2037
- * $('a:Styled link');
2112
+ * $('a#Styled link');
2038
2113
  * ```
2039
2114
  */
2040
2115
  export function insertCss(style: object, global = false): string {
2041
2116
  const prefix = global ? "" : `.AbdStl${++cssCount}`;
2042
2117
  const css = styleToCss(style, prefix);
2043
- if (css) $(`style:${css}`);
2118
+ if (css) $(`style#${css}`);
2044
2119
  return prefix;
2045
2120
  }
2046
2121
 
@@ -2069,8 +2144,7 @@ function styleToCss(style: object, prefix: string): string {
2069
2144
  return rules;
2070
2145
  }
2071
2146
 
2072
- function applyArg(key: string, value: any) {
2073
- const el = currentScope.parentElement;
2147
+ function applyArg(el: Element, key: string, value: any) {
2074
2148
  if (typeof value === "object" && value !== null && value[TARGET_SYMBOL]) {
2075
2149
  // Value is a proxy
2076
2150
  if (key === "bind") {
@@ -2094,11 +2168,11 @@ function applyArg(key: string, value: any) {
2094
2168
  // Do nothing
2095
2169
  } else if (key in SPECIAL_PROPS) {
2096
2170
  // Special property
2097
- SPECIAL_PROPS[key](value);
2171
+ SPECIAL_PROPS[key](el, value);
2098
2172
  } else if (typeof value === "function") {
2099
2173
  // Event listener
2100
2174
  el.addEventListener(key, value);
2101
- clean(() => el.removeEventListener(key, value));
2175
+ if (el === currentScope.el) clean(() => el.removeEventListener(key, value));
2102
2176
  } else if (
2103
2177
  value === true ||
2104
2178
  value === false ||
@@ -2143,7 +2217,7 @@ let onError: (error: Error) => boolean | undefined = defaultOnError;
2143
2217
  *
2144
2218
  * try {
2145
2219
  * // Attempt to show a custom message in the UI
2146
- * $('div.error-message:Oops, something went wrong!');
2220
+ * $('div.error-message#Oops, something went wrong!');
2147
2221
  * } catch (e) {
2148
2222
  * // Ignore errors during error handling itself
2149
2223
  * }
@@ -2200,7 +2274,7 @@ export function setErrorHandler(
2200
2274
  * ```
2201
2275
  */
2202
2276
  export function getParentElement(): Element {
2203
- return currentScope.parentElement;
2277
+ return currentScope.el;
2204
2278
  }
2205
2279
 
2206
2280
  /**
@@ -2222,7 +2296,7 @@ export function getParentElement(): Element {
2222
2296
  *
2223
2297
  * // Show the array items and maintain the sum
2224
2298
  * onEach(myArray, (item, index) => {
2225
- * $(`code:${index}→${item}`);
2299
+ * $(`code#${index}→${item}`);
2226
2300
  * // We'll update sum.value using peek, as += first does a read, but
2227
2301
  * // we don't want to subscribe.
2228
2302
  * peek(() => sum.value += item);
@@ -2265,14 +2339,14 @@ export function clean(cleaner: () => void) {
2265
2339
  *
2266
2340
  * $('main', () => {
2267
2341
  * console.log('Welcome');
2268
- * $('h3:Welcome, ' + data.user); // Reactive text
2342
+ * $('h3#Welcome, ' + data.user); // Reactive text
2269
2343
  *
2270
2344
  * derive(() => {
2271
2345
  * // When data.notifications changes, only this inner scope reruns,
2272
2346
  * // leaving the `<p>Welcome, ..</p>` untouched.
2273
2347
  * console.log('Notifications');
2274
- * $('code.notification-badge:' + data.notifications);
2275
- * $('a:Notify!', {click: () => data.notifications++});
2348
+ * $('code.notification-badge#' + data.notifications);
2349
+ * $('a#Notify!', {click: () => data.notifications++});
2276
2350
  * });
2277
2351
  * });
2278
2352
  * ```
@@ -2286,7 +2360,7 @@ export function clean(cleaner: () => void) {
2286
2360
  * const double = derive(() => counter.value * 2);
2287
2361
  *
2288
2362
  * $('h3', () => {
2289
- * $(`:counter=${counter.value} double=${double.value}`);
2363
+ * $(`#counter=${counter.value} double=${double.value}`);
2290
2364
  * })
2291
2365
  * ```
2292
2366
  *
@@ -2294,7 +2368,7 @@ export function clean(cleaner: () => void) {
2294
2368
  * @param func Func without a return value.
2295
2369
  */
2296
2370
  export function derive<T>(func: () => T): ValueRef<T> {
2297
- return new ResultScope<T>(currentScope.parentElement, func).result;
2371
+ return new ResultScope<T>(func).result;
2298
2372
  }
2299
2373
 
2300
2374
  /**
@@ -2326,12 +2400,12 @@ export function derive<T>(func: () => T): ValueRef<T> {
2326
2400
  * setInterval(() => runTime.value++, 1000);
2327
2401
  *
2328
2402
  * mount(document.getElementById('app-root'), () => {
2329
- * $('h4:Aberdeen App');
2330
- * $(`p:Run time: ${runTime.value}s`);
2403
+ * $('h4#Aberdeen App');
2404
+ * $(`p#Run time: ${runTime.value}s`);
2331
2405
  * // Conditionally render some content somewhere else in the static page
2332
2406
  * if (runTime.value&1) {
2333
2407
  * mount(document.getElementById('title-extra'), () =>
2334
- * $(`i:(${runTime.value}s)`)
2408
+ * $(`i#(${runTime.value}s)`)
2335
2409
  * );
2336
2410
  * }
2337
2411
  * });
@@ -2664,7 +2738,7 @@ export function partition<
2664
2738
  func: (value: IN_V, key: KeyToString<IN_K>) => undefined | OUT_K | OUT_K[],
2665
2739
  ): Record<OUT_K, Record<KeyToString<IN_K>, IN_V>> {
2666
2740
  const unproxiedOut = {} as Record<OUT_K, Record<KeyToString<IN_K>, IN_V>>;
2667
- const out = proxy(unproxiedOut);
2741
+ const out = optProxy(unproxiedOut);
2668
2742
  onEach(source, (item: IN_V, key: KeyToString<IN_K>) => {
2669
2743
  const rsp = func(item, key);
2670
2744
  if (rsp != null) {
@@ -2706,7 +2780,7 @@ export function partition<
2706
2780
  * items: ['a', 'b']
2707
2781
  * });
2708
2782
  *
2709
- * $('h2:Live State Dump');
2783
+ * $('h2#Live State Dump');
2710
2784
  * dump(state);
2711
2785
  *
2712
2786
  * // Change state later, the dump in the DOM will update
@@ -2715,24 +2789,22 @@ export function partition<
2715
2789
  */
2716
2790
  export function dump<T>(data: T): T {
2717
2791
  if (data && typeof data === "object") {
2718
- let label;
2719
- if (data instanceof Array) {
2720
- label = "<array>";
2721
- } else if (data instanceof Map) {
2722
- label = "<Map>";
2792
+ const name = data.constructor.name.toLowerCase() || "unknown object";
2793
+ $(`#<${name}>`);
2794
+ if (NO_COPY in data ) {
2795
+ $("# [NO_COPY]");
2723
2796
  } else {
2724
- label = "<object>";
2725
- }
2726
- $({ text: label });
2727
- $("ul", () => {
2728
- onEach(data as any, (value, key) => {
2729
- $(`li:${JSON.stringify(key)}: `, () => {
2730
- dump(value);
2797
+ $("ul", () => {
2798
+ onEach(data as any, (value, key) => {
2799
+ $("li", () => {
2800
+ $(`#${JSON.stringify(key)}: `);
2801
+ dump(value);
2802
+ });
2731
2803
  });
2732
2804
  });
2733
- });
2734
- } else {
2735
- $({ text: JSON.stringify(data) });
2805
+ }
2806
+ } else if (data !== undefined) {
2807
+ $("#" + JSON.stringify(data));
2736
2808
  }
2737
2809
  return data;
2738
2810
  }
@@ -2754,7 +2826,7 @@ function handleError(e: any, showMessage: boolean) {
2754
2826
  console.error(e);
2755
2827
  }
2756
2828
  try {
2757
- if (showMessage) $("div.aberdeen-error:Error");
2829
+ if (showMessage) $("div.aberdeen-error#Error");
2758
2830
  } catch {
2759
2831
  // Error while adding the error marker to the DOM. Apparently, we're in
2760
2832
  // an awkward context. The error should already have been logged by