@oscarpalmer/abydon 0.23.1 → 0.24.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.
@@ -9,23 +9,25 @@ import {
9
9
  PROPERTY_VALUE,
10
10
  } from '../constants';
11
11
  import {setComputedValue} from '../helpers';
12
- import type {FragmentData} from '../models';
12
+ import type {FragmentState} from '../models';
13
13
  import {mapEvent} from '../node/event';
14
14
  import {setAttribute} from './value';
15
15
  import {isNullableOrWhitespace} from '@oscarpalmer/atoms/is';
16
16
 
17
+ // #region Functions
18
+
17
19
  function compareAttributes(first: Attr, second: Attr): number {
18
20
  return first.name.localeCompare(second.name);
19
21
  }
20
22
 
21
- function getValue(data: FragmentData, original: string): unknown {
23
+ function getValue(data: FragmentState, original: string): unknown {
22
24
  const matches = EXPRESSION_ABYDON_CONTENT.exec(original);
23
25
 
24
26
  return matches == null ? original : data.values[+matches[1]];
25
27
  }
26
28
 
27
29
  export function mapAttributeValue(
28
- data: FragmentData,
30
+ data: FragmentState,
29
31
  element: HTMLElement | SVGElement,
30
32
  name: string,
31
33
  value: unknown,
@@ -44,7 +46,7 @@ export function mapAttributeValue(
44
46
  }
45
47
 
46
48
  export function mapAttributes(
47
- data: FragmentData,
49
+ data: FragmentState,
48
50
  element: HTMLElement | SVGElement,
49
51
  ignoreValue: boolean,
50
52
  ): void {
@@ -80,7 +82,7 @@ export function mapAttributes(
80
82
  }
81
83
 
82
84
  function setComputedAttribute(
83
- data: FragmentData,
85
+ data: FragmentState,
84
86
  element: HTMLElement | SVGElement,
85
87
  name: string,
86
88
  callback: GenericCallback,
@@ -89,3 +91,5 @@ function setComputedAttribute(
89
91
  setAttribute(data, element, name, computation);
90
92
  });
91
93
  }
94
+
95
+ // #endregion
@@ -12,7 +12,9 @@ import {
12
12
  EXPRESSION_ATTRIBUTE_STYLE_VARIABLE,
13
13
  VALUE_TRUE,
14
14
  } from '../constants';
15
- import type {FragmentData} from '../models';
15
+ import type {FragmentState} from '../models';
16
+
17
+ // #region Functions
16
18
 
17
19
  function getStyleValue(value: unknown, unit: string, isVariable: boolean): string | undefined {
18
20
  if (removeStyleValue(value, unit, isVariable)) {
@@ -31,7 +33,7 @@ function removeStyleValue(value: unknown, unit: unknown, isVariable: boolean): b
31
33
  }
32
34
 
33
35
  export function setAttribute(
34
- data: FragmentData,
36
+ data: FragmentState,
35
37
  element: HTMLElement | SVGElement,
36
38
  name: string,
37
39
  value: unknown,
@@ -52,7 +54,7 @@ export function setAttribute(
52
54
  }
53
55
 
54
56
  function setClassValues(
55
- data: FragmentData,
57
+ data: FragmentState,
56
58
  element: HTMLElement | SVGElement,
57
59
  name: string,
58
60
  value: unknown,
@@ -69,7 +71,7 @@ function setClassValues(
69
71
  }
70
72
 
71
73
  function setStyleValues(
72
- data: FragmentData,
74
+ data: FragmentState,
73
75
  element: HTMLElement | SVGElement,
74
76
  name: string,
75
77
  value: unknown,
@@ -88,7 +90,7 @@ function setStyleValues(
88
90
  }
89
91
 
90
92
  function setValue(
91
- data: FragmentData,
93
+ data: FragmentState,
92
94
  element: HTMLElement | SVGElement,
93
95
  name: string,
94
96
  value: unknown,
@@ -98,10 +100,12 @@ function setValue(
98
100
  });
99
101
  }
100
102
 
101
- function updateValue(data: FragmentData, value: unknown, updater: (value: unknown) => void): void {
103
+ function updateValue(data: FragmentState, value: unknown, updater: (value: unknown) => void): void {
102
104
  if (isReactive(value)) {
103
- data.mora.subscribers.add(value.subscribe(updater));
105
+ data.mora.subscriptions.add(value.subscribe(updater));
104
106
  } else {
105
107
  updater(value);
106
108
  }
107
109
  }
110
+
111
+ // #endregion
package/src/constants.ts CHANGED
@@ -1,3 +1,5 @@
1
+ // #region Variables
2
+
1
3
  export const ARRAY_COMPARISON_ADDED = 'added';
2
4
 
3
5
  export const ARRAY_COMPARISON_DISSIMILAR = 'dissimilar';
@@ -10,12 +12,6 @@ export const ATTRIBUTE_NAME_DELIMITER = '.';
10
12
 
11
13
  export const CHANGE_INPUTS = new Set(['checkbox', 'radio']);
12
14
 
13
- export const ERROR_FRAGMENT = 'Fragment function must return a Fragment instance';
14
-
15
- export const ERROR_IDENTIFIER_DUPLICATE = "Duplicate identifier found: '<>'";
16
-
17
- export const ERROR_IDENTIFIER_TYPE = 'Identifier cannot be null or undefined';
18
-
19
15
  export const EVENT_CHANGE = 'change';
20
16
 
21
17
  export const EVENT_INPUT = 'input';
@@ -63,16 +59,36 @@ export const EXPRESSION_EVENT_PREFIX = /^@/;
63
59
 
64
60
  export const EXPRESSION_TEXTAREA_VALUE = /(?:<|&lt;)!--abydon\.(\d+)--(?:>|&gt;)/;
65
61
 
66
- export const NAME_FRAGMENT = '$fragment';
62
+ export const MESSAGE_FRAGMENT_VALUE =
63
+ 'Fragment template must be a string or a template strings array';
64
+
65
+ export const MESSAGE_FRAGMENTS_FRAGMENT_RESULT =
66
+ 'Fragment function must return a Fragment instance';
67
+
68
+ export const MESSAGE_FRAGMENTS_FRAGMENT_TYPE = 'Fragment handler must be a function';
69
+
70
+ export const MESSAGE_FRAGMENTS_IDENTIFIER_RESULT_DUPLICATE = "Duplicate identifier found: '<>'";
67
71
 
68
- export const NAME_FRAGMENTS = '$fragments';
72
+ export const MESSAGE_FRAGMENTS_IDENTIFIER_RESULT_TYPE = 'Identifier cannot be null or undefined';
73
+
74
+ export const MESSAGE_FRAGMENTS_IDENTIFIER_TYPE = 'Identifier handler must be a function';
75
+
76
+ export const MESSAGE_FRAGMENTS_VALUE = 'Fragments array must be a reactive array';
77
+
78
+ export const NAME_FRAGMENT = 'fragment';
79
+
80
+ export const NAME_FRAGMENTS = 'fragments';
69
81
 
70
82
  export const PROPERTY_IDENTIFIER = 'identifier';
71
83
 
72
84
  export const PROPERTY_VALUE = 'value';
73
85
 
86
+ export const SYMBOL = Symbol('abydon');
87
+
74
88
  export const TEMPLATE_ITEM = '<>';
75
89
 
76
90
  export const VALUE_TRUE = 'true';
77
91
 
78
92
  export const WHITESPACE = /\s+/g;
93
+
94
+ // #endregion
package/src/fragment.ts CHANGED
@@ -1,158 +1,174 @@
1
- import {isPlainObject} from '@oscarpalmer/atoms/is';
1
+ import {isNonTemplateStringsArray, isPlainObject} from '@oscarpalmer/atoms/is';
2
2
  import {html} from '@oscarpalmer/toretto/html';
3
- import {NAME_FRAGMENT, PROPERTY_IDENTIFIER} from './constants';
3
+ import {MESSAGE_FRAGMENT_VALUE, NAME_FRAGMENT, PROPERTY_IDENTIFIER, SYMBOL} from './constants';
4
4
  import {handleFragments} from './fragments';
5
5
  import {isFragments} from './helpers';
6
6
  import {removeNodes} from './helpers/dom';
7
- import type {FragmentConfiguration, FragmentData} from './models';
7
+ import type {Fragment, FragmentConfiguration, FragmentState, InternalFragment} from './models';
8
8
  import {mapNodes} from './node';
9
9
  import {parse} from './parse';
10
10
 
11
- export class Fragment {
12
- readonly #data: FragmentData;
11
+ // #region Instances
13
12
 
14
- readonly #configuration: Required<FragmentConfiguration> = {
15
- identifier: undefined,
13
+ function Fragment(this: any, strings: TemplateStringsArray, expressions: unknown[]) {
14
+ this[SYMBOL] = {
15
+ expressions,
16
+ strings,
16
17
  cache: true,
18
+ identifier: undefined,
19
+ items: [],
20
+ mora: {
21
+ subscriptions: new Set(),
22
+ values: new Set(),
23
+ },
24
+ name: NAME_FRAGMENT,
25
+ values: [],
17
26
  };
27
+ }
18
28
 
19
- /**
20
- * Is template caching enabled?
21
- */
22
- get cache(): boolean {
23
- return this.#configuration.cache;
24
- }
29
+ Fragment.prototype.after = insertFragmentAfter;
30
+ Fragment.prototype.appendTo = appendFragmentTo;
31
+ Fragment.prototype.before = insertFragmentBefore;
32
+ Fragment.prototype.configure = configureFragment;
33
+ Fragment.prototype.get = getFragmentNodes;
34
+ Fragment.prototype.prependTo = prependFragmentTo;
35
+ Fragment.prototype.remove = removeFragment;
36
+
37
+ Object.defineProperties(Fragment.prototype, {
38
+ cache: {
39
+ enumerable: true,
40
+ get: getFragmentCache,
41
+ },
42
+ identifier: {
43
+ enumerable: true,
44
+ get: getFragmentIdentifier,
45
+ },
46
+ });
47
+
48
+ // #endregion
49
+
50
+ // #region Functions
51
+
52
+ function appendFragmentTo(this: InternalFragment, element: Element): void {
53
+ element.append(...this.get());
54
+ }
25
55
 
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_
30
- */
31
- get identifier(): unknown {
32
- return this.#configuration.identifier;
33
- }
56
+ function configureFragment(
57
+ this: InternalFragment,
58
+ configuration: FragmentConfiguration,
59
+ ): InternalFragment {
60
+ const state = this[SYMBOL];
34
61
 
35
- constructor(strings: TemplateStringsArray, expressions: unknown[]) {
36
- Object.defineProperty(this, NAME_FRAGMENT, {
37
- value: true,
38
- });
62
+ const actual = isPlainObject(configuration) ? configuration : {};
39
63
 
40
- this.#data = {
41
- expressions,
42
- strings,
43
- items: [],
44
- mora: {
45
- subscribers: new Set(),
46
- values: new Set(),
47
- },
48
- values: [],
49
- };
64
+ if (PROPERTY_IDENTIFIER in actual) {
65
+ state.identifier = actual.identifier;
50
66
  }
51
67
 
52
- /**
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());
68
+ if (typeof actual.cache === 'boolean') {
69
+ state.cache = actual.cache;
58
70
  }
59
71
 
60
- /**
61
- * Append the _Fragment_ to the given element
62
- * @param element Element to append to
63
- */
64
- appendTo(element: Element): void {
65
- element.append(...this.get());
66
- }
72
+ return this;
73
+ }
67
74
 
68
- /**
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());
75
+ /**
76
+ * Create a _Fragment_ from a template
77
+ *
78
+ * _A Fragment can be used to efficiently render a template that may change over time, only updating the necessary parts of the DOM._
79
+ *
80
+ * @example
81
+ * ```ts
82
+ * const name = signal('World');
83
+ * const fragment = fragment`<p>Hello, ${name}!</p>`; // or `html`<p>Hello, ${name}!</p>`
84
+ * fragment.appendTo(document.body); // Renders '<p>Hello, World!</p>'
85
+ * name.set('Alice'); // Replaces 'World' with 'Alice'
86
+ * ```
87
+ *
88
+ * @returns _Fragment_
89
+ */
90
+ export function fragment(template: TemplateStringsArray, ...values: unknown[]): Fragment;
91
+
92
+ /**
93
+ * Create a _Fragment_ from a simple template
94
+ *
95
+ * @example
96
+ * ```ts
97
+ * const fragment = fragment('<p>Hello, World!</p>'); // or `html`('<p>Hello, World!</p>')
98
+ * fragment.appendTo(document.body); // Renders '<p>Hello, World!</p>'
99
+ * ```
100
+ *
101
+ * @returns _Fragment_
102
+ */
103
+ export function fragment(template: string): Fragment;
104
+
105
+ export function fragment(template: string | TemplateStringsArray, ...values: unknown[]): Fragment {
106
+ if (typeof template !== 'string' && isNonTemplateStringsArray(template)) {
107
+ throw new TypeError(MESSAGE_FRAGMENT_VALUE);
74
108
  }
75
109
 
76
- /**
77
- * Configure the _Fragment_
78
- *
79
- * _Returns the Fragment instance for chaining_
80
- * @param configuration Configuration options
81
- * @returns _Fragment_
82
- */
83
- configure(configuration: FragmentConfiguration): Fragment {
84
- const actual = isPlainObject(configuration) ? configuration : {};
85
-
86
- if (PROPERTY_IDENTIFIER in actual) {
87
- this.#configuration.identifier = actual.identifier;
88
- }
110
+ // @ts-expect-error All good, no worries :-)
111
+ return new Fragment(template, values);
112
+ }
89
113
 
90
- if (typeof actual.cache === 'boolean') {
91
- this.#configuration.cache = actual.cache;
92
- }
114
+ function getFragmentCache(this: InternalFragment): boolean {
115
+ return this[SYMBOL].cache;
116
+ }
93
117
 
94
- return this;
95
- }
118
+ function getFragmentIdentifier(this: InternalFragment): unknown {
119
+ return this[SYMBOL].identifier;
120
+ }
96
121
 
97
- /**
98
- * Get a list of the _Fragment_'s nodes
99
- * @returns List of nodes
100
- */
101
- get(): ChildNode[] {
102
- const data = this.#data;
103
-
104
- if (data.items.length === 0) {
105
- const parsed = parse(data);
106
-
107
- const templated = html(parsed, {
108
- cache: this.#configuration.cache,
109
- });
110
-
111
- data.items.splice(
112
- 0,
113
- data.items.length,
114
- ...templated.map(node => ({
115
- nodes: [node as ChildNode],
116
- })),
117
- );
118
-
119
- mapNodes(
120
- data,
121
- data.items.flatMap(item => item.nodes!),
122
- );
123
- }
122
+ function getFragmentNodes(this: InternalFragment): ChildNode[] {
123
+ const state = this[SYMBOL];
124
+
125
+ if (state.items.length === 0) {
126
+ const parsed = parse(state);
124
127
 
125
- return data.items.flatMap(
126
- item => item.fragments?.flatMap(fragment => fragment.get()) ?? item.nodes!,
128
+ const templated = html(parsed, {
129
+ cache: state.cache,
130
+ });
131
+
132
+ state.items.splice(
133
+ 0,
134
+ state.items.length,
135
+ ...templated.map(node => ({
136
+ nodes: [node as ChildNode],
137
+ })),
127
138
  );
128
- }
129
139
 
130
- /**
131
- * Prepend the _Fragment_ to the given element
132
- * @param element Element to prepend to
133
- */
134
- prependTo(element: Element): void {
135
- element.prepend(...this.get());
140
+ mapNodes(
141
+ state,
142
+ state.items.flatMap(item => item.nodes!),
143
+ );
136
144
  }
137
145
 
138
- /**
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_
143
- */
144
- remove(): void {
145
- removeFragment(this.#data);
146
- }
146
+ return state.items.flatMap(
147
+ item => item.fragments?.flatMap(fragment => fragment.get()) ?? item.nodes!,
148
+ );
149
+ }
150
+
151
+ function insertFragmentAfter(this: InternalFragment, element: Element): void {
152
+ element.after(...this.get());
153
+ }
154
+
155
+ function insertFragmentBefore(this: InternalFragment, element: Element): void {
156
+ element.before(...this.get());
157
+ }
158
+
159
+ function prependFragmentTo(this: InternalFragment, element: Element): void {
160
+ element.prepend(...this.get());
147
161
  }
148
162
 
149
- function removeFragment(data: FragmentData): void {
150
- removeMora(data);
163
+ function removeFragment(this: InternalFragment): void {
164
+ const state = this[SYMBOL];
165
+
166
+ removeMora(state);
151
167
 
152
- let {length} = data.items;
168
+ let {length} = state.items;
153
169
 
154
170
  for (let index = 0; index < length; index += 1) {
155
- const {fragments, nodes} = data.items[index];
171
+ const {fragments, nodes} = state.items[index];
156
172
  const fragmentsLength = fragments?.length ?? 0;
157
173
 
158
174
  for (let fragmentIndex = 0; fragmentIndex < fragmentsLength; fragmentIndex += 1) {
@@ -162,12 +178,12 @@ function removeFragment(data: FragmentData): void {
162
178
  removeNodes(nodes!);
163
179
  }
164
180
 
165
- data.items.length = 0;
181
+ state.items.length = 0;
166
182
 
167
- length = data.values.length;
183
+ length = state.values.length;
168
184
 
169
185
  for (let index = 0; index < length; index += 1) {
170
- const value = data.values[index];
186
+ const value = state.values[index];
171
187
 
172
188
  if (isFragments(value)) {
173
189
  handleFragments(value, true);
@@ -175,13 +191,21 @@ function removeFragment(data: FragmentData): void {
175
191
  }
176
192
  }
177
193
 
178
- function removeMora(data: FragmentData): void {
179
- const unsubscribers = [...data.mora.subscribers];
194
+ function removeMora(state: FragmentState): void {
195
+ const subscriptions = [...state.mora.subscriptions];
180
196
 
181
- data.mora.subscribers.clear();
182
- data.mora.values.clear();
197
+ state.mora.subscriptions.clear();
198
+ state.mora.values.clear();
183
199
 
184
- for (const unsubscribe of unsubscribers) {
185
- unsubscribe();
200
+ for (const subscription of subscriptions) {
201
+ subscription.unsubscribe();
186
202
  }
187
203
  }
204
+
205
+ // #endregion
206
+
207
+ // #region Exports
208
+
209
+ export {fragment as html};
210
+
211
+ // #endregion