@joist/templating 4.2.4-next.9 → 4.2.4
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/LICENSE +2 -2
- package/package.json +1 -1
- package/src/lib/define.ts +26 -6
- package/src/lib/elements/async.element.test.ts +37 -1
- package/src/lib/elements/async.element.ts +12 -8
- package/src/lib/elements/bind.element.test.ts +60 -5
- package/src/lib/elements/bind.element.ts +30 -19
- package/src/lib/elements/for.element.test.ts +54 -9
- package/src/lib/elements/for.element.ts +91 -41
- package/src/lib/elements/if.element.test.ts +314 -260
- package/src/lib/elements/if.element.ts +40 -17
- package/src/lib/elements/scope.element.test.ts +1 -2
- package/src/lib/elements/scope.element.ts +0 -7
- package/src/lib/elements/value.element.test.ts +33 -1
- package/src/lib/elements/value.element.ts +13 -8
- package/src/lib/events.ts +1 -1
- package/target/lib/define.d.ts +16 -6
- package/target/lib/define.js +13 -6
- package/target/lib/define.js.map +1 -1
- package/target/lib/elements/async.element.d.ts +2 -6
- package/target/lib/elements/async.element.js +15 -3
- package/target/lib/elements/async.element.js.map +1 -1
- package/target/lib/elements/async.element.test.d.ts +1 -1
- package/target/lib/elements/async.element.test.js +30 -1
- package/target/lib/elements/async.element.test.js.map +1 -1
- package/target/lib/elements/bind.element.d.ts +2 -6
- package/target/lib/elements/bind.element.js +34 -16
- package/target/lib/elements/bind.element.js.map +1 -1
- package/target/lib/elements/bind.element.test.d.ts +1 -1
- package/target/lib/elements/bind.element.test.js +50 -5
- package/target/lib/elements/bind.element.test.js.map +1 -1
- package/target/lib/elements/for.element.d.ts +3 -12
- package/target/lib/elements/for.element.js +84 -57
- package/target/lib/elements/for.element.js.map +1 -1
- package/target/lib/elements/for.element.test.d.ts +1 -2
- package/target/lib/elements/for.element.test.js +41 -5
- package/target/lib/elements/for.element.test.js.map +1 -1
- package/target/lib/elements/if.element.d.ts +3 -6
- package/target/lib/elements/if.element.js +42 -10
- package/target/lib/elements/if.element.js.map +1 -1
- package/target/lib/elements/if.element.test.d.ts +1 -1
- package/target/lib/elements/if.element.test.js +273 -1
- package/target/lib/elements/if.element.test.js.map +1 -1
- package/target/lib/elements/scope.element.d.ts +0 -5
- package/target/lib/elements/scope.element.js +0 -1
- package/target/lib/elements/scope.element.js.map +1 -1
- package/target/lib/elements/scope.element.test.d.ts +1 -2
- package/target/lib/elements/scope.element.test.js +1 -2
- package/target/lib/elements/scope.element.test.js.map +1 -1
- package/target/lib/elements/value.element.d.ts +2 -6
- package/target/lib/elements/value.element.js +15 -3
- package/target/lib/elements/value.element.js.map +1 -1
- package/target/lib/elements/value.element.test.d.ts +1 -1
- package/target/lib/elements/value.element.test.js +26 -1
- package/target/lib/elements/value.element.test.js.map +1 -1
- package/target/lib/events.d.ts +1 -1
package/LICENSE
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
The MIT License
|
|
2
2
|
|
|
3
|
-
Copyright (c) 2019-
|
|
3
|
+
Copyright (c) 2019-2025 Danny Blue
|
|
4
4
|
|
|
5
5
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
6
|
of this software and associated documentation files (the "Software"), to deal
|
|
@@ -18,4 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
|
18
18
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
19
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
20
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
-
THE SOFTWARE.
|
|
21
|
+
THE SOFTWARE.
|
package/package.json
CHANGED
package/src/lib/define.ts
CHANGED
|
@@ -1,6 +1,26 @@
|
|
|
1
|
-
import "
|
|
2
|
-
|
|
3
|
-
import "./elements/
|
|
4
|
-
import "./elements/
|
|
5
|
-
import "./elements/
|
|
6
|
-
import "./elements/
|
|
1
|
+
import { define } from "@joist/element/define.js";
|
|
2
|
+
|
|
3
|
+
import { JoistAsyncElement } from "./elements/async.element.js";
|
|
4
|
+
import { JoistForElement } from "./elements/for.element.js";
|
|
5
|
+
import { JoistIfElement } from "./elements/if.element.js";
|
|
6
|
+
import { JoistBindElement } from "./elements/bind.element.js";
|
|
7
|
+
import { JoistValueElement } from "./elements/value.element.js";
|
|
8
|
+
import { JoistScopeElement } from "./elements/scope.element.js";
|
|
9
|
+
|
|
10
|
+
declare global {
|
|
11
|
+
interface HTMLElementTagNameMap {
|
|
12
|
+
"j-async": JoistAsyncElement;
|
|
13
|
+
"j-for": JoistForElement;
|
|
14
|
+
"j-if": JoistIfElement;
|
|
15
|
+
"j-bind": JoistBindElement;
|
|
16
|
+
"j-val": JoistValueElement;
|
|
17
|
+
"j-scope": JoistScopeElement;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
define({ tagName: "j-async" }, JoistAsyncElement);
|
|
22
|
+
define({ tagName: "j-for" }, JoistForElement);
|
|
23
|
+
define({ tagName: "j-if" }, JoistIfElement);
|
|
24
|
+
define({ tagName: "j-bind" }, JoistBindElement);
|
|
25
|
+
define({ tagName: "j-val" }, JoistValueElement);
|
|
26
|
+
define({ tagName: "j-scope" }, JoistScopeElement);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import "
|
|
1
|
+
import "../define.js";
|
|
2
2
|
|
|
3
3
|
import { fixtureSync, html } from "@open-wc/testing";
|
|
4
4
|
import { assert } from "chai";
|
|
@@ -178,3 +178,39 @@ it("should handle AsyncState transitions", () => {
|
|
|
178
178
|
}, 150);
|
|
179
179
|
});
|
|
180
180
|
});
|
|
181
|
+
|
|
182
|
+
it("should wait for depends-on before dispatching events", async () => {
|
|
183
|
+
let eventDispatched = false;
|
|
184
|
+
|
|
185
|
+
customElements.define("dependency-1", class extends HTMLElement {});
|
|
186
|
+
customElements.define("dependency-2", class extends HTMLElement {});
|
|
187
|
+
|
|
188
|
+
fixtureSync(html`
|
|
189
|
+
<div
|
|
190
|
+
@joist::value=${(e: JoistValueEvent) => {
|
|
191
|
+
if (e.expression.bindTo === "test") {
|
|
192
|
+
eventDispatched = true;
|
|
193
|
+
e.update({ oldValue: null, newValue: Promise.resolve("data") });
|
|
194
|
+
}
|
|
195
|
+
}}
|
|
196
|
+
>
|
|
197
|
+
<j-async bind="test" depends-on="dependency-1,dependency-2">
|
|
198
|
+
<template loading>Loading...</template>
|
|
199
|
+
<template success>Success!</template>
|
|
200
|
+
<template error>Error!</template>
|
|
201
|
+
</j-async>
|
|
202
|
+
</div>
|
|
203
|
+
`);
|
|
204
|
+
|
|
205
|
+
// Initially, no event should be dispatched
|
|
206
|
+
assert.isFalse(eventDispatched);
|
|
207
|
+
|
|
208
|
+
// Wait for the custom elements to be defined
|
|
209
|
+
await Promise.all([
|
|
210
|
+
customElements.whenDefined("dependency-1"),
|
|
211
|
+
customElements.whenDefined("dependency-2"),
|
|
212
|
+
]);
|
|
213
|
+
|
|
214
|
+
// Now the event should be dispatched
|
|
215
|
+
assert.isTrue(eventDispatched);
|
|
216
|
+
});
|
|
@@ -4,12 +4,6 @@ import { bind } from "../bind.js";
|
|
|
4
4
|
import { JoistValueEvent } from "../events.js";
|
|
5
5
|
import { JExpression } from "../expression.js";
|
|
6
6
|
|
|
7
|
-
declare global {
|
|
8
|
-
interface HTMLElementTagNameMap {
|
|
9
|
-
"j-async": JoistAsyncElement;
|
|
10
|
-
}
|
|
11
|
-
}
|
|
12
|
-
|
|
13
7
|
export type AsyncState<T = unknown, E = unknown> = {
|
|
14
8
|
status: "loading" | "error" | "success";
|
|
15
9
|
data?: T;
|
|
@@ -17,7 +11,6 @@ export type AsyncState<T = unknown, E = unknown> = {
|
|
|
17
11
|
};
|
|
18
12
|
|
|
19
13
|
@element({
|
|
20
|
-
tagName: "j-async",
|
|
21
14
|
// prettier-ignore
|
|
22
15
|
shadowDom: [css`:host{display: contents;}`, html`<slot></slot>`],
|
|
23
16
|
})
|
|
@@ -25,6 +18,11 @@ export class JoistAsyncElement extends HTMLElement {
|
|
|
25
18
|
@attr()
|
|
26
19
|
accessor bind = "";
|
|
27
20
|
|
|
21
|
+
@attr({
|
|
22
|
+
name: "depends-on",
|
|
23
|
+
})
|
|
24
|
+
accessor dependsOn = "";
|
|
25
|
+
|
|
28
26
|
@bind()
|
|
29
27
|
accessor state: AsyncState | null = null;
|
|
30
28
|
|
|
@@ -40,7 +38,7 @@ export class JoistAsyncElement extends HTMLElement {
|
|
|
40
38
|
success: undefined,
|
|
41
39
|
};
|
|
42
40
|
|
|
43
|
-
connectedCallback(): void {
|
|
41
|
+
async connectedCallback(): Promise<void> {
|
|
44
42
|
this.#clean();
|
|
45
43
|
|
|
46
44
|
// Cache all templates
|
|
@@ -52,6 +50,12 @@ export class JoistAsyncElement extends HTMLElement {
|
|
|
52
50
|
success: templates.find((t) => t.hasAttribute("success")),
|
|
53
51
|
};
|
|
54
52
|
|
|
53
|
+
if (this.dependsOn) {
|
|
54
|
+
await Promise.all(
|
|
55
|
+
this.dependsOn.split(",").map((tag) => window.customElements.whenDefined(tag)),
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
55
59
|
const token = new JExpression(this.bind);
|
|
56
60
|
|
|
57
61
|
this.dispatchEvent(
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import "
|
|
1
|
+
import "../define.js";
|
|
2
2
|
|
|
3
3
|
import { fixtureSync, html } from "@open-wc/testing";
|
|
4
4
|
import { assert } from "chai";
|
|
@@ -48,10 +48,10 @@ it("should pass props to specified child", () => {
|
|
|
48
48
|
});
|
|
49
49
|
}}
|
|
50
50
|
>
|
|
51
|
-
<j-bind attrs="href:href" target="#test">
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
</
|
|
51
|
+
<j-bind attrs="href:href" target="#test"></j-bind>
|
|
52
|
+
|
|
53
|
+
<a>Default</a>
|
|
54
|
+
<a id="test">Target</a>
|
|
55
55
|
</div>
|
|
56
56
|
`);
|
|
57
57
|
|
|
@@ -103,3 +103,58 @@ it("should default to the mapTo value if bindTo is not provided", () => {
|
|
|
103
103
|
assert.equal(input?.selectionStart, 8);
|
|
104
104
|
assert.equal(input?.selectionEnd, 8);
|
|
105
105
|
});
|
|
106
|
+
|
|
107
|
+
it("should write not update if the calculated value is the same as the old value", () => {
|
|
108
|
+
const element = fixtureSync(html`
|
|
109
|
+
<div
|
|
110
|
+
@joist::value=${(e: JoistValueEvent) => {
|
|
111
|
+
e.update({ oldValue: { foo: "bar" }, newValue: { foo: "bar" } });
|
|
112
|
+
}}
|
|
113
|
+
>
|
|
114
|
+
<j-bind props="value:data.foo">
|
|
115
|
+
<input />
|
|
116
|
+
</j-bind>
|
|
117
|
+
</div>
|
|
118
|
+
`);
|
|
119
|
+
|
|
120
|
+
const input = element.querySelector("input");
|
|
121
|
+
|
|
122
|
+
assert.equal(input?.value, "");
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it("should wait for depends-on before dispatching events", async () => {
|
|
126
|
+
let eventDispatched = false;
|
|
127
|
+
|
|
128
|
+
customElements.define("dependency-1", class extends HTMLElement {});
|
|
129
|
+
customElements.define("dependency-2", class extends HTMLElement {});
|
|
130
|
+
|
|
131
|
+
fixtureSync(html`
|
|
132
|
+
<div
|
|
133
|
+
@joist::value=${(e: JoistValueEvent) => {
|
|
134
|
+
if (e.expression.bindTo === "href") {
|
|
135
|
+
eventDispatched = true;
|
|
136
|
+
e.update({
|
|
137
|
+
oldValue: null,
|
|
138
|
+
newValue: "$foo",
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
}}
|
|
142
|
+
>
|
|
143
|
+
<j-bind attrs="href:href" depends-on="dependency-1,dependency-2">
|
|
144
|
+
<a>Hello World</a>
|
|
145
|
+
</j-bind>
|
|
146
|
+
</div>
|
|
147
|
+
`);
|
|
148
|
+
|
|
149
|
+
// Initially, no event should be dispatched
|
|
150
|
+
assert.isFalse(eventDispatched);
|
|
151
|
+
|
|
152
|
+
// Wait for the custom elements to be defined
|
|
153
|
+
await Promise.all([
|
|
154
|
+
customElements.whenDefined("dependency-1"),
|
|
155
|
+
customElements.whenDefined("dependency-2"),
|
|
156
|
+
]);
|
|
157
|
+
|
|
158
|
+
// Now the event should be dispatched
|
|
159
|
+
assert.isTrue(eventDispatched);
|
|
160
|
+
});
|
|
@@ -3,12 +3,6 @@ import { attr, element, css, html } from "@joist/element";
|
|
|
3
3
|
import { JExpression } from "../expression.js";
|
|
4
4
|
import { JoistValueEvent } from "../events.js";
|
|
5
5
|
|
|
6
|
-
declare global {
|
|
7
|
-
interface HTMLElementTagNameMap {
|
|
8
|
-
"j-bind": JoistBindElement;
|
|
9
|
-
}
|
|
10
|
-
}
|
|
11
|
-
|
|
12
6
|
export class JAttrToken extends JExpression {
|
|
13
7
|
mapTo: string;
|
|
14
8
|
|
|
@@ -22,7 +16,6 @@ export class JAttrToken extends JExpression {
|
|
|
22
16
|
}
|
|
23
17
|
|
|
24
18
|
@element({
|
|
25
|
-
tagName: "j-bind",
|
|
26
19
|
// prettier-ignore
|
|
27
20
|
shadowDom: [css`:host{display: contents;}`, html`<slot></slot>`],
|
|
28
21
|
})
|
|
@@ -36,30 +29,41 @@ export class JoistBindElement extends HTMLElement {
|
|
|
36
29
|
@attr()
|
|
37
30
|
accessor target = "";
|
|
38
31
|
|
|
39
|
-
|
|
32
|
+
@attr({
|
|
33
|
+
name: "depends-on",
|
|
34
|
+
})
|
|
35
|
+
accessor dependsOn = "";
|
|
36
|
+
|
|
37
|
+
async connectedCallback(): Promise<void> {
|
|
40
38
|
const attrBindings = this.#parseBinding(this.attrs);
|
|
41
39
|
const propBindings = this.#parseBinding(this.props);
|
|
42
40
|
|
|
43
|
-
let
|
|
41
|
+
let children: Iterable<Element> = this.children;
|
|
42
|
+
|
|
43
|
+
const root = this.getRootNode() as Document | ShadowRoot;
|
|
44
44
|
|
|
45
45
|
if (this.target) {
|
|
46
|
-
|
|
46
|
+
children = root.querySelectorAll(this.target);
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
if (
|
|
50
|
-
|
|
49
|
+
if (this.dependsOn) {
|
|
50
|
+
await Promise.all(
|
|
51
|
+
this.dependsOn.split(",").map((tag) => window.customElements.whenDefined(tag)),
|
|
52
|
+
);
|
|
51
53
|
}
|
|
52
54
|
|
|
53
55
|
for (const attrValue of attrBindings) {
|
|
54
56
|
const token = new JAttrToken(attrValue);
|
|
55
57
|
|
|
56
58
|
this.#dispatch(token, (value) => {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
59
|
+
for (const child of children) {
|
|
60
|
+
if (value === true) {
|
|
61
|
+
child.setAttribute(token.mapTo, "");
|
|
62
|
+
} else if (value === false) {
|
|
63
|
+
child.removeAttribute(token.mapTo);
|
|
64
|
+
} else {
|
|
65
|
+
child.setAttribute(token.mapTo, String(value));
|
|
66
|
+
}
|
|
63
67
|
}
|
|
64
68
|
});
|
|
65
69
|
}
|
|
@@ -68,7 +72,9 @@ export class JoistBindElement extends HTMLElement {
|
|
|
68
72
|
const token = new JAttrToken(propValue);
|
|
69
73
|
|
|
70
74
|
this.#dispatch(token, (value) => {
|
|
71
|
-
|
|
75
|
+
for (const child of children) {
|
|
76
|
+
Reflect.set(child, token.mapTo, value);
|
|
77
|
+
}
|
|
72
78
|
});
|
|
73
79
|
}
|
|
74
80
|
}
|
|
@@ -88,6 +94,11 @@ export class JoistBindElement extends HTMLElement {
|
|
|
88
94
|
}
|
|
89
95
|
|
|
90
96
|
let valueToWrite = token.evaluate(newValue);
|
|
97
|
+
let oldWrittenValue = token.evaluate(oldValue);
|
|
98
|
+
|
|
99
|
+
if (oldWrittenValue === valueToWrite) {
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
91
102
|
|
|
92
103
|
if (token.isNegated) {
|
|
93
104
|
valueToWrite = !valueToWrite;
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import "
|
|
2
|
-
import "./value.element.js";
|
|
1
|
+
import "../define.js";
|
|
3
2
|
|
|
4
3
|
import { fixtureSync, html } from "@open-wc/testing";
|
|
5
4
|
import { assert } from "chai";
|
|
@@ -118,14 +117,17 @@ it("should provide index and position information", () => {
|
|
|
118
117
|
>
|
|
119
118
|
<j-for bind="items">
|
|
120
119
|
<template>
|
|
121
|
-
<
|
|
122
|
-
|
|
120
|
+
<div class="item">
|
|
121
|
+
<j-val bind="each.value"></j-val>
|
|
122
|
+
(index: <j-val bind="each.index"></j-val>, position:
|
|
123
|
+
<j-val bind="each.position"></j-val>)
|
|
124
|
+
</div>
|
|
123
125
|
</template>
|
|
124
126
|
</j-for>
|
|
125
127
|
</div>
|
|
126
128
|
`);
|
|
127
129
|
|
|
128
|
-
const items = element.querySelectorAll("
|
|
130
|
+
const items = element.querySelectorAll(".item");
|
|
129
131
|
assert.equal(items.length, 3);
|
|
130
132
|
assert.equal(
|
|
131
133
|
items[0].textContent?.trim().replaceAll("\n", "").replaceAll(" ", ""),
|
|
@@ -171,12 +173,14 @@ it("should provide index and position information", () => {
|
|
|
171
173
|
// const groups = element.querySelectorAll(".group");
|
|
172
174
|
// assert.equal(groups.length, 2);
|
|
173
175
|
|
|
176
|
+
// console.log(groups);
|
|
177
|
+
|
|
174
178
|
// const items = element.querySelectorAll(".child");
|
|
175
179
|
// assert.equal(items.length, 4);
|
|
176
|
-
// assert.equal(items[0].textContent?.trim(), "A");
|
|
177
|
-
// assert.equal(items[1].textContent?.trim(), "B");
|
|
178
|
-
// assert.equal(items[2].textContent?.trim(), "C");
|
|
179
|
-
// assert.equal(items[3].textContent?.trim(), "D");
|
|
180
|
+
// // assert.equal(items[0].textContent?.trim(), "A");
|
|
181
|
+
// // assert.equal(items[1].textContent?.trim(), "B");
|
|
182
|
+
// // assert.equal(items[2].textContent?.trim(), "C");
|
|
183
|
+
// // assert.equal(items[3].textContent?.trim(), "D");
|
|
180
184
|
// });
|
|
181
185
|
|
|
182
186
|
it("should maintain DOM order when items are reordered", () => {
|
|
@@ -218,3 +222,44 @@ it("should maintain DOM order when items are reordered", () => {
|
|
|
218
222
|
assert.equal(items[1].textContent?.trim(), "First");
|
|
219
223
|
assert.equal(items[2].textContent?.trim(), "Second");
|
|
220
224
|
});
|
|
225
|
+
|
|
226
|
+
it("should wait for depends-on before dispatching events", async () => {
|
|
227
|
+
let eventDispatched = false;
|
|
228
|
+
|
|
229
|
+
customElements.define("dependency-1", class extends HTMLElement {});
|
|
230
|
+
customElements.define("dependency-2", class extends HTMLElement {});
|
|
231
|
+
|
|
232
|
+
fixtureSync(html`
|
|
233
|
+
<div
|
|
234
|
+
@joist::value=${(e: JoistValueEvent) => {
|
|
235
|
+
if (e.expression.bindTo === "items") {
|
|
236
|
+
eventDispatched = true;
|
|
237
|
+
e.update({
|
|
238
|
+
oldValue: null,
|
|
239
|
+
newValue: ["A", "B", "C"],
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
}}
|
|
243
|
+
>
|
|
244
|
+
<j-for bind="items" depends-on="dependency-1,dependency-2">
|
|
245
|
+
<template>
|
|
246
|
+
<div class="item">
|
|
247
|
+
<j-val bind="each.value"></j-val>
|
|
248
|
+
</div>
|
|
249
|
+
</template>
|
|
250
|
+
</j-for>
|
|
251
|
+
</div>
|
|
252
|
+
`);
|
|
253
|
+
|
|
254
|
+
// Initially, no event should be dispatched
|
|
255
|
+
assert.isFalse(eventDispatched);
|
|
256
|
+
|
|
257
|
+
// Wait for the custom elements to be defined
|
|
258
|
+
await Promise.all([
|
|
259
|
+
customElements.whenDefined("dependency-1"),
|
|
260
|
+
customElements.whenDefined("dependency-2"),
|
|
261
|
+
]);
|
|
262
|
+
|
|
263
|
+
// Now the event should be dispatched
|
|
264
|
+
assert.isTrue(eventDispatched);
|
|
265
|
+
});
|
|
@@ -1,56 +1,88 @@
|
|
|
1
1
|
import { attr, element, query, css, html } from "@joist/element";
|
|
2
|
+
import { Change, Changes, effect, observe } from "@joist/observable";
|
|
2
3
|
|
|
3
|
-
import {
|
|
4
|
-
import { JoistValueEvent } from "../events.js";
|
|
4
|
+
import { BindChange, JoistValueEvent } from "../events.js";
|
|
5
5
|
import { JExpression } from "../expression.js";
|
|
6
6
|
|
|
7
|
-
declare global {
|
|
8
|
-
interface HTMLElementTagNameMap {
|
|
9
|
-
"j-for": JositForElement;
|
|
10
|
-
"j-for-scope": JForScope;
|
|
11
|
-
}
|
|
12
|
-
}
|
|
13
|
-
|
|
14
7
|
export interface EachCtx<T> {
|
|
15
8
|
value: T | null;
|
|
16
9
|
index: number | null;
|
|
17
10
|
position: number | null;
|
|
18
11
|
}
|
|
19
12
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
13
|
+
class JoistForScopeContainer<T = unknown> {
|
|
14
|
+
host: Element;
|
|
15
|
+
|
|
16
|
+
get key(): string | null {
|
|
17
|
+
return this.host.getAttribute("key");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
#callbacks: Array<(val: BindChange<EachCtx<T>>) => void> = [];
|
|
21
|
+
|
|
22
|
+
@observe()
|
|
27
23
|
accessor each: EachCtx<T> = {
|
|
28
24
|
value: null,
|
|
29
25
|
index: null,
|
|
30
26
|
position: null,
|
|
31
27
|
};
|
|
32
28
|
|
|
33
|
-
|
|
34
|
-
|
|
29
|
+
constructor(host: Element | null) {
|
|
30
|
+
if (host == null) {
|
|
31
|
+
throw new Error("JForScope required a host element");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
this.host = host;
|
|
35
|
+
|
|
36
|
+
this.host.addEventListener("joist::value", (e) => {
|
|
37
|
+
if (e.expression.bindTo === "each") {
|
|
38
|
+
e.stopPropagation();
|
|
39
|
+
|
|
40
|
+
this.#callbacks.push(e.update);
|
|
41
|
+
|
|
42
|
+
e.update({
|
|
43
|
+
oldValue: null,
|
|
44
|
+
newValue: this.each,
|
|
45
|
+
firstChange: true,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
@effect()
|
|
52
|
+
onChange(changes: Changes<this>): void {
|
|
53
|
+
const change = changes.get("each") as Change<EachCtx<T>>;
|
|
54
|
+
|
|
55
|
+
for (let cb of this.#callbacks) {
|
|
56
|
+
cb({
|
|
57
|
+
oldValue: change.oldValue,
|
|
58
|
+
newValue: change.newValue,
|
|
59
|
+
firstChange: false,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
}
|
|
35
63
|
}
|
|
36
64
|
|
|
37
65
|
@element({
|
|
38
|
-
tagName: "j-for",
|
|
39
66
|
// prettier-ignore
|
|
40
67
|
shadowDom: [css`:host{display:contents;}`, html`<slot></slot>`],
|
|
41
68
|
})
|
|
42
|
-
export class
|
|
69
|
+
export class JoistForElement extends HTMLElement {
|
|
43
70
|
@attr()
|
|
44
71
|
accessor bind = "";
|
|
45
72
|
|
|
46
73
|
@attr()
|
|
47
74
|
accessor key = "";
|
|
48
75
|
|
|
76
|
+
@attr({
|
|
77
|
+
name: "depends-on",
|
|
78
|
+
})
|
|
79
|
+
accessor dependsOn = "";
|
|
80
|
+
|
|
49
81
|
#template = query("template", this);
|
|
50
82
|
#items: Iterable<unknown> = [];
|
|
51
|
-
#scopes = new Map<
|
|
83
|
+
#scopes = new Map<string, JoistForScopeContainer>();
|
|
52
84
|
|
|
53
|
-
connectedCallback(): void {
|
|
85
|
+
async connectedCallback(): Promise<void> {
|
|
54
86
|
const template = this.#template();
|
|
55
87
|
|
|
56
88
|
if (this.firstElementChild !== template) {
|
|
@@ -59,11 +91,17 @@ export class JositForElement extends HTMLElement {
|
|
|
59
91
|
|
|
60
92
|
// collect all scopes from the template to be matched against later
|
|
61
93
|
let currentScope = template.nextElementSibling;
|
|
62
|
-
while (currentScope instanceof
|
|
63
|
-
this.#scopes.set(currentScope.key, currentScope);
|
|
94
|
+
while (currentScope instanceof JoistForScopeContainer) {
|
|
95
|
+
this.#scopes.set(String(currentScope.key), currentScope);
|
|
64
96
|
currentScope = currentScope.nextElementSibling;
|
|
65
97
|
}
|
|
66
98
|
|
|
99
|
+
if (this.dependsOn) {
|
|
100
|
+
await Promise.all(
|
|
101
|
+
this.dependsOn.split(",").map((tag) => window.customElements.whenDefined(tag)),
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
67
105
|
const token = new JExpression(this.bind);
|
|
68
106
|
|
|
69
107
|
this.dispatchEvent(
|
|
@@ -91,8 +129,6 @@ export class JositForElement extends HTMLElement {
|
|
|
91
129
|
// Updates the DOM by either inserting new scopes or moving existing ones
|
|
92
130
|
// to their correct positions based on the current iteration order
|
|
93
131
|
createFromEmpty(): void {
|
|
94
|
-
const template = this.#template();
|
|
95
|
-
const templateContent = template.content;
|
|
96
132
|
const keyProperty = this.key;
|
|
97
133
|
const fragment = document.createDocumentFragment();
|
|
98
134
|
|
|
@@ -104,13 +140,15 @@ export class JositForElement extends HTMLElement {
|
|
|
104
140
|
key = value[keyProperty];
|
|
105
141
|
}
|
|
106
142
|
|
|
107
|
-
const scope =
|
|
108
|
-
|
|
109
|
-
scope.key
|
|
143
|
+
const scope = this.#createScopeContainer();
|
|
144
|
+
|
|
145
|
+
scope.host.setAttribute("key", String(key));
|
|
110
146
|
scope.each = { position: index + 1, index, value };
|
|
111
147
|
|
|
112
|
-
fragment.appendChild(scope);
|
|
113
|
-
|
|
148
|
+
fragment.appendChild(scope.host);
|
|
149
|
+
|
|
150
|
+
this.#scopes.set(String(key), scope);
|
|
151
|
+
|
|
114
152
|
index++;
|
|
115
153
|
}
|
|
116
154
|
|
|
@@ -120,8 +158,7 @@ export class JositForElement extends HTMLElement {
|
|
|
120
158
|
// Updates the DOM by either inserting new scopes or moving existing ones
|
|
121
159
|
// to their correct positions based on the current iteration order
|
|
122
160
|
updateItems(): void {
|
|
123
|
-
const
|
|
124
|
-
const leftoverScopes = new Map<unknown, JForScope>(this.#scopes);
|
|
161
|
+
const leftoverScopes = new Map<unknown, JoistForScopeContainer>(this.#scopes);
|
|
125
162
|
const keyProperty = this.key;
|
|
126
163
|
|
|
127
164
|
let index = 0;
|
|
@@ -136,23 +173,23 @@ export class JositForElement extends HTMLElement {
|
|
|
136
173
|
let scope = leftoverScopes.get(key);
|
|
137
174
|
|
|
138
175
|
if (!scope) {
|
|
139
|
-
scope =
|
|
140
|
-
|
|
141
|
-
this.#scopes.set(key, scope);
|
|
176
|
+
scope = scope = this.#createScopeContainer();
|
|
177
|
+
|
|
178
|
+
this.#scopes.set(String(key), scope);
|
|
142
179
|
} else {
|
|
143
180
|
leftoverScopes.delete(key); // Remove from map to track unused scopes
|
|
144
181
|
}
|
|
145
182
|
|
|
146
183
|
// Only update if values have changed
|
|
147
184
|
if (scope.key !== key || scope.each.value !== value) {
|
|
148
|
-
scope.key
|
|
185
|
+
scope.host.setAttribute("key", String(key));
|
|
149
186
|
scope.each = { position: index + 1, index, value };
|
|
150
187
|
}
|
|
151
188
|
|
|
152
189
|
const child = this.children[index + 1];
|
|
153
190
|
|
|
154
|
-
if (child !== scope) {
|
|
155
|
-
this.insertBefore(scope, child);
|
|
191
|
+
if (child !== scope.host) {
|
|
192
|
+
this.insertBefore(scope.host, child);
|
|
156
193
|
}
|
|
157
194
|
|
|
158
195
|
index++;
|
|
@@ -160,18 +197,31 @@ export class JositForElement extends HTMLElement {
|
|
|
160
197
|
|
|
161
198
|
// Remove unused scopes
|
|
162
199
|
for (const scope of leftoverScopes.values()) {
|
|
163
|
-
scope.remove();
|
|
200
|
+
scope.host.remove();
|
|
164
201
|
}
|
|
165
202
|
}
|
|
166
203
|
|
|
167
204
|
disconnectedCallback(): void {
|
|
168
205
|
for (const scope of this.#scopes.values()) {
|
|
169
|
-
scope.remove();
|
|
206
|
+
scope.host.remove();
|
|
170
207
|
}
|
|
171
208
|
|
|
172
209
|
this.#scopes.clear();
|
|
173
210
|
this.#items = [];
|
|
174
211
|
}
|
|
212
|
+
|
|
213
|
+
#createScopeContainer() {
|
|
214
|
+
const template = this.#template();
|
|
215
|
+
const content = template.content.firstElementChild;
|
|
216
|
+
|
|
217
|
+
if (content === null) {
|
|
218
|
+
throw new Error("template must contain a single parent element");
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const fragment = document.importNode(content, true);
|
|
222
|
+
|
|
223
|
+
return new JoistForScopeContainer(fragment);
|
|
224
|
+
}
|
|
175
225
|
}
|
|
176
226
|
|
|
177
227
|
function isIterable<T = unknown>(obj: any): obj is Iterable<T> {
|