@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/dist/abydon.full.mjs +223 -161
- package/dist/attribute/index.d.mts +3 -2
- package/dist/attribute/index.mjs +23 -17
- package/dist/attribute/value.mjs +18 -28
- package/dist/constants.d.mts +4 -8
- package/dist/constants.mjs +6 -10
- package/dist/fragment.d.mts +27 -14
- package/dist/fragment.mjs +32 -16
- package/dist/fragments.d.mts +1 -2
- package/dist/fragments.mjs +2 -2
- package/dist/helpers/dom.d.mts +2 -3
- package/dist/helpers/dom.mjs +2 -4
- package/dist/helpers/index.d.mts +9 -5
- package/dist/helpers/index.mjs +11 -5
- package/dist/index.d.mts +33 -6
- package/dist/index.mjs +33 -6
- package/dist/models.d.mts +6 -4
- package/dist/node/index.mjs +18 -33
- package/dist/node/value.mjs +67 -50
- package/package.json +2 -2
- package/src/attribute/index.ts +43 -35
- package/src/attribute/value.ts +20 -53
- package/src/constants.ts +6 -14
- package/src/fragment.ts +34 -17
- package/src/fragments.ts +4 -2
- package/src/helpers/dom.ts +3 -5
- package/src/helpers/index.ts +19 -5
- package/src/index.ts +33 -6
- package/src/models.ts +6 -4
- package/src/node/index.ts +25 -63
- package/src/node/value.ts +117 -85
- package/src/parse.ts +1 -1
package/src/fragment.ts
CHANGED
|
@@ -19,12 +19,14 @@ export class Fragment {
|
|
|
19
19
|
/**
|
|
20
20
|
* Is template caching enabled?
|
|
21
21
|
*/
|
|
22
|
-
get
|
|
22
|
+
get cache(): boolean {
|
|
23
23
|
return this.#configuration.cache;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
/**
|
|
27
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
|
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
|
|
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
|
-
*
|
|
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
|
-
|
|
119
|
-
this
|
|
120
|
-
|
|
121
|
-
return this;
|
|
134
|
+
prependTo(element: Element): void {
|
|
135
|
+
element.prepend(...this.get());
|
|
122
136
|
}
|
|
123
137
|
|
|
124
138
|
/**
|
|
125
|
-
* Remove the
|
|
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.
|
|
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
|
-
|
|
93
|
+
function initializeFragments(state: FragmentsState): void {
|
|
92
94
|
state.subscriber ??= state.array.subscribe(items => {
|
|
93
95
|
handleItems(state, items);
|
|
94
96
|
});
|
package/src/helpers/dom.ts
CHANGED
|
@@ -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[]):
|
|
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
|
}
|
package/src/helpers/index.ts
CHANGED
|
@@ -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
|
|
33
|
+
* Is the value a _Fragment_?
|
|
32
34
|
* @param value Value to check
|
|
33
|
-
* @returns `true` if the value is a
|
|
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
|
|
42
|
+
* Is the value a _Fragments_ instance?
|
|
41
43
|
* @param value Value to check
|
|
42
|
-
* @returns `true` if the value is a
|
|
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
|
|
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
|
|
11
|
-
* @returns
|
|
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
|
|
35
|
-
*
|
|
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
|
|
13
|
+
* Identifier for the _Fragment_
|
|
11
14
|
*
|
|
12
|
-
*
|
|
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 {
|
|
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 {
|
|
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
|
-
|
|
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):
|
|
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
|
-
|
|
65
|
+
mapAttributeValue(data, element, 'value', data.values[Number.parseInt(index, 10)]);
|
|
70
66
|
|
|
71
|
-
|
|
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
|
-
|
|
73
|
+
setComputedNode(data, comment, value as GenericCallback);
|
|
110
74
|
break;
|
|
111
75
|
|
|
112
76
|
case isFragments(value):
|
|
113
|
-
|
|
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
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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
|
|
108
|
+
function setFragmentsNode(data: FragmentData, comment: Comment, fragments: Fragments): void {
|
|
147
109
|
const state = fragmentsStates.get(fragments)!;
|
|
148
110
|
|
|
149
111
|
handleFragments(state, false);
|