@oscarpalmer/abydon 0.14.0 → 0.16.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/dist/abydon.full.js +814 -645
- package/dist/constants.js +20 -5
- package/dist/fragment.js +31 -12
- package/dist/fragments.js +75 -0
- package/dist/helpers/dom.js +1 -11
- package/dist/helpers/index.js +14 -4
- package/dist/index.js +7 -3
- package/dist/node/attribute/index.js +14 -14
- package/dist/node/attribute/value.js +19 -13
- package/dist/node/event.js +15 -22
- package/dist/node/index.js +9 -4
- package/dist/node/value.js +49 -44
- package/dist/parse.js +15 -4
- package/package.json +13 -15
- package/src/constants.ts +30 -10
- package/src/fragment.ts +53 -30
- package/src/fragments.ts +132 -0
- package/src/helpers/dom.ts +1 -20
- package/src/helpers/index.ts +22 -9
- package/src/index.ts +26 -8
- package/src/models.ts +20 -2
- package/src/node/attribute/index.ts +22 -21
- package/src/node/attribute/value.ts +44 -37
- package/src/node/event.ts +26 -33
- package/src/node/index.ts +12 -14
- package/src/node/value.ts +35 -44
- package/src/parse.ts +15 -17
- package/types/constants.d.ts +12 -4
- package/types/fragment.d.ts +23 -3
- package/types/fragments.d.ts +13 -0
- package/types/helpers/index.d.ts +2 -0
- package/types/index.d.ts +16 -1
- package/types/models.d.ts +25 -3
- package/types/node/attribute/index.d.ts +1 -2
- package/types/node/attribute/value.d.ts +1 -2
- package/types/node/event.d.ts +1 -3
package/package.json
CHANGED
|
@@ -4,23 +4,21 @@
|
|
|
4
4
|
"url": "https://oscarpalmer.se"
|
|
5
5
|
},
|
|
6
6
|
"dependencies": {
|
|
7
|
-
"@oscarpalmer/atoms": "^0.
|
|
8
|
-
"@oscarpalmer/mora": "^0.
|
|
9
|
-
"@oscarpalmer/toretto": "^0.
|
|
7
|
+
"@oscarpalmer/atoms": "^0.118",
|
|
8
|
+
"@oscarpalmer/mora": "^0.25",
|
|
9
|
+
"@oscarpalmer/toretto": "^0.32"
|
|
10
10
|
},
|
|
11
11
|
"devDependencies": {
|
|
12
|
-
"@biomejs/biome": "^2.3",
|
|
13
12
|
"@oscarpalmer/oui": "^0.18",
|
|
14
|
-
"@
|
|
15
|
-
"@rollup/plugin-typescript": "^12.3",
|
|
16
|
-
"@types/node": "^24.9",
|
|
13
|
+
"@types/node": "^25",
|
|
17
14
|
"@vitest/coverage-istanbul": "^4",
|
|
18
|
-
"
|
|
19
|
-
"
|
|
20
|
-
"
|
|
15
|
+
"jsdom": "^27.3",
|
|
16
|
+
"oxfmt": "^0.19",
|
|
17
|
+
"oxlint": "^1.34",
|
|
18
|
+
"rolldown": "1.0.0-beta.56",
|
|
21
19
|
"tslib": "^2.8",
|
|
22
20
|
"typescript": "^5.9",
|
|
23
|
-
"vite": "
|
|
21
|
+
"vite": "8.0.0-beta.2",
|
|
24
22
|
"vitest": "^4"
|
|
25
23
|
},
|
|
26
24
|
"exports": {
|
|
@@ -49,14 +47,14 @@
|
|
|
49
47
|
"url": "git+https://github.com/oscarpalmer/abydon.git"
|
|
50
48
|
},
|
|
51
49
|
"scripts": {
|
|
52
|
-
"build": "npm run clean && npx vite build && npm run
|
|
50
|
+
"build": "npm run clean && npx vite build && npm run rolldown:build && npx tsc",
|
|
53
51
|
"clean": "rm -rf ./dist && rm -rf ./types && rm -f ./tsconfig.tsbuildinfo",
|
|
54
|
-
"
|
|
55
|
-
"
|
|
52
|
+
"rolldown:build": "npx rolldown -c",
|
|
53
|
+
"rolldown:watch": "npx rolldown -c ./rolldown.config.js --watch",
|
|
56
54
|
"test": "npx vitest --coverage",
|
|
57
55
|
"watch": "npx vite build --watch"
|
|
58
56
|
},
|
|
59
57
|
"type": "module",
|
|
60
58
|
"types": "types/index.d.ts",
|
|
61
|
-
"version": "0.
|
|
59
|
+
"version": "0.16.0"
|
|
62
60
|
}
|
package/src/constants.ts
CHANGED
|
@@ -1,20 +1,40 @@
|
|
|
1
|
-
export const ABORT_CONTROLLERS: WeakMap<HTMLOrSVGElement, AbortController> =
|
|
2
|
-
new WeakMap();
|
|
3
|
-
|
|
4
1
|
export const ATTRIBUTE_CLASS_PREFIX_LENGTH = 6;
|
|
5
2
|
|
|
3
|
+
export const EVENT_DEFAULTS: Record<string, string> = {
|
|
4
|
+
A: 'click',
|
|
5
|
+
BUTTON: 'click',
|
|
6
|
+
DETAILS: 'toggle',
|
|
7
|
+
FORM: 'submit',
|
|
8
|
+
SELECT: 'change',
|
|
9
|
+
TEXTAREA: 'input',
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export const EXPRESSION_ABYDON_ATTRIBUTE_FULL = /(@?\w+)="<!--abydon.(\d+)-->"/g;
|
|
13
|
+
|
|
14
|
+
export const EXPRESSION_ABYDON_ATTRIBUTE_PREFIX = /^_/;
|
|
15
|
+
|
|
16
|
+
export const EXPRESSION_ABYDON_CONTENT = /^@?abydon\.(\d+)@?$/;
|
|
17
|
+
|
|
6
18
|
export const EXPRESSION_ATTRIBUTE_CLASS = /^class\./;
|
|
7
19
|
|
|
8
|
-
export const EXPRESSION_ATTRIBUTE_STYLE_FULL =
|
|
9
|
-
/^style\.([\w-]+)(?:\.([\w-]+))?$/;
|
|
20
|
+
export const EXPRESSION_ATTRIBUTE_STYLE_FULL = /^style\.([\w-]+)(?:\.([\w-]+))?$/;
|
|
10
21
|
|
|
11
22
|
export const EXPRESSION_ATTRIBUTE_STYLE_PREFIX = /^style\./;
|
|
12
23
|
|
|
13
|
-
export const
|
|
14
|
-
|
|
15
|
-
export const EXPRESSION_COMMENT_CONTENT = /^abydon\.(\d+)$/;
|
|
24
|
+
export const EXPRESSION_EVENT_CHANGE_TYPES = /^(checkbox|radio)$/;
|
|
16
25
|
|
|
17
26
|
export const EXPRESSION_EVENT_NAME = /^@([\w-]+)(?::([a-z:]+))?$/i;
|
|
18
27
|
|
|
19
|
-
export const
|
|
20
|
-
|
|
28
|
+
export const EXPRESSION_EVENT_OPTIONS_ACTIVE = /^a(ctive)$/i;
|
|
29
|
+
|
|
30
|
+
export const EXPRESSION_EVENT_OPTIONS_CAPTURE = /^c(apture)$/i;
|
|
31
|
+
|
|
32
|
+
export const EXPRESSION_EVENT_OPTIONS_ONCE = /^o(nce)$/i;
|
|
33
|
+
|
|
34
|
+
export const EXPRESSION_EVENT_PREFIX = /^@/;
|
|
35
|
+
|
|
36
|
+
export const EXPRESSION_PERIOD = /\./;
|
|
37
|
+
|
|
38
|
+
export const NAME_FRAGMENT = '$fragment';
|
|
39
|
+
|
|
40
|
+
export const NAME_FRAGMENTS = '$fragments';
|
package/src/fragment.ts
CHANGED
|
@@ -1,23 +1,33 @@
|
|
|
1
|
+
import {isPlainObject} from '@oscarpalmer/atoms/is';
|
|
1
2
|
import {html} from '@oscarpalmer/toretto/html';
|
|
3
|
+
import {NAME_FRAGMENT} from './constants';
|
|
4
|
+
import {handleFragments} from './fragments';
|
|
5
|
+
import {isFragments} from './helpers';
|
|
2
6
|
import {removeNodes} from './helpers/dom';
|
|
3
7
|
import type {FragmentConfiguration, FragmentData} from './models';
|
|
4
8
|
import {mapNodes} from './node';
|
|
5
9
|
import {parse} from './parse';
|
|
6
|
-
import { isPlainObject } from '@oscarpalmer/atoms';
|
|
7
10
|
|
|
8
11
|
export class Fragment {
|
|
9
12
|
readonly #data: FragmentData;
|
|
10
13
|
|
|
11
14
|
readonly #configuration: Required<FragmentConfiguration> = {
|
|
12
15
|
identifier: undefined,
|
|
13
|
-
|
|
14
|
-
}
|
|
16
|
+
cache: true,
|
|
17
|
+
};
|
|
15
18
|
|
|
19
|
+
/**
|
|
20
|
+
* Fragment identifier
|
|
21
|
+
*/
|
|
16
22
|
get identifier(): unknown {
|
|
17
23
|
return this.#configuration.identifier;
|
|
18
24
|
}
|
|
19
25
|
|
|
20
26
|
constructor(strings: TemplateStringsArray, expressions: unknown[]) {
|
|
27
|
+
Object.defineProperty(this, NAME_FRAGMENT, {
|
|
28
|
+
value: true,
|
|
29
|
+
});
|
|
30
|
+
|
|
21
31
|
this.#data = {
|
|
22
32
|
expressions,
|
|
23
33
|
strings,
|
|
@@ -28,35 +38,38 @@ export class Fragment {
|
|
|
28
38
|
},
|
|
29
39
|
values: [],
|
|
30
40
|
};
|
|
31
|
-
|
|
32
|
-
Object.defineProperty(this, '$fragment', {
|
|
33
|
-
value: true,
|
|
34
|
-
});
|
|
35
41
|
}
|
|
36
42
|
|
|
37
43
|
/**
|
|
38
|
-
*
|
|
44
|
+
* Append the fragment to the given element
|
|
45
|
+
* @param element Element to append to
|
|
39
46
|
*/
|
|
40
47
|
appendTo(element: Element): void {
|
|
41
48
|
element.append(...this.get());
|
|
42
49
|
}
|
|
43
50
|
|
|
51
|
+
/**
|
|
52
|
+
* Configure the fragment
|
|
53
|
+
* @param configuration Configuration options
|
|
54
|
+
* @returns Fragment
|
|
55
|
+
*/
|
|
44
56
|
configure(configuration: FragmentConfiguration): Fragment {
|
|
45
57
|
const actual = isPlainObject(configuration) ? configuration : {};
|
|
46
58
|
|
|
47
|
-
if (
|
|
59
|
+
if ('identifier' in actual) {
|
|
48
60
|
this.#configuration.identifier = actual.identifier;
|
|
49
61
|
}
|
|
50
62
|
|
|
51
|
-
if (typeof actual.
|
|
52
|
-
this.#configuration.
|
|
63
|
+
if (typeof actual.cache === 'boolean') {
|
|
64
|
+
this.#configuration.cache = actual.cache;
|
|
53
65
|
}
|
|
54
66
|
|
|
55
67
|
return this;
|
|
56
68
|
}
|
|
57
69
|
|
|
58
70
|
/**
|
|
59
|
-
*
|
|
71
|
+
* Get a list of the fragment's nodes
|
|
72
|
+
* @returns List of nodes
|
|
60
73
|
*/
|
|
61
74
|
get(): ChildNode[] {
|
|
62
75
|
const data = this.#data;
|
|
@@ -65,13 +78,9 @@ export class Fragment {
|
|
|
65
78
|
const parsed = parse(data);
|
|
66
79
|
|
|
67
80
|
const templated = html(parsed, {
|
|
68
|
-
|
|
81
|
+
cache: this.#configuration.cache,
|
|
69
82
|
});
|
|
70
83
|
|
|
71
|
-
if (this.#configuration.ignoreCache) {
|
|
72
|
-
html.remove(parsed);
|
|
73
|
-
}
|
|
74
|
-
|
|
75
84
|
data.items.splice(
|
|
76
85
|
0,
|
|
77
86
|
data.items.length,
|
|
@@ -83,20 +92,24 @@ export class Fragment {
|
|
|
83
92
|
mapNodes(
|
|
84
93
|
data,
|
|
85
94
|
data.items.flatMap(
|
|
86
|
-
item =>
|
|
87
|
-
item.fragments?.flatMap(fragment => fragment.get()) ?? item.nodes ?? [],
|
|
95
|
+
item => item.fragments?.flatMap(fragment => fragment.get()) ?? item.nodes ?? [],
|
|
88
96
|
),
|
|
89
97
|
);
|
|
90
98
|
}
|
|
91
99
|
|
|
92
|
-
return
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
item.fragments?.flatMap(fragment => fragment.get()) ?? item.nodes ?? [],
|
|
96
|
-
),
|
|
97
|
-
];
|
|
100
|
+
return data.items.flatMap(
|
|
101
|
+
item => item.fragments?.flatMap(fragment => fragment.get()) ?? item.nodes ?? [],
|
|
102
|
+
);
|
|
98
103
|
}
|
|
99
104
|
|
|
105
|
+
/**
|
|
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
|
|
112
|
+
*/
|
|
100
113
|
identify(identifier: unknown): Fragment {
|
|
101
114
|
this.#configuration.identifier = identifier;
|
|
102
115
|
|
|
@@ -104,7 +117,7 @@ export class Fragment {
|
|
|
104
117
|
}
|
|
105
118
|
|
|
106
119
|
/**
|
|
107
|
-
*
|
|
120
|
+
* Remove the fragment from the DOM
|
|
108
121
|
*/
|
|
109
122
|
remove(): void {
|
|
110
123
|
removeFragment(this.#data);
|
|
@@ -114,20 +127,30 @@ export class Fragment {
|
|
|
114
127
|
function removeFragment(data: FragmentData): void {
|
|
115
128
|
removeMora(data);
|
|
116
129
|
|
|
117
|
-
|
|
130
|
+
let {length} = data.items;
|
|
118
131
|
|
|
119
132
|
for (let index = 0; index < length; index += 1) {
|
|
120
133
|
const {fragments, nodes} = data.items[index];
|
|
121
|
-
const
|
|
134
|
+
const fragmentsLength = fragments?.length ?? 0;
|
|
122
135
|
|
|
123
|
-
for (let
|
|
124
|
-
fragments?.[
|
|
136
|
+
for (let fragmentIndex = 0; fragmentIndex < fragmentsLength; fragmentIndex += 1) {
|
|
137
|
+
fragments?.[fragmentIndex]?.remove();
|
|
125
138
|
}
|
|
126
139
|
|
|
127
140
|
removeNodes(nodes ?? []);
|
|
128
141
|
}
|
|
129
142
|
|
|
130
143
|
data.items.length = 0;
|
|
144
|
+
|
|
145
|
+
length = data.values.length;
|
|
146
|
+
|
|
147
|
+
for (let index = 0; index < length; index += 1) {
|
|
148
|
+
const value = data.values[index];
|
|
149
|
+
|
|
150
|
+
if (isFragments(value)) {
|
|
151
|
+
handleFragments(value, true);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
131
154
|
}
|
|
132
155
|
|
|
133
156
|
function removeMora(data: FragmentData): void {
|
package/src/fragments.ts
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import {getString} from '@oscarpalmer/atoms/string';
|
|
2
|
+
import {array, type ReactiveArray} from '@oscarpalmer/mora';
|
|
3
|
+
import {NAME_FRAGMENTS} from './constants';
|
|
4
|
+
import type {Fragment} from './fragment';
|
|
5
|
+
import {isFragment, isFragments} from './helpers';
|
|
6
|
+
import type {FragmentsState} from './models';
|
|
7
|
+
|
|
8
|
+
export class Fragments {
|
|
9
|
+
readonly #state: FragmentsState;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Fragment items
|
|
13
|
+
*/
|
|
14
|
+
get items(): ReactiveArray<Fragment> {
|
|
15
|
+
return this.#state.mapped;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
constructor(
|
|
19
|
+
items: ReactiveArray<unknown>,
|
|
20
|
+
identify: (item: unknown) => unknown,
|
|
21
|
+
fragment: (item: unknown) => Fragment,
|
|
22
|
+
) {
|
|
23
|
+
Object.defineProperty(this, NAME_FRAGMENTS, {
|
|
24
|
+
value: true,
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
this.#state = {
|
|
28
|
+
fragment,
|
|
29
|
+
identify,
|
|
30
|
+
array: items,
|
|
31
|
+
instances: {},
|
|
32
|
+
mapped: array<Fragment>([]),
|
|
33
|
+
subscriber: undefined,
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
states.set(this, this.#state);
|
|
37
|
+
|
|
38
|
+
initializeFragments(this.#state);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function handleFragments(item: Fragments | FragmentsState, remove: boolean): void {
|
|
43
|
+
const state = isFragments(item) ? states.get(item) : item;
|
|
44
|
+
|
|
45
|
+
if (state != null) {
|
|
46
|
+
(remove ? removeFragments : initializeFragments)(state);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function handleItems(state: FragmentsState, items: unknown[]): void {
|
|
51
|
+
const keys = new Set<string>();
|
|
52
|
+
const mapped: Fragment[] = [];
|
|
53
|
+
|
|
54
|
+
const {length} = items;
|
|
55
|
+
|
|
56
|
+
for (let index = 0; index < length; index += 1) {
|
|
57
|
+
const item = items[index];
|
|
58
|
+
const identifier = state.identify(item);
|
|
59
|
+
|
|
60
|
+
if (identifier == null) {
|
|
61
|
+
throw new Error('Identifier cannot be null or undefined');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const key = getString(identifier);
|
|
65
|
+
|
|
66
|
+
if (keys.has(key)) {
|
|
67
|
+
throw new Error(`Duplicate identifier found: "${key}"`);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
let instance = state.instances[key];
|
|
71
|
+
|
|
72
|
+
if (instance == null) {
|
|
73
|
+
instance = state.fragment(item);
|
|
74
|
+
|
|
75
|
+
if (!isFragment(instance)) {
|
|
76
|
+
throw new Error('Fragment function must return a Fragment instance');
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
instance.identify(key);
|
|
81
|
+
|
|
82
|
+
state.instances[key] = instance;
|
|
83
|
+
|
|
84
|
+
keys.add(key);
|
|
85
|
+
|
|
86
|
+
mapped.push(instance);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
state.mapped.set(mapped);
|
|
90
|
+
|
|
91
|
+
updateFragments(state, keys);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function initializeFragments(state: FragmentsState): void {
|
|
95
|
+
state.subscriber ??= state.array.subscribe(items => {
|
|
96
|
+
handleItems(state, items);
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function removeFragments(state: FragmentsState): void {
|
|
101
|
+
state.subscriber?.();
|
|
102
|
+
|
|
103
|
+
updateFragments(state);
|
|
104
|
+
|
|
105
|
+
state.subscriber = undefined;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function updateFragments(state: FragmentsState, active?: Set<string>): void {
|
|
109
|
+
const next: Record<string, Fragment> = {};
|
|
110
|
+
|
|
111
|
+
const previous = {...state.instances};
|
|
112
|
+
const keys = Object.keys(previous);
|
|
113
|
+
const {length} = keys;
|
|
114
|
+
|
|
115
|
+
for (let index = 0; index < length; index += 1) {
|
|
116
|
+
const key = keys[index];
|
|
117
|
+
|
|
118
|
+
if (active?.has(key)) {
|
|
119
|
+
next[key] = previous[key];
|
|
120
|
+
} else {
|
|
121
|
+
previous[key].remove();
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
state.instances = next;
|
|
126
|
+
|
|
127
|
+
active?.clear();
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
//
|
|
131
|
+
|
|
132
|
+
const states: WeakMap<Fragments, FragmentsState> = new WeakMap();
|
package/src/helpers/dom.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import {getString} from '@oscarpalmer/atoms/string';
|
|
2
|
-
import {isChildNode
|
|
3
|
-
import {removeEvents} from '../node/event';
|
|
2
|
+
import {isChildNode} from '@oscarpalmer/toretto/is';
|
|
4
3
|
import {isFragment} from './index';
|
|
5
4
|
|
|
6
5
|
export function createNodes(value: unknown): ChildNode[] {
|
|
@@ -16,8 +15,6 @@ export function createNodes(value: unknown): ChildNode[] {
|
|
|
16
15
|
}
|
|
17
16
|
|
|
18
17
|
export function removeNodes(nodes: ChildNode[]): void {
|
|
19
|
-
sanitizeNodes(nodes);
|
|
20
|
-
|
|
21
18
|
const {length} = nodes;
|
|
22
19
|
|
|
23
20
|
for (let index = 0; index < length; index += 1) {
|
|
@@ -34,19 +31,3 @@ export function replaceNodes(from: ChildNode[], to: ChildNode[]): void {
|
|
|
34
31
|
from[index].remove();
|
|
35
32
|
}
|
|
36
33
|
}
|
|
37
|
-
|
|
38
|
-
function sanitizeNodes(nodes: ChildNode[]): void {
|
|
39
|
-
const {length} = nodes;
|
|
40
|
-
|
|
41
|
-
for (let index = 0; index < length; index += 1) {
|
|
42
|
-
const node = nodes[index];
|
|
43
|
-
|
|
44
|
-
if (isHTMLOrSVGElement(node)) {
|
|
45
|
-
removeEvents(node);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
if (node.hasChildNodes()) {
|
|
49
|
-
sanitizeNodes([...node.childNodes] as ChildNode[]);
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
}
|
package/src/helpers/index.ts
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
+
import type {PlainObject} from '@oscarpalmer/atoms/models';
|
|
2
|
+
import {NAME_FRAGMENT, NAME_FRAGMENTS} from '../constants';
|
|
1
3
|
import type {Fragment} from '../fragment';
|
|
4
|
+
import type {Fragments} from '../fragments';
|
|
2
5
|
|
|
3
6
|
export function compareArrays(
|
|
4
7
|
first: unknown[],
|
|
@@ -8,22 +11,32 @@ export function compareArrays(
|
|
|
8
11
|
const from = firstIsLarger ? first : second;
|
|
9
12
|
const to = firstIsLarger ? second : first;
|
|
10
13
|
|
|
11
|
-
if (
|
|
12
|
-
|
|
13
|
-
.filter(key => to.includes(key))
|
|
14
|
-
.every((key, index) => to[index] === key)
|
|
15
|
-
) {
|
|
16
|
-
return 'dissimilar';
|
|
14
|
+
if (!from.filter(key => to.includes(key)).every((key, index) => to[index] === key)) {
|
|
15
|
+
return COMPARISON_DISSIMILAR;
|
|
17
16
|
}
|
|
18
17
|
|
|
19
|
-
return firstIsLarger ?
|
|
18
|
+
return firstIsLarger ? COMPARISON_REMOVED : COMPARISON_ADDED;
|
|
20
19
|
}
|
|
21
20
|
|
|
22
21
|
export function isFragment(value: unknown): value is Fragment {
|
|
22
|
+
return isNamed(value, NAME_FRAGMENT);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function isFragments(value: unknown): value is Fragments {
|
|
26
|
+
return isNamed(value, NAME_FRAGMENTS);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function isNamed(value: unknown, name: string): boolean {
|
|
23
30
|
return (
|
|
24
31
|
typeof value === 'object' &&
|
|
25
32
|
value != null &&
|
|
26
|
-
|
|
27
|
-
value
|
|
33
|
+
name in value &&
|
|
34
|
+
(value as PlainObject)[name] === true
|
|
28
35
|
);
|
|
29
36
|
}
|
|
37
|
+
|
|
38
|
+
const COMPARISON_ADDED = 'added';
|
|
39
|
+
|
|
40
|
+
const COMPARISON_DISSIMILAR = 'dissimilar';
|
|
41
|
+
|
|
42
|
+
const COMPARISON_REMOVED = 'removed';
|
package/src/index.ts
CHANGED
|
@@ -1,13 +1,31 @@
|
|
|
1
1
|
import '@oscarpalmer/mora';
|
|
2
|
-
import {
|
|
2
|
+
import type {ReactiveArray} from '@oscarpalmer/mora';
|
|
3
|
+
import {Fragment} from './fragment';
|
|
4
|
+
import {Fragments} from './fragments';
|
|
3
5
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
/**
|
|
7
|
+
* Create Fragments from a reactive array
|
|
8
|
+
* @param array Reactive array
|
|
9
|
+
* @param identify Function to identify item
|
|
10
|
+
* @param fragment Function to create fragment from item
|
|
11
|
+
* @returns Fragments
|
|
12
|
+
*/
|
|
13
|
+
export function fragments<Item>(
|
|
14
|
+
array: ReactiveArray<Item>,
|
|
15
|
+
identify: (item: Item) => unknown,
|
|
16
|
+
fragment: (item: Item) => Fragment,
|
|
17
|
+
): Fragments {
|
|
18
|
+
return new Fragments(array as ReactiveArray<unknown>, identify as never, fragment as never);
|
|
9
19
|
}
|
|
10
20
|
|
|
11
|
-
|
|
12
|
-
|
|
21
|
+
/**
|
|
22
|
+
* Create Fragment from a template
|
|
23
|
+
* @returns Fragment
|
|
24
|
+
*/
|
|
25
|
+
export function html(template: TemplateStringsArray, ...values: unknown[]): Fragment {
|
|
26
|
+
return new Fragment(template, values);
|
|
27
|
+
}
|
|
13
28
|
|
|
29
|
+
export * from '@oscarpalmer/mora';
|
|
30
|
+
export type {Fragment} from './fragment';
|
|
31
|
+
export type {Fragments} from './fragments';
|
package/src/models.ts
CHANGED
|
@@ -1,9 +1,18 @@
|
|
|
1
|
-
import type {Reactive} from '@oscarpalmer/mora';
|
|
1
|
+
import type {Reactive, ReactiveArray, Unsubscribe} from '@oscarpalmer/mora';
|
|
2
2
|
import type {Fragment} from './fragment';
|
|
3
3
|
|
|
4
4
|
export type FragmentConfiguration = {
|
|
5
|
+
/**
|
|
6
|
+
* Should the template be cached? _(defaults to `true`)_
|
|
7
|
+
*/
|
|
8
|
+
cache?: boolean;
|
|
9
|
+
/**
|
|
10
|
+
* Identifier for the fragment
|
|
11
|
+
*
|
|
12
|
+
* _(An identifier can be used to uniquely identify a fragment,
|
|
13
|
+
* which helps prevent re-rendering in certain scenarios)_
|
|
14
|
+
*/
|
|
5
15
|
identifier?: unknown;
|
|
6
|
-
ignoreCache?: boolean;
|
|
7
16
|
};
|
|
8
17
|
|
|
9
18
|
export type FragmentData = {
|
|
@@ -21,6 +30,15 @@ export type FragmentItem = {
|
|
|
21
30
|
text?: Text;
|
|
22
31
|
};
|
|
23
32
|
|
|
33
|
+
export type FragmentsState = {
|
|
34
|
+
array: ReactiveArray<unknown>;
|
|
35
|
+
fragment: (item: unknown) => Fragment;
|
|
36
|
+
identify: (item: unknown) => unknown;
|
|
37
|
+
instances: Record<string, Fragment>;
|
|
38
|
+
mapped: ReactiveArray<Fragment>;
|
|
39
|
+
subscriber: Unsubscribe | undefined;
|
|
40
|
+
};
|
|
41
|
+
|
|
24
42
|
type MoraData = {
|
|
25
43
|
subscribers: Set<() => void>;
|
|
26
44
|
values: Set<Reactive<unknown>>;
|
|
@@ -1,43 +1,44 @@
|
|
|
1
1
|
import type {GenericCallback} from '@oscarpalmer/atoms/models';
|
|
2
2
|
import {computed, isReactive} from '@oscarpalmer/mora';
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
import {
|
|
4
|
+
EXPRESSION_ABYDON_ATTRIBUTE_PREFIX,
|
|
5
|
+
EXPRESSION_ABYDON_CONTENT,
|
|
6
|
+
EXPRESSION_EVENT_PREFIX,
|
|
7
|
+
EXPRESSION_PERIOD,
|
|
8
|
+
} from '../../constants';
|
|
6
9
|
import type {FragmentData} from '../../models';
|
|
7
10
|
import {mapEvent} from '../event';
|
|
8
11
|
import {setAttribute} from './value';
|
|
9
12
|
|
|
10
13
|
function getValue(data: FragmentData, original: string): unknown {
|
|
11
|
-
const matches =
|
|
14
|
+
const matches = EXPRESSION_ABYDON_CONTENT.exec(original ?? '');
|
|
12
15
|
|
|
13
16
|
return matches == null ? original : data.values[+matches[1]];
|
|
14
17
|
}
|
|
15
18
|
|
|
16
|
-
export function mapAttributes(
|
|
17
|
-
data: FragmentData,
|
|
18
|
-
element: HTMLOrSVGElement,
|
|
19
|
-
): void {
|
|
19
|
+
export function mapAttributes(data: FragmentData, element: HTMLElement | SVGElement): void {
|
|
20
20
|
const attributes = [...element.attributes];
|
|
21
21
|
const {length} = attributes;
|
|
22
22
|
|
|
23
23
|
for (let index = 0; index < length; index += 1) {
|
|
24
24
|
const {name, value} = attributes[index];
|
|
25
25
|
|
|
26
|
-
const
|
|
26
|
+
const actualName = name.replace(EXPRESSION_ABYDON_ATTRIBUTE_PREFIX, '');
|
|
27
|
+
const actualValue = getValue(data, value) as never;
|
|
27
28
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
break;
|
|
29
|
+
if (actualName !== name) {
|
|
30
|
+
element.removeAttribute(name);
|
|
31
|
+
}
|
|
32
32
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
mapValue(data, element, name, actual);
|
|
33
|
+
switch (true) {
|
|
34
|
+
case EXPRESSION_EVENT_PREFIX.test(actualName):
|
|
35
|
+
mapEvent(element, actualName, actualValue);
|
|
37
36
|
break;
|
|
38
37
|
|
|
39
|
-
case
|
|
40
|
-
|
|
38
|
+
case EXPRESSION_PERIOD.test(actualName):
|
|
39
|
+
case typeof actualValue === 'function':
|
|
40
|
+
case isReactive(actualValue):
|
|
41
|
+
mapValue(data, element, actualName, actualValue);
|
|
41
42
|
break;
|
|
42
43
|
|
|
43
44
|
default:
|
|
@@ -48,7 +49,7 @@ export function mapAttributes(
|
|
|
48
49
|
|
|
49
50
|
function mapValue(
|
|
50
51
|
data: FragmentData,
|
|
51
|
-
element:
|
|
52
|
+
element: HTMLElement | SVGElement,
|
|
52
53
|
name: string,
|
|
53
54
|
value: unknown,
|
|
54
55
|
): void {
|
|
@@ -61,7 +62,7 @@ function mapValue(
|
|
|
61
62
|
|
|
62
63
|
function setComputedAttribute(
|
|
63
64
|
data: FragmentData,
|
|
64
|
-
element:
|
|
65
|
+
element: HTMLElement | SVGElement,
|
|
65
66
|
name: string,
|
|
66
67
|
callback: GenericCallback,
|
|
67
68
|
): void {
|