@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.
- package/README.md +2 -0
- package/dist/abydon.full.mjs +422 -359
- package/dist/attribute/index.d.mts +7 -0
- package/dist/attribute/index.mjs +47 -0
- package/dist/{node/attribute → attribute}/value.d.mts +2 -2
- package/dist/attribute/value.mjs +53 -0
- package/dist/constants.d.mts +8 -8
- package/dist/constants.mjs +15 -15
- package/dist/fragment.d.mts +30 -13
- package/dist/fragment.mjs +40 -18
- package/dist/fragments.d.mts +3 -7
- package/dist/fragments.mjs +6 -12
- package/dist/helpers/dom.d.mts +2 -3
- package/dist/helpers/dom.mjs +2 -4
- package/dist/helpers/index.d.mts +16 -2
- package/dist/helpers/index.mjs +17 -1
- package/dist/index.d.mts +35 -7
- package/dist/index.mjs +39 -8
- package/dist/models.d.mts +6 -4
- package/dist/node/event.mjs +9 -4
- package/dist/node/index.mjs +26 -32
- package/dist/node/value.mjs +70 -56
- package/dist/parse.mjs +6 -4
- package/package.json +6 -5
- package/src/attribute/index.ts +89 -0
- package/src/attribute/value.ts +107 -0
- package/src/constants.ts +19 -19
- package/src/fragment.ts +43 -21
- package/src/fragments.ts +8 -15
- package/src/helpers/dom.ts +3 -5
- package/src/helpers/index.ts +25 -1
- package/src/index.ts +47 -7
- package/src/models.ts +6 -4
- package/src/node/event.ts +12 -5
- package/src/node/index.ts +36 -56
- package/src/node/value.ts +121 -104
- package/src/parse.ts +15 -5
- package/dist/node/attribute/index.d.mts +0 -6
- package/dist/node/attribute/index.mjs +0 -51
- package/dist/node/attribute/value.mjs +0 -65
- package/src/node/attribute/index.ts +0 -100
- 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 =
|
|
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: '
|
|
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 =
|
|
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
|
|
50
|
+
export const EXPRESSION_ATTRIBUTE_STYLE_VARIABLE = /^style\.--/;
|
|
51
51
|
|
|
52
|
-
export const
|
|
52
|
+
export const EXPRESSION_EVENT_ATTRIBUTE = /^@([\w-]+)(?::([a-z:]+))?="$/i;
|
|
53
53
|
|
|
54
|
-
export const
|
|
54
|
+
export const EXPRESSION_EVENT_NAME = /^@?([\w-]+)(?::([a-z:]+))?$/i;
|
|
55
55
|
|
|
56
|
-
export const
|
|
56
|
+
export const EXPRESSION_EVENT_OPTIONS_ACTIVE = /^a(?:ctive)$/i;
|
|
57
57
|
|
|
58
|
-
export const
|
|
59
|
-
|
|
60
|
-
export const EXPRESSION_EVENT_PREFIX = /^@/;
|
|
58
|
+
export const EXPRESSION_EVENT_OPTIONS_CAPTURE = /^c(?:apture)$/i;
|
|
61
59
|
|
|
62
|
-
export const
|
|
60
|
+
export const EXPRESSION_EVENT_OPTIONS_ONCE = /^o(?:nce)$/i;
|
|
63
61
|
|
|
64
|
-
export const
|
|
62
|
+
export const EXPRESSION_EVENT_PREFIX = /^@/;
|
|
65
63
|
|
|
66
|
-
export const
|
|
64
|
+
export const EXPRESSION_TEXTAREA_VALUE = /(?:<|<)!--abydon\.(\d+)--(?:>|>)/;
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
|
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
|
|
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
|
-
*
|
|
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
|
-
|
|
114
|
-
this
|
|
115
|
-
|
|
116
|
-
return this;
|
|
134
|
+
prependTo(element: Element): void {
|
|
135
|
+
element.prepend(...this.get());
|
|
117
136
|
}
|
|
118
137
|
|
|
119
138
|
/**
|
|
120
|
-
* 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_
|
|
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
|
-
|
|
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) ?
|
|
42
|
+
const state = isFragments(item) ? fragmentsStates.get(item) : item;
|
|
50
43
|
|
|
51
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
|
131
|
+
export const fragmentsStates: WeakMap<Fragments, FragmentsState> = new WeakMap();
|
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[],
|
|
@@ -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
|
|
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
|
|
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>,
|
|
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
|
|
23
|
-
*
|
|
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
|
|
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/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 (
|
|
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
|
-
|
|
46
|
-
|
|
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
|
-
|
|
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):
|
|
56
|
-
const [, index] =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
73
|
+
setComputedNode(data, comment, value as GenericCallback);
|
|
99
74
|
break;
|
|
100
75
|
|
|
101
76
|
case isFragments(value):
|
|
102
|
-
|
|
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
|
|
129
|
-
|
|
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
|
-
|
|
111
|
+
handleFragments(state, false);
|
|
132
112
|
|
|
133
|
-
setReactiveValue(data, comment,
|
|
113
|
+
setReactiveValue(data, comment, state.mapped as ReactiveArray<unknown>);
|
|
134
114
|
}
|