@oscarpalmer/abydon 0.14.0 → 0.15.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 +229 -108
- package/dist/fragment.js +32 -11
- package/dist/fragments.js +74 -0
- package/dist/helpers/dom.js +1 -1
- package/dist/helpers/index.js +4 -1
- package/dist/index.js +5 -1
- package/dist/node/index.js +7 -2
- package/dist/node/value.js +44 -39
- package/dist/parse.js +6 -3
- package/package.json +2 -2
- package/src/fragment.ts +55 -20
- package/src/fragments.ts +134 -0
- package/src/helpers/index.ts +10 -0
- package/src/index.ts +18 -4
- package/src/models.ts +10 -1
- package/src/node/index.ts +12 -2
- package/src/node/value.ts +25 -3
- 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 +5 -1
- package/types/models.d.ts +16 -3
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { isFragment, isFragments } from "./helpers/index.js";
|
|
2
|
+
import { array } from "@oscarpalmer/mora";
|
|
3
|
+
import { getString } from "@oscarpalmer/atoms/string";
|
|
4
|
+
var Fragments = class {
|
|
5
|
+
#state;
|
|
6
|
+
get items() {
|
|
7
|
+
return this.#state.mapped;
|
|
8
|
+
}
|
|
9
|
+
constructor(items, identify, fragment) {
|
|
10
|
+
Object.defineProperty(this, "$fragments", { value: true });
|
|
11
|
+
this.#state = {
|
|
12
|
+
fragment,
|
|
13
|
+
identify,
|
|
14
|
+
array: items,
|
|
15
|
+
instances: {},
|
|
16
|
+
mapped: array([]),
|
|
17
|
+
subscriber: void 0
|
|
18
|
+
};
|
|
19
|
+
states.set(this, this.#state);
|
|
20
|
+
initializeFragments(this.#state);
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
function handleFragments(item, remove) {
|
|
24
|
+
const state = isFragments(item) ? states.get(item) : item;
|
|
25
|
+
if (state != null) (remove ? removeFragments : initializeFragments)(state);
|
|
26
|
+
}
|
|
27
|
+
function handleItems(state, items) {
|
|
28
|
+
const keys = /* @__PURE__ */ new Set();
|
|
29
|
+
const mapped = [];
|
|
30
|
+
const { length } = items;
|
|
31
|
+
for (let index = 0; index < length; index += 1) {
|
|
32
|
+
const item = items[index];
|
|
33
|
+
const identifier = state.identify(item);
|
|
34
|
+
if (identifier == null) throw new Error("Identifier cannot be null or undefined");
|
|
35
|
+
const key = getString(identifier);
|
|
36
|
+
if (keys.has(key)) throw new Error(`Duplicate identifier found: "${key}"`);
|
|
37
|
+
let instance = state.instances[key];
|
|
38
|
+
if (instance == null) {
|
|
39
|
+
instance = state.fragment(item);
|
|
40
|
+
if (!isFragment(instance)) throw new Error("Fragment function must return a Fragment instance");
|
|
41
|
+
}
|
|
42
|
+
instance.identify(key);
|
|
43
|
+
state.instances[key] = instance;
|
|
44
|
+
keys.add(key);
|
|
45
|
+
mapped.push(instance);
|
|
46
|
+
}
|
|
47
|
+
state.mapped.set(mapped);
|
|
48
|
+
updateFragments(state, keys);
|
|
49
|
+
}
|
|
50
|
+
function initializeFragments(state) {
|
|
51
|
+
state.subscriber ??= state.array.subscribe((items) => {
|
|
52
|
+
handleItems(state, items);
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
function removeFragments(state) {
|
|
56
|
+
state.subscriber?.();
|
|
57
|
+
updateFragments(state);
|
|
58
|
+
state.subscriber = void 0;
|
|
59
|
+
}
|
|
60
|
+
function updateFragments(state, active) {
|
|
61
|
+
const next = {};
|
|
62
|
+
const previous = { ...state.instances };
|
|
63
|
+
const keys = Object.keys(previous);
|
|
64
|
+
const { length } = keys;
|
|
65
|
+
for (let index = 0; index < length; index += 1) {
|
|
66
|
+
const key = keys[index];
|
|
67
|
+
if (active?.has(key)) next[key] = previous[key];
|
|
68
|
+
else previous[key].remove();
|
|
69
|
+
}
|
|
70
|
+
state.instances = next;
|
|
71
|
+
active?.clear();
|
|
72
|
+
}
|
|
73
|
+
var states = /* @__PURE__ */ new WeakMap();
|
|
74
|
+
export { Fragments, handleFragments, initializeFragments };
|
package/dist/helpers/dom.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { removeEvents } from "../node/event.js";
|
|
2
1
|
import { isFragment } from "./index.js";
|
|
2
|
+
import { removeEvents } from "../node/event.js";
|
|
3
3
|
import { getString } from "@oscarpalmer/atoms/string";
|
|
4
4
|
import { isChildNode, isHTMLOrSVGElement } from "@oscarpalmer/toretto/is";
|
|
5
5
|
function createNodes(value) {
|
package/dist/helpers/index.js
CHANGED
|
@@ -8,4 +8,7 @@ function compareArrays(first, second) {
|
|
|
8
8
|
function isFragment(value) {
|
|
9
9
|
return typeof value === "object" && value != null && "$fragment" in value && value.$fragment === true;
|
|
10
10
|
}
|
|
11
|
-
|
|
11
|
+
function isFragments(value) {
|
|
12
|
+
return typeof value === "object" && value != null && "$fragments" in value && value.$fragments === true;
|
|
13
|
+
}
|
|
14
|
+
export { compareArrays, isFragment, isFragments };
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
|
+
import { Fragments } from "./fragments.js";
|
|
1
2
|
import { Fragment } from "./fragment.js";
|
|
2
3
|
import "@oscarpalmer/mora";
|
|
3
4
|
export * from "@oscarpalmer/mora";
|
|
5
|
+
function fragments(array$1, identify, fragment) {
|
|
6
|
+
return new Fragments(array$1, identify, fragment);
|
|
7
|
+
}
|
|
4
8
|
function html(strings, ...values) {
|
|
5
9
|
return new Fragment(strings, values);
|
|
6
10
|
}
|
|
7
|
-
export { html };
|
|
11
|
+
export { fragments, html };
|
package/dist/node/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { isFragment, isFragments } from "../helpers/index.js";
|
|
2
|
+
import { handleFragments } from "../fragments.js";
|
|
1
3
|
import { EXPRESSION_COMMENT_CONTENT } from "../constants.js";
|
|
2
|
-
import { isFragment } from "../helpers/index.js";
|
|
3
4
|
import { createNodes } from "../helpers/dom.js";
|
|
4
5
|
import { mapAttributes } from "./attribute/index.js";
|
|
5
6
|
import { setReactiveValue } from "./value.js";
|
|
@@ -27,6 +28,10 @@ function mapValue(data, comment, value) {
|
|
|
27
28
|
case typeof value === "function":
|
|
28
29
|
setComputedValue(data, comment, value);
|
|
29
30
|
break;
|
|
31
|
+
case isFragments(value):
|
|
32
|
+
handleFragments(value, false);
|
|
33
|
+
setReactiveValue(data, comment, value.items);
|
|
34
|
+
break;
|
|
30
35
|
case isReactive(value):
|
|
31
36
|
setReactiveValue(data, comment, value);
|
|
32
37
|
break;
|
|
@@ -36,7 +41,7 @@ function mapValue(data, comment, value) {
|
|
|
36
41
|
}
|
|
37
42
|
}
|
|
38
43
|
function replaceComment(data, comment, value) {
|
|
39
|
-
const item = data.items.find((item$1) => item$1.nodes
|
|
44
|
+
const item = data.items.find((item$1) => item$1.nodes?.includes(comment));
|
|
40
45
|
const nodes = createNodes(value);
|
|
41
46
|
if (item != null) {
|
|
42
47
|
item.fragments = isFragment(value) ? [value] : void 0;
|
package/dist/node/value.js
CHANGED
|
@@ -39,70 +39,75 @@ function removeFragments(fragments) {
|
|
|
39
39
|
for (let index = 0; index < length; index += 1) fragments[index].remove();
|
|
40
40
|
}
|
|
41
41
|
}
|
|
42
|
-
function
|
|
43
|
-
|
|
44
|
-
|
|
42
|
+
function replaceText(item, comment, isNullable) {
|
|
43
|
+
let to;
|
|
44
|
+
if (isNullable) to = [comment];
|
|
45
|
+
else to = item.text == null ? [] : [item.text];
|
|
46
|
+
replaceNodes(item.nodes ?? [], to);
|
|
47
|
+
}
|
|
48
|
+
function setArray(item, comment, value) {
|
|
49
|
+
if (value.length === 0) return { nodes: setText(item, comment, value) };
|
|
50
|
+
let templates = value.filter((item$1) => isFragment(item$1) && item$1.identifier != null);
|
|
45
51
|
const next = templates.map((fragment) => fragment.identifier);
|
|
46
|
-
const previous =
|
|
52
|
+
const previous = item.fragments?.map((fragment) => fragment.identifier) ?? [];
|
|
47
53
|
if (new Set(next).size !== templates.length) templates = [];
|
|
48
54
|
const noTemplates = templates.length === 0;
|
|
49
|
-
if (noTemplates ||
|
|
55
|
+
if (noTemplates || item.nodes == null || previous.some((identifier) => identifier == null)) return {
|
|
50
56
|
fragments: noTemplates ? void 0 : templates,
|
|
51
|
-
nodes: setNodes(
|
|
57
|
+
nodes: setNodes(item, comment, noTemplates ? value.flatMap((item$1) => createNodes(item$1)) : templates.flatMap((template) => template.get()))
|
|
52
58
|
};
|
|
53
59
|
return handleArray({
|
|
54
60
|
next,
|
|
55
61
|
previous
|
|
56
62
|
}, {
|
|
57
63
|
templates,
|
|
58
|
-
fragments:
|
|
59
|
-
},
|
|
64
|
+
fragments: item.fragments ?? []
|
|
65
|
+
}, item.nodes);
|
|
60
66
|
}
|
|
61
|
-
function setNodes(
|
|
62
|
-
if (
|
|
67
|
+
function setNodes(item, comment, next) {
|
|
68
|
+
if (item.nodes == null) {
|
|
63
69
|
if (comment.parentNode != null) replaceNodes([comment], next);
|
|
64
|
-
else if (
|
|
65
|
-
} else replaceNodes(
|
|
66
|
-
removeFragments(
|
|
70
|
+
else if (item.text?.parentNode != null) replaceNodes([item.text], next);
|
|
71
|
+
} else replaceNodes(item.nodes, next);
|
|
72
|
+
removeFragments(item.fragments);
|
|
67
73
|
return next;
|
|
68
74
|
}
|
|
69
75
|
function setReactiveValue(data, comment, reactive) {
|
|
70
|
-
|
|
71
|
-
|
|
76
|
+
let item = data.items.find((item$1) => item$1.nodes?.includes(comment));
|
|
77
|
+
item ??= {};
|
|
78
|
+
item.text = new Text();
|
|
72
79
|
data.mora.subscribers.add(reactive.subscribe((value) => {
|
|
73
|
-
if (Array.isArray(value)) setReactiveValueForArray(
|
|
74
|
-
else setReactiveValueForSingle(
|
|
75
|
-
|
|
76
|
-
item.fragments = state.fragments;
|
|
77
|
-
item.nodes = [...state.nodes ?? [comment]];
|
|
78
|
-
}
|
|
80
|
+
if (Array.isArray(value)) setReactiveValueForArray(item, comment, value);
|
|
81
|
+
else setReactiveValueForSingle(item, comment, value);
|
|
82
|
+
item.nodes = [...item.nodes ?? [comment]];
|
|
79
83
|
}));
|
|
80
84
|
}
|
|
81
|
-
function setReactiveValueForArray(
|
|
82
|
-
const result = setArray(
|
|
83
|
-
|
|
84
|
-
if (typeof result === "boolean")
|
|
85
|
-
else
|
|
85
|
+
function setReactiveValueForArray(item, comment, value) {
|
|
86
|
+
const result = setArray(item, comment, value);
|
|
87
|
+
item.fragments = typeof result === "boolean" ? void 0 : result?.fragments;
|
|
88
|
+
if (typeof result === "boolean") if (result) item.nodes = item.text == null ? [] : [item.text];
|
|
89
|
+
else item.nodes = void 0;
|
|
90
|
+
else item.nodes = result?.nodes;
|
|
86
91
|
}
|
|
87
|
-
function setReactiveValueForSingle(
|
|
92
|
+
function setReactiveValueForSingle(item, comment, value) {
|
|
88
93
|
const valueIsFragment = isFragment(value);
|
|
89
|
-
|
|
90
|
-
if (valueIsFragment || isChildNode(value))
|
|
91
|
-
else
|
|
94
|
+
item.fragments = valueIsFragment ? [value] : void 0;
|
|
95
|
+
if (valueIsFragment || isChildNode(value)) item.nodes = setNodes(item, comment, createNodes(value));
|
|
96
|
+
else item.nodes = setText(item, comment, value);
|
|
92
97
|
}
|
|
93
|
-
function setText(
|
|
98
|
+
function setText(item, comment, value) {
|
|
94
99
|
const isNullable = isNullableOrWhitespace(value);
|
|
95
|
-
|
|
100
|
+
if (item.text != null) item.text.textContent = isNullable ? "" : getString(value);
|
|
96
101
|
let result = false;
|
|
97
|
-
if (
|
|
98
|
-
|
|
102
|
+
if (item.nodes != null) {
|
|
103
|
+
replaceText(item, comment, isNullable);
|
|
99
104
|
result = !isNullable;
|
|
100
|
-
} else if (isNullable && comment.parentNode == null)
|
|
101
|
-
else if (!isNullable &&
|
|
102
|
-
comment.replaceWith(
|
|
105
|
+
} else if (isNullable && comment.parentNode == null) item.text?.replaceWith(comment);
|
|
106
|
+
else if (!isNullable && item?.text?.parentNode == null) {
|
|
107
|
+
if (item.text != null) comment.replaceWith(item.text);
|
|
103
108
|
result = true;
|
|
104
109
|
}
|
|
105
|
-
removeFragments(
|
|
106
|
-
|
|
110
|
+
removeFragments(item.fragments);
|
|
111
|
+
if (result) return item.text == null ? [] : [item.text];
|
|
107
112
|
}
|
|
108
113
|
export { setReactiveValue };
|
package/dist/parse.js
CHANGED
|
@@ -10,9 +10,12 @@ function handleExpression(data, prefix, expression) {
|
|
|
10
10
|
return isNullableOrWhitespace(expression) ? prefix : `${prefix}${expression}`;
|
|
11
11
|
}
|
|
12
12
|
function parse(data) {
|
|
13
|
+
if (data.template != null) return data.template;
|
|
13
14
|
const { length } = data.strings;
|
|
14
|
-
|
|
15
|
-
for (let index = 0; index < length; index += 1) template += handleExpression(data, data.strings[index], data.expressions[index]);
|
|
16
|
-
|
|
15
|
+
data.template = "";
|
|
16
|
+
for (let index = 0; index < length; index += 1) data.template += handleExpression(data, data.strings[index], data.expressions[index]);
|
|
17
|
+
data.expressions = [];
|
|
18
|
+
data.strings = [];
|
|
19
|
+
return data.template;
|
|
17
20
|
}
|
|
18
21
|
export { parse };
|
package/package.json
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
"dependencies": {
|
|
7
7
|
"@oscarpalmer/atoms": "^0.114",
|
|
8
8
|
"@oscarpalmer/mora": "^0.23",
|
|
9
|
-
"@oscarpalmer/toretto": "^0.
|
|
9
|
+
"@oscarpalmer/toretto": "^0.25"
|
|
10
10
|
},
|
|
11
11
|
"devDependencies": {
|
|
12
12
|
"@biomejs/biome": "^2.3",
|
|
@@ -58,5 +58,5 @@
|
|
|
58
58
|
},
|
|
59
59
|
"type": "module",
|
|
60
60
|
"types": "types/index.d.ts",
|
|
61
|
-
"version": "0.
|
|
61
|
+
"version": "0.15.0"
|
|
62
62
|
}
|
package/src/fragment.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
|
+
import {isPlainObject} from '@oscarpalmer/atoms/is';
|
|
1
2
|
import {html} from '@oscarpalmer/toretto/html';
|
|
3
|
+
import {handleFragments} from './fragments';
|
|
4
|
+
import {isFragments} from './helpers';
|
|
2
5
|
import {removeNodes} from './helpers/dom';
|
|
3
6
|
import type {FragmentConfiguration, FragmentData} from './models';
|
|
4
7
|
import {mapNodes} from './node';
|
|
5
8
|
import {parse} from './parse';
|
|
6
|
-
import { isPlainObject } from '@oscarpalmer/atoms';
|
|
7
9
|
|
|
8
10
|
export class Fragment {
|
|
9
11
|
readonly #data: FragmentData;
|
|
@@ -11,13 +13,20 @@ export class Fragment {
|
|
|
11
13
|
readonly #configuration: Required<FragmentConfiguration> = {
|
|
12
14
|
identifier: undefined,
|
|
13
15
|
ignoreCache: false,
|
|
14
|
-
}
|
|
16
|
+
};
|
|
15
17
|
|
|
18
|
+
/**
|
|
19
|
+
* Fragment identifier
|
|
20
|
+
*/
|
|
16
21
|
get identifier(): unknown {
|
|
17
22
|
return this.#configuration.identifier;
|
|
18
23
|
}
|
|
19
24
|
|
|
20
25
|
constructor(strings: TemplateStringsArray, expressions: unknown[]) {
|
|
26
|
+
Object.defineProperty(this, '$fragment', {
|
|
27
|
+
value: true,
|
|
28
|
+
});
|
|
29
|
+
|
|
21
30
|
this.#data = {
|
|
22
31
|
expressions,
|
|
23
32
|
strings,
|
|
@@ -28,19 +37,21 @@ export class Fragment {
|
|
|
28
37
|
},
|
|
29
38
|
values: [],
|
|
30
39
|
};
|
|
31
|
-
|
|
32
|
-
Object.defineProperty(this, '$fragment', {
|
|
33
|
-
value: true,
|
|
34
|
-
});
|
|
35
40
|
}
|
|
36
41
|
|
|
37
42
|
/**
|
|
38
|
-
*
|
|
43
|
+
* Append the fragment to the given element
|
|
44
|
+
* @param element Element to append to
|
|
39
45
|
*/
|
|
40
46
|
appendTo(element: Element): void {
|
|
41
47
|
element.append(...this.get());
|
|
42
48
|
}
|
|
43
49
|
|
|
50
|
+
/**
|
|
51
|
+
* Configure the fragment
|
|
52
|
+
* @param configuration Configuration options
|
|
53
|
+
* @returns Fragment
|
|
54
|
+
*/
|
|
44
55
|
configure(configuration: FragmentConfiguration): Fragment {
|
|
45
56
|
const actual = isPlainObject(configuration) ? configuration : {};
|
|
46
57
|
|
|
@@ -56,7 +67,8 @@ export class Fragment {
|
|
|
56
67
|
}
|
|
57
68
|
|
|
58
69
|
/**
|
|
59
|
-
*
|
|
70
|
+
* Get a list of the fragment's nodes
|
|
71
|
+
* @returns List of nodes
|
|
60
72
|
*/
|
|
61
73
|
get(): ChildNode[] {
|
|
62
74
|
const data = this.#data;
|
|
@@ -65,13 +77,10 @@ export class Fragment {
|
|
|
65
77
|
const parsed = parse(data);
|
|
66
78
|
|
|
67
79
|
const templated = html(parsed, {
|
|
80
|
+
ignoreCache: this.#configuration.ignoreCache,
|
|
68
81
|
sanitizeBooleanAttributes: false,
|
|
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,
|
|
@@ -84,7 +93,9 @@ export class Fragment {
|
|
|
84
93
|
data,
|
|
85
94
|
data.items.flatMap(
|
|
86
95
|
item =>
|
|
87
|
-
item.fragments?.flatMap(fragment => fragment.get()) ??
|
|
96
|
+
item.fragments?.flatMap(fragment => fragment.get()) ??
|
|
97
|
+
item.nodes ??
|
|
98
|
+
[],
|
|
88
99
|
),
|
|
89
100
|
);
|
|
90
101
|
}
|
|
@@ -92,11 +103,21 @@ export class Fragment {
|
|
|
92
103
|
return [
|
|
93
104
|
...data.items.flatMap(
|
|
94
105
|
item =>
|
|
95
|
-
item.fragments?.flatMap(fragment => fragment.get()) ??
|
|
106
|
+
item.fragments?.flatMap(fragment => fragment.get()) ??
|
|
107
|
+
item.nodes ??
|
|
108
|
+
[],
|
|
96
109
|
),
|
|
97
110
|
];
|
|
98
111
|
}
|
|
99
112
|
|
|
113
|
+
/**
|
|
114
|
+
* Set an identifier for the fragment
|
|
115
|
+
*
|
|
116
|
+
* _An identifier can be used to uniquely identify a fragment,
|
|
117
|
+
* which helps prevent re-rendering in certain scenarios._
|
|
118
|
+
* @param identifier Identifier
|
|
119
|
+
* @returns Fragment
|
|
120
|
+
*/
|
|
100
121
|
identify(identifier: unknown): Fragment {
|
|
101
122
|
this.#configuration.identifier = identifier;
|
|
102
123
|
|
|
@@ -104,7 +125,7 @@ export class Fragment {
|
|
|
104
125
|
}
|
|
105
126
|
|
|
106
127
|
/**
|
|
107
|
-
*
|
|
128
|
+
* Remove the fragment from the DOM
|
|
108
129
|
*/
|
|
109
130
|
remove(): void {
|
|
110
131
|
removeFragment(this.#data);
|
|
@@ -114,20 +135,34 @@ export class Fragment {
|
|
|
114
135
|
function removeFragment(data: FragmentData): void {
|
|
115
136
|
removeMora(data);
|
|
116
137
|
|
|
117
|
-
|
|
138
|
+
let {length} = data.items;
|
|
118
139
|
|
|
119
140
|
for (let index = 0; index < length; index += 1) {
|
|
120
141
|
const {fragments, nodes} = data.items[index];
|
|
121
|
-
const
|
|
122
|
-
|
|
123
|
-
for (
|
|
124
|
-
|
|
142
|
+
const fragmentsLength = fragments?.length ?? 0;
|
|
143
|
+
|
|
144
|
+
for (
|
|
145
|
+
let fragmentIndex = 0;
|
|
146
|
+
fragmentIndex < fragmentsLength;
|
|
147
|
+
fragmentIndex += 1
|
|
148
|
+
) {
|
|
149
|
+
fragments?.[fragmentIndex]?.remove();
|
|
125
150
|
}
|
|
126
151
|
|
|
127
152
|
removeNodes(nodes ?? []);
|
|
128
153
|
}
|
|
129
154
|
|
|
130
155
|
data.items.length = 0;
|
|
156
|
+
|
|
157
|
+
length = data.values.length;
|
|
158
|
+
|
|
159
|
+
for (let index = 0; index < length; index += 1) {
|
|
160
|
+
const value = data.values[index];
|
|
161
|
+
|
|
162
|
+
if (isFragments(value)) {
|
|
163
|
+
handleFragments(value, true);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
131
166
|
}
|
|
132
167
|
|
|
133
168
|
function removeMora(data: FragmentData): void {
|
package/src/fragments.ts
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import {getString} from '@oscarpalmer/atoms/string';
|
|
2
|
+
import {array, type ReactiveArray} from '@oscarpalmer/mora';
|
|
3
|
+
import type {Fragment} from './fragment';
|
|
4
|
+
import {isFragment, isFragments} from './helpers';
|
|
5
|
+
import type {FragmentsState} from './models';
|
|
6
|
+
|
|
7
|
+
export class Fragments {
|
|
8
|
+
readonly #state: FragmentsState;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Fragment items
|
|
12
|
+
*/
|
|
13
|
+
get items(): ReactiveArray<Fragment> {
|
|
14
|
+
return this.#state.mapped;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
constructor(
|
|
18
|
+
items: ReactiveArray<unknown>,
|
|
19
|
+
identify: (item: unknown) => unknown,
|
|
20
|
+
fragment: (item: unknown) => Fragment,
|
|
21
|
+
) {
|
|
22
|
+
Object.defineProperty(this, '$fragments', {
|
|
23
|
+
value: true,
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
this.#state = {
|
|
27
|
+
fragment,
|
|
28
|
+
identify,
|
|
29
|
+
array: items,
|
|
30
|
+
instances: {},
|
|
31
|
+
mapped: array<Fragment>([]),
|
|
32
|
+
subscriber: undefined,
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
states.set(this, this.#state);
|
|
36
|
+
|
|
37
|
+
initializeFragments(this.#state);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function handleFragments(
|
|
42
|
+
item: Fragments | FragmentsState,
|
|
43
|
+
remove: boolean,
|
|
44
|
+
): void {
|
|
45
|
+
const state = isFragments(item) ? states.get(item) : item;
|
|
46
|
+
|
|
47
|
+
if (state != null) {
|
|
48
|
+
(remove ? removeFragments : initializeFragments)(state);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function handleItems(state: FragmentsState, items: unknown[]): void {
|
|
53
|
+
const keys = new Set<string>();
|
|
54
|
+
const mapped: Fragment[] = [];
|
|
55
|
+
|
|
56
|
+
const {length} = items;
|
|
57
|
+
|
|
58
|
+
for (let index = 0; index < length; index += 1) {
|
|
59
|
+
const item = items[index];
|
|
60
|
+
const identifier = state.identify(item);
|
|
61
|
+
|
|
62
|
+
if (identifier == null) {
|
|
63
|
+
throw new Error('Identifier cannot be null or undefined');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const key = getString(identifier);
|
|
67
|
+
|
|
68
|
+
if (keys.has(key)) {
|
|
69
|
+
throw new Error(`Duplicate identifier found: "${key}"`);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
let instance = state.instances[key];
|
|
73
|
+
|
|
74
|
+
if (instance == null) {
|
|
75
|
+
instance = state.fragment(item);
|
|
76
|
+
|
|
77
|
+
if (!isFragment(instance)) {
|
|
78
|
+
throw new Error('Fragment function must return a Fragment instance');
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
instance.identify(key);
|
|
83
|
+
|
|
84
|
+
state.instances[key] = instance;
|
|
85
|
+
|
|
86
|
+
keys.add(key);
|
|
87
|
+
|
|
88
|
+
mapped.push(instance);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
state.mapped.set(mapped);
|
|
92
|
+
|
|
93
|
+
updateFragments(state, keys);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function initializeFragments(state: FragmentsState): void {
|
|
97
|
+
state.subscriber ??= state.array.subscribe(items => {
|
|
98
|
+
handleItems(state, items);
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function removeFragments(state: FragmentsState): void {
|
|
103
|
+
state.subscriber?.();
|
|
104
|
+
|
|
105
|
+
updateFragments(state);
|
|
106
|
+
|
|
107
|
+
state.subscriber = undefined;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function updateFragments(state: FragmentsState, active?: Set<string>): void {
|
|
111
|
+
const next: Record<string, Fragment> = {};
|
|
112
|
+
|
|
113
|
+
const previous = {...state.instances};
|
|
114
|
+
const keys = Object.keys(previous);
|
|
115
|
+
const {length} = keys;
|
|
116
|
+
|
|
117
|
+
for (let index = 0; index < length; index += 1) {
|
|
118
|
+
const key = keys[index];
|
|
119
|
+
|
|
120
|
+
if (active?.has(key)) {
|
|
121
|
+
next[key] = previous[key];
|
|
122
|
+
} else {
|
|
123
|
+
previous[key].remove();
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
state.instances = next;
|
|
128
|
+
|
|
129
|
+
active?.clear();
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
//
|
|
133
|
+
|
|
134
|
+
const states: WeakMap<Fragments, FragmentsState> = new WeakMap();
|
package/src/helpers/index.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type {Fragment} from '../fragment';
|
|
2
|
+
import type {Fragments} from '../fragments';
|
|
2
3
|
|
|
3
4
|
export function compareArrays(
|
|
4
5
|
first: unknown[],
|
|
@@ -27,3 +28,12 @@ export function isFragment(value: unknown): value is Fragment {
|
|
|
27
28
|
value.$fragment === true
|
|
28
29
|
);
|
|
29
30
|
}
|
|
31
|
+
|
|
32
|
+
export function isFragments(value: unknown): value is Fragments {
|
|
33
|
+
return (
|
|
34
|
+
typeof value === 'object' &&
|
|
35
|
+
value != null &&
|
|
36
|
+
'$fragments' in value &&
|
|
37
|
+
value.$fragments === true
|
|
38
|
+
);
|
|
39
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,13 +1,27 @@
|
|
|
1
1
|
import '@oscarpalmer/mora';
|
|
2
|
-
import {
|
|
2
|
+
import type {ReactiveArray} from '@oscarpalmer/mora';
|
|
3
|
+
import {Fragment} from './fragment';
|
|
4
|
+
import {Fragments} from './fragments';
|
|
5
|
+
|
|
6
|
+
export function fragments<Item>(
|
|
7
|
+
array: ReactiveArray<Item>,
|
|
8
|
+
identify: (item: Item) => unknown,
|
|
9
|
+
fragment: (item: Item) => Fragment,
|
|
10
|
+
): Fragments {
|
|
11
|
+
return new Fragments(
|
|
12
|
+
array as ReactiveArray<unknown>,
|
|
13
|
+
identify as never,
|
|
14
|
+
fragment as never,
|
|
15
|
+
);
|
|
16
|
+
}
|
|
3
17
|
|
|
4
18
|
export function html(
|
|
5
19
|
strings: TemplateStringsArray,
|
|
6
20
|
...values: unknown[]
|
|
7
|
-
):
|
|
21
|
+
): unknown {
|
|
8
22
|
return new Fragment(strings, values);
|
|
9
23
|
}
|
|
10
24
|
|
|
11
25
|
export * from '@oscarpalmer/mora';
|
|
12
|
-
export type {
|
|
13
|
-
|
|
26
|
+
export type {Fragment} from './fragment';
|
|
27
|
+
export type {Fragments} from './fragments';
|
package/src/models.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
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 = {
|
|
@@ -21,6 +21,15 @@ export type FragmentItem = {
|
|
|
21
21
|
text?: Text;
|
|
22
22
|
};
|
|
23
23
|
|
|
24
|
+
export type FragmentsState = {
|
|
25
|
+
array: ReactiveArray<unknown>;
|
|
26
|
+
fragment: (item: unknown) => Fragment;
|
|
27
|
+
identify: (item: unknown) => unknown;
|
|
28
|
+
instances: Record<string, Fragment>;
|
|
29
|
+
mapped: ReactiveArray<Fragment>;
|
|
30
|
+
subscriber: Unsubscribe | undefined;
|
|
31
|
+
};
|
|
32
|
+
|
|
24
33
|
type MoraData = {
|
|
25
34
|
subscribers: Set<() => void>;
|
|
26
35
|
values: Set<Reactive<unknown>>;
|
package/src/node/index.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import type {GenericCallback} from '@oscarpalmer/atoms/models';
|
|
2
|
-
import {computed, isReactive} from '@oscarpalmer/mora';
|
|
2
|
+
import {computed, isReactive, Reactive} from '@oscarpalmer/mora';
|
|
3
3
|
import {isHTMLOrSVGElement} from '@oscarpalmer/toretto/is';
|
|
4
4
|
import {EXPRESSION_COMMENT_CONTENT} from '../constants';
|
|
5
|
-
import {
|
|
5
|
+
import {handleFragments} from '../fragments';
|
|
6
|
+
import {isFragment, isFragments} from '../helpers';
|
|
6
7
|
import {createNodes} from '../helpers/dom';
|
|
7
8
|
import type {FragmentData} from '../models';
|
|
8
9
|
import {mapAttributes} from './attribute';
|
|
@@ -45,6 +46,15 @@ function mapValue(data: FragmentData, comment: Comment, value: unknown): void {
|
|
|
45
46
|
setComputedValue(data, comment, value as GenericCallback);
|
|
46
47
|
break;
|
|
47
48
|
|
|
49
|
+
case isFragments(value):
|
|
50
|
+
handleFragments(value, false);
|
|
51
|
+
setReactiveValue(
|
|
52
|
+
data,
|
|
53
|
+
comment,
|
|
54
|
+
value.items as Reactive<unknown[], unknown>,
|
|
55
|
+
);
|
|
56
|
+
break;
|
|
57
|
+
|
|
48
58
|
case isReactive(value):
|
|
49
59
|
setReactiveValue(data, comment, value);
|
|
50
60
|
break;
|