@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.
Files changed (56) hide show
  1. package/LICENSE +2 -2
  2. package/package.json +1 -1
  3. package/src/lib/define.ts +26 -6
  4. package/src/lib/elements/async.element.test.ts +37 -1
  5. package/src/lib/elements/async.element.ts +12 -8
  6. package/src/lib/elements/bind.element.test.ts +60 -5
  7. package/src/lib/elements/bind.element.ts +30 -19
  8. package/src/lib/elements/for.element.test.ts +54 -9
  9. package/src/lib/elements/for.element.ts +91 -41
  10. package/src/lib/elements/if.element.test.ts +314 -260
  11. package/src/lib/elements/if.element.ts +40 -17
  12. package/src/lib/elements/scope.element.test.ts +1 -2
  13. package/src/lib/elements/scope.element.ts +0 -7
  14. package/src/lib/elements/value.element.test.ts +33 -1
  15. package/src/lib/elements/value.element.ts +13 -8
  16. package/src/lib/events.ts +1 -1
  17. package/target/lib/define.d.ts +16 -6
  18. package/target/lib/define.js +13 -6
  19. package/target/lib/define.js.map +1 -1
  20. package/target/lib/elements/async.element.d.ts +2 -6
  21. package/target/lib/elements/async.element.js +15 -3
  22. package/target/lib/elements/async.element.js.map +1 -1
  23. package/target/lib/elements/async.element.test.d.ts +1 -1
  24. package/target/lib/elements/async.element.test.js +30 -1
  25. package/target/lib/elements/async.element.test.js.map +1 -1
  26. package/target/lib/elements/bind.element.d.ts +2 -6
  27. package/target/lib/elements/bind.element.js +34 -16
  28. package/target/lib/elements/bind.element.js.map +1 -1
  29. package/target/lib/elements/bind.element.test.d.ts +1 -1
  30. package/target/lib/elements/bind.element.test.js +50 -5
  31. package/target/lib/elements/bind.element.test.js.map +1 -1
  32. package/target/lib/elements/for.element.d.ts +3 -12
  33. package/target/lib/elements/for.element.js +84 -57
  34. package/target/lib/elements/for.element.js.map +1 -1
  35. package/target/lib/elements/for.element.test.d.ts +1 -2
  36. package/target/lib/elements/for.element.test.js +41 -5
  37. package/target/lib/elements/for.element.test.js.map +1 -1
  38. package/target/lib/elements/if.element.d.ts +3 -6
  39. package/target/lib/elements/if.element.js +42 -10
  40. package/target/lib/elements/if.element.js.map +1 -1
  41. package/target/lib/elements/if.element.test.d.ts +1 -1
  42. package/target/lib/elements/if.element.test.js +273 -1
  43. package/target/lib/elements/if.element.test.js.map +1 -1
  44. package/target/lib/elements/scope.element.d.ts +0 -5
  45. package/target/lib/elements/scope.element.js +0 -1
  46. package/target/lib/elements/scope.element.js.map +1 -1
  47. package/target/lib/elements/scope.element.test.d.ts +1 -2
  48. package/target/lib/elements/scope.element.test.js +1 -2
  49. package/target/lib/elements/scope.element.test.js.map +1 -1
  50. package/target/lib/elements/value.element.d.ts +2 -6
  51. package/target/lib/elements/value.element.js +15 -3
  52. package/target/lib/elements/value.element.js.map +1 -1
  53. package/target/lib/elements/value.element.test.d.ts +1 -1
  54. package/target/lib/elements/value.element.test.js +26 -1
  55. package/target/lib/elements/value.element.test.js.map +1 -1
  56. 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-2020 Danny Blue
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@joist/templating",
3
- "version": "4.2.4-next.9",
3
+ "version": "4.2.4",
4
4
  "type": "module",
5
5
  "main": "./target/lib.js",
6
6
  "module": "./target/lib.js",
package/src/lib/define.ts CHANGED
@@ -1,6 +1,26 @@
1
- import "./elements/async.element.js";
2
- import "./elements/for.element.js";
3
- import "./elements/if.element.js";
4
- import "./elements/bind.element.js";
5
- import "./elements/value.element.js";
6
- import "./elements/scope.element.js";
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 "./async.element.js";
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 "./bind.element.js";
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
- <a>Default</a>
53
- <a id="test">Target</a>
54
- </j-bind>
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
- connectedCallback(): void {
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 child = this.firstElementChild;
41
+ let children: Iterable<Element> = this.children;
42
+
43
+ const root = this.getRootNode() as Document | ShadowRoot;
44
44
 
45
45
  if (this.target) {
46
- child = this.querySelector(this.target);
46
+ children = root.querySelectorAll(this.target);
47
47
  }
48
48
 
49
- if (!child) {
50
- throw new Error("j-bind must have a child element or defined target");
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
- if (value === true) {
58
- child.setAttribute(token.mapTo, "");
59
- } else if (value === false) {
60
- child.removeAttribute(token.mapTo);
61
- } else {
62
- child.setAttribute(token.mapTo, String(value));
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
- Reflect.set(child, token.mapTo, value);
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 "./for.element.js";
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
- <j-val bind="each.value"></j-val>
122
- (index: <j-val bind="each.index"></j-val>, position: <j-val bind="each.position"></j-val>)
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("j-for-scope");
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 { bind } from "../bind.js";
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
- @element({
21
- tagName: "j-for-scope",
22
- // prettier-ignore
23
- shadowDom: [css`:host{display: contents;}`, html`<slot></slot>`],
24
- })
25
- export class JForScope<T = unknown> extends HTMLElement {
26
- @bind()
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
- @attr()
34
- accessor key: unknown;
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 JositForElement extends HTMLElement {
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<unknown, JForScope>();
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 JForScope) {
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 = new JForScope();
108
- scope.append(document.importNode(templateContent, true));
109
- scope.key = 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
- this.#scopes.set(key, scope);
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 template = this.#template();
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 = new JForScope();
140
- scope.append(document.importNode(template.content, true));
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 = 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> {