aberdeen 1.2.0 → 1.3.1

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
  *
@@ -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,10 +1294,22 @@ function optProxy(value: any): any {
1289
1294
  return proxied;
1290
1295
  }
1291
1296
 
1292
- interface PromiseProxy<T> {
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
+ */
1293
1304
  busy: boolean;
1294
- error?: any;
1305
+ /**
1306
+ * If the promise has resolved, this contains the resolved value.
1307
+ */
1295
1308
  value?: T;
1309
+ /**
1310
+ * If the promise has rejected, this contains the rejection error.
1311
+ */
1312
+ error?: any;
1296
1313
  }
1297
1314
 
1298
1315
  export function proxy<T extends any>(target: Promise<T>): PromiseProxy<T>;
@@ -1556,7 +1573,7 @@ function copyRecursive<T extends object>(dst: T, src: T, flags: number): boolean
1556
1573
  }
1557
1574
  else if (dstValue !== srcValue) {
1558
1575
  if (srcValue && typeof srcValue === "object") {
1559
- if (typeof dstValue === "object" && dstValue && srcValue.constructor === dstValue.constructor) {
1576
+ if (typeof dstValue === "object" && dstValue && srcValue.constructor === dstValue.constructor && !(NO_COPY in srcValue)) {
1560
1577
  changed = copyRecursive(dstValue, srcValue, flags) || changed;
1561
1578
  continue;
1562
1579
  }
@@ -1592,7 +1609,7 @@ function copyRecursive<T extends object>(dst: T, src: T, flags: number): boolean
1592
1609
  if (dstValue === undefined && !dst.has(key)) dstValue = EMPTY;
1593
1610
  if (dstValue !== srcValue) {
1594
1611
  if (srcValue && typeof srcValue === "object") {
1595
- if (typeof dstValue === "object" && dstValue && srcValue.constructor === dstValue.constructor) {
1612
+ if (typeof dstValue === "object" && dstValue && srcValue.constructor === dstValue.constructor && !(NO_COPY in srcValue)) {
1596
1613
  changed = copyRecursive(dstValue, srcValue, flags) || changed;
1597
1614
  continue;
1598
1615
  }
@@ -1625,7 +1642,7 @@ function copyRecursive<T extends object>(dst: T, src: T, flags: number): boolean
1625
1642
  const dstValue = dst.hasOwnProperty(key) ? dst[key] : EMPTY;
1626
1643
  if (dstValue !== srcValue) {
1627
1644
  if (srcValue && typeof srcValue === "object") {
1628
- if (typeof dstValue === "object" && dstValue && srcValue.constructor === dstValue.constructor) {
1645
+ if (typeof dstValue === "object" && dstValue && srcValue.constructor === dstValue.constructor && !(NO_COPY in srcValue)) {
1629
1646
  changed = copyRecursive(dstValue as typeof srcValue, srcValue, flags) || changed;
1630
1647
  continue;
1631
1648
  }
@@ -1661,6 +1678,16 @@ const MERGE = 1;
1661
1678
  const COPY_SUBSCRIBE = 32;
1662
1679
  const COPY_EMIT = 64;
1663
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
+
1664
1691
  /**
1665
1692
  * Clone an (optionally proxied) object or array.
1666
1693
  *
@@ -1669,6 +1696,7 @@ const COPY_EMIT = 64;
1669
1696
  * @returns A new unproxied array or object (of the same type as `src`), containing a deep copy of `src`.
1670
1697
  */
1671
1698
  export function clone<T extends object>(src: T): T {
1699
+ if (NO_COPY in src) return src;
1672
1700
  // Create an empty object of the same type
1673
1701
  const copied = Array.isArray(src) ? [] : src instanceof Map ? new Map() : Object.create(Object.getPrototypeOf(src));
1674
1702
  // Copy all properties to it. This doesn't need to emit anything, and because
@@ -1726,7 +1754,7 @@ const refHandler: ProxyHandler<RefTarget> = {
1726
1754
  * });
1727
1755
  *
1728
1756
  * // Usage as a dynamic property, causes a TextNode with just the name to be created and live-updated
1729
- * $('p:Selected color: ', {
1757
+ * $('p#Selected color: ', {
1730
1758
  * text: ref(formData, 'color'),
1731
1759
  * $color: ref(formData, 'color')
1732
1760
  * });
@@ -1789,9 +1817,8 @@ function applyBind(el: HTMLInputElement, target: any) {
1789
1817
  });
1790
1818
  }
1791
1819
 
1792
- const SPECIAL_PROPS: { [key: string]: (value: any) => void } = {
1793
- create: (value: any) => {
1794
- const el = currentScope.parentElement;
1820
+ const SPECIAL_PROPS: { [key: string]: (el: Element, value: any) => void } = {
1821
+ create: (el: Element, value: any) => {
1795
1822
  if (currentScope !== topRedrawScope) return;
1796
1823
  if (typeof value === "function") {
1797
1824
  value(el);
@@ -1805,19 +1832,18 @@ const SPECIAL_PROPS: { [key: string]: (value: any) => void } = {
1805
1832
  })();
1806
1833
  }
1807
1834
  },
1808
- destroy: (value: any) => {
1809
- const el = currentScope.parentElement;
1835
+ destroy: (el: Element, value: any) => {
1810
1836
  onDestroyMap.set(el, value);
1811
1837
  },
1812
- html: (value: any) => {
1838
+ html: (el: Element, value: any) => {
1813
1839
  const tmpParent = document.createElement(
1814
- currentScope.parentElement.tagName,
1840
+ currentScope.el.tagName,
1815
1841
  );
1816
1842
  tmpParent.innerHTML = `${value}`;
1817
- while (tmpParent.firstChild) addNode(tmpParent.firstChild);
1843
+ while (tmpParent.firstChild) addNode(el, tmpParent.firstChild);
1818
1844
  },
1819
- text: (value: any) => {
1820
- addNode(document.createTextNode(value));
1845
+ text: (el: Element, value: any) => {
1846
+ addNode(el, document.createTextNode(value));
1821
1847
  },
1822
1848
  };
1823
1849
 
@@ -1829,12 +1855,12 @@ const SPECIAL_PROPS: { [key: string]: (value: any) => void } = {
1829
1855
  * @param {...(string | function | object | false | undefined | null)} args - Any number of arguments can be given. How they're interpreted depends on their types:
1830
1856
  *
1831
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.
1832
- * The format of a string is: (**tag** | `.` **class** | **key**=**val** | **key**="**long val**")* (':' **text** | **key**=)?
1858
+ * The format of a string is: (**tag** | `.` **class** | **key**=**val** | **key**="**long val**")* ('#' **text** | **key**=)?
1833
1859
  * So there can be:
1834
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.
1835
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.
1836
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).
1837
- * - 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.
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.
1838
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")`.
1839
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}).
1840
1866
  * - `object`: When an object is passed in, its key-value pairs are used to modify the *current* element in the following ways...
@@ -1850,7 +1876,7 @@ const SPECIAL_PROPS: { [key: string]: (value: any) => void } = {
1850
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:
1851
1877
  * ```typescript
1852
1878
  * const myColor = proxy('red');
1853
- * $('p:Test', {$color: myColor, click: () => myColor.value = 'yellow'})
1879
+ * $('p#Test', {$color: myColor, click: () => myColor.value = 'yellow'})
1854
1880
  * // Clicking the text will cause it to change color without recreating the <p> itself
1855
1881
  * ```
1856
1882
  * This is often used together with {@link ref}, in order to use properties other than `.value`.
@@ -1863,7 +1889,7 @@ const SPECIAL_PROPS: { [key: string]: (value: any) => void } = {
1863
1889
  *
1864
1890
  * @example Create Element
1865
1891
  * ```typescript
1866
- * $('button.secondary.outline:Submit', {
1892
+ * $('button.secondary.outline#Submit', {
1867
1893
  * disabled: false,
1868
1894
  * click: () => console.log('Clicked!'),
1869
1895
  * $color: 'red'
@@ -1880,7 +1906,7 @@ const SPECIAL_PROPS: { [key: string]: (value: any) => void } = {
1880
1906
  *
1881
1907
  * @example Create Nested Elements
1882
1908
  * ```typescript
1883
- * let inputElement: Element = $('label:Click me', 'input', {type: 'checkbox'});
1909
+ * let inputElement: Element = $('label#Click me', 'input', {type: 'checkbox'});
1884
1910
  * // You should usually not touch raw DOM elements, unless when integrating
1885
1911
  * // with non-Aberdeen code.
1886
1912
  * console.log('DOM element:', inputElement);
@@ -1891,8 +1917,8 @@ const SPECIAL_PROPS: { [key: string]: (value: any) => void } = {
1891
1917
  * const state = proxy({ count: 0 });
1892
1918
  * $('div', () => { // Outer element
1893
1919
  * // This scope re-renders when state.count changes
1894
- * $(`p:Count is ${state.count}`);
1895
- * $('button:Increment', { click: () => state.count++ });
1920
+ * $(`p#Count is ${state.count}`);
1921
+ * $('button#Increment', { click: () => state.count++ });
1896
1922
  * });
1897
1923
  * ```
1898
1924
  *
@@ -1901,154 +1927,121 @@ const SPECIAL_PROPS: { [key: string]: (value: any) => void } = {
1901
1927
  * const user = proxy({ name: '' });
1902
1928
  * $('input', { placeholder: 'Name', bind: ref(user, 'name') });
1903
1929
  * $('h3', () => { // Reactive scope
1904
- * $(`:Hello ${user.name || 'stranger'}`);
1930
+ * $(`#Hello ${user.name || 'stranger'}`);
1905
1931
  * });
1906
1932
  * ```
1907
1933
  *
1908
1934
  * @example Conditional Rendering
1909
1935
  * ```typescript
1910
1936
  * const show = proxy(false);
1911
- * $('button', { click: () => show.value = !show.value }, () => $(show.value ? ':Hide' : ':Show'));
1937
+ * $('button', { click: () => show.value = !show.value }, () => $(show.value ? '#Hide' : '#Show'));
1912
1938
  * $(() => { // Reactive scope
1913
1939
  * if (show.value) {
1914
- * $('p:Details are visible!');
1940
+ * $('p#Details are visible!');
1915
1941
  * }
1916
1942
  * });
1917
1943
  * ```
1918
1944
  */
1919
1945
 
1920
- export function $(
1921
- ...args: (
1922
- | string
1923
- | null
1924
- | undefined
1925
- | false
1926
- | (() => void)
1927
- | Record<string, any>
1928
- )[]
1929
- ): undefined | Element {
1930
- let savedCurrentScope: undefined | ContentScope;
1931
- let err: undefined | string;
1932
- let result: undefined | Element;
1933
- let nextArgIsProp: undefined | string;
1934
-
1935
- for (let arg of args) {
1936
- if (nextArgIsProp) {
1937
- applyArg(nextArgIsProp, arg);
1938
- nextArgIsProp = undefined;
1939
- } else if (arg == null || arg === false) {
1946
+ export function $(...args: any[]): undefined | Element {
1947
+ let el: undefined | Element = currentScope.el;
1948
+ let svg: boolean = currentScope.svg
1949
+
1950
+ const argCount = args.length;
1951
+ for(let argIndex = 0; argIndex < argCount; argIndex++) {
1952
+ const arg = args[argIndex];
1953
+ if (arg == null || arg === false) {
1940
1954
  // Ignore
1941
1955
  } else if (typeof arg === "string") {
1942
- let pos = 0;
1943
1956
  let argLen = arg.length;
1944
- while(pos < argLen) {
1945
- let nextSpace = arg.indexOf(" ", pos);
1946
- if (nextSpace < 0) nextSpace = arg.length;
1947
- let part = arg.substring(pos, nextSpace);
1948
- const oldPos = pos;
1949
- pos = nextSpace + 1;
1950
-
1951
- const firstIs = part.indexOf('=');
1952
- const firstColon = part.indexOf(':');
1953
- if (firstIs >= 0 && (firstColon < 0 || firstIs < firstColon)) {
1954
- const prop = part.substring(0, firstIs);
1955
- if (firstIs < part.length - 1) {
1956
- let value = part.substring(firstIs + 1);
1957
- if (value[0] === '"') {
1958
- const closeIndex = arg.indexOf('"', firstIs+2+oldPos);
1959
- if (closeIndex < 0) throw new Error(`Unterminated string for '${prop}'`);
1960
- value = arg.substring(firstIs+2+oldPos, closeIndex);
1961
- pos = closeIndex + 1;
1962
- if (arg[pos] === ' ') pos++;
1963
- }
1964
- applyArg(prop, value);
1965
- continue;
1957
+ let nextPos = 0;
1958
+ for(let pos=0; pos<argLen; pos=nextPos+1) {
1959
+ nextPos = findFirst(arg, " .=:#", pos);
1960
+ const next = arg[nextPos];
1961
+
1962
+ if (next === ":" || next === "=") {
1963
+ let key = arg.substring(pos, nextPos);
1964
+ if (next === ':') key = '$' + key; // Style prefix
1965
+ if (nextPos + 1 >= argLen) {
1966
+ applyArg(el, key, args[++argIndex]);
1967
+ break;
1968
+ }
1969
+ if (arg[nextPos+1] === '"') {
1970
+ const endIndex = findFirst(arg, '"', nextPos + 2);
1971
+ const value = arg.substring(nextPos+2, endIndex);
1972
+ applyArg(el, key, value);
1973
+ nextPos = endIndex;
1966
1974
  } else {
1967
- if (pos < argLen) throw new Error(`No value given for '${part}'`);
1968
- nextArgIsProp = prop;
1969
- break
1975
+ const endIndex = findFirst(arg, " ", nextPos + 1);
1976
+ const value = arg.substring(nextPos + 1, endIndex);
1977
+ applyArg(el, key, value);
1978
+ nextPos = endIndex;
1970
1979
  }
1971
- }
1972
-
1973
- let text;
1974
- if (firstColon >= 0) {
1975
- // Read to the end of the arg, ignoring any spaces
1976
- text = arg.substring(firstColon + 1 + oldPos);
1977
- part = part.substring(0, firstColon);
1978
- if (!text) {
1979
- if (pos < argLen) throw new Error(`No value given for '${part}'`);
1980
- nextArgIsProp = 'text';
1981
- break;
1980
+ } else {
1981
+ if (nextPos > pos) { // Up til this point if non-empty, is a tag
1982
+ const tag = arg.substring(pos, nextPos);
1983
+ // Determine which namespace to use for element creation
1984
+ svg ||= tag === 'svg';
1985
+ let newEl = svg ? document.createElementNS('http://www.w3.org/2000/svg', tag) : document.createElement(tag);
1986
+ addNode(el, newEl);
1987
+ el = newEl;
1982
1988
  }
1983
- pos = argLen;
1984
- }
1985
-
1986
- let classes: undefined | string;
1987
- const classPos = part.indexOf(".");
1988
- if (classPos >= 0) {
1989
- classes = part.substring(classPos + 1);
1990
- part = part.substring(0, classPos);
1991
- }
1992
1989
 
1993
- if (part) { // Add an element
1994
- // Determine which namespace to use for element creation
1995
- const svg = currentScope.inSvgNamespace || part === 'svg';
1996
- if (svg) {
1997
- result = document.createElementNS('http://www.w3.org/2000/svg', part);
1998
- } else {
1999
- result = document.createElement(part);
1990
+ if (next === "#") { // The rest of the string is text (or a text arg follows)
1991
+ const text = nextPos + 1 < argLen ? arg.substring(nextPos + 1) : args[++argIndex];
1992
+ applyArg(el, "text", text);
1993
+ break;
2000
1994
  }
2001
- addNode(result);
2002
- if (!savedCurrentScope) savedCurrentScope = currentScope;
2003
- const newScope = new ChainedScope(result, true);
2004
-
2005
- // SVG namespace should be inherited by children
2006
- if (svg) newScope.inSvgNamespace = true;
2007
-
2008
- if (topRedrawScope === currentScope) topRedrawScope = newScope;
2009
- currentScope = newScope;
2010
- }
2011
1995
 
2012
- if (text) addNode(document.createTextNode(text));
2013
- if (classes) {
2014
- const el = currentScope.parentElement;
2015
- el.classList.add(...classes.split("."));
2016
- if (!savedCurrentScope) {
2017
- clean(() => el.classList.remove(...classes.split(".")));
1996
+ if (next === ".") { // Class name
1997
+ let classEnd = findFirst(arg, " #=.", nextPos + 1);
1998
+ if (arg[classEnd] === '=' && classEnd + 1 >= argLen) {
1999
+ // Conditional class name. Pass to applyArg including the leading '.'
2000
+ applyArg(el, arg.substring(nextPos, classEnd), args[++argIndex]);
2001
+ nextPos = classEnd;
2002
+ } else {
2003
+ let className: any = arg.substring(nextPos + 1, classEnd);
2004
+ el.classList.add(className || args[++argIndex]);
2005
+ nextPos = classEnd - 1;
2006
+ }
2018
2007
  }
2019
2008
  }
2020
2009
  }
2021
2010
  } else if (typeof arg === "object") {
2022
2011
  if (arg.constructor !== Object) {
2023
2012
  if (arg instanceof Node) {
2024
- addNode(arg);
2013
+ addNode(el, arg);
2025
2014
  if (arg instanceof Element) {
2026
- // If it's an Element, it may contain children, so we make it the current scope
2027
- if (!savedCurrentScope) savedCurrentScope = currentScope;
2028
- currentScope = new ChainedScope(arg, true);
2029
- currentScope.lastChild = arg.lastChild || undefined;
2015
+ el = arg;
2016
+ svg = arg.namespaceURI === 'http://www.w3.org/2000/svg';
2030
2017
  }
2031
2018
  } else {
2032
- err = `Unexpected argument: ${arg}`;
2033
- break;
2019
+ throw new Error(`Unexpected argument: ${arg}`);
2034
2020
  }
2035
2021
  } else {
2036
2022
  for (const key of Object.keys(arg)) {
2037
- const val = arg[key];
2038
- applyArg(key, val);
2023
+ applyArg(el, key, arg[key]);
2039
2024
  }
2040
2025
  }
2041
2026
  } else if (typeof arg === "function") {
2042
- new RegularScope(currentScope.parentElement, arg);
2027
+ new RegularScope(el, svg, arg);
2043
2028
  } else {
2044
- err = `Unexpected argument: ${arg}`;
2045
- break;
2029
+ throw new Error(`Unexpected argument: ${arg}`);
2046
2030
  }
2047
2031
  }
2048
- if (nextArgIsProp !== undefined) throw new Error(`No value given for '${nextArgIsProp}='`);
2049
- if (savedCurrentScope) currentScope = savedCurrentScope;
2050
- if (err) throw new Error(err);
2051
- return result;
2032
+ return el;
2033
+ }
2034
+
2035
+ function findFirst(str: string, chars: string, startPos: number): number {
2036
+ if (chars.length === 1) {
2037
+ const idx = str.indexOf(chars, startPos);
2038
+ return idx >= 0 ? idx : str.length;
2039
+ }
2040
+ const strLen = str.length;
2041
+ for (let i = startPos; i < strLen; i++) {
2042
+ if (chars.indexOf(str[i]) >= 0) return i;
2043
+ }
2044
+ return strLen;
2052
2045
  }
2053
2046
 
2054
2047
  let cssCount = 0;
@@ -2090,8 +2083,8 @@ let cssCount = 0;
2090
2083
  *
2091
2084
  * // Apply the styles
2092
2085
  * $(scopeClass, () => { // Add class to the div
2093
- * $(`:Scoped content`);
2094
- * $('div.child-element:Child'); // .AbdStl1 .child-element rule applies
2086
+ * $(`#Scoped content`);
2087
+ * $('div.child-element#Child'); // .AbdStl1 .child-element rule applies
2095
2088
  * });
2096
2089
  * ```
2097
2090
  *
@@ -2107,13 +2100,13 @@ let cssCount = 0;
2107
2100
  * }
2108
2101
  * }, true); // Pass true for global
2109
2102
  *
2110
- * $('a:Styled link');
2103
+ * $('a#Styled link');
2111
2104
  * ```
2112
2105
  */
2113
2106
  export function insertCss(style: object, global = false): string {
2114
2107
  const prefix = global ? "" : `.AbdStl${++cssCount}`;
2115
2108
  const css = styleToCss(style, prefix);
2116
- if (css) $(`style:${css}`);
2109
+ if (css) $(`style#${css}`);
2117
2110
  return prefix;
2118
2111
  }
2119
2112
 
@@ -2142,8 +2135,7 @@ function styleToCss(style: object, prefix: string): string {
2142
2135
  return rules;
2143
2136
  }
2144
2137
 
2145
- function applyArg(key: string, value: any) {
2146
- const el = currentScope.parentElement;
2138
+ function applyArg(el: Element, key: string, value: any) {
2147
2139
  if (typeof value === "object" && value !== null && value[TARGET_SYMBOL]) {
2148
2140
  // Value is a proxy
2149
2141
  if (key === "bind") {
@@ -2167,11 +2159,11 @@ function applyArg(key: string, value: any) {
2167
2159
  // Do nothing
2168
2160
  } else if (key in SPECIAL_PROPS) {
2169
2161
  // Special property
2170
- SPECIAL_PROPS[key](value);
2162
+ SPECIAL_PROPS[key](el, value);
2171
2163
  } else if (typeof value === "function") {
2172
2164
  // Event listener
2173
2165
  el.addEventListener(key, value);
2174
- clean(() => el.removeEventListener(key, value));
2166
+ if (el === currentScope.el) clean(() => el.removeEventListener(key, value));
2175
2167
  } else if (
2176
2168
  value === true ||
2177
2169
  value === false ||
@@ -2216,7 +2208,7 @@ let onError: (error: Error) => boolean | undefined = defaultOnError;
2216
2208
  *
2217
2209
  * try {
2218
2210
  * // Attempt to show a custom message in the UI
2219
- * $('div.error-message:Oops, something went wrong!');
2211
+ * $('div.error-message#Oops, something went wrong!');
2220
2212
  * } catch (e) {
2221
2213
  * // Ignore errors during error handling itself
2222
2214
  * }
@@ -2273,7 +2265,7 @@ export function setErrorHandler(
2273
2265
  * ```
2274
2266
  */
2275
2267
  export function getParentElement(): Element {
2276
- return currentScope.parentElement;
2268
+ return currentScope.el;
2277
2269
  }
2278
2270
 
2279
2271
  /**
@@ -2295,7 +2287,7 @@ export function getParentElement(): Element {
2295
2287
  *
2296
2288
  * // Show the array items and maintain the sum
2297
2289
  * onEach(myArray, (item, index) => {
2298
- * $(`code:${index}→${item}`);
2290
+ * $(`code#${index}→${item}`);
2299
2291
  * // We'll update sum.value using peek, as += first does a read, but
2300
2292
  * // we don't want to subscribe.
2301
2293
  * peek(() => sum.value += item);
@@ -2338,14 +2330,14 @@ export function clean(cleaner: () => void) {
2338
2330
  *
2339
2331
  * $('main', () => {
2340
2332
  * console.log('Welcome');
2341
- * $('h3:Welcome, ' + data.user); // Reactive text
2333
+ * $('h3#Welcome, ' + data.user); // Reactive text
2342
2334
  *
2343
2335
  * derive(() => {
2344
2336
  * // When data.notifications changes, only this inner scope reruns,
2345
2337
  * // leaving the `<p>Welcome, ..</p>` untouched.
2346
2338
  * console.log('Notifications');
2347
- * $('code.notification-badge:' + data.notifications);
2348
- * $('a:Notify!', {click: () => data.notifications++});
2339
+ * $('code.notification-badge#' + data.notifications);
2340
+ * $('a#Notify!', {click: () => data.notifications++});
2349
2341
  * });
2350
2342
  * });
2351
2343
  * ```
@@ -2359,7 +2351,7 @@ export function clean(cleaner: () => void) {
2359
2351
  * const double = derive(() => counter.value * 2);
2360
2352
  *
2361
2353
  * $('h3', () => {
2362
- * $(`:counter=${counter.value} double=${double.value}`);
2354
+ * $(`#counter=${counter.value} double=${double.value}`);
2363
2355
  * })
2364
2356
  * ```
2365
2357
  *
@@ -2367,7 +2359,7 @@ export function clean(cleaner: () => void) {
2367
2359
  * @param func Func without a return value.
2368
2360
  */
2369
2361
  export function derive<T>(func: () => T): ValueRef<T> {
2370
- return new ResultScope<T>(currentScope.parentElement, func).result;
2362
+ return new ResultScope<T>(func).result;
2371
2363
  }
2372
2364
 
2373
2365
  /**
@@ -2399,12 +2391,12 @@ export function derive<T>(func: () => T): ValueRef<T> {
2399
2391
  * setInterval(() => runTime.value++, 1000);
2400
2392
  *
2401
2393
  * mount(document.getElementById('app-root'), () => {
2402
- * $('h4:Aberdeen App');
2403
- * $(`p:Run time: ${runTime.value}s`);
2394
+ * $('h4#Aberdeen App');
2395
+ * $(`p#Run time: ${runTime.value}s`);
2404
2396
  * // Conditionally render some content somewhere else in the static page
2405
2397
  * if (runTime.value&1) {
2406
2398
  * mount(document.getElementById('title-extra'), () =>
2407
- * $(`i:(${runTime.value}s)`)
2399
+ * $(`i#(${runTime.value}s)`)
2408
2400
  * );
2409
2401
  * }
2410
2402
  * });
@@ -2737,7 +2729,7 @@ export function partition<
2737
2729
  func: (value: IN_V, key: KeyToString<IN_K>) => undefined | OUT_K | OUT_K[],
2738
2730
  ): Record<OUT_K, Record<KeyToString<IN_K>, IN_V>> {
2739
2731
  const unproxiedOut = {} as Record<OUT_K, Record<KeyToString<IN_K>, IN_V>>;
2740
- const out = proxy(unproxiedOut);
2732
+ const out = optProxy(unproxiedOut);
2741
2733
  onEach(source, (item: IN_V, key: KeyToString<IN_K>) => {
2742
2734
  const rsp = func(item, key);
2743
2735
  if (rsp != null) {
@@ -2779,7 +2771,7 @@ export function partition<
2779
2771
  * items: ['a', 'b']
2780
2772
  * });
2781
2773
  *
2782
- * $('h2:Live State Dump');
2774
+ * $('h2#Live State Dump');
2783
2775
  * dump(state);
2784
2776
  *
2785
2777
  * // Change state later, the dump in the DOM will update
@@ -2788,24 +2780,22 @@ export function partition<
2788
2780
  */
2789
2781
  export function dump<T>(data: T): T {
2790
2782
  if (data && typeof data === "object") {
2791
- let label;
2792
- if (data instanceof Array) {
2793
- label = "<array>";
2794
- } else if (data instanceof Map) {
2795
- label = "<Map>";
2783
+ const name = data.constructor.name.toLowerCase() || "unknown object";
2784
+ $(`#<${name}>`);
2785
+ if (NO_COPY in data ) {
2786
+ $("# [NO_COPY]");
2796
2787
  } else {
2797
- label = "<object>";
2798
- }
2799
- $({ text: label });
2800
- $("ul", () => {
2801
- onEach(data as any, (value, key) => {
2802
- $(`li:${JSON.stringify(key)}: `, () => {
2803
- dump(value);
2788
+ $("ul", () => {
2789
+ onEach(data as any, (value, key) => {
2790
+ $("li", () => {
2791
+ $(`#${JSON.stringify(key)}: `);
2792
+ dump(value);
2793
+ });
2804
2794
  });
2805
2795
  });
2806
- });
2807
- } else {
2808
- $({ text: JSON.stringify(data) });
2796
+ }
2797
+ } else if (data !== undefined) {
2798
+ $("#" + JSON.stringify(data));
2809
2799
  }
2810
2800
  return data;
2811
2801
  }
@@ -2827,7 +2817,7 @@ function handleError(e: any, showMessage: boolean) {
2827
2817
  console.error(e);
2828
2818
  }
2829
2819
  try {
2830
- if (showMessage) $("div.aberdeen-error:Error");
2820
+ if (showMessage) $("div.aberdeen-error#Error");
2831
2821
  } catch {
2832
2822
  // Error while adding the error marker to the DOM. Apparently, we're in
2833
2823
  // an awkward context. The error should already have been logged by