@joist/templating 4.2.4-next.1 → 4.2.4-next.11

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 (74) hide show
  1. package/README.md +43 -164
  2. package/package.json +1 -1
  3. package/src/lib/bind.test.ts +72 -0
  4. package/src/lib/bind.ts +32 -9
  5. package/src/lib/define.ts +1 -0
  6. package/src/lib/elements/async.element.test.ts +90 -0
  7. package/src/lib/elements/async.element.ts +13 -2
  8. package/src/lib/elements/bind.element.test.ts +20 -2
  9. package/src/lib/elements/bind.element.ts +16 -10
  10. package/src/lib/elements/for.element.ts +16 -9
  11. package/src/lib/elements/if.element.test.ts +210 -0
  12. package/src/lib/elements/if.element.ts +16 -11
  13. package/src/lib/elements/scope.element.test.ts +32 -0
  14. package/src/lib/elements/scope.element.ts +19 -0
  15. package/src/lib/elements/value.element.test.ts +28 -0
  16. package/src/lib/elements/value.element.ts +9 -11
  17. package/src/lib/events.ts +10 -5
  18. package/src/lib/expression.test.ts +204 -0
  19. package/src/lib/expression.ts +179 -0
  20. package/target/lib/bind.d.ts +5 -1
  21. package/target/lib/bind.js +22 -8
  22. package/target/lib/bind.js.map +1 -1
  23. package/target/lib/bind.test.js +76 -0
  24. package/target/lib/bind.test.js.map +1 -0
  25. package/target/lib/define.d.ts +1 -0
  26. package/target/lib/define.js +1 -0
  27. package/target/lib/define.js.map +1 -1
  28. package/target/lib/elements/async.element.js +11 -2
  29. package/target/lib/elements/async.element.js.map +1 -1
  30. package/target/lib/elements/async.element.test.js +76 -0
  31. package/target/lib/elements/async.element.test.js.map +1 -1
  32. package/target/lib/elements/bind.element.d.ts +8 -3
  33. package/target/lib/elements/bind.element.js +13 -12
  34. package/target/lib/elements/bind.element.js.map +1 -1
  35. package/target/lib/elements/bind.element.test.js +17 -2
  36. package/target/lib/elements/bind.element.test.js.map +1 -1
  37. package/target/lib/elements/for.element.d.ts +1 -1
  38. package/target/lib/elements/for.element.js +13 -7
  39. package/target/lib/elements/for.element.js.map +1 -1
  40. package/target/lib/elements/if.element.js +12 -12
  41. package/target/lib/elements/if.element.js.map +1 -1
  42. package/target/lib/elements/if.element.test.js +184 -0
  43. package/target/lib/elements/if.element.test.js.map +1 -1
  44. package/target/lib/elements/scope.element.d.ts +8 -0
  45. package/target/lib/elements/scope.element.js +38 -0
  46. package/target/lib/elements/scope.element.js.map +1 -0
  47. package/target/lib/elements/scope.element.test.d.ts +2 -0
  48. package/target/lib/elements/scope.element.test.js +25 -0
  49. package/target/lib/elements/scope.element.test.js.map +1 -0
  50. package/target/lib/elements/value.element.js +7 -11
  51. package/target/lib/elements/value.element.js.map +1 -1
  52. package/target/lib/elements/value.element.test.js +24 -0
  53. package/target/lib/elements/value.element.test.js.map +1 -1
  54. package/target/lib/events.d.ts +8 -4
  55. package/target/lib/events.js +3 -3
  56. package/target/lib/events.js.map +1 -1
  57. package/target/lib/expression.d.ts +13 -0
  58. package/target/lib/expression.js +87 -0
  59. package/target/lib/expression.js.map +1 -0
  60. package/target/lib/expression.test.d.ts +1 -0
  61. package/target/lib/expression.test.js +171 -0
  62. package/target/lib/expression.test.js.map +1 -0
  63. package/src/lib/elements/scope.ts +0 -39
  64. package/src/lib/token.test.ts +0 -74
  65. package/src/lib/token.ts +0 -34
  66. package/target/lib/elements/scope.d.ts +0 -13
  67. package/target/lib/elements/scope.js +0 -56
  68. package/target/lib/elements/scope.js.map +0 -1
  69. package/target/lib/token.d.ts +0 -8
  70. package/target/lib/token.js +0 -27
  71. package/target/lib/token.js.map +0 -1
  72. package/target/lib/token.test.js +0 -56
  73. package/target/lib/token.test.js.map +0 -1
  74. /package/target/lib/{token.test.d.ts → bind.test.d.ts} +0 -0
@@ -2,7 +2,7 @@ import { attr, element, query, css, html } from "@joist/element";
2
2
 
3
3
  import { bind } from "../bind.js";
4
4
  import { JoistValueEvent } from "../events.js";
5
- import { JToken } from "../token.js";
5
+ import { JExpression } from "../expression.js";
6
6
 
7
7
  declare global {
8
8
  interface HTMLElementTagNameMap {
@@ -31,7 +31,7 @@ export class JForScope<T = unknown> extends HTMLElement {
31
31
  };
32
32
 
33
33
  @attr()
34
- accessor key = "";
34
+ accessor key: unknown;
35
35
  }
36
36
 
37
37
  @element({
@@ -64,7 +64,7 @@ export class JositForElement extends HTMLElement {
64
64
  currentScope = currentScope.nextElementSibling;
65
65
  }
66
66
 
67
- const token = new JToken(this.bind);
67
+ const token = new JExpression(this.bind);
68
68
 
69
69
  this.dispatchEvent(
70
70
  new JoistValueEvent(token, ({ newValue, oldValue }) => {
@@ -98,11 +98,15 @@ export class JositForElement extends HTMLElement {
98
98
 
99
99
  let index = 0;
100
100
  for (const value of this.#items) {
101
- const key = keyProperty && hasProperty(value, keyProperty) ? value[keyProperty] : index;
101
+ let key: unknown = index;
102
+
103
+ if (keyProperty && hasProperty(value, keyProperty)) {
104
+ key = value[keyProperty];
105
+ }
102
106
 
103
107
  const scope = new JForScope();
104
108
  scope.append(document.importNode(templateContent, true));
105
- scope.key = String(key);
109
+ scope.key = key;
106
110
  scope.each = { position: index + 1, index, value };
107
111
 
108
112
  fragment.appendChild(scope);
@@ -118,13 +122,16 @@ export class JositForElement extends HTMLElement {
118
122
  updateItems(): void {
119
123
  const template = this.#template();
120
124
  const leftoverScopes = new Map<unknown, JForScope>(this.#scopes);
121
- const keyProperty = this.key; // Cache the key property
125
+ const keyProperty = this.key;
122
126
 
123
127
  let index = 0;
124
128
 
125
129
  for (const value of this.#items) {
126
- // Optimize key lookup by caching the property check
127
- const key = keyProperty && hasProperty(value, keyProperty) ? value[keyProperty] : index;
130
+ let key: unknown = index;
131
+
132
+ if (keyProperty && hasProperty(value, keyProperty)) {
133
+ key = value[keyProperty];
134
+ }
128
135
 
129
136
  let scope = leftoverScopes.get(key);
130
137
 
@@ -138,7 +145,7 @@ export class JositForElement extends HTMLElement {
138
145
 
139
146
  // Only update if values have changed
140
147
  if (scope.key !== key || scope.each.value !== value) {
141
- scope.key = String(key);
148
+ scope.key = key;
142
149
  scope.each = { position: index + 1, index, value };
143
150
  }
144
151
 
@@ -88,3 +88,213 @@ it("should switch between if and else templates", () => {
88
88
 
89
89
  assert.equal(element.textContent?.trim(), "If Content");
90
90
  });
91
+
92
+ it("should handle equality comparison", () => {
93
+ const element = fixtureSync(html`
94
+ <div
95
+ @joist::value=${(e: JoistValueEvent) => {
96
+ e.update({ oldValue: null, newValue: { status: "active" } });
97
+ }}
98
+ >
99
+ <j-if bind="example.status == active">
100
+ <template>Status is Active</template>
101
+ </j-if>
102
+ </div>
103
+ `);
104
+
105
+ assert.equal(element.textContent?.trim(), "Status is Active");
106
+ });
107
+
108
+ it("should handle greater than comparison", () => {
109
+ const element = fixtureSync(html`
110
+ <div
111
+ @joist::value=${(e: JoistValueEvent) => {
112
+ e.update({ oldValue: null, newValue: { count: 10 } });
113
+ }}
114
+ >
115
+ <j-if bind="example.count > 5">
116
+ <template>Count is Greater Than 5</template>
117
+ </j-if>
118
+ </div>
119
+ `);
120
+
121
+ assert.equal(element.textContent?.trim(), "Count is Greater Than 5");
122
+ });
123
+
124
+ it("should handle less than comparison", () => {
125
+ const element = fixtureSync(html`
126
+ <div
127
+ @joist::value=${(e: JoistValueEvent) => {
128
+ e.update({ oldValue: null, newValue: { score: 75 } });
129
+ }}
130
+ >
131
+ <j-if bind="example.score < 100">
132
+ <template>Score is Less Than 100</template>
133
+ </j-if>
134
+ </div>
135
+ `);
136
+
137
+ assert.equal(element.textContent?.trim(), "Score is Less Than 100");
138
+ });
139
+
140
+ it("should handle nested path comparisons", () => {
141
+ const element = fixtureSync(html`
142
+ <div
143
+ @joist::value=${(e: JoistValueEvent) => {
144
+ e.update({ oldValue: null, newValue: { user: { score: 150 } } });
145
+ }}
146
+ >
147
+ <j-if bind="example.user.score > 100">
148
+ <template>User Score is Above 100</template>
149
+ </j-if>
150
+ </div>
151
+ `);
152
+
153
+ assert.equal(element.textContent?.trim(), "User Score is Above 100");
154
+ });
155
+
156
+ it("should handle negated comparisons", () => {
157
+ const element = fixtureSync(html`
158
+ <div
159
+ @joist::value=${(e: JoistValueEvent) => {
160
+ e.update({ oldValue: null, newValue: { status: "inactive" } });
161
+ }}
162
+ >
163
+ <j-if bind="!example.status == active">
164
+ <template>Status is Not Active</template>
165
+ </j-if>
166
+ </div>
167
+ `);
168
+
169
+ assert.equal(element.textContent?.trim(), "Status is Not Active");
170
+ });
171
+
172
+ it("should handle string number comparisons", () => {
173
+ const element = fixtureSync(html`
174
+ <div
175
+ @joist::value=${(e: JoistValueEvent) => {
176
+ e.update({ oldValue: null, newValue: { count: "10" } });
177
+ }}
178
+ >
179
+ <j-if bind="example.count > 5">
180
+ <template>String Count is Greater Than 5</template>
181
+ </j-if>
182
+ </div>
183
+ `);
184
+
185
+ assert.equal(element.textContent?.trim(), "String Count is Greater Than 5");
186
+ });
187
+
188
+ it("should handle undefined values in comparisons", () => {
189
+ const element = fixtureSync(html`
190
+ <div
191
+ @joist::value=${(e: JoistValueEvent) => {
192
+ e.update({ oldValue: null, newValue: { count: undefined } });
193
+ }}
194
+ >
195
+ <j-if bind="example.count > 5">
196
+ <template>Count is Greater Than 5</template>
197
+ </j-if>
198
+ </div>
199
+ `);
200
+
201
+ assert.equal(element.textContent?.trim(), "");
202
+ });
203
+
204
+ it("should handle not equal comparison", () => {
205
+ const element = fixtureSync(html`
206
+ <div
207
+ @joist::value=${(e: JoistValueEvent) => {
208
+ e.update({ oldValue: null, newValue: { status: "inactive" } });
209
+ }}
210
+ >
211
+ <j-if bind="example.status != active">
212
+ <template>Status is Not Active</template>
213
+ </j-if>
214
+ </div>
215
+ `);
216
+
217
+ assert.equal(element.textContent?.trim(), "Status is Not Active");
218
+ });
219
+
220
+ it("should handle not equal comparison with matching value", () => {
221
+ const element = fixtureSync(html`
222
+ <div
223
+ @joist::value=${(e: JoistValueEvent) => {
224
+ e.update({ oldValue: null, newValue: { status: "active" } });
225
+ }}
226
+ >
227
+ <j-if bind="example.status != active">
228
+ <template>Status is Not Active</template>
229
+ </j-if>
230
+ </div>
231
+ `);
232
+
233
+ assert.equal(element.textContent?.trim(), "");
234
+ });
235
+
236
+ it("should handle not equal comparison with string numbers", () => {
237
+ const element = fixtureSync(html`
238
+ <div
239
+ @joist::value=${(e: JoistValueEvent) => {
240
+ e.update({ oldValue: null, newValue: { count: "10" } });
241
+ }}
242
+ >
243
+ <j-if bind="example.count != 5">
244
+ <template>Count is Not 5</template>
245
+ </j-if>
246
+ </div>
247
+ `);
248
+
249
+ assert.equal(element.textContent?.trim(), "Count is Not 5");
250
+ });
251
+
252
+ it("should handle not equal comparison with undefined", () => {
253
+ const element = fixtureSync(html`
254
+ <div
255
+ @joist::value=${(e: JoistValueEvent) => {
256
+ e.update({ oldValue: null, newValue: { status: undefined } });
257
+ }}
258
+ >
259
+ <j-if bind="example.status != active">
260
+ <template>Status is Not Active</template>
261
+ </j-if>
262
+ </div>
263
+ `);
264
+
265
+ assert.equal(element.textContent?.trim(), "Status is Not Active");
266
+ });
267
+
268
+ it("should handle array length", () => {
269
+ const element = fixtureSync(html`
270
+ <div
271
+ @joist::value=${(e: JoistValueEvent) => {
272
+ e.update({ oldValue: null, newValue: [0, 1, 2] });
273
+ }}
274
+ >
275
+ <j-if bind="example.length">
276
+ <template>Array has length</template>
277
+ <template else>Array has no length</template>
278
+ </j-if>
279
+ </div>
280
+ `);
281
+
282
+ assert.equal(element.textContent?.trim(), "Array has length");
283
+ });
284
+
285
+ it("should handle a first change even if the value is the same", () => {
286
+ const element = fixtureSync(html`
287
+ <div
288
+ @joist::value=${(e: JoistValueEvent) => {
289
+ e.update({ oldValue: null, newValue: null, firstChange: true });
290
+ }}
291
+ >
292
+ <j-if bind="example.length">
293
+ <template>Array has length</template>
294
+ <template else>Array has no length</template>
295
+ </j-if>
296
+ </div>
297
+ `);
298
+
299
+ assert.equal(element.textContent?.trim(), "Array has no length");
300
+ });
@@ -1,7 +1,7 @@
1
1
  import { attr, element, queryAll, css, html } from "@joist/element";
2
2
 
3
3
  import { JoistValueEvent } from "../events.js";
4
- import { JToken } from "../token.js";
4
+ import { JExpression } from "../expression.js";
5
5
 
6
6
  declare global {
7
7
  interface HTMLElementTagNameMap {
@@ -20,6 +20,8 @@ export class JoistIfElement extends HTMLElement {
20
20
 
21
21
  #templates = queryAll<HTMLTemplateElement>("template", this);
22
22
 
23
+ #shouldShowIf: boolean | null = null;
24
+
23
25
  connectedCallback(): void {
24
26
  const templates = Array.from(this.#templates());
25
27
 
@@ -43,28 +45,31 @@ export class JoistIfElement extends HTMLElement {
43
45
  // make sure there are no other nodes after the template
44
46
  this.#clean();
45
47
 
46
- const token = new JToken(this.bind);
48
+ const token = new JExpression(this.bind);
47
49
 
48
50
  this.dispatchEvent(
49
- new JoistValueEvent(token, ({ newValue, oldValue }) => {
50
- if (newValue !== oldValue) {
51
- if (typeof newValue === "object" && newValue !== null) {
52
- this.apply(token.readTokenValueFrom(newValue), token.isNegated);
53
- } else {
54
- this.apply(newValue, token.isNegated);
55
- }
51
+ new JoistValueEvent(token, ({ newValue, oldValue, firstChange }) => {
52
+ if (firstChange || newValue !== oldValue) {
53
+ this.apply(token.evaluate(newValue), token.isNegated);
56
54
  }
57
55
  }),
58
56
  );
59
57
  }
60
58
 
61
59
  apply(value: unknown, isNegative: boolean): void {
60
+ const shouldShowIf = isNegative ? !value : !!value;
61
+
62
+ if (shouldShowIf === this.#shouldShowIf) {
63
+ return;
64
+ }
65
+
66
+ this.#shouldShowIf = shouldShowIf;
67
+
62
68
  this.#clean();
63
69
 
64
70
  const templates = this.#templates();
65
71
 
66
- const shouldShowIf = isNegative ? !value : value;
67
- const templateToUse = shouldShowIf ? templates[0] : templates[1];
72
+ const templateToUse = this.#shouldShowIf ? templates[0] : templates[1];
68
73
 
69
74
  if (templateToUse) {
70
75
  const content = document.importNode(templateToUse.content, true);
@@ -0,0 +1,32 @@
1
+ import "./scope.element.js";
2
+ import "./value.element.js";
3
+
4
+ import { fixtureSync, html } from "@open-wc/testing";
5
+ import { assert } from "chai";
6
+ import { JoistScopeElement } from "./scope.element.js";
7
+
8
+ describe("j-scope", () => {
9
+ it("should render its children", () => {
10
+ const element = fixtureSync<JoistScopeElement>(html`
11
+ <j-scope>
12
+ <div>Test Content</div>
13
+ </j-scope>
14
+ `);
15
+
16
+ assert.equal(element.textContent?.trim(), "Test Content");
17
+ });
18
+
19
+ it("should set and get scope property", async () => {
20
+ const element = fixtureSync<JoistScopeElement>(html`
21
+ <j-scope>
22
+ <j-val bind="scope.foo"></j-val>
23
+ </j-scope>
24
+ `);
25
+
26
+ element.scope = { foo: "bar" };
27
+
28
+ await Promise.resolve();
29
+
30
+ assert.equal(element.textContent?.trim(), "bar");
31
+ });
32
+ });
@@ -0,0 +1,19 @@
1
+ import { element, css, html } from "@joist/element";
2
+
3
+ import { bind } from "../bind.js";
4
+
5
+ declare global {
6
+ interface HTMLElementTagNameMap {
7
+ "j-scope": JoistScopeElement;
8
+ }
9
+ }
10
+
11
+ @element({
12
+ tagName: "j-scope",
13
+ // prettier-ignore
14
+ shadowDom: [css`:host{display: contents;}`, html`<slot></slot>`],
15
+ })
16
+ export class JoistScopeElement extends HTMLElement {
17
+ @bind()
18
+ accessor scope: any = null;
19
+ }
@@ -18,3 +18,31 @@ it("should render content when the bind value is truthy", () => {
18
18
 
19
19
  assert.equal(element.textContent?.trim(), "Hello World");
20
20
  });
21
+
22
+ it("should not write null values to textContent", () => {
23
+ const element = fixtureSync(html`
24
+ <div
25
+ @joist::value=${(e: JoistValueEvent) => {
26
+ e.update({ oldValue: "Hello", newValue: null });
27
+ }}
28
+ >
29
+ <j-val bind="test">Hello World</j-val>
30
+ </div>
31
+ `);
32
+
33
+ assert.equal(element.textContent?.trim(), "Hello World");
34
+ });
35
+
36
+ it("should not write undefined values to textContent", () => {
37
+ const element = fixtureSync(html`
38
+ <div
39
+ @joist::value=${(e: JoistValueEvent) => {
40
+ e.update({ oldValue: "Hello", newValue: undefined });
41
+ }}
42
+ >
43
+ <j-val bind="test">Hello World</j-val>
44
+ </div>
45
+ `);
46
+
47
+ assert.equal(element.textContent?.trim(), "Hello World");
48
+ });
@@ -1,6 +1,6 @@
1
1
  import { attr, element, css, html } from "@joist/element";
2
2
  import { JoistValueEvent } from "../events.js";
3
- import { JToken } from "../token.js";
3
+ import { JExpression } from "../expression.js";
4
4
 
5
5
  declare global {
6
6
  interface HTMLElementTagNameMap {
@@ -18,20 +18,18 @@ export class JoistValueElement extends HTMLElement {
18
18
  accessor bind = "";
19
19
 
20
20
  connectedCallback(): void {
21
- const token = new JToken(this.bind);
21
+ const token = new JExpression(this.bind);
22
22
 
23
23
  this.dispatchEvent(
24
24
  new JoistValueEvent(token, (value) => {
25
- let valueToWrite: string;
25
+ const valueToWrite = token.evaluate(value.newValue);
26
26
 
27
- if (typeof value.newValue === "object" && value.newValue !== null) {
28
- valueToWrite = String(token.readTokenValueFrom(value.newValue));
29
- } else {
30
- valueToWrite = String(value.newValue);
31
- }
32
-
33
- if (this.textContent !== valueToWrite) {
34
- this.textContent = valueToWrite;
27
+ if (
28
+ valueToWrite !== null &&
29
+ valueToWrite !== undefined &&
30
+ this.textContent !== valueToWrite
31
+ ) {
32
+ this.textContent = String(valueToWrite);
35
33
  }
36
34
  }),
37
35
  );
package/src/lib/events.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import type { Change } from "@joist/observable";
2
2
 
3
- import type { JToken } from "./token.js";
3
+ import type { JExpression } from "./expression.js";
4
4
 
5
5
  declare global {
6
6
  interface HTMLElementEventMap {
@@ -8,14 +8,19 @@ declare global {
8
8
  }
9
9
  }
10
10
 
11
+ export interface BindChange<T> extends Change<T> {
12
+ alwaysUpdate?: boolean;
13
+ firstChange?: boolean;
14
+ }
15
+
11
16
  export class JoistValueEvent extends Event {
12
- readonly token: JToken;
13
- readonly update: (value: Change<unknown>) => void;
17
+ readonly expression: JExpression;
18
+ readonly update: (value: BindChange<unknown>) => void;
14
19
 
15
- constructor(bindTo: JToken, update: (value: Change<unknown>) => void) {
20
+ constructor(expression: JExpression, update: (value: BindChange<unknown>) => void) {
16
21
  super("joist::value", { bubbles: true, composed: true });
17
22
 
18
- this.token = bindTo;
23
+ this.expression = expression;
19
24
  this.update = update;
20
25
  }
21
26
  }