@joist/element 4.2.3-next.0 → 4.2.3-next.1
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/package.json +1 -1
- package/src/lib/attr-changed.test.ts +0 -1
- package/src/lib/templating/README.md +115 -1
- package/src/lib/templating/define.ts +1 -0
- package/src/lib/templating/elements/async.element.test.ts +90 -0
- package/src/lib/templating/elements/async.element.ts +122 -0
- package/src/lib/templating/elements/for.element.test.ts +182 -0
- package/src/lib/templating/elements/for.element.ts +6 -4
- package/src/lib/templating/elements/if.element.test.ts +35 -0
- package/src/lib/templating/elements/if.element.ts +42 -16
- package/target/lib/attr-changed.test.js +0 -1
- package/target/lib/attr-changed.test.js.map +1 -1
- package/target/lib/templating/define.d.ts +1 -0
- package/target/lib/templating/define.js +1 -0
- package/target/lib/templating/define.js.map +1 -1
- package/target/lib/templating/elements/async.element.d.ts +17 -0
- package/target/lib/templating/elements/async.element.js +115 -0
- package/target/lib/templating/elements/async.element.js.map +1 -0
- package/target/lib/templating/elements/async.element.test.d.ts +1 -0
- package/target/lib/templating/elements/async.element.test.js +75 -0
- package/target/lib/templating/elements/async.element.test.js.map +1 -0
- package/target/lib/templating/elements/for.element.js +5 -2
- package/target/lib/templating/elements/for.element.js.map +1 -1
- package/target/lib/templating/elements/for.element.test.js +119 -0
- package/target/lib/templating/elements/for.element.test.js.map +1 -1
- package/target/lib/templating/elements/if.element.d.ts +1 -2
- package/target/lib/templating/elements/if.element.js +28 -14
- package/target/lib/templating/elements/if.element.js.map +1 -1
- package/target/lib/templating/elements/if.element.test.js +31 -0
- package/target/lib/templating/elements/if.element.test.js.map +1 -1
package/package.json
CHANGED
|
@@ -65,8 +65,31 @@ Conditionally renders content based on a boolean expression:
|
|
|
65
65
|
<div>This content is shown when isHidden is false</div>
|
|
66
66
|
</template>
|
|
67
67
|
</j-if>
|
|
68
|
+
|
|
69
|
+
<!-- With else template -->
|
|
70
|
+
<j-if bind="isLoggedIn">
|
|
71
|
+
<template>
|
|
72
|
+
<div>Welcome back!</div>
|
|
73
|
+
</template>
|
|
74
|
+
<template else>
|
|
75
|
+
<div>Please log in</div>
|
|
76
|
+
</template>
|
|
77
|
+
</j-if>
|
|
68
78
|
```
|
|
69
79
|
|
|
80
|
+
The `j-if` element supports:
|
|
81
|
+
- Boolean expressions for conditional rendering
|
|
82
|
+
- Negation operator (`!`) for inverse conditions
|
|
83
|
+
- Optional `else` template for fallback content
|
|
84
|
+
- Automatic cleanup of removed content
|
|
85
|
+
|
|
86
|
+
Common use cases:
|
|
87
|
+
- Toggling visibility of UI elements
|
|
88
|
+
- Conditional form fields
|
|
89
|
+
- Feature flags
|
|
90
|
+
- Authentication states
|
|
91
|
+
- Loading states
|
|
92
|
+
|
|
70
93
|
### Property Binding (`j-props`)
|
|
71
94
|
|
|
72
95
|
Binds values to element properties (rather than attributes). This is particularly useful for boolean properties, form inputs, and other cases where attribute binding isn't sufficient:
|
|
@@ -134,6 +157,48 @@ Displays a bound value as text content:
|
|
|
134
157
|
<j-value bind="formattedPrice"></j-value>
|
|
135
158
|
```
|
|
136
159
|
|
|
160
|
+
### Async State Handling (`j-async`)
|
|
161
|
+
|
|
162
|
+
Handles asynchronous operations and state management with loading, success, and error states:
|
|
163
|
+
|
|
164
|
+
```html
|
|
165
|
+
<j-async bind="userPromise">
|
|
166
|
+
<template loading>Loading user data...</template>
|
|
167
|
+
<template success>
|
|
168
|
+
<div>Welcome, <j-value bind="data.name"></j-value>!</div>
|
|
169
|
+
</template>
|
|
170
|
+
<template error>
|
|
171
|
+
<div>Error loading user data: <j-value bind="error"></j-value></div>
|
|
172
|
+
</template>
|
|
173
|
+
</j-async>
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
The `j-async` element supports:
|
|
177
|
+
- Promise handling with automatic state transitions
|
|
178
|
+
- Loading, success, and error templates
|
|
179
|
+
- Automatic cleanup on disconnection
|
|
180
|
+
|
|
181
|
+
Example usage:
|
|
182
|
+
```typescript
|
|
183
|
+
// In your component
|
|
184
|
+
@bind()
|
|
185
|
+
accessor userPromise = fetch('/api/user').then(r => r.json());
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
```html
|
|
189
|
+
<j-async bind="userPromise">
|
|
190
|
+
<template loading>Loading...</template>
|
|
191
|
+
|
|
192
|
+
<template success>
|
|
193
|
+
<div>Welcome, <j-value bind="state.data.name"></j-value>!</div>
|
|
194
|
+
</template>
|
|
195
|
+
|
|
196
|
+
<template error>
|
|
197
|
+
<div>Error: <j-value bind="state.error"></j-value></div>
|
|
198
|
+
</template>
|
|
199
|
+
</j-async>
|
|
200
|
+
```
|
|
201
|
+
|
|
137
202
|
## Complete Example
|
|
138
203
|
|
|
139
204
|
Here's a complete todo application in a single component:
|
|
@@ -289,4 +354,53 @@ The templating system is built on top of Joist's observable system (`@joist/obse
|
|
|
289
354
|
2. Keep binding expressions simple and avoid deep nesting
|
|
290
355
|
3. Consider performance implications when binding to frequently changing values
|
|
291
356
|
4. Always use a `key` attribute with `j-for` when items can be reordered
|
|
292
|
-
5. Place template content directly inside `j-if` and `j-for` elements
|
|
357
|
+
5. Place template content directly inside `j-if` and `j-for` elements
|
|
358
|
+
|
|
359
|
+
## Manual Value Handling
|
|
360
|
+
|
|
361
|
+
You can manually handle value requests and updates by listening for the `joist::value` event. This is useful when you need more control over the binding process or want to implement custom binding logic:
|
|
362
|
+
|
|
363
|
+
```typescript
|
|
364
|
+
class MyElement extends HTMLElement {
|
|
365
|
+
connectedCallback() {
|
|
366
|
+
// Listen for value requests
|
|
367
|
+
this.addEventListener('joist::value', (e: JoistValueEvent) => {
|
|
368
|
+
const token = e.token;
|
|
369
|
+
|
|
370
|
+
// Handle the value request
|
|
371
|
+
if (token.bindTo === 'myValue') {
|
|
372
|
+
// Update the value
|
|
373
|
+
e.update({
|
|
374
|
+
oldValue: this.myValue,
|
|
375
|
+
newValue: this.myValue
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
Example with async value handling:
|
|
384
|
+
```typescript
|
|
385
|
+
class MyElement extends HTMLElement {
|
|
386
|
+
connectedCallback() {
|
|
387
|
+
this.addEventListener('joist::value', (e: JoistValueEvent) => {
|
|
388
|
+
const token = e.token;
|
|
389
|
+
|
|
390
|
+
if (token.bindTo === 'userData') {
|
|
391
|
+
e.update({
|
|
392
|
+
oldValue: this.userData,
|
|
393
|
+
newValue: data
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
Common use cases for manual value handling:
|
|
402
|
+
- Custom data transformation before binding
|
|
403
|
+
- Async data loading and caching
|
|
404
|
+
- Complex state management
|
|
405
|
+
- Integration with external data sources
|
|
406
|
+
- Custom validation or error handling
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import "./async.element.js";
|
|
2
|
+
|
|
3
|
+
import { fixtureSync, html } from "@open-wc/testing";
|
|
4
|
+
import { assert } from "chai";
|
|
5
|
+
|
|
6
|
+
import type { JoistValueEvent } from "../events.js";
|
|
7
|
+
|
|
8
|
+
it("should show loading template when promise is pending", async () => {
|
|
9
|
+
const element = fixtureSync(html`
|
|
10
|
+
<div
|
|
11
|
+
@joist::value=${(e: JoistValueEvent) => {
|
|
12
|
+
e.update({ oldValue: null, newValue: new Promise(() => {}) });
|
|
13
|
+
}}
|
|
14
|
+
>
|
|
15
|
+
<j-async bind="test">
|
|
16
|
+
<template loading>Loading...</template>
|
|
17
|
+
<template success>Success!</template>
|
|
18
|
+
<template error>Error!</template>
|
|
19
|
+
</j-async>
|
|
20
|
+
</div>
|
|
21
|
+
`);
|
|
22
|
+
|
|
23
|
+
assert.equal(element.textContent?.trim(), "Loading...");
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("should show success template when promise resolves", async () => {
|
|
27
|
+
const element = fixtureSync(html`
|
|
28
|
+
<div
|
|
29
|
+
@joist::value=${(e: JoistValueEvent) => {
|
|
30
|
+
e.update({ oldValue: null, newValue: Promise.resolve("data") });
|
|
31
|
+
}}
|
|
32
|
+
>
|
|
33
|
+
<j-async bind="test">
|
|
34
|
+
<template loading>Loading...</template>
|
|
35
|
+
<template success>Success!</template>
|
|
36
|
+
<template error>Error!</template>
|
|
37
|
+
</j-async>
|
|
38
|
+
</div>
|
|
39
|
+
`);
|
|
40
|
+
|
|
41
|
+
// Wait for promise to resolve
|
|
42
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
43
|
+
assert.equal(element.textContent?.trim(), "Success!");
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("should show error template when promise rejects", async () => {
|
|
47
|
+
const element = fixtureSync(html`
|
|
48
|
+
<div
|
|
49
|
+
@joist::value=${(e: JoistValueEvent) => {
|
|
50
|
+
e.update({ oldValue: null, newValue: Promise.reject("error") });
|
|
51
|
+
}}
|
|
52
|
+
>
|
|
53
|
+
<j-async bind="test">
|
|
54
|
+
<template loading>Loading...</template>
|
|
55
|
+
<template success>Success!</template>
|
|
56
|
+
<template error>Error!</template>
|
|
57
|
+
</j-async>
|
|
58
|
+
</div>
|
|
59
|
+
`);
|
|
60
|
+
|
|
61
|
+
// Wait for promise to reject
|
|
62
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
63
|
+
assert.equal(element.textContent?.trim(), "Error!");
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("should handle state transitions", async () => {
|
|
67
|
+
const element = fixtureSync(html`
|
|
68
|
+
<div
|
|
69
|
+
@joist::value=${(e: JoistValueEvent) => {
|
|
70
|
+
const promise = new Promise((resolve) => {
|
|
71
|
+
setTimeout(() => resolve("data"), 100);
|
|
72
|
+
});
|
|
73
|
+
e.update({ oldValue: null, newValue: promise });
|
|
74
|
+
}}
|
|
75
|
+
>
|
|
76
|
+
<j-async bind="test">
|
|
77
|
+
<template loading>Loading...</template>
|
|
78
|
+
<template success>Success!</template>
|
|
79
|
+
<template error>Error!</template>
|
|
80
|
+
</j-async>
|
|
81
|
+
</div>
|
|
82
|
+
`);
|
|
83
|
+
|
|
84
|
+
// Initially should show loading
|
|
85
|
+
assert.equal(element.textContent?.trim(), "Loading...");
|
|
86
|
+
|
|
87
|
+
// Wait for promise to resolve
|
|
88
|
+
await new Promise((resolve) => setTimeout(resolve, 150));
|
|
89
|
+
assert.equal(element.textContent?.trim(), "Success!");
|
|
90
|
+
});
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { attr } from "../../attr.js";
|
|
2
|
+
import { element } from "../../element.js";
|
|
3
|
+
import { queryAll } from "../../query-all.js";
|
|
4
|
+
import { css, html } from "../../tags.js";
|
|
5
|
+
import { bind } from "../bind.js";
|
|
6
|
+
|
|
7
|
+
import { JoistValueEvent } from "../events.js";
|
|
8
|
+
import { JToken } from "../token.js";
|
|
9
|
+
|
|
10
|
+
declare global {
|
|
11
|
+
interface HTMLElementTagNameMap {
|
|
12
|
+
"j-async": JoistAsyncElement;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export type AsyncState<T = unknown, E = unknown> = {
|
|
17
|
+
status: "loading" | "error" | "success";
|
|
18
|
+
data?: T;
|
|
19
|
+
error?: E;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
@element({
|
|
23
|
+
tagName: "j-async",
|
|
24
|
+
// prettier-ignore
|
|
25
|
+
shadowDom: [css`:host{display: contents;}`, html`<slot></slot>`],
|
|
26
|
+
})
|
|
27
|
+
export class JoistAsyncElement extends HTMLElement {
|
|
28
|
+
@attr()
|
|
29
|
+
accessor bind = "";
|
|
30
|
+
|
|
31
|
+
@bind()
|
|
32
|
+
accessor state: AsyncState | null = null;
|
|
33
|
+
|
|
34
|
+
#templates = queryAll<HTMLTemplateElement>("template", this);
|
|
35
|
+
#currentNodes: Node[] = [];
|
|
36
|
+
#cachedTemplates: {
|
|
37
|
+
loading?: HTMLTemplateElement;
|
|
38
|
+
error?: HTMLTemplateElement;
|
|
39
|
+
success?: HTMLTemplateElement;
|
|
40
|
+
} = {
|
|
41
|
+
loading: undefined,
|
|
42
|
+
error: undefined,
|
|
43
|
+
success: undefined,
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
connectedCallback(): void {
|
|
47
|
+
this.#clean();
|
|
48
|
+
|
|
49
|
+
// Cache all templates
|
|
50
|
+
const templates = Array.from(this.#templates());
|
|
51
|
+
|
|
52
|
+
this.#cachedTemplates = {
|
|
53
|
+
loading: templates.find((t) => t.hasAttribute("loading")),
|
|
54
|
+
error: templates.find((t) => t.hasAttribute("error")),
|
|
55
|
+
success: templates.find((t) => t.hasAttribute("success")),
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const token = new JToken(this.bind);
|
|
59
|
+
|
|
60
|
+
this.dispatchEvent(
|
|
61
|
+
new JoistValueEvent(token, ({ newValue, oldValue }) => {
|
|
62
|
+
if (newValue !== oldValue) {
|
|
63
|
+
if (newValue instanceof Promise) {
|
|
64
|
+
this.#handlePromise(newValue);
|
|
65
|
+
} else {
|
|
66
|
+
console.warn("j-async bind value must be a Promise or AsyncState");
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}),
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async #handlePromise(promise: Promise<unknown>): Promise<void> {
|
|
74
|
+
try {
|
|
75
|
+
this.#handleState({ status: "loading" });
|
|
76
|
+
const data = await promise;
|
|
77
|
+
this.#handleState({ status: "success", data });
|
|
78
|
+
} catch (error) {
|
|
79
|
+
this.#handleState({ status: "error", error });
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
#handleState(state: AsyncState): void {
|
|
84
|
+
this.#clean();
|
|
85
|
+
|
|
86
|
+
let template: HTMLTemplateElement | undefined = undefined;
|
|
87
|
+
|
|
88
|
+
this.state = state;
|
|
89
|
+
|
|
90
|
+
switch (state.status) {
|
|
91
|
+
case "loading":
|
|
92
|
+
template = this.#cachedTemplates.loading;
|
|
93
|
+
break;
|
|
94
|
+
|
|
95
|
+
case "error":
|
|
96
|
+
template = this.#cachedTemplates.error;
|
|
97
|
+
break;
|
|
98
|
+
|
|
99
|
+
case "success":
|
|
100
|
+
template = this.#cachedTemplates.success;
|
|
101
|
+
break;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (template) {
|
|
105
|
+
const content = document.importNode(template.content, true);
|
|
106
|
+
const nodes = Array.from(content.childNodes);
|
|
107
|
+
this.appendChild(content);
|
|
108
|
+
this.#currentNodes = nodes;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
#clean(): void {
|
|
113
|
+
for (const node of this.#currentNodes) {
|
|
114
|
+
node.parentNode?.removeChild(node);
|
|
115
|
+
}
|
|
116
|
+
this.#currentNodes = [];
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
disconnectedCallback(): void {
|
|
120
|
+
this.#clean();
|
|
121
|
+
}
|
|
122
|
+
}
|
|
@@ -37,3 +37,185 @@ it("should iterate over an iterable", () => {
|
|
|
37
37
|
assert.equal(listItems[0].textContent?.trim(), "Hello");
|
|
38
38
|
assert.equal(listItems[1].textContent?.trim(), "World");
|
|
39
39
|
});
|
|
40
|
+
|
|
41
|
+
it("should handle empty arrays", () => {
|
|
42
|
+
const element = fixtureSync(html`
|
|
43
|
+
<div
|
|
44
|
+
@joist::value=${(e: JoistValueEvent) => {
|
|
45
|
+
e.update({
|
|
46
|
+
oldValue: null,
|
|
47
|
+
newValue: [],
|
|
48
|
+
});
|
|
49
|
+
}}
|
|
50
|
+
>
|
|
51
|
+
<j-for bind="items">
|
|
52
|
+
<template>
|
|
53
|
+
<div>Item</div>
|
|
54
|
+
</template>
|
|
55
|
+
</j-for>
|
|
56
|
+
</div>
|
|
57
|
+
`);
|
|
58
|
+
|
|
59
|
+
assert.equal(element.querySelectorAll("div").length, 0);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("should update when items are added or removed", () => {
|
|
63
|
+
const element = fixtureSync(html`
|
|
64
|
+
<div
|
|
65
|
+
@joist::value=${(e: JoistValueEvent) => {
|
|
66
|
+
// Initial items
|
|
67
|
+
e.update({
|
|
68
|
+
oldValue: null,
|
|
69
|
+
newValue: [
|
|
70
|
+
{ id: "1", text: "First" },
|
|
71
|
+
{ id: "2", text: "Second" },
|
|
72
|
+
],
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// Add an item
|
|
76
|
+
e.update({
|
|
77
|
+
oldValue: null,
|
|
78
|
+
newValue: [
|
|
79
|
+
{ id: "1", text: "First" },
|
|
80
|
+
{ id: "2", text: "Second" },
|
|
81
|
+
{ id: "3", text: "Third" },
|
|
82
|
+
],
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// Remove an item
|
|
86
|
+
e.update({
|
|
87
|
+
oldValue: null,
|
|
88
|
+
newValue: [
|
|
89
|
+
{ id: "1", text: "First" },
|
|
90
|
+
{ id: "3", text: "Third" },
|
|
91
|
+
],
|
|
92
|
+
});
|
|
93
|
+
}}
|
|
94
|
+
>
|
|
95
|
+
<j-for bind="items" key="id">
|
|
96
|
+
<template>
|
|
97
|
+
<j-value bind="each.value.text"></j-value>
|
|
98
|
+
</template>
|
|
99
|
+
</j-for>
|
|
100
|
+
</div>
|
|
101
|
+
`);
|
|
102
|
+
|
|
103
|
+
const items = element.querySelectorAll("j-value");
|
|
104
|
+
assert.equal(items.length, 2);
|
|
105
|
+
assert.equal(items[0].textContent?.trim(), "First");
|
|
106
|
+
assert.equal(items[1].textContent?.trim(), "Third");
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("should provide index and position information", () => {
|
|
110
|
+
const element = fixtureSync(html`
|
|
111
|
+
<div
|
|
112
|
+
@joist::value=${(e: JoistValueEvent) => {
|
|
113
|
+
e.update({
|
|
114
|
+
oldValue: null,
|
|
115
|
+
newValue: ["A", "B", "C"],
|
|
116
|
+
});
|
|
117
|
+
}}
|
|
118
|
+
>
|
|
119
|
+
<j-for bind="items">
|
|
120
|
+
<template>
|
|
121
|
+
<j-value bind="each.value"></j-value>
|
|
122
|
+
(index: <j-value bind="each.index"></j-value>,
|
|
123
|
+
position: <j-value bind="each.position"></j-value>)
|
|
124
|
+
</template>
|
|
125
|
+
</j-for>
|
|
126
|
+
</div>
|
|
127
|
+
`);
|
|
128
|
+
|
|
129
|
+
const items = element.querySelectorAll("j-for-scope");
|
|
130
|
+
assert.equal(items.length, 3);
|
|
131
|
+
assert.equal(
|
|
132
|
+
items[0].textContent?.trim().replaceAll("\n", "").replaceAll(" ", ""),
|
|
133
|
+
"A(index:0,position:1)",
|
|
134
|
+
);
|
|
135
|
+
assert.equal(
|
|
136
|
+
items[1].textContent?.trim().replaceAll("\n", "").replaceAll(" ", ""),
|
|
137
|
+
"B(index:1,position:2)",
|
|
138
|
+
);
|
|
139
|
+
assert.equal(
|
|
140
|
+
items[2].textContent?.trim().replaceAll("\n", "").replaceAll(" ", ""),
|
|
141
|
+
"C(index:2,position:3)",
|
|
142
|
+
);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
// it("should handle nested j-for elements", () => {
|
|
146
|
+
// const element = fixtureSync(html`
|
|
147
|
+
// <div
|
|
148
|
+
// @joist::value=${(e: JoistValueEvent) => {
|
|
149
|
+
// e.update({
|
|
150
|
+
// oldValue: null,
|
|
151
|
+
// newValue: [
|
|
152
|
+
// { id: "1", items: ["A", "B"] },
|
|
153
|
+
// { id: "2", items: ["C", "D"] },
|
|
154
|
+
// ],
|
|
155
|
+
// });
|
|
156
|
+
// }}
|
|
157
|
+
// >
|
|
158
|
+
// <j-for bind="groups" key="id">
|
|
159
|
+
// <template>
|
|
160
|
+
// <div class="group">
|
|
161
|
+
// <j-for bind="each.value.items">
|
|
162
|
+
// <template>
|
|
163
|
+
// <j-value class="child" bind="each.value"></j-value>
|
|
164
|
+
// </template>
|
|
165
|
+
// </j-for>
|
|
166
|
+
// </div>
|
|
167
|
+
// </template>
|
|
168
|
+
// </j-for>
|
|
169
|
+
// </div>
|
|
170
|
+
// `);
|
|
171
|
+
|
|
172
|
+
// const groups = element.querySelectorAll(".group");
|
|
173
|
+
// assert.equal(groups.length, 2);
|
|
174
|
+
|
|
175
|
+
// const items = element.querySelectorAll(".child");
|
|
176
|
+
// assert.equal(items.length, 4);
|
|
177
|
+
// assert.equal(items[0].textContent?.trim(), "A");
|
|
178
|
+
// assert.equal(items[1].textContent?.trim(), "B");
|
|
179
|
+
// assert.equal(items[2].textContent?.trim(), "C");
|
|
180
|
+
// assert.equal(items[3].textContent?.trim(), "D");
|
|
181
|
+
// });
|
|
182
|
+
|
|
183
|
+
it("should maintain DOM order when items are reordered", () => {
|
|
184
|
+
const element = fixtureSync(html`
|
|
185
|
+
<div
|
|
186
|
+
@joist::value=${(e: JoistValueEvent) => {
|
|
187
|
+
// Initial order
|
|
188
|
+
e.update({
|
|
189
|
+
oldValue: null,
|
|
190
|
+
newValue: [
|
|
191
|
+
{ id: "1", text: "First" },
|
|
192
|
+
{ id: "2", text: "Second" },
|
|
193
|
+
{ id: "3", text: "Third" },
|
|
194
|
+
],
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
// Reorder items
|
|
198
|
+
e.update({
|
|
199
|
+
oldValue: null,
|
|
200
|
+
newValue: [
|
|
201
|
+
{ id: "3", text: "Third" },
|
|
202
|
+
{ id: "1", text: "First" },
|
|
203
|
+
{ id: "2", text: "Second" },
|
|
204
|
+
],
|
|
205
|
+
});
|
|
206
|
+
}}
|
|
207
|
+
>
|
|
208
|
+
<j-for bind="items" key="id">
|
|
209
|
+
<template>
|
|
210
|
+
<j-value bind="each.value.text"></j-value>
|
|
211
|
+
</template>
|
|
212
|
+
</j-for>
|
|
213
|
+
</div>
|
|
214
|
+
`);
|
|
215
|
+
|
|
216
|
+
const items = element.querySelectorAll("j-value");
|
|
217
|
+
assert.equal(items.length, 3);
|
|
218
|
+
assert.equal(items[0].textContent?.trim(), "Third");
|
|
219
|
+
assert.equal(items[1].textContent?.trim(), "First");
|
|
220
|
+
assert.equal(items[2].textContent?.trim(), "Second");
|
|
221
|
+
});
|
|
@@ -109,18 +109,20 @@ export class JositForElement extends HTMLElement {
|
|
|
109
109
|
value: item,
|
|
110
110
|
};
|
|
111
111
|
|
|
112
|
-
|
|
113
|
-
const child = this.children[index + 1]; // skip first child since it should be the template element
|
|
112
|
+
const child = this.children[index + 1]; // skip first child since it should be the template element
|
|
114
113
|
|
|
114
|
+
if (!scope.isConnected) {
|
|
115
115
|
if (child) {
|
|
116
116
|
child.before(scope);
|
|
117
117
|
} else {
|
|
118
118
|
this.append(scope);
|
|
119
119
|
}
|
|
120
|
-
|
|
121
|
-
|
|
120
|
+
} else if (child !== scope) {
|
|
121
|
+
child.before(scope);
|
|
122
122
|
}
|
|
123
123
|
|
|
124
|
+
this.#scopes.set(key, scope);
|
|
125
|
+
|
|
124
126
|
index++;
|
|
125
127
|
}
|
|
126
128
|
|
|
@@ -53,3 +53,38 @@ it("should handle negated tokens correctly", () => {
|
|
|
53
53
|
|
|
54
54
|
assert.equal(element.textContent?.trim(), "Visible Content");
|
|
55
55
|
});
|
|
56
|
+
|
|
57
|
+
it("should render else template when condition is falsy", () => {
|
|
58
|
+
const element = fixtureSync(html`
|
|
59
|
+
<div
|
|
60
|
+
@joist::value=${(e: JoistValueEvent) => {
|
|
61
|
+
e.update({ oldValue: null, newValue: false });
|
|
62
|
+
}}
|
|
63
|
+
>
|
|
64
|
+
<j-if bind="test">
|
|
65
|
+
<template>If Content</template>
|
|
66
|
+
<template else>Else Content</template>
|
|
67
|
+
</j-if>
|
|
68
|
+
</div>
|
|
69
|
+
`);
|
|
70
|
+
|
|
71
|
+
assert.equal(element.textContent?.trim(), "Else Content");
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("should switch between if and else templates", () => {
|
|
75
|
+
const element = fixtureSync(html`
|
|
76
|
+
<div
|
|
77
|
+
@joist::value=${(e: JoistValueEvent) => {
|
|
78
|
+
e.update({ oldValue: null, newValue: false });
|
|
79
|
+
e.update({ oldValue: false, newValue: true });
|
|
80
|
+
}}
|
|
81
|
+
>
|
|
82
|
+
<j-if bind="test">
|
|
83
|
+
<template>If Content</template>
|
|
84
|
+
<template else>Else Content</template>
|
|
85
|
+
</j-if>
|
|
86
|
+
</div>
|
|
87
|
+
`);
|
|
88
|
+
|
|
89
|
+
assert.equal(element.textContent?.trim(), "If Content");
|
|
90
|
+
});
|