@joist/templating 4.2.4-next.3 → 4.2.4-next.5

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 (54) 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 +7 -4
  5. package/src/lib/elements/async.element.ts +2 -2
  6. package/src/lib/elements/bind.element.test.ts +2 -2
  7. package/src/lib/elements/bind.element.ts +4 -4
  8. package/src/lib/elements/for.element.ts +16 -9
  9. package/src/lib/elements/if.element.test.ts +266 -56
  10. package/src/lib/elements/if.element.ts +5 -5
  11. package/src/lib/elements/scope.ts +1 -1
  12. package/src/lib/elements/value.element.ts +8 -4
  13. package/src/lib/events.ts +5 -4
  14. package/src/lib/expression.test.ts +204 -0
  15. package/src/lib/expression.ts +179 -0
  16. package/target/lib/bind.js +6 -4
  17. package/target/lib/bind.js.map +1 -1
  18. package/target/lib/bind.test.js +76 -0
  19. package/target/lib/bind.test.js.map +1 -0
  20. package/target/lib/elements/async.element.js +2 -2
  21. package/target/lib/elements/async.element.js.map +1 -1
  22. package/target/lib/elements/bind.element.d.ts +2 -2
  23. package/target/lib/elements/bind.element.js +3 -3
  24. package/target/lib/elements/bind.element.js.map +1 -1
  25. package/target/lib/elements/bind.element.test.js +2 -2
  26. package/target/lib/elements/bind.element.test.js.map +1 -1
  27. package/target/lib/elements/for.element.d.ts +1 -1
  28. package/target/lib/elements/for.element.js +13 -7
  29. package/target/lib/elements/for.element.js.map +1 -1
  30. package/target/lib/elements/if.element.js +5 -5
  31. package/target/lib/elements/if.element.js.map +1 -1
  32. package/target/lib/elements/if.element.test.js +12 -56
  33. package/target/lib/elements/if.element.test.js.map +1 -1
  34. package/target/lib/elements/scope.js +1 -1
  35. package/target/lib/elements/scope.js.map +1 -1
  36. package/target/lib/elements/value.element.js +6 -4
  37. package/target/lib/elements/value.element.js.map +1 -1
  38. package/target/lib/events.d.ts +4 -3
  39. package/target/lib/events.js +3 -3
  40. package/target/lib/events.js.map +1 -1
  41. package/target/lib/expression.d.ts +13 -0
  42. package/target/lib/expression.js +87 -0
  43. package/target/lib/expression.js.map +1 -0
  44. package/target/lib/expression.test.d.ts +1 -0
  45. package/target/lib/expression.test.js +171 -0
  46. package/target/lib/expression.test.js.map +1 -0
  47. package/src/lib/token.test.ts +0 -68
  48. package/src/lib/token.ts +0 -36
  49. package/target/lib/token.d.ts +0 -8
  50. package/target/lib/token.js +0 -29
  51. package/target/lib/token.js.map +0 -1
  52. package/target/lib/token.test.js +0 -56
  53. package/target/lib/token.test.js.map +0 -1
  54. /package/target/lib/{token.test.d.ts → bind.test.d.ts} +0 -0
package/README.md CHANGED
@@ -7,7 +7,6 @@ The Joist templating system provides a powerful and flexible way to handle data
7
7
  - [Core Components](#core-components)
8
8
  - [Built-in Template Elements](#built-in-template-elements)
9
9
  - [Complete Example](#complete-example)
10
- - [Troubleshooting](#troubleshooting)
11
10
 
12
11
  ## Core Components
13
12
 
@@ -20,7 +19,7 @@ import { bind } from "@joist/templating";
20
19
 
21
20
  class MyElement extends HTMLElement {
22
21
  @bind()
23
- accessor myProperty: string;
22
+ accessor myProperty = "";
24
23
  }
25
24
  ```
26
25
 
@@ -36,33 +35,13 @@ class MyElement extends HTMLElement {
36
35
  @observe()
37
36
  assessor value = "Hello World";
38
37
 
39
- @bind((instance) => instance.value.toUpperCase())
38
+ @bind({
39
+ compute: (i) => i.value.toUpperCase()
40
+ })
40
41
  accessor formattedValue = "";
41
42
  }
42
43
  ```
43
44
 
44
- ### Token System (`token.ts`)
45
-
46
- The `JToken` class handles parsing and evaluation of binding expressions. It supports:
47
-
48
- NOTE: Most of the time you will not be using this yourself.
49
-
50
- - Simple property bindings: `propertyName`
51
- - Nested property access: `user.profile.name`
52
- - Negation operator: `!isVisible`
53
- - Array access: `items.0.name`
54
-
55
- Example usage:
56
-
57
- ```typescript
58
- const token = new JToken("user.name");
59
- const value = token.readTokenValueFrom(context);
60
-
61
- // With negation
62
- const negatedToken = new JToken("!isVisible");
63
- const isHidden = negatedToken.readTokenValueFrom(context);
64
- ```
65
-
66
45
  ## Built-in Template Elements
67
46
 
68
47
  Joist provides several built-in template elements for common templating needs:
@@ -82,7 +61,7 @@ Displays a bound value as text content:
82
61
  <j-val bind="user.profile.address.city"></j-val>
83
62
 
84
63
  <!-- With array access -->
85
- <j-val bind="items[0].name"></j-val>
64
+ <j-val bind="items.0.name"></j-val>
86
65
  ```
87
66
 
88
67
  ### Conditional Rendering (`j-if`)
@@ -104,6 +83,38 @@ Conditionally renders content based on a boolean expression:
104
83
  </template>
105
84
  </j-if>
106
85
 
86
+ <!-- With comparison operators -->
87
+ <j-if bind="status == active">
88
+ <template>
89
+ <div>Status is active</div>
90
+ </template>
91
+ </j-if>
92
+
93
+ <j-if bind="status != active">
94
+ <template>
95
+ <div>Status is not active</div>
96
+ </template>
97
+ </j-if>
98
+
99
+ <j-if bind="count > 5">
100
+ <template>
101
+ <div>Count is greater than 5</div>
102
+ </template>
103
+ </j-if>
104
+
105
+ <j-if bind="score < 100">
106
+ <template>
107
+ <div>Score is less than 100</div>
108
+ </template>
109
+ </j-if>
110
+
111
+ <!-- With nested paths -->
112
+ <j-if bind="user.score > 100">
113
+ <template>
114
+ <div>User's score is above 100</div>
115
+ </template>
116
+ </j-if>
117
+
107
118
  <!-- With else template -->
108
119
  <j-if bind="isLoggedIn">
109
120
  <template>
@@ -119,6 +130,12 @@ The `j-if` element supports:
119
130
 
120
131
  - Boolean expressions for conditional rendering
121
132
  - Negation operator (`!`) for inverse conditions
133
+ - Comparison operators:
134
+ - Equality (`==`): `status == active`
135
+ - Inequality (`!=`): `status != active`
136
+ - Greater than (`>`): `count > 5`
137
+ - Less than (`<`): `score < 100`
138
+ - Nested property paths: `user.score > 100`
122
139
  - Optional `else` template for fallback content
123
140
  - Automatic cleanup of removed content
124
141
 
@@ -258,141 +275,3 @@ The `j-async` element supports:
258
275
  - Promise handling with automatic state transitions
259
276
  - Loading, success, and error templates
260
277
  - State object with typed data and error fields
261
-
262
- ## Troubleshooting
263
-
264
- ### Common Issues
265
-
266
- 1. **Binding Not Updating**
267
-
268
- - Check if the property is decorated with `@bind()`
269
- - Verify the binding expression is correct
270
- - Ensure the property is being updated correctly
271
-
272
- 2. **List Rendering Issues**
273
-
274
- - Verify the `key` attribute is unique and stable
275
- - Check if the list items are properly structured
276
- - Ensure the binding expression matches the data structure
277
-
278
- 3. **Async State Problems**
279
- - Verify the Promise is properly resolved/rejected
280
- - Check if all required templates are present
281
- - Ensure error handling is implemented
282
-
283
- ## Manual Value Handling
284
-
285
- 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:
286
-
287
- ```typescript
288
- class MyElement extends HTMLElement {
289
- connectedCallback() {
290
- // Listen for value requests
291
- this.addEventListener("joist::value", (e) => {
292
- const token = e.token;
293
-
294
- // Handle the value request
295
- if (token.bindTo === "myValue") {
296
- // Update the value
297
- e.update({
298
- oldValue: this.myValue,
299
- newValue: this.myValue,
300
- });
301
- }
302
- });
303
- }
304
- }
305
- ```
306
-
307
- ## Complete Example
308
-
309
- Here's a complete todo application in a single component:
310
-
311
- ```typescript
312
- import { bind } from "@joist/templating";
313
- import { element, html, css, listen, query } from "@joist/element";
314
-
315
- interface Todo {
316
- id: string;
317
- text: string;
318
- }
319
-
320
- @element({
321
- tagName: "todo-list",
322
- shadowDom: [
323
- css`
324
- :host {
325
- display: block;
326
- max-width: 600px;
327
- margin: 2rem auto;
328
- }
329
- .form {
330
- display: flex;
331
- gap: 1rem;
332
- }
333
- .todo-item {
334
- align-items: center;
335
- display: flex;
336
- gap: 0.5rem;
337
- margin: 0.5rem 0;
338
- }
339
- .todo-text {
340
- flex: 1;
341
- }
342
- `,
343
- html`
344
- <form class="form">
345
- <input type="text" placeholder="What needs to be done?" />
346
- <button type="submit">Add</button>
347
- </form>
348
-
349
- <j-if bind="!todos.length">
350
- <template>
351
- <p>No todos yet!</p>
352
- </template>
353
- </j-if>
354
-
355
- <j-for id="todos" bind="todos" key="id">
356
- <template>
357
- <div class="todo-item">
358
- <j-val class="todo-text" bind="each.value.text"></j-val>
359
-
360
- <j-bind attrs="data-id:each.value.id">
361
- <button>×</button>
362
- </j-bind>
363
- </div>
364
- </template>
365
- </j-for>
366
-
367
- <j-val bind="todos.length"></j-val> remaining
368
- `,
369
- ],
370
- })
371
- export class TodoList extends HTMLElement {
372
- @bind()
373
- accessor todos: Todo[] = [];
374
-
375
- #nextId = 1;
376
- #input = query("input");
377
-
378
- @listen("submit", "form")
379
- onSubmit(e: SubmitEvent) {
380
- e.preventDefault();
381
-
382
- const input = this.#input();
383
-
384
- this.todos = [...this.todos, { id: String(this.#nextId++), text: input.value.trim() }];
385
-
386
- input.value = "";
387
- }
388
-
389
- @listen("click", "#todos")
390
- onDelete(e: Event) {
391
- if (e.target instanceof HTMLButtonElement) {
392
- const id = Number(e.target.dataset.id);
393
-
394
- this.todos = this.todos.filter((todo) => todo.id !== id);
395
- }
396
- }
397
- }
398
- ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@joist/templating",
3
- "version": "4.2.4-next.3",
3
+ "version": "4.2.4-next.5",
4
4
  "type": "module",
5
5
  "main": "./target/lib.js",
6
6
  "module": "./target/lib.js",
@@ -0,0 +1,72 @@
1
+ import { assert } from "chai";
2
+ import { bind } from "./bind.js";
3
+ import { JoistValueEvent } from "./events.js";
4
+ import { JExpression } from "./expression.js";
5
+
6
+ describe("bind decorator", () => {
7
+ class TestElement extends HTMLElement {
8
+ @bind()
9
+ accessor value = "initial";
10
+
11
+ @bind({ alwaysUpdate: true })
12
+ accessor alwaysUpdateValue = "initial";
13
+ }
14
+
15
+ customElements.define("test-element", TestElement);
16
+
17
+ it("should initialize with default value", () => {
18
+ const element = new TestElement();
19
+ assert.equal(element.value, "initial");
20
+ });
21
+
22
+ it("should update value and trigger binding", async () => {
23
+ const element = new TestElement();
24
+ let oldValue: unknown = null;
25
+ let newValue: unknown = null;
26
+
27
+ element.dispatchEvent(
28
+ new JoistValueEvent(new JExpression("value"), (update) => {
29
+ oldValue = update.oldValue;
30
+ newValue = update.newValue;
31
+ }),
32
+ );
33
+
34
+ assert.equal(oldValue, null);
35
+ assert.equal(newValue, "initial");
36
+
37
+ element.value = "updated";
38
+
39
+ await Promise.resolve();
40
+
41
+ assert.equal(oldValue, "initial");
42
+ assert.equal(newValue, "updated");
43
+ });
44
+
45
+ it("should trigger binding on every change with alwaysUpdate option", async () => {
46
+ const element = new TestElement();
47
+ let bindingCount = 0;
48
+ let oldValue: unknown;
49
+ let newValue: unknown;
50
+
51
+ element.dispatchEvent(
52
+ new JoistValueEvent(new JExpression("alwaysUpdateValue"), (update) => {
53
+ bindingCount++;
54
+ oldValue = update.oldValue;
55
+ newValue = update.newValue;
56
+ }),
57
+ );
58
+
59
+ assert.equal(bindingCount, 1);
60
+ assert.equal(oldValue, null);
61
+ assert.equal(newValue, "initial");
62
+
63
+ // Change some other value in the model
64
+ element.value = "something else";
65
+
66
+ await Promise.resolve();
67
+
68
+ assert.equal(bindingCount, 2);
69
+ assert.equal(oldValue, "initial");
70
+ assert.equal(newValue, "initial");
71
+ });
72
+ });
package/src/lib/bind.ts CHANGED
@@ -18,7 +18,7 @@ export function bind<This extends HTMLElement, Value>(opts: BindOpts<This, Value
18
18
  return {
19
19
  init(value) {
20
20
  this.addEventListener("joist::value", (e) => {
21
- if (e.token.bindTo === ctx.name) {
21
+ if (e.expression.bindTo === ctx.name) {
22
22
  const instanceMeta = instanceMetadataStore.read<This>(this);
23
23
 
24
24
  e.stopPropagation();
@@ -27,14 +27,16 @@ export function bind<This extends HTMLElement, Value>(opts: BindOpts<This, Value
27
27
  oldValue: null,
28
28
  newValue: ctx.access.get(this),
29
29
  alwaysUpdate: opts.alwaysUpdate,
30
+ firstChange: true,
30
31
  });
31
32
 
33
+ const name = ctx.name as keyof This;
34
+
32
35
  instanceMeta.bindings.add((changes) => {
33
- const key = ctx.name as keyof This;
34
- const change = changes.get(key);
36
+ const change = changes.get(name);
35
37
 
36
38
  if (change) {
37
- e.update({ ...change, alwaysUpdate: opts.alwaysUpdate });
39
+ e.update({ ...change, alwaysUpdate: opts.alwaysUpdate, firstChange: false });
38
40
  } else if (opts.alwaysUpdate) {
39
41
  const value = ctx.access.get(this);
40
42
 
@@ -42,6 +44,7 @@ export function bind<This extends HTMLElement, Value>(opts: BindOpts<This, Value
42
44
  oldValue: value,
43
45
  newValue: value,
44
46
  alwaysUpdate: opts.alwaysUpdate,
47
+ firstChange: false,
45
48
  });
46
49
  }
47
50
  });
@@ -2,7 +2,7 @@ import { attr, element, queryAll, 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 {
@@ -52,7 +52,7 @@ export class JoistAsyncElement extends HTMLElement {
52
52
  success: templates.find((t) => t.hasAttribute("success")),
53
53
  };
54
54
 
55
- const token = new JToken(this.bind);
55
+ const token = new JExpression(this.bind);
56
56
 
57
57
  this.dispatchEvent(
58
58
  new JoistValueEvent(token, ({ newValue, oldValue }) => {
@@ -9,14 +9,14 @@ it("should pass props to child", () => {
9
9
  const element = fixtureSync(html`
10
10
  <div
11
11
  @joist::value=${(e: JoistValueEvent) => {
12
- if (e.token.bindTo === "href") {
12
+ if (e.expression.bindTo === "href") {
13
13
  e.update({
14
14
  oldValue: null,
15
15
  newValue: "$foo",
16
16
  });
17
17
  }
18
18
 
19
- if (e.token.bindTo === "target") {
19
+ if (e.expression.bindTo === "target") {
20
20
  e.update({
21
21
  oldValue: null,
22
22
  newValue: {
@@ -1,6 +1,6 @@
1
1
  import { attr, element, css, html } from "@joist/element";
2
2
 
3
- import { JToken } from "../token.js";
3
+ import { JExpression } from "../expression.js";
4
4
  import { JoistValueEvent } from "../events.js";
5
5
 
6
6
  declare global {
@@ -9,7 +9,7 @@ declare global {
9
9
  }
10
10
  }
11
11
 
12
- export class JAttrToken extends JToken {
12
+ export class JAttrToken extends JExpression {
13
13
  mapTo: string;
14
14
 
15
15
  constructor(binding: string) {
@@ -80,14 +80,14 @@ export class JoistBindElement extends HTMLElement {
80
80
  .filter((b) => b);
81
81
  }
82
82
 
83
- #dispatch(token: JToken, write: (value: unknown) => void) {
83
+ #dispatch(token: JExpression, write: (value: unknown) => void) {
84
84
  this.dispatchEvent(
85
85
  new JoistValueEvent(token, ({ newValue, oldValue, alwaysUpdate }) => {
86
86
  if (newValue === oldValue && !alwaysUpdate) {
87
87
  return;
88
88
  }
89
89
 
90
- let valueToWrite = token.readTokenValueFrom(newValue);
90
+ let valueToWrite = token.readBoundValueFrom(newValue);
91
91
 
92
92
  if (token.isNegated) {
93
93
  valueToWrite = !valueToWrite;
@@ -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