@oscarpalmer/abydon 0.22.0 → 0.23.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/fragment.ts CHANGED
@@ -19,12 +19,14 @@ export class Fragment {
19
19
  /**
20
20
  * Is template caching enabled?
21
21
  */
22
- get caching(): boolean {
22
+ get cache(): boolean {
23
23
  return this.#configuration.cache;
24
24
  }
25
25
 
26
26
  /**
27
- * Fragment identifier
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_
28
30
  */
29
31
  get identifier(): unknown {
30
32
  return this.#configuration.identifier;
@@ -48,7 +50,15 @@ export class Fragment {
48
50
  }
49
51
 
50
52
  /**
51
- * 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
52
62
  * @param element Element to append to
53
63
  */
54
64
  appendTo(element: Element): void {
@@ -56,9 +66,19 @@ export class Fragment {
56
66
  }
57
67
 
58
68
  /**
59
- * 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_
60
80
  * @param configuration Configuration options
61
- * @returns Fragment
81
+ * @returns _Fragment_
62
82
  */
63
83
  configure(configuration: FragmentConfiguration): Fragment {
64
84
  const actual = isPlainObject(configuration) ? configuration : {};
@@ -75,7 +95,7 @@ export class Fragment {
75
95
  }
76
96
 
77
97
  /**
78
- * Get a list of the fragment's nodes
98
+ * Get a list of the _Fragment_'s nodes
79
99
  * @returns List of nodes
80
100
  */
81
101
  get(): ChildNode[] {
@@ -108,21 +128,18 @@ export class Fragment {
108
128
  }
109
129
 
110
130
  /**
111
- * Set an identifier for the fragment
112
- *
113
- * _An identifier can be used to uniquely identify a fragment,
114
- * which helps prevent re-rendering in certain scenarios._
115
- * @param identifier Identifier
116
- * @returns Fragment
131
+ * Prepend the _Fragment_ to the given element
132
+ * @param element Element to prepend to
117
133
  */
118
- identify(identifier: unknown): Fragment {
119
- this.#configuration.identifier = identifier;
120
-
121
- return this;
134
+ prependTo(element: Element): void {
135
+ element.prepend(...this.get());
122
136
  }
123
137
 
124
138
  /**
125
- * 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_
126
143
  */
127
144
  remove(): void {
128
145
  removeFragment(this.#data);
package/src/fragments.ts CHANGED
@@ -74,7 +74,9 @@ function handleItems(state: FragmentsState, items: unknown[]): void {
74
74
  }
75
75
  }
76
76
 
77
- instance.identify(key);
77
+ instance.configure({
78
+ identifier: key,
79
+ });
78
80
 
79
81
  state.instances[key] = instance;
80
82
 
@@ -88,7 +90,7 @@ function handleItems(state: FragmentsState, items: unknown[]): void {
88
90
  updateFragments(state, keys);
89
91
  }
90
92
 
91
- export function initializeFragments(state: FragmentsState): void {
93
+ function initializeFragments(state: FragmentsState): void {
92
94
  state.subscriber ??= state.array.subscribe(items => {
93
95
  handleItems(state, items);
94
96
  });
@@ -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[],
@@ -28,18 +30,18 @@ export function compareArrays(
28
30
  }
29
31
 
30
32
  /**
31
- * Is the value a Fragment?
33
+ * Is the value a _Fragment_?
32
34
  * @param value Value to check
33
- * @returns `true` if the value is a Fragment, otherwise `false`
35
+ * @returns `true` if the value is a _Fragment_, otherwise `false`
34
36
  */
35
37
  export function isFragment(value: unknown): value is Fragment {
36
38
  return isNamed(value, NAME_FRAGMENT);
37
39
  }
38
40
 
39
41
  /**
40
- * Is the value a Fragments?
42
+ * Is the value a _Fragments_ instance?
41
43
  * @param value Value to check
42
- * @returns `true` if the value is a Fragments, otherwise `false`
44
+ * @returns `true` if the value is a _Fragments_ instance, otherwise `false`
43
45
  */
44
46
  export function isFragments(value: unknown): value is Fragments {
45
47
  return isNamed(value, NAME_FRAGMENTS);
@@ -53,3 +55,15 @@ function isNamed(value: unknown, name: string): boolean {
53
55
  (value as PlainObject)[name] === true
54
56
  );
55
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
@@ -4,11 +4,27 @@ import {Fragment} from './fragment';
4
4
  import {Fragments} from './fragments';
5
5
 
6
6
  /**
7
- * Create a 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>,
@@ -31,8 +47,19 @@ export function fragments<Item>(
31
47
  }
32
48
 
33
49
  /**
34
- * Create a Fragment from a template
35
- * @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_
36
63
  */
37
64
  export function html(template: TemplateStringsArray, ...values: unknown[]): Fragment {
38
65
  return new Fragment(template, values);
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/index.ts CHANGED
@@ -1,21 +1,12 @@
1
1
  import type {GenericCallback} from '@oscarpalmer/atoms/models';
2
- import {getString} from '@oscarpalmer/atoms/string';
3
- import {
4
- computed,
5
- isComputed,
6
- isReactive,
7
- isSignal,
8
- type Computed,
9
- type ReactiveArray,
10
- } from '@oscarpalmer/mora';
2
+ import {isReactive, type ReactiveArray} from '@oscarpalmer/mora';
11
3
  import {isHTMLOrSVGElement} from '@oscarpalmer/toretto/is';
12
- import {mapAttributes} from '../attribute/index';
13
- import {EVENT_ON_VALUE, EXPRESSION_ABYDON_CONTENT, EXPRESSION_TEXTAREA_VALUE} from '../constants';
4
+ import {mapAttributes, mapAttributeValue} from '../attribute/index';
5
+ import {EXPRESSION_ABYDON_CONTENT, EXPRESSION_TEXTAREA_VALUE} from '../constants';
14
6
  import {Fragments, fragmentsStates, handleFragments} from '../fragments';
15
- import {isFragment, isFragments} from '../helpers';
7
+ import {isFragment, isFragments, setComputedValue} from '../helpers';
16
8
  import {createNodes} from '../helpers/dom';
17
9
  import type {FragmentData} from '../models';
18
- import {mapEvent} from './event';
19
10
  import {setReactiveValue} from './value';
20
11
 
21
12
  function mapNode(data: FragmentData, comment: Comment): void {
@@ -39,12 +30,17 @@ export function mapNodes(data: FragmentData, nodes: ChildNode[]): void {
39
30
  continue;
40
31
  }
41
32
 
42
- if (node instanceof HTMLTextAreaElement) {
43
- mapTextarea(data, node);
44
- }
45
-
46
33
  if (isHTMLOrSVGElement(node)) {
47
- 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);
48
44
  }
49
45
 
50
46
  if (node.hasChildNodes()) {
@@ -53,64 +49,32 @@ export function mapNodes(data: FragmentData, nodes: ChildNode[]): void {
53
49
  }
54
50
  }
55
51
 
56
- function mapTextarea(data: FragmentData, element: HTMLTextAreaElement): void {
52
+ function mapTextarea(data: FragmentData, element: HTMLTextAreaElement): boolean {
57
53
  const [, index] =
58
54
  EXPRESSION_TEXTAREA_VALUE.exec(element.textContent) ??
59
55
  EXPRESSION_TEXTAREA_VALUE.exec(element.value) ??
60
56
  [];
61
57
 
62
58
  if (index == null) {
63
- return;
59
+ return false;
64
60
  }
65
61
 
66
62
  element.textContent = '';
67
63
  element.value = '';
68
64
 
69
- const value = data.values[Number.parseInt(index, 10)];
65
+ mapAttributeValue(data, element, 'value', data.values[Number.parseInt(index, 10)]);
70
66
 
71
- if (isSignal(value)) {
72
- element.value = getString(value.peek());
73
-
74
- mapEvent(element, EVENT_ON_VALUE, () => {
75
- value.set(element.value);
76
- });
77
-
78
- data.mora.subscribers.add(
79
- value.subscribe(value => {
80
- element.value = getString(value);
81
- }),
82
- );
83
-
84
- return;
85
- }
86
-
87
- let reactive: Computed<unknown> | undefined;
88
-
89
- if (typeof value === 'function') {
90
- reactive = computed(value as GenericCallback);
91
- } else if (isComputed(value)) {
92
- reactive = value;
93
- }
94
-
95
- if (reactive == null) {
96
- element.value = '';
97
- } else {
98
- data.mora.subscribers.add(
99
- reactive.subscribe(value => {
100
- element.value = getString(value);
101
- }),
102
- );
103
- }
67
+ return true;
104
68
  }
105
69
 
106
70
  function mapValue(data: FragmentData, comment: Comment, value: unknown): void {
107
71
  switch (true) {
108
72
  case typeof value === 'function':
109
- setComputedValue(data, comment, value as GenericCallback);
73
+ setComputedNode(data, comment, value as GenericCallback);
110
74
  break;
111
75
 
112
76
  case isFragments(value):
113
- setFragmentsValue(data, comment, value);
77
+ setFragmentsNode(data, comment, value);
114
78
  break;
115
79
 
116
80
  case isReactive(value):
@@ -135,15 +99,13 @@ function replaceComment(data: FragmentData, comment: Comment, value: unknown): v
135
99
  comment.replaceWith(...nodes);
136
100
  }
137
101
 
138
- function setComputedValue(data: FragmentData, comment: Comment, callback: GenericCallback): void {
139
- const value = computed(callback);
140
-
141
- data.mora.values.add(value);
142
-
143
- setReactiveValue(data, comment, value);
102
+ function setComputedNode(data: FragmentData, comment: Comment, callback: GenericCallback): void {
103
+ setComputedValue(data, callback, computation => {
104
+ setReactiveValue(data, comment, computation);
105
+ });
144
106
  }
145
107
 
146
- function setFragmentsValue(data: FragmentData, comment: Comment, fragments: Fragments): void {
108
+ function setFragmentsNode(data: FragmentData, comment: Comment, fragments: Fragments): void {
147
109
  const state = fragmentsStates.get(fragments)!;
148
110
 
149
111
  handleFragments(state, false);