@joist/templating 4.2.4-next.4 → 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 (49) hide show
  1. package/README.md +0 -9
  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 +272 -238
  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/{token.test.ts → expression.test.ts} +51 -51
  15. package/src/lib/{token.ts → expression.ts} +20 -11
  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 -210
  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/{token.d.ts → expression.d.ts} +2 -2
  42. package/target/lib/{token.js → expression.js} +6 -3
  43. package/target/lib/expression.js.map +1 -0
  44. package/target/lib/expression.test.d.ts +1 -0
  45. package/target/lib/{token.test.js → expression.test.js} +52 -52
  46. package/target/lib/expression.test.js.map +1 -0
  47. package/target/lib/token.js.map +0 -1
  48. package/target/lib/token.test.js.map +0 -1
  49. /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
 
@@ -276,11 +275,3 @@ The `j-async` element supports:
276
275
  - Promise handling with automatic state transitions
277
276
  - Loading, success, and error templates
278
277
  - State object with typed data and error fields
279
-
280
- ## Troubleshooting
281
-
282
- ### Common Issues
283
-
284
- 1. **Binding Not Updating**
285
-
286
- - Check if the property is decorated with `@bind()`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@joist/templating",
3
- "version": "4.2.4-next.4",
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