@joist/element 4.2.2 → 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 +4 -1
- package/src/lib/attr-changed.test.ts +0 -1
- package/src/lib/query.ts +1 -1
- package/src/lib/templating/README.md +406 -0
- package/src/lib/templating/bind.ts +40 -0
- package/src/lib/templating/define.ts +5 -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 +221 -0
- package/src/lib/templating/elements/for.element.ts +153 -0
- package/src/lib/templating/elements/if.element.test.ts +90 -0
- package/src/lib/templating/elements/if.element.ts +93 -0
- package/src/lib/templating/elements/props.element.test.ts +62 -0
- package/src/lib/templating/elements/props.element.ts +80 -0
- package/src/lib/templating/elements/scope.ts +45 -0
- package/src/lib/templating/elements/value.element.test.ts +20 -0
- package/src/lib/templating/elements/value.element.ts +41 -0
- package/src/lib/templating/events.ts +21 -0
- package/src/lib/templating/token.test.ts +74 -0
- package/src/lib/templating/token.ts +34 -0
- package/src/lib/templating.ts +2 -0
- package/src/lib.ts +1 -0
- package/target/lib/attr-changed.test.js +0 -1
- package/target/lib/attr-changed.test.js.map +1 -1
- package/target/lib/query.d.ts +1 -1
- package/target/lib/templating/bind.d.ts +1 -0
- package/target/lib/templating/bind.js +30 -0
- package/target/lib/templating/bind.js.map +1 -0
- package/target/lib/templating/define.d.ts +5 -0
- package/target/lib/templating/define.js +6 -0
- package/target/lib/templating/define.js.map +1 -0
- 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.d.ts +23 -0
- package/target/lib/templating/elements/for.element.js +171 -0
- package/target/lib/templating/elements/for.element.js.map +1 -0
- package/target/lib/templating/elements/for.element.test.d.ts +2 -0
- package/target/lib/templating/elements/for.element.test.js +153 -0
- package/target/lib/templating/elements/for.element.test.js.map +1 -0
- package/target/lib/templating/elements/if.element.d.ts +12 -0
- package/target/lib/templating/elements/if.element.js +85 -0
- package/target/lib/templating/elements/if.element.js.map +1 -0
- package/target/lib/templating/elements/if.element.test.d.ts +1 -0
- package/target/lib/templating/elements/if.element.test.js +78 -0
- package/target/lib/templating/elements/if.element.test.js.map +1 -0
- package/target/lib/templating/elements/props.element.d.ts +11 -0
- package/target/lib/templating/elements/props.element.js +92 -0
- package/target/lib/templating/elements/props.element.js.map +1 -0
- package/target/lib/templating/elements/props.element.test.d.ts +1 -0
- package/target/lib/templating/elements/props.element.test.js +53 -0
- package/target/lib/templating/elements/props.element.test.js.map +1 -0
- package/target/lib/templating/elements/scope.d.ts +13 -0
- package/target/lib/templating/elements/scope.js +59 -0
- package/target/lib/templating/elements/scope.js.map +1 -0
- package/target/lib/templating/elements/value.element.d.ts +9 -0
- package/target/lib/templating/elements/value.element.js +56 -0
- package/target/lib/templating/elements/value.element.js.map +1 -0
- package/target/lib/templating/elements/value.element.test.d.ts +1 -0
- package/target/lib/templating/elements/value.element.test.js +16 -0
- package/target/lib/templating/elements/value.element.test.js.map +1 -0
- package/target/lib/templating/events.d.ts +12 -0
- package/target/lib/templating/events.js +10 -0
- package/target/lib/templating/events.js.map +1 -0
- package/target/lib/templating/token.d.ts +8 -0
- package/target/lib/templating/token.js +27 -0
- package/target/lib/templating/token.js.map +1 -0
- package/target/lib/templating/token.test.js +56 -0
- package/target/lib/templating/token.test.js.map +1 -0
- package/target/lib/templating.d.ts +2 -0
- package/target/lib/templating.js +3 -0
- package/target/lib/templating.js.map +1 -0
- package/target/lib.d.ts +1 -0
- package/target/lib.js.map +1 -1
- package/src/lib/template.test.ts +0 -123
- package/src/lib/template.ts +0 -130
- package/target/lib/template.d.ts +0 -11
- package/target/lib/template.js +0 -89
- package/target/lib/template.js.map +0 -1
- package/target/lib/template.test.js +0 -91
- package/target/lib/template.test.js.map +0 -1
- /package/target/lib/{template.test.d.ts → templating/token.test.d.ts} +0 -0
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import "./for.element.js";
|
|
2
|
+
import "./value.element.js";
|
|
3
|
+
|
|
4
|
+
import { fixtureSync, html } from "@open-wc/testing";
|
|
5
|
+
import { assert } from "chai";
|
|
6
|
+
|
|
7
|
+
import type { JoistValueEvent } from "../events.js";
|
|
8
|
+
|
|
9
|
+
it("should iterate over an iterable", () => {
|
|
10
|
+
const element = fixtureSync(html`
|
|
11
|
+
<div
|
|
12
|
+
@joist::value=${(e: JoistValueEvent) => {
|
|
13
|
+
e.update({
|
|
14
|
+
oldValue: null,
|
|
15
|
+
newValue: new Set([
|
|
16
|
+
{ id: "123", label: "Hello" },
|
|
17
|
+
{ id: "456", label: "World" },
|
|
18
|
+
]),
|
|
19
|
+
});
|
|
20
|
+
}}
|
|
21
|
+
>
|
|
22
|
+
<ul>
|
|
23
|
+
<j-for bind="items" key="id">
|
|
24
|
+
<template>
|
|
25
|
+
<li>
|
|
26
|
+
<j-value bind="each.value.label"></j-value>
|
|
27
|
+
</li>
|
|
28
|
+
</template>
|
|
29
|
+
</j-for>
|
|
30
|
+
</ul>
|
|
31
|
+
</div>
|
|
32
|
+
`);
|
|
33
|
+
|
|
34
|
+
const listItems = element.querySelectorAll("li");
|
|
35
|
+
|
|
36
|
+
assert.equal(listItems.length, 2);
|
|
37
|
+
assert.equal(listItems[0].textContent?.trim(), "Hello");
|
|
38
|
+
assert.equal(listItems[1].textContent?.trim(), "World");
|
|
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
|
+
});
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { attr } from "../../attr.js";
|
|
2
|
+
import { element } from "../../element.js";
|
|
3
|
+
import { query } from "../../query.js";
|
|
4
|
+
import { css, html } from "../../tags.js";
|
|
5
|
+
|
|
6
|
+
import { bind } from "../bind.js";
|
|
7
|
+
import { JoistValueEvent } from "../events.js";
|
|
8
|
+
import { JToken } from "../token.js";
|
|
9
|
+
|
|
10
|
+
declare global {
|
|
11
|
+
interface HTMLElementTagNameMap {
|
|
12
|
+
"j-for": JositForElement;
|
|
13
|
+
"j-for-scope": JForScope;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface EachCtx<T> {
|
|
18
|
+
value: T | null;
|
|
19
|
+
index: number | null;
|
|
20
|
+
position: number | null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
@element({
|
|
24
|
+
tagName: "j-for-scope",
|
|
25
|
+
// prettier-ignore
|
|
26
|
+
shadowDom: [css`:host{display: contents;}`, html`<slot></slot>`],
|
|
27
|
+
})
|
|
28
|
+
export class JForScope<T = unknown> extends HTMLElement {
|
|
29
|
+
@bind()
|
|
30
|
+
accessor each: EachCtx<T> = {
|
|
31
|
+
value: null,
|
|
32
|
+
index: null,
|
|
33
|
+
position: null,
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
@attr()
|
|
37
|
+
accessor key = "";
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
@element({
|
|
41
|
+
tagName: "j-for",
|
|
42
|
+
// prettier-ignore
|
|
43
|
+
shadowDom: [css`:host{display:contents;}`, html`<slot></slot>`],
|
|
44
|
+
})
|
|
45
|
+
export class JositForElement extends HTMLElement {
|
|
46
|
+
@attr()
|
|
47
|
+
accessor bind = "";
|
|
48
|
+
|
|
49
|
+
@attr()
|
|
50
|
+
accessor key = "";
|
|
51
|
+
|
|
52
|
+
#template = query("template", this);
|
|
53
|
+
#items: Iterable<unknown> = [];
|
|
54
|
+
#scopes = new Map<unknown, JForScope>();
|
|
55
|
+
|
|
56
|
+
connectedCallback(): void {
|
|
57
|
+
const template = this.#template();
|
|
58
|
+
|
|
59
|
+
if (this.firstElementChild !== template) {
|
|
60
|
+
throw new Error("The first Node in j-for needs to be a template");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
let currentScope = template.nextElementSibling;
|
|
64
|
+
|
|
65
|
+
while (currentScope instanceof JForScope) {
|
|
66
|
+
this.#scopes.set(currentScope.key, currentScope);
|
|
67
|
+
currentScope = currentScope.nextElementSibling;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const token = new JToken(this.bind);
|
|
71
|
+
|
|
72
|
+
this.dispatchEvent(
|
|
73
|
+
new JoistValueEvent(token, ({ newValue, oldValue }) => {
|
|
74
|
+
if (newValue !== oldValue) {
|
|
75
|
+
if (isIterable(newValue)) {
|
|
76
|
+
this.#items = newValue;
|
|
77
|
+
} else {
|
|
78
|
+
this.#items = [];
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
this.updateItems();
|
|
82
|
+
}
|
|
83
|
+
}),
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
updateItems(): void {
|
|
88
|
+
const template = this.#template();
|
|
89
|
+
const leftoverScopes = new Map<unknown, JForScope>(this.#scopes);
|
|
90
|
+
|
|
91
|
+
let index = 0;
|
|
92
|
+
|
|
93
|
+
for (const item of this.#items) {
|
|
94
|
+
const key = hasProperty(item, this.key) ? item[this.key] : index;
|
|
95
|
+
|
|
96
|
+
let scope = leftoverScopes.get(key);
|
|
97
|
+
|
|
98
|
+
if (!scope) {
|
|
99
|
+
scope = new JForScope();
|
|
100
|
+
scope.append(document.importNode(template.content, true));
|
|
101
|
+
} else {
|
|
102
|
+
leftoverScopes.delete(key); // Remove from map to track unused scopes
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
scope.key = String(key);
|
|
106
|
+
scope.each = {
|
|
107
|
+
position: index + 1,
|
|
108
|
+
index: index,
|
|
109
|
+
value: item,
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const child = this.children[index + 1]; // skip first child since it should be the template element
|
|
113
|
+
|
|
114
|
+
if (!scope.isConnected) {
|
|
115
|
+
if (child) {
|
|
116
|
+
child.before(scope);
|
|
117
|
+
} else {
|
|
118
|
+
this.append(scope);
|
|
119
|
+
}
|
|
120
|
+
} else if (child !== scope) {
|
|
121
|
+
child.before(scope);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
this.#scopes.set(key, scope);
|
|
125
|
+
|
|
126
|
+
index++;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Remove unused scopes
|
|
130
|
+
for (const scope of leftoverScopes.values()) {
|
|
131
|
+
scope.remove();
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
disconnectedCallback(): void {
|
|
136
|
+
for (const scope of this.#scopes.values()) {
|
|
137
|
+
scope.remove();
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
this.#scopes.clear();
|
|
141
|
+
this.#items = [];
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function isIterable<T = unknown>(obj: any): obj is Iterable<T> {
|
|
146
|
+
return obj != null && typeof obj[Symbol.iterator] === "function";
|
|
147
|
+
}
|
|
148
|
+
function hasProperty(
|
|
149
|
+
item: unknown,
|
|
150
|
+
key: string,
|
|
151
|
+
): item is Record<string, unknown> {
|
|
152
|
+
return Object.prototype.hasOwnProperty.call(item, key);
|
|
153
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import "./if.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 render content when the bind value is truthy", () => {
|
|
9
|
+
const element = fixtureSync(html`
|
|
10
|
+
<div
|
|
11
|
+
@joist::value=${(e: JoistValueEvent) => {
|
|
12
|
+
e.update({ oldValue: null, newValue: true });
|
|
13
|
+
}}
|
|
14
|
+
>
|
|
15
|
+
<j-if bind="test">
|
|
16
|
+
<template>Visible Content</template>
|
|
17
|
+
</j-if>
|
|
18
|
+
</div>
|
|
19
|
+
`);
|
|
20
|
+
|
|
21
|
+
assert.equal(element.textContent?.trim(), "Visible Content");
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("should not render content when the bind value is falsy", () => {
|
|
25
|
+
const element = fixtureSync(html`
|
|
26
|
+
<div
|
|
27
|
+
@joist::value=${(e: JoistValueEvent) => {
|
|
28
|
+
e.update({ oldValue: null, newValue: true });
|
|
29
|
+
e.update({ oldValue: null, newValue: false });
|
|
30
|
+
}}
|
|
31
|
+
>
|
|
32
|
+
<j-if bind="test">
|
|
33
|
+
<template>Visible Content</template>
|
|
34
|
+
</j-if>
|
|
35
|
+
</div>
|
|
36
|
+
`);
|
|
37
|
+
|
|
38
|
+
assert.equal(element.textContent?.trim(), "");
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("should handle negated tokens correctly", () => {
|
|
42
|
+
const element = fixtureSync(html`
|
|
43
|
+
<div
|
|
44
|
+
@joist::value=${(e: JoistValueEvent) => {
|
|
45
|
+
e.update({ oldValue: null, newValue: false });
|
|
46
|
+
}}
|
|
47
|
+
>
|
|
48
|
+
<j-if bind="!test">
|
|
49
|
+
<template>Visible Content</template>
|
|
50
|
+
</j-if>
|
|
51
|
+
</div>
|
|
52
|
+
`);
|
|
53
|
+
|
|
54
|
+
assert.equal(element.textContent?.trim(), "Visible Content");
|
|
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
|
+
});
|
|
@@ -0,0 +1,93 @@
|
|
|
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
|
+
|
|
6
|
+
import { JoistValueEvent } from "../events.js";
|
|
7
|
+
import { JToken } from "../token.js";
|
|
8
|
+
|
|
9
|
+
declare global {
|
|
10
|
+
interface HTMLElementTagNameMap {
|
|
11
|
+
"j-if": JoistIfElement;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
@element({
|
|
16
|
+
tagName: "j-if",
|
|
17
|
+
// prettier-ignore
|
|
18
|
+
shadowDom: [css`:host{display: contents;}`, html`<slot></slot>`],
|
|
19
|
+
})
|
|
20
|
+
export class JoistIfElement extends HTMLElement {
|
|
21
|
+
@attr()
|
|
22
|
+
accessor bind = "";
|
|
23
|
+
|
|
24
|
+
#templates = queryAll<HTMLTemplateElement>("template", this);
|
|
25
|
+
|
|
26
|
+
connectedCallback(): void {
|
|
27
|
+
const templates = Array.from(this.#templates());
|
|
28
|
+
|
|
29
|
+
if (templates.length === 0) {
|
|
30
|
+
throw new Error("j-if requires at least one template element");
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (templates.length > 2) {
|
|
34
|
+
throw new Error("j-if can only have two template elements (if and else)");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (
|
|
38
|
+
templates.length === 2 &&
|
|
39
|
+
!templates.some((t) => t.hasAttribute("else"))
|
|
40
|
+
) {
|
|
41
|
+
throw new Error(
|
|
42
|
+
"When using two templates, one must have the else attribute",
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (templates.length === 2 && templates[0].hasAttribute("else")) {
|
|
47
|
+
// Swap templates to ensure if template is first
|
|
48
|
+
[templates[0], templates[1]] = [templates[1], templates[0]];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// make sure there are no other nodes after the template
|
|
52
|
+
this.#clean();
|
|
53
|
+
|
|
54
|
+
const token = new JToken(this.bind);
|
|
55
|
+
|
|
56
|
+
this.dispatchEvent(
|
|
57
|
+
new JoistValueEvent(token, ({ newValue, oldValue }) => {
|
|
58
|
+
if (newValue !== oldValue) {
|
|
59
|
+
if (typeof newValue === "object" && newValue !== null) {
|
|
60
|
+
this.apply(token.readTokenValueFrom(newValue), token.isNegated);
|
|
61
|
+
} else {
|
|
62
|
+
this.apply(newValue, token.isNegated);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}),
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
apply(value: unknown, isNegative: boolean): void {
|
|
70
|
+
this.#clean();
|
|
71
|
+
|
|
72
|
+
const templates = this.#templates();
|
|
73
|
+
|
|
74
|
+
const shouldShowIf = isNegative ? !value : value;
|
|
75
|
+
const templateToUse = shouldShowIf ? templates[0] : templates[1];
|
|
76
|
+
|
|
77
|
+
if (templateToUse) {
|
|
78
|
+
const content = document.importNode(templateToUse.content, true);
|
|
79
|
+
|
|
80
|
+
this.appendChild(content);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
#clean(): void {
|
|
85
|
+
while (!(this.lastChild instanceof HTMLTemplateElement)) {
|
|
86
|
+
this.lastChild?.remove();
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
disconnectedCallback(): void {
|
|
91
|
+
this.#clean();
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import "./props.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 pass props to child", () => {
|
|
9
|
+
const element = fixtureSync(html`
|
|
10
|
+
<div
|
|
11
|
+
@joist::value=${(e: JoistValueEvent) => {
|
|
12
|
+
if (e.token.bindTo === "href") {
|
|
13
|
+
e.update({
|
|
14
|
+
oldValue: null,
|
|
15
|
+
newValue: "$foo",
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (e.token.bindTo === "target") {
|
|
20
|
+
e.update({
|
|
21
|
+
oldValue: null,
|
|
22
|
+
newValue: {
|
|
23
|
+
value: "_blank",
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
}}
|
|
28
|
+
>
|
|
29
|
+
<j-props>
|
|
30
|
+
<a $href="href" $target="target.value">Hello World</a>
|
|
31
|
+
</j-props>
|
|
32
|
+
</div>
|
|
33
|
+
`);
|
|
34
|
+
|
|
35
|
+
const anchor = element.querySelector("a");
|
|
36
|
+
|
|
37
|
+
assert.equal(anchor?.getAttribute("href"), "$foo");
|
|
38
|
+
assert.equal(anchor?.getAttribute("target"), "_blank");
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("should pass props to specified child", () => {
|
|
42
|
+
const element = fixtureSync(html`
|
|
43
|
+
<div
|
|
44
|
+
@joist::value=${(e: JoistValueEvent) => {
|
|
45
|
+
e.update({
|
|
46
|
+
oldValue: null,
|
|
47
|
+
newValue: "#foo",
|
|
48
|
+
});
|
|
49
|
+
}}
|
|
50
|
+
>
|
|
51
|
+
<j-props>
|
|
52
|
+
<a>Default</a>
|
|
53
|
+
<a id="test" $href="href">Target</a>
|
|
54
|
+
</j-props>
|
|
55
|
+
</div>
|
|
56
|
+
`);
|
|
57
|
+
|
|
58
|
+
const anchor = element.querySelectorAll("a");
|
|
59
|
+
|
|
60
|
+
assert.equal(anchor[0].getAttribute("href"), null);
|
|
61
|
+
assert.equal(anchor[1].getAttribute("href"), "#foo");
|
|
62
|
+
});
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { attr } from "../../attr.js";
|
|
2
|
+
import { element } from "../../element.js";
|
|
3
|
+
import { css, html } from "../../tags.js";
|
|
4
|
+
|
|
5
|
+
import { JoistValueEvent } from "../events.js";
|
|
6
|
+
import { JToken } from "../token.js";
|
|
7
|
+
|
|
8
|
+
export class JAttrToken extends JToken {
|
|
9
|
+
mapTo: string;
|
|
10
|
+
mapsToProp: boolean;
|
|
11
|
+
|
|
12
|
+
constructor(attr: Attr) {
|
|
13
|
+
if (!attr.name.startsWith("$")) {
|
|
14
|
+
throw new Error(
|
|
15
|
+
`Invalid attribute token: ${attr.name}, should start with $`,
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
super(attr.value);
|
|
20
|
+
|
|
21
|
+
this.mapsToProp = attr.name.startsWith("$.");
|
|
22
|
+
|
|
23
|
+
this.mapTo = attr.name.slice(this.mapsToProp ? 2 : 1);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
@element({
|
|
28
|
+
tagName: "j-props",
|
|
29
|
+
// prettier-ignore
|
|
30
|
+
shadowDom: [css`:host{display: contents;}`, html`<slot></slot>`],
|
|
31
|
+
})
|
|
32
|
+
export class JoistIfElement extends HTMLElement {
|
|
33
|
+
@attr()
|
|
34
|
+
accessor target = "";
|
|
35
|
+
|
|
36
|
+
connectedCallback(): void {
|
|
37
|
+
this.#bindProps([this]); // bind own props
|
|
38
|
+
this.#bindProps(this.children); // bind child props
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
#bindProps(children: Iterable<Element>) {
|
|
42
|
+
for (const child of children) {
|
|
43
|
+
for (const attr of child.attributes) {
|
|
44
|
+
if (attr.name.startsWith("$")) {
|
|
45
|
+
const token = new JAttrToken(attr);
|
|
46
|
+
|
|
47
|
+
this.dispatchEvent(
|
|
48
|
+
new JoistValueEvent(token, ({ newValue, oldValue }) => {
|
|
49
|
+
if (newValue === oldValue) {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
let valueToWrite = newValue;
|
|
54
|
+
|
|
55
|
+
if (typeof newValue === "object" && newValue !== null) {
|
|
56
|
+
valueToWrite = token.readTokenValueFrom(newValue);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (token.isNegated) {
|
|
60
|
+
valueToWrite = !valueToWrite;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (token.mapsToProp) {
|
|
64
|
+
Reflect.set(child, token.mapTo, valueToWrite);
|
|
65
|
+
} else {
|
|
66
|
+
if (valueToWrite === true) {
|
|
67
|
+
child.setAttribute(token.mapTo, "");
|
|
68
|
+
} else if (valueToWrite === false) {
|
|
69
|
+
child.removeAttribute(token.mapTo);
|
|
70
|
+
} else {
|
|
71
|
+
child.setAttribute(token.mapTo, String(valueToWrite));
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}),
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|