@oscarpalmer/abydon 0.21.0 → 0.23.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.
Files changed (42) hide show
  1. package/README.md +2 -0
  2. package/dist/abydon.full.mjs +422 -359
  3. package/dist/attribute/index.d.mts +7 -0
  4. package/dist/attribute/index.mjs +47 -0
  5. package/dist/{node/attribute → attribute}/value.d.mts +2 -2
  6. package/dist/attribute/value.mjs +53 -0
  7. package/dist/constants.d.mts +8 -8
  8. package/dist/constants.mjs +15 -15
  9. package/dist/fragment.d.mts +30 -13
  10. package/dist/fragment.mjs +40 -18
  11. package/dist/fragments.d.mts +3 -7
  12. package/dist/fragments.mjs +6 -12
  13. package/dist/helpers/dom.d.mts +2 -3
  14. package/dist/helpers/dom.mjs +2 -4
  15. package/dist/helpers/index.d.mts +16 -2
  16. package/dist/helpers/index.mjs +17 -1
  17. package/dist/index.d.mts +35 -7
  18. package/dist/index.mjs +39 -8
  19. package/dist/models.d.mts +6 -4
  20. package/dist/node/event.mjs +9 -4
  21. package/dist/node/index.mjs +26 -32
  22. package/dist/node/value.mjs +70 -56
  23. package/dist/parse.mjs +6 -4
  24. package/package.json +6 -5
  25. package/src/attribute/index.ts +89 -0
  26. package/src/attribute/value.ts +107 -0
  27. package/src/constants.ts +19 -19
  28. package/src/fragment.ts +43 -21
  29. package/src/fragments.ts +8 -15
  30. package/src/helpers/dom.ts +3 -5
  31. package/src/helpers/index.ts +25 -1
  32. package/src/index.ts +47 -7
  33. package/src/models.ts +6 -4
  34. package/src/node/event.ts +12 -5
  35. package/src/node/index.ts +36 -56
  36. package/src/node/value.ts +121 -104
  37. package/src/parse.ts +15 -5
  38. package/dist/node/attribute/index.d.mts +0 -6
  39. package/dist/node/attribute/index.mjs +0 -51
  40. package/dist/node/attribute/value.mjs +0 -65
  41. package/src/node/attribute/index.ts +0 -100
  42. package/src/node/attribute/value.ts +0 -158
package/src/constants.ts CHANGED
@@ -4,13 +4,15 @@ export const ARRAY_COMPARISON_DISSIMILAR = 'dissimilar';
4
4
 
5
5
  export const ARRAY_COMPARISON_REMOVED = 'removed';
6
6
 
7
- export const ATTRIBUTE_CLASS_PREFIX_LENGTH = 6;
7
+ export const ATTRIBUTE_CLASS_PREFIX_LENGTH = 'class.'.length;
8
8
 
9
9
  export const ATTRIBUTE_NAME_DELIMITER = '.';
10
10
 
11
+ export const CHANGE_INPUTS = new Set(['checkbox', 'radio']);
12
+
11
13
  export const ERROR_FRAGMENT = 'Fragment function must return a Fragment instance';
12
14
 
13
- export const ERROR_IDENTIFIER_DUPLICATE = "Duplicate identifier found: '<id>'";
15
+ export const ERROR_IDENTIFIER_DUPLICATE = "Duplicate identifier found: '<>'";
14
16
 
15
17
  export const ERROR_IDENTIFIER_TYPE = 'Identifier cannot be null or undefined';
16
18
 
@@ -29,17 +31,15 @@ export const EVENT_DEFAULTS: Record<string, string> = {
29
31
  TEXTAREA: EVENT_INPUT,
30
32
  };
31
33
 
32
- export const EVENT_ON_PREFIXED = '@on';
33
-
34
34
  export const EVENT_ON_VALUE = 'on';
35
35
 
36
36
  export const EVENT_OPTIONS_DELIMITER = ':';
37
37
 
38
- export const EXPRESSION_ABYDON_ATTRIBUTE_FULL = /(@?[\w-]+)="<!--abydon.(\d+)-->"/g;
38
+ export const EXPRESSION_ABYDON_ATTRIBUTE_FULL = /(@?[\w-.]+(?::[a-z:]+)?)="<!--abydon.(\d+)-->"/g;
39
39
 
40
- export const EXPRESSION_ABYDON_ATTRIBUTE_PREFIX = /^_/;
40
+ export const EXPRESSION_ABYDON_ATTRIBUTE_PREFIX = /^abydon-/;
41
41
 
42
- export const EXPRESSION_ABYDON_CONTENT = /^@?abydon\.(\d+)@?$/;
42
+ export const EXPRESSION_ABYDON_CONTENT = /^abydon\.(\d+)$/;
43
43
 
44
44
  export const EXPRESSION_ATTRIBUTE_CLASS = /^class\./;
45
45
 
@@ -47,32 +47,32 @@ export const EXPRESSION_ATTRIBUTE_STYLE_FULL = /^style\.([\w-]+)(?:\.([\w-]+))?$
47
47
 
48
48
  export const EXPRESSION_ATTRIBUTE_STYLE_PREFIX = /^style\./;
49
49
 
50
- export const EXPRESSION_EVENT_CHANGE_TYPES = /^(checkbox|radio)$/;
50
+ export const EXPRESSION_ATTRIBUTE_STYLE_VARIABLE = /^style\.--/;
51
51
 
52
- export const EXPRESSION_EVENT_NAME = /^@([\w-]+)(?::([a-z:]+))?$/i;
52
+ export const EXPRESSION_EVENT_ATTRIBUTE = /^@([\w-]+)(?::([a-z:]+))?="$/i;
53
53
 
54
- export const EXPRESSION_EVENT_OPTIONS_ACTIVE = /^a(ctive)$/i;
54
+ export const EXPRESSION_EVENT_NAME = /^@?([\w-]+)(?::([a-z:]+))?$/i;
55
55
 
56
- export const EXPRESSION_EVENT_OPTIONS_CAPTURE = /^c(apture)$/i;
56
+ export const EXPRESSION_EVENT_OPTIONS_ACTIVE = /^a(?:ctive)$/i;
57
57
 
58
- export const EXPRESSION_EVENT_OPTIONS_ONCE = /^o(nce)$/i;
59
-
60
- export const EXPRESSION_EVENT_PREFIX = /^@/;
58
+ export const EXPRESSION_EVENT_OPTIONS_CAPTURE = /^c(?:apture)$/i;
61
59
 
62
- export const EXPRESSION_PERIOD = /\./;
60
+ export const EXPRESSION_EVENT_OPTIONS_ONCE = /^o(?:nce)$/i;
63
61
 
64
- export const EXPRESSION_TEXTAREA_VALUE = /<!--abydon\.(\d+)-->/;
62
+ export const EXPRESSION_EVENT_PREFIX = /^@/;
65
63
 
66
- export const INPUT_TYPE_CHECKBOX = 'checkbox';
64
+ export const EXPRESSION_TEXTAREA_VALUE = /(?:<|&lt;)!--abydon\.(\d+)--(?:>|&gt;)/;
67
65
 
68
66
  export const NAME_FRAGMENT = '$fragment';
69
67
 
70
68
  export const NAME_FRAGMENTS = '$fragments';
71
69
 
72
- export const PROPERTY_CHECKED = 'checked';
73
-
74
70
  export const PROPERTY_IDENTIFIER = 'identifier';
75
71
 
76
72
  export const PROPERTY_VALUE = 'value';
77
73
 
78
74
  export const TEMPLATE_ITEM = '<>';
75
+
76
+ export const VALUE_TRUE = 'true';
77
+
78
+ export const WHITESPACE = /\s+/g;
package/src/fragment.ts CHANGED
@@ -17,7 +17,16 @@ export class Fragment {
17
17
  };
18
18
 
19
19
  /**
20
- * Fragment identifier
20
+ * Is template caching enabled?
21
+ */
22
+ get cache(): boolean {
23
+ return this.#configuration.cache;
24
+ }
25
+
26
+ /**
27
+ * Identifier for the _Fragment_
28
+ *
29
+ * _An identifier can be used to uniquely identify a Fragment, which helps prevent re-rendering in reactive arrays and Fragments_
21
30
  */
22
31
  get identifier(): unknown {
23
32
  return this.#configuration.identifier;
@@ -41,7 +50,15 @@ export class Fragment {
41
50
  }
42
51
 
43
52
  /**
44
- * Append the fragment to the given element
53
+ * Insert the _Fragment_ after the given element
54
+ * @param element Element to insert after
55
+ */
56
+ after(element: Element): void {
57
+ element.after(...this.get());
58
+ }
59
+
60
+ /**
61
+ * Append the _Fragment_ to the given element
45
62
  * @param element Element to append to
46
63
  */
47
64
  appendTo(element: Element): void {
@@ -49,9 +66,19 @@ export class Fragment {
49
66
  }
50
67
 
51
68
  /**
52
- * Configure the fragment
69
+ * Insert the _Fragment_ before the given element
70
+ * @param element Element to insert before
71
+ */
72
+ before(element: Element): void {
73
+ element.before(...this.get());
74
+ }
75
+
76
+ /**
77
+ * Configure the _Fragment_
78
+ *
79
+ * _Returns the Fragment instance for chaining_
53
80
  * @param configuration Configuration options
54
- * @returns Fragment
81
+ * @returns _Fragment_
55
82
  */
56
83
  configure(configuration: FragmentConfiguration): Fragment {
57
84
  const actual = isPlainObject(configuration) ? configuration : {};
@@ -68,7 +95,7 @@ export class Fragment {
68
95
  }
69
96
 
70
97
  /**
71
- * Get a list of the fragment's nodes
98
+ * Get a list of the _Fragment_'s nodes
72
99
  * @returns List of nodes
73
100
  */
74
101
  get(): ChildNode[] {
@@ -91,33 +118,28 @@ export class Fragment {
91
118
 
92
119
  mapNodes(
93
120
  data,
94
- data.items.flatMap(
95
- item => item.fragments?.flatMap(fragment => fragment.get()) ?? item.nodes ?? [],
96
- ),
121
+ data.items.flatMap(item => item.nodes!),
97
122
  );
98
123
  }
99
124
 
100
125
  return data.items.flatMap(
101
- item => item.fragments?.flatMap(fragment => fragment.get()) ?? item.nodes ?? [],
126
+ item => item.fragments?.flatMap(fragment => fragment.get()) ?? item.nodes!,
102
127
  );
103
128
  }
104
129
 
105
130
  /**
106
- * Set an identifier for the fragment
107
- *
108
- * _An identifier can be used to uniquely identify a fragment,
109
- * which helps prevent re-rendering in certain scenarios._
110
- * @param identifier Identifier
111
- * @returns Fragment
131
+ * Prepend the _Fragment_ to the given element
132
+ * @param element Element to prepend to
112
133
  */
113
- identify(identifier: unknown): Fragment {
114
- this.#configuration.identifier = identifier;
115
-
116
- return this;
134
+ prependTo(element: Element): void {
135
+ element.prepend(...this.get());
117
136
  }
118
137
 
119
138
  /**
120
- * Remove the fragment from the DOM
139
+ * Remove the _Fragment_ _(and all its descendants)_ from the _DOM_
140
+ *
141
+ * - _Any events, reactive values, and Fragments will also be cleaned up and removed_
142
+ * - _After being removed, the Fragment can be re-inserted into the DOM_
121
143
  */
122
144
  remove(): void {
123
145
  removeFragment(this.#data);
@@ -137,7 +159,7 @@ function removeFragment(data: FragmentData): void {
137
159
  fragments?.[fragmentIndex]?.remove();
138
160
  }
139
161
 
140
- removeNodes(nodes ?? []);
162
+ removeNodes(nodes!);
141
163
  }
142
164
 
143
165
  data.items.length = 0;
package/src/fragments.ts CHANGED
@@ -14,13 +14,6 @@ import type {FragmentsState} from './models';
14
14
  export class Fragments {
15
15
  readonly #state: FragmentsState;
16
16
 
17
- /**
18
- * Fragment items
19
- */
20
- get items(): ReactiveArray<Fragment> {
21
- return this.#state.mapped;
22
- }
23
-
24
17
  constructor(
25
18
  items: ReactiveArray<unknown>,
26
19
  identify: (item: unknown) => unknown,
@@ -39,18 +32,16 @@ export class Fragments {
39
32
  subscriber: undefined,
40
33
  };
41
34
 
42
- states.set(this, this.#state);
35
+ fragmentsStates.set(this, this.#state);
43
36
 
44
37
  initializeFragments(this.#state);
45
38
  }
46
39
  }
47
40
 
48
41
  export function handleFragments(item: Fragments | FragmentsState, remove: boolean): void {
49
- const state = isFragments(item) ? states.get(item) : item;
42
+ const state = isFragments(item) ? fragmentsStates.get(item) : item;
50
43
 
51
- if (state != null) {
52
- (remove ? removeFragments : initializeFragments)(state);
53
- }
44
+ (remove ? removeFragments : initializeFragments)(state!);
54
45
  }
55
46
 
56
47
  function handleItems(state: FragmentsState, items: unknown[]): void {
@@ -83,7 +74,9 @@ function handleItems(state: FragmentsState, items: unknown[]): void {
83
74
  }
84
75
  }
85
76
 
86
- instance.identify(key);
77
+ instance.configure({
78
+ identifier: key,
79
+ });
87
80
 
88
81
  state.instances[key] = instance;
89
82
 
@@ -97,7 +90,7 @@ function handleItems(state: FragmentsState, items: unknown[]): void {
97
90
  updateFragments(state, keys);
98
91
  }
99
92
 
100
- export function initializeFragments(state: FragmentsState): void {
93
+ function initializeFragments(state: FragmentsState): void {
101
94
  state.subscriber ??= state.array.subscribe(items => {
102
95
  handleItems(state, items);
103
96
  });
@@ -135,4 +128,4 @@ function updateFragments(state: FragmentsState, active?: Set<string>): void {
135
128
 
136
129
  //
137
130
 
138
- const states: WeakMap<Fragments, FragmentsState> = new WeakMap();
131
+ export const fragmentsStates: WeakMap<Fragments, FragmentsState> = new WeakMap();
@@ -14,10 +14,6 @@ export function createNodes(value: unknown): ChildNode[] {
14
14
  return [new Text(getString(value))];
15
15
  }
16
16
 
17
- export function isInputElement(node: Node): node is HTMLInputElement | HTMLSelectElement {
18
- return node instanceof HTMLInputElement || node instanceof HTMLSelectElement;
19
- }
20
-
21
17
  export function removeNodes(nodes: ChildNode[]): void {
22
18
  const {length} = nodes;
23
19
 
@@ -26,7 +22,7 @@ export function removeNodes(nodes: ChildNode[]): void {
26
22
  }
27
23
  }
28
24
 
29
- export function replaceNodes(from: ChildNode[], to: ChildNode[]): void {
25
+ export function replaceNodes(from: ChildNode[], to: ChildNode[]): ChildNode[] {
30
26
  from[0]?.replaceWith(...to);
31
27
 
32
28
  const {length} = from;
@@ -34,4 +30,6 @@ export function replaceNodes(from: ChildNode[], to: ChildNode[]): void {
34
30
  for (let index = 1; index < length; index += 1) {
35
31
  from[index].remove();
36
32
  }
33
+
34
+ return to;
37
35
  }
@@ -1,4 +1,5 @@
1
- import type {PlainObject} from '@oscarpalmer/atoms/models';
1
+ import type {GenericCallback, PlainObject} from '@oscarpalmer/atoms/models';
2
+ import {computed, type Computed} from '@oscarpalmer/mora';
2
3
  import {
3
4
  ARRAY_COMPARISON_ADDED,
4
5
  ARRAY_COMPARISON_DISSIMILAR,
@@ -8,6 +9,7 @@ import {
8
9
  } from '../constants';
9
10
  import type {Fragment} from '../fragment';
10
11
  import type {Fragments} from '../fragments';
12
+ import type {FragmentData} from '../models';
11
13
 
12
14
  export function compareArrays(
13
15
  first: unknown[],
@@ -27,10 +29,20 @@ export function compareArrays(
27
29
  return firstIsLarger ? ARRAY_COMPARISON_REMOVED : ARRAY_COMPARISON_ADDED;
28
30
  }
29
31
 
32
+ /**
33
+ * Is the value a _Fragment_?
34
+ * @param value Value to check
35
+ * @returns `true` if the value is a _Fragment_, otherwise `false`
36
+ */
30
37
  export function isFragment(value: unknown): value is Fragment {
31
38
  return isNamed(value, NAME_FRAGMENT);
32
39
  }
33
40
 
41
+ /**
42
+ * Is the value a _Fragments_ instance?
43
+ * @param value Value to check
44
+ * @returns `true` if the value is a _Fragments_ instance, otherwise `false`
45
+ */
34
46
  export function isFragments(value: unknown): value is Fragments {
35
47
  return isNamed(value, NAME_FRAGMENTS);
36
48
  }
@@ -43,3 +55,15 @@ function isNamed(value: unknown, name: string): boolean {
43
55
  (value as PlainObject)[name] === true
44
56
  );
45
57
  }
58
+
59
+ export function setComputedValue(
60
+ data: FragmentData,
61
+ callback: GenericCallback,
62
+ after: (computation: Computed<unknown>) => void,
63
+ ): void {
64
+ const computation = computed(callback);
65
+
66
+ data.mora.values.add(computation);
67
+
68
+ after(computation);
69
+ }
package/src/index.ts CHANGED
@@ -1,26 +1,65 @@
1
1
  import '@oscarpalmer/mora';
2
- import type {ReactiveArray} from '@oscarpalmer/mora';
2
+ import {isArray, type ReactiveArray} from '@oscarpalmer/mora';
3
3
  import {Fragment} from './fragment';
4
4
  import {Fragments} from './fragments';
5
5
 
6
6
  /**
7
- * Create Fragments from a reactive array
7
+ * Create a _Fragments_ instance from a reactive array
8
+ *
9
+ * _A Fragments instance can be used to efficiently render a list of items that may change over time, using unique identifiers to track each item, only adding, removing, or updating each related Fragment._
10
+ *
11
+ * @example
12
+ * ```ts
13
+ * const fruits = array(['Apple', 'Banana', 'Cherry']);
14
+ * const items = fragments(
15
+ * fruits,
16
+ * fruit => fruit, // Identifies a unique item
17
+ * fruit => html`<p>${fruit}</p>`, // Creates a Fragment from an item
18
+ * );
19
+ * html`${items}`.appendTo(document.body) // Renders '<p>Apple</p><p>Banana</p><p>Cherry</p>'
20
+ * fruits.push(['Date', 'Elderberry', 'Fig']); // Appends '<p>Date</p><p>Elderberry</p><p>Fig</p>'
21
+ * // without re-rendering the existing Fragments
22
+ * ```
23
+ *
8
24
  * @param array Reactive array
9
- * @param identify Function to identify item
10
- * @param fragment Function to create fragment from item
11
- * @returns Fragments
25
+ * @param identify Function to identify item uniquely _(non-nullable)_
26
+ * @param fragment Function to create _Fragment_ from item
27
+ * @returns _Fragments_
12
28
  */
13
29
  export function fragments<Item>(
14
30
  array: ReactiveArray<Item>,
15
31
  identify: (item: Item) => unknown,
16
32
  fragment: (item: Item) => Fragment,
17
33
  ): Fragments {
34
+ if (!isArray(array)) {
35
+ throw new TypeError('Fragments array must be a reactive array');
36
+ }
37
+
38
+ if (typeof identify !== 'function') {
39
+ throw new TypeError('Fragments identify must be a function');
40
+ }
41
+
42
+ if (typeof fragment !== 'function') {
43
+ throw new TypeError('Fragments fragment must be a function');
44
+ }
45
+
18
46
  return new Fragments(array as ReactiveArray<unknown>, identify as never, fragment as never);
19
47
  }
20
48
 
21
49
  /**
22
- * Create Fragment from a template
23
- * @returns Fragment
50
+ * Create a _Fragment_ from a template
51
+ *
52
+ * _A Fragment can be used to efficiently render a template that may change over time, only updating the necessary parts of the DOM._
53
+ *
54
+ * @example
55
+ * ```ts
56
+ * const name = signal('World');
57
+ * const fragment = html`<p>Hello, ${name}!</p>`;
58
+ * fragment.appendTo(document.body); // Renders '<p>Hello, World!</p>'
59
+ * name.set('Alice'); // Replaces 'World' with 'Alice'
60
+ * ```
61
+ *
62
+ * @returns _Fragment_
24
63
  */
25
64
  export function html(template: TemplateStringsArray, ...values: unknown[]): Fragment {
26
65
  return new Fragment(template, values);
@@ -29,3 +68,4 @@ export function html(template: TemplateStringsArray, ...values: unknown[]): Frag
29
68
  export * from '@oscarpalmer/mora';
30
69
  export type {Fragment} from './fragment';
31
70
  export type {Fragments} from './fragments';
71
+ export {isFragment, isFragments} from './helpers';
package/src/models.ts CHANGED
@@ -1,16 +1,18 @@
1
1
  import type {Reactive, ReactiveArray, Unsubscribe} from '@oscarpalmer/mora';
2
2
  import type {Fragment} from './fragment';
3
3
 
4
+ /**
5
+ * Configuration for a _Fragment_
6
+ */
4
7
  export type FragmentConfiguration = {
5
8
  /**
6
9
  * Should the template be cached? _(defaults to `true`)_
7
10
  */
8
11
  cache?: boolean;
9
12
  /**
10
- * Identifier for the fragment
13
+ * Identifier for the _Fragment_
11
14
  *
12
- * _(An identifier can be used to uniquely identify a fragment,
13
- * which helps prevent re-rendering in certain scenarios)_
15
+ * _An identifier can be used to uniquely identify a Fragment, which helps prevent re-rendering in reactive arrays and Fragments_
14
16
  */
15
17
  identifier?: unknown;
16
18
  };
@@ -19,7 +21,7 @@ export type FragmentData = {
19
21
  expressions: unknown[];
20
22
  items: FragmentItem[];
21
23
  mora: MoraData;
22
- strings: TemplateStringsArray;
24
+ strings: TemplateStringsArray | string[];
23
25
  template?: string;
24
26
  values: unknown[];
25
27
  };
package/src/node/event.ts CHANGED
@@ -1,12 +1,12 @@
1
1
  import {on} from '@oscarpalmer/toretto/event';
2
2
  import {
3
+ CHANGE_INPUTS,
3
4
  EVENT_CHANGE,
4
5
  EVENT_DEFAULTS,
5
6
  EVENT_INPUT,
6
7
  EVENT_ON_VALUE,
7
8
  EVENT_OPTIONS_DELIMITER,
8
9
  EVENT_SUBMIT,
9
- EXPRESSION_EVENT_CHANGE_TYPES,
10
10
  EXPRESSION_EVENT_NAME,
11
11
  EXPRESSION_EVENT_OPTIONS_ACTIVE,
12
12
  EXPRESSION_EVENT_OPTIONS_CAPTURE,
@@ -29,7 +29,7 @@ function getType(element: HTMLElement | SVGElement, type: string): string {
29
29
  }
30
30
 
31
31
  if (element instanceof HTMLInputElement) {
32
- if (EXPRESSION_EVENT_CHANGE_TYPES.test(element.type)) {
32
+ if (CHANGE_INPUTS.has(element.type)) {
33
33
  return EVENT_CHANGE;
34
34
  }
35
35
 
@@ -40,9 +40,16 @@ function getType(element: HTMLElement | SVGElement, type: string): string {
40
40
  }
41
41
 
42
42
  export function mapEvent(element: HTMLElement | SVGElement, name: string, value: unknown): void {
43
- const [, type, options] = EXPRESSION_EVENT_NAME.exec(name) ?? [];
43
+ const [, type, options] = EXPRESSION_EVENT_NAME.exec(name)!;
44
44
 
45
- if (type != null && typeof value === 'function') {
46
- on(element, getType(element, type), value as EventListener, getOptions(options ?? ''));
45
+ const values = Array.isArray(value) ? value : [value];
46
+ const {length} = values;
47
+
48
+ for (let index = 0; index < length; index += 1) {
49
+ const item = values[index];
50
+
51
+ if (typeof item === 'function') {
52
+ on(element, getType(element, type), item as EventListener, getOptions(options ?? ''));
53
+ }
47
54
  }
48
55
  }
package/src/node/index.ts CHANGED
@@ -1,24 +1,16 @@
1
1
  import type {GenericCallback} from '@oscarpalmer/atoms/models';
2
- import {
3
- computed,
4
- isComputed,
5
- isReactive,
6
- isSignal,
7
- type Computed,
8
- type Reactive,
9
- } from '@oscarpalmer/mora';
2
+ import {isReactive, type ReactiveArray} from '@oscarpalmer/mora';
10
3
  import {isHTMLOrSVGElement} from '@oscarpalmer/toretto/is';
4
+ import {mapAttributes, mapAttributeValue} from '../attribute/index';
11
5
  import {EXPRESSION_ABYDON_CONTENT, EXPRESSION_TEXTAREA_VALUE} from '../constants';
12
- import {handleFragments} from '../fragments';
13
- import {isFragment, isFragments} from '../helpers';
6
+ import {Fragments, fragmentsStates, handleFragments} from '../fragments';
7
+ import {isFragment, isFragments, setComputedValue} from '../helpers';
14
8
  import {createNodes} from '../helpers/dom';
15
9
  import type {FragmentData} from '../models';
16
- import {mapAttributes} from './attribute';
17
10
  import {setReactiveValue} from './value';
18
- import {mapEvent} from './event';
19
11
 
20
12
  function mapNode(data: FragmentData, comment: Comment): void {
21
- const matches = EXPRESSION_ABYDON_CONTENT.exec(comment.textContent ?? '');
13
+ const matches = EXPRESSION_ABYDON_CONTENT.exec(comment.textContent);
22
14
  const value = matches == null ? null : data.values[+matches[1]];
23
15
 
24
16
  if (value != null) {
@@ -38,12 +30,17 @@ export function mapNodes(data: FragmentData, nodes: ChildNode[]): void {
38
30
  continue;
39
31
  }
40
32
 
41
- if (node instanceof HTMLTextAreaElement) {
42
- mapTextarea(data, node);
43
- }
44
-
45
33
  if (isHTMLOrSVGElement(node)) {
46
- mapAttributes(data, node);
34
+ let ignoreValueAttribute = false;
35
+
36
+ if (node instanceof HTMLTextAreaElement) {
37
+ // Textareas are a special case because their value can be set via the `value` property
38
+ // or the text content. In order to support both, we need to check for the presence of
39
+ // the expression in both places and map it accordingly.
40
+ ignoreValueAttribute = mapTextarea(data, node);
41
+ }
42
+
43
+ mapAttributes(data, node, ignoreValueAttribute);
47
44
  }
48
45
 
49
46
  if (node.hasChildNodes()) {
@@ -52,55 +49,32 @@ export function mapNodes(data: FragmentData, nodes: ChildNode[]): void {
52
49
  }
53
50
  }
54
51
 
55
- function mapTextarea(data: FragmentData, element: HTMLTextAreaElement): void {
56
- const [, index] = EXPRESSION_TEXTAREA_VALUE.exec(element.value) ?? [];
52
+ function mapTextarea(data: FragmentData, element: HTMLTextAreaElement): boolean {
53
+ const [, index] =
54
+ EXPRESSION_TEXTAREA_VALUE.exec(element.textContent) ??
55
+ EXPRESSION_TEXTAREA_VALUE.exec(element.value) ??
56
+ [];
57
57
 
58
58
  if (index == null) {
59
- return;
59
+ return false;
60
60
  }
61
61
 
62
+ element.textContent = '';
62
63
  element.value = '';
63
64
 
64
- const value = data.values[Number.parseInt(index, 10)];
65
-
66
- if (isSignal(value)) {
67
- element.value = String(value.peek());
68
-
69
- mapEvent(element, '@on', () => {
70
- value.set(element.value);
71
- });
72
-
73
- return;
74
- }
75
-
76
- let reactive: Computed<unknown> | undefined;
77
-
78
- if (typeof value === 'function') {
79
- reactive = computed(value as GenericCallback);
80
- } else if (isComputed(value)) {
81
- reactive = value;
82
- }
65
+ mapAttributeValue(data, element, 'value', data.values[Number.parseInt(index, 10)]);
83
66
 
84
- if (reactive == null) {
85
- element.value = String(value);
86
- } else {
87
- data.mora.subscribers.add(
88
- reactive.subscribe(value => {
89
- element.value = String(value);
90
- }),
91
- );
92
- }
67
+ return true;
93
68
  }
94
69
 
95
70
  function mapValue(data: FragmentData, comment: Comment, value: unknown): void {
96
71
  switch (true) {
97
72
  case typeof value === 'function':
98
- setComputedValue(data, comment, value as GenericCallback);
73
+ setComputedNode(data, comment, value as GenericCallback);
99
74
  break;
100
75
 
101
76
  case isFragments(value):
102
- handleFragments(value, false);
103
- setReactiveValue(data, comment, value.items as Reactive<unknown[], unknown>);
77
+ setFragmentsNode(data, comment, value);
104
78
  break;
105
79
 
106
80
  case isReactive(value):
@@ -125,10 +99,16 @@ function replaceComment(data: FragmentData, comment: Comment, value: unknown): v
125
99
  comment.replaceWith(...nodes);
126
100
  }
127
101
 
128
- function setComputedValue(data: FragmentData, comment: Comment, callback: GenericCallback): void {
129
- const value = computed(callback);
102
+ function setComputedNode(data: FragmentData, comment: Comment, callback: GenericCallback): void {
103
+ setComputedValue(data, callback, computation => {
104
+ setReactiveValue(data, comment, computation);
105
+ });
106
+ }
107
+
108
+ function setFragmentsNode(data: FragmentData, comment: Comment, fragments: Fragments): void {
109
+ const state = fragmentsStates.get(fragments)!;
130
110
 
131
- data.mora.values.add(value);
111
+ handleFragments(state, false);
132
112
 
133
- setReactiveValue(data, comment, value);
113
+ setReactiveValue(data, comment, state.mapped as ReactiveArray<unknown>);
134
114
  }