@joist/templating 4.2.3 → 4.2.4-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.
Files changed (32) hide show
  1. package/README.md +172 -144
  2. package/package.json +1 -2
  3. package/src/lib/define.ts +1 -1
  4. package/src/lib/elements/{props.element.test.ts → bind.element.test.ts} +50 -7
  5. package/src/lib/elements/bind.element.ts +99 -0
  6. package/src/lib/elements/for.element.test.ts +8 -9
  7. package/src/lib/elements/scope.ts +1 -1
  8. package/src/lib/elements/value.element.test.ts +1 -1
  9. package/src/lib/elements/value.element.ts +2 -2
  10. package/target/lib/define.d.ts +1 -1
  11. package/target/lib/define.js +1 -1
  12. package/target/lib/define.js.map +1 -1
  13. package/target/lib/elements/{props.element.d.ts → bind.element.d.ts} +3 -2
  14. package/target/lib/elements/bind.element.js +115 -0
  15. package/target/lib/elements/bind.element.js.map +1 -0
  16. package/target/lib/elements/bind.element.test.d.ts +1 -0
  17. package/target/lib/elements/bind.element.test.js +90 -0
  18. package/target/lib/elements/bind.element.test.js.map +1 -0
  19. package/target/lib/elements/for.element.test.js +7 -8
  20. package/target/lib/elements/for.element.test.js.map +1 -1
  21. package/target/lib/elements/scope.js +1 -1
  22. package/target/lib/elements/scope.js.map +1 -1
  23. package/target/lib/elements/value.element.d.ts +1 -1
  24. package/target/lib/elements/value.element.js +1 -1
  25. package/target/lib/elements/value.element.js.map +1 -1
  26. package/target/lib/elements/value.element.test.js +1 -1
  27. package/src/lib/elements/props.element.ts +0 -76
  28. package/target/lib/elements/props.element.js +0 -90
  29. package/target/lib/elements/props.element.js.map +0 -1
  30. package/target/lib/elements/props.element.test.d.ts +0 -1
  31. package/target/lib/elements/props.element.test.js +0 -53
  32. package/target/lib/elements/props.element.test.js.map +0 -1
package/README.md CHANGED
@@ -2,6 +2,13 @@
2
2
 
3
3
  The Joist templating system provides a powerful and flexible way to handle data binding and templating in web components. This documentation covers the core components and their usage.
4
4
 
5
+ ## Table of Contents
6
+
7
+ - [Core Components](#core-components)
8
+ - [Built-in Template Elements](#built-in-template-elements)
9
+ - [Complete Example](#complete-example)
10
+ - [Troubleshooting](#troubleshooting)
11
+
5
12
  ## Core Components
6
13
 
7
14
  ### Bind Decorator (`bind.ts`)
@@ -19,42 +26,71 @@ class MyElement extends HTMLElement {
19
26
 
20
27
  The decorator:
21
28
 
22
- - Creates a two-way binding between component properties and templates
29
+ - Creates a one-way binding between component properties and templates
23
30
  - Automatically handles value propagation through the `joist::value` event
24
31
  - Integrates with Joist's observable system for efficient change detection
32
+ - Supports computed properties
33
+
34
+ ```typescript
35
+ class MyElement extends HTMLElement {
36
+ @observe()
37
+ assessor value = "Hello World";
38
+
39
+ @bind((instance) => instance.value.toUpperCase())
40
+ accessor formattedValue = "";
41
+ }
42
+ ```
25
43
 
26
44
  ### Token System (`token.ts`)
27
45
 
28
46
  The `JToken` class handles parsing and evaluation of binding expressions. It supports:
29
47
 
48
+ NOTE: Most of the time you will not be using this yourself.
49
+
30
50
  - Simple property bindings: `propertyName`
31
51
  - Nested property access: `user.profile.name`
32
52
  - Negation operator: `!isVisible`
53
+ - Array access: `items.0.name`
33
54
 
34
55
  Example usage:
35
56
 
36
57
  ```typescript
37
58
  const token = new JToken("user.name");
38
59
  const value = token.readTokenValueFrom(context);
60
+
61
+ // With negation
62
+ const negatedToken = new JToken("!isVisible");
63
+ const isHidden = negatedToken.readTokenValueFrom(context);
39
64
  ```
40
65
 
41
- ### Events (`events.ts`)
66
+ ## Built-in Template Elements
42
67
 
43
- The system uses custom events for value propagation:
68
+ Joist provides several built-in template elements for common templating needs:
44
69
 
45
- - `JoistValueEvent`: A custom event that handles value updates in the templating system
46
- - Bubbles through the DOM tree
47
- - Carries both the token and update mechanism
70
+ ### Value Display (`j-val`)
48
71
 
49
- ## Built-in Template Elements
72
+ Displays a bound value as text content:
50
73
 
51
- Joist provides several built-in template elements for common templating needs:
74
+ ```html
75
+ <!-- Basic usage -->
76
+ <j-val bind="user.name"></j-val>
77
+
78
+ <!-- With formatting -->
79
+ <j-val bind="formattedPrice"></j-val>
80
+
81
+ <!-- With nested properties -->
82
+ <j-val bind="user.profile.address.city"></j-val>
83
+
84
+ <!-- With array access -->
85
+ <j-val bind="items[0].name"></j-val>
86
+ ```
52
87
 
53
88
  ### Conditional Rendering (`j-if`)
54
89
 
55
90
  Conditionally renders content based on a boolean expression:
56
91
 
57
92
  ```html
93
+ <!-- Basic usage -->
58
94
  <j-if bind="isVisible">
59
95
  <template>
60
96
  <div>This content is only shown when isVisible is true</div>
@@ -86,37 +122,64 @@ The `j-if` element supports:
86
122
  - Optional `else` template for fallback content
87
123
  - Automatic cleanup of removed content
88
124
 
89
- Common use cases:
125
+ ### Property Binding (`j-bind`)
126
+
127
+ Binds values to element properties and attributes. By default it will bind values to the first child element of `j-bind`
90
128
 
91
- - Toggling visibility of UI elements
92
- - Conditional form fields
93
- - Feature flags
94
- - Authentication states
95
- - Loading states
129
+ - `props` Binds to element properties
130
+ - `attrs` prefix: Binds to element attributes
96
131
 
97
- ### Property Binding (`j-props`)
132
+ #### Binding Syntax
98
133
 
99
- Binds values to element properties and attributes. The prefix determines the binding type:
134
+ The binding syntax follows the format `target:source` where:
100
135
 
101
- - `$.` prefix: Binds to element properties
102
- - `$` prefix: Binds to element attributes
136
+ - `target` is the property/attribute name to bind to
137
+ - `source` is the value to bind from
103
138
 
104
139
  ```html
105
- <j-props>
106
- <!-- Bind to boolean properties -->
107
- <input type="checkbox" $.checked="isComplete">
140
+ <!-- Basic attribute binding -->
141
+ <j-bind attrs="href:href">
142
+ <a>Link</a>
143
+ </j-bind>
144
+
145
+ <!-- Property binding -->
146
+ <j-bind props="target:some.value">
147
+ <a>Link</a>
148
+ </j-bind>
149
+
150
+ <!-- Multiple bindings -->
151
+ <j-bind props="selectionStart:foo, selectionEnd:foo">
152
+ <input value="1234567890" />
153
+ </j-bind>
154
+
155
+ <!-- Style binding -->
156
+ <j-bind props="style.color:color, style.backgroundColor:bgColor">
157
+ <div>Styled content</div>
158
+ </j-bind>
159
+ ```
108
160
 
109
- <!-- Bind to form input values -->
110
- <input type="text" $.value="userName">
161
+ #### Targeting Specific Elements
111
162
 
112
- <!-- Bind to custom element properties -->
113
- <my-element $.data="complexObject">
163
+ You can target a specific child element using the `target` attribute:
114
164
 
115
- <!-- Bind to attributes -->
116
- <div $class="dynamicClass">
117
- <img $src="imageUrl">
118
- <input $placeholder="placeholderText">
119
- </j-props>
165
+ ```html
166
+ <j-bind attrs="href:href" target="#test">
167
+ <a>Default</a>
168
+ <a id="test">Target</a>
169
+ </j-bind>
170
+ ```
171
+
172
+ #### Boolean Attributes
173
+
174
+ Boolean attributes are handled specially:
175
+
176
+ - `true` values set the attribute
177
+ - `false` values remove the attribute
178
+
179
+ ```html
180
+ <j-bind attrs="disabled:isDisabled">
181
+ <button>Click me</button>
182
+ </j-bind>
120
183
  ```
121
184
 
122
185
  ### List Rendering (`j-for`)
@@ -124,25 +187,26 @@ Binds values to element properties and attributes. The prefix determines the bin
124
187
  Renders lists of items with support for keyed updates:
125
188
 
126
189
  ```html
190
+ <!-- Basic list rendering -->
127
191
  <j-for bind="todos" key="id">
128
192
  <template>
129
- <j-props>
130
- <div
131
- class="todo-item"
132
- $.dataset.id="each.value.id"
133
- $.dataset.completed="each.value.completed"
134
- >
135
- <j-props>
136
- <input type="checkbox" $.checked="each.value.completed" />
137
- </j-props>
138
-
139
- <j-value bind="each.value.text"></j-value>
140
-
141
- <j-props>
142
- <button $.disabled="!each.value.text">×</button>
143
- </j-props>
144
- </div>
145
- </j-props>
193
+ <div class="todo-item">
194
+ <j-val bind="each.value.text"></j-val>
195
+ </div>
196
+ </template>
197
+ </j-for>
198
+
199
+ <!-- With complex item structure -->
200
+ <j-for bind="users" key="id">
201
+ <template>
202
+ <div class="user-card">
203
+ <j-bind>
204
+ <img props="src:each.value.avatar" />
205
+ </j-bind>
206
+
207
+ <h3><j-val bind="each.value.name"></j-val></h3>
208
+ <p><j-val bind="each.value.bio"></j-val></p>
209
+ </div>
146
210
  </template>
147
211
  </j-for>
148
212
  ```
@@ -153,17 +217,9 @@ The `j-for` element provides context variables:
153
217
  - `each.index`: The zero-based index of the current item
154
218
  - `each.position`: The one-based position of the current item
155
219
 
156
- ### Value Display (`j-value`)
157
-
158
- Displays a bound value as text content:
159
-
160
- ```html
161
- <j-value bind="user.name"></j-value> <j-value bind="formattedPrice"></j-value>
162
- ```
163
-
164
220
  ### Async State Handling (`j-async`)
165
221
 
166
- Handles asynchronous operations and state management with loading, success, and error states. The element accepts either a Promise or an AsyncState object:
222
+ Handles asynchronous operations and state management with loading, success, and error states:
167
223
 
168
224
  ```typescript
169
225
  // AsyncState type
@@ -183,15 +239,16 @@ accessor userPromise = fetch('/api/user').then(r => r.json());
183
239
  ```
184
240
 
185
241
  ```html
242
+ <!-- Basic async handling -->
186
243
  <j-async bind="userPromise">
187
244
  <template loading>Loading...</template>
188
245
 
189
246
  <template success>
190
- <div>Welcome, <j-value bind="state.data.name"></j-value>!</div>
247
+ <div>Welcome, <j-val bind="state.data.name"></j-val>!</div>
191
248
  </template>
192
249
 
193
250
  <template error>
194
- <div>Error: <j-value bind="state.error"></j-value></div>
251
+ <div>Error: <j-val bind="state.error"></j-val></div>
195
252
  </template>
196
253
  </j-async>
197
254
  ```
@@ -200,9 +257,53 @@ The `j-async` element supports:
200
257
 
201
258
  - Promise handling with automatic state transitions
202
259
  - Loading, success, and error templates
203
- - Automatic cleanup on disconnection
204
260
  - State object with typed data and error fields
205
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
+
206
307
  ## Complete Example
207
308
 
208
309
  Here's a complete todo application in a single component:
@@ -212,7 +313,7 @@ import { bind } from "@joist/templating";
212
313
  import { element, html, css, listen, query } from "@joist/element";
213
314
 
214
315
  interface Todo {
215
- id: number;
316
+ id: string;
216
317
  text: string;
217
318
  }
218
319
 
@@ -230,6 +331,7 @@ interface Todo {
230
331
  gap: 1rem;
231
332
  }
232
333
  .todo-item {
334
+ align-items: center;
233
335
  display: flex;
234
336
  gap: 0.5rem;
235
337
  margin: 0.5rem 0;
@@ -252,14 +354,17 @@ interface Todo {
252
354
 
253
355
  <j-for id="todos" bind="todos" key="id">
254
356
  <template>
255
- <j-props class="todo-item">
256
- <j-value class="todo-text" bind="each.value.text"></j-value>
257
- <button $.id="each.value.id" $.disabled="!each.value.text">×</button>
258
- </j-props>
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>
259
364
  </template>
260
365
  </j-for>
261
366
 
262
- <j-value bind="todos.length"></j-value> remaining
367
+ <j-val bind="todos.length"></j-val> remaining
263
368
  `,
264
369
  ],
265
370
  })
@@ -276,7 +381,7 @@ export class TodoList extends HTMLElement {
276
381
 
277
382
  const input = this.#input();
278
383
 
279
- this.todos = [...this.todos, { id: this.#nextId++, text: input.value.trim() }];
384
+ this.todos = [...this.todos, { id: String(this.#nextId++), text: input.value.trim() }];
280
385
 
281
386
  input.value = "";
282
387
  }
@@ -284,87 +389,10 @@ export class TodoList extends HTMLElement {
284
389
  @listen("click", "#todos")
285
390
  onDelete(e: Event) {
286
391
  if (e.target instanceof HTMLButtonElement) {
287
- const id = Number(e.target.id);
392
+ const id = Number(e.target.dataset.id);
288
393
 
289
394
  this.todos = this.todos.filter((todo) => todo.id !== id);
290
395
  }
291
396
  }
292
397
  }
293
398
  ```
294
-
295
- ## Usage
296
-
297
- 1. Use the `@bind()` decorator on properties you want to make bindable
298
- 2. Properties will automatically integrate with the templating system
299
- 3. Changes are propagated through the component tree using the custom event system
300
-
301
- ## Integration with Observable
302
-
303
- The templating system is built on top of Joist's observable system (`@joist/observable`), providing:
304
-
305
- - Automatic change detection
306
- - Efficient updates
307
- - Integration with the component lifecycle
308
-
309
- ## Best Practices
310
-
311
- 1. Use the `@bind()` decorator only on properties that need reactivity
312
- 2. Keep binding expressions simple and avoid deep nesting
313
- 3. Consider performance implications when binding to frequently changing values
314
- 4. Always use a `key` attribute with `j-for` when items can be reordered
315
- 5. Place template content directly inside `j-if` and `j-for` elements
316
-
317
- ## Manual Value Handling
318
-
319
- 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:
320
-
321
- ```typescript
322
- import { JoistValueEvent } from "@joist/templating";
323
-
324
- class MyElement extends HTMLElement {
325
- connectedCallback() {
326
- // Listen for value requests
327
- this.addEventListener("joist::value", (e: JoistValueEvent) => {
328
- const token = e.token;
329
-
330
- // Handle the value request
331
- if (token.bindTo === "myValue") {
332
- // Update the value
333
- e.update({
334
- oldValue: this.myValue,
335
- newValue: this.myValue,
336
- });
337
- }
338
- });
339
- }
340
- }
341
- ```
342
-
343
- Example with async value handling:
344
-
345
- ```typescript
346
- import { JoistValueEvent } from "@joist/templating";
347
-
348
- class MyElement extends HTMLElement {
349
- connectedCallback() {
350
- this.addEventListener("joist::value", (e: JoistValueEvent) => {
351
- const token = e.token;
352
-
353
- if (token.bindTo === "userData") {
354
- e.update({
355
- oldValue: this.userData,
356
- newValue: data,
357
- });
358
- }
359
- });
360
- }
361
- }
362
- ```
363
-
364
- Common use cases for manual value handling:
365
-
366
- - Custom data transformation before binding
367
- - Async data loading and caching
368
- - Complex state management
369
- - Integration with external data sources
370
- - Custom validation or error handling
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@joist/templating",
3
- "version": "4.2.3",
3
+ "version": "4.2.4-next.1",
4
4
  "type": "module",
5
5
  "main": "./target/lib.js",
6
6
  "module": "./target/lib.js",
@@ -60,7 +60,6 @@
60
60
  "vitest.config.js",
61
61
  "target/**"
62
62
  ],
63
- "output": [],
64
63
  "dependencies": [
65
64
  "build"
66
65
  ]
package/src/lib/define.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import "./elements/async.element.js";
2
2
  import "./elements/for.element.js";
3
3
  import "./elements/if.element.js";
4
- import "./elements/props.element.js";
4
+ import "./elements/bind.element.js";
5
5
  import "./elements/value.element.js";
@@ -1,4 +1,4 @@
1
- import "./props.element.js";
1
+ import "./bind.element.js";
2
2
 
3
3
  import { fixtureSync, html } from "@open-wc/testing";
4
4
  import { assert } from "chai";
@@ -26,9 +26,9 @@ it("should pass props to child", () => {
26
26
  }
27
27
  }}
28
28
  >
29
- <j-props>
30
- <a $href="href" $target="target.value">Hello World</a>
31
- </j-props>
29
+ <j-bind attrs="href:href" props="target:target.value">
30
+ <a>Hello World</a>
31
+ </j-bind>
32
32
  </div>
33
33
  `);
34
34
 
@@ -48,10 +48,10 @@ it("should pass props to specified child", () => {
48
48
  });
49
49
  }}
50
50
  >
51
- <j-props>
51
+ <j-bind attrs="href:href" target="#test">
52
52
  <a>Default</a>
53
- <a id="test" $href="href">Target</a>
54
- </j-props>
53
+ <a id="test">Target</a>
54
+ </j-bind>
55
55
  </div>
56
56
  `);
57
57
 
@@ -60,3 +60,46 @@ it("should pass props to specified child", () => {
60
60
  assert.equal(anchor[0].getAttribute("href"), null);
61
61
  assert.equal(anchor[1].getAttribute("href"), "#foo");
62
62
  });
63
+
64
+ it("should be case sensitive", () => {
65
+ const element = fixtureSync(html`
66
+ <div
67
+ @joist::value=${(e: JoistValueEvent) => {
68
+ e.update({ oldValue: null, newValue: 8 });
69
+ }}
70
+ >
71
+ <j-bind
72
+ props="
73
+ selectionStart:foo,
74
+ selectionEnd:foo
75
+ "
76
+ >
77
+ <input value="1234567890" />
78
+ </j-bind>
79
+ </div>
80
+ `);
81
+
82
+ const input = element.querySelector("input");
83
+
84
+ assert.equal(input?.selectionStart, 8);
85
+ assert.equal(input?.selectionEnd, 8);
86
+ });
87
+
88
+ it("should default to the mapTo value if bindTo is not provided", () => {
89
+ const element = fixtureSync(html`
90
+ <div
91
+ @joist::value=${(e: JoistValueEvent) => {
92
+ e.update({ oldValue: null, newValue: 8 });
93
+ }}
94
+ >
95
+ <j-bind props="selectionStart, selectionEnd">
96
+ <input value="1234567890" />
97
+ </j-bind>
98
+ </div>
99
+ `);
100
+
101
+ const input = element.querySelector("input");
102
+
103
+ assert.equal(input?.selectionStart, 8);
104
+ assert.equal(input?.selectionEnd, 8);
105
+ });
@@ -0,0 +1,99 @@
1
+ import { attr, element, css, html } from "@joist/element";
2
+
3
+ // import { JoistValueEvent } from "../events.js";
4
+ import { JToken } from "../token.js";
5
+ import { JoistValueEvent } from "../events.js";
6
+
7
+ export class JAttrToken extends JToken {
8
+ mapTo: string;
9
+
10
+ constructor(binding: string) {
11
+ const [mapTo, bindTo] = binding.split(":");
12
+
13
+ super(bindTo ?? mapTo);
14
+
15
+ this.mapTo = mapTo;
16
+ }
17
+ }
18
+
19
+ @element({
20
+ tagName: "j-bind",
21
+ // prettier-ignore
22
+ shadowDom: [css`:host{display: contents;}`, html`<slot></slot>`],
23
+ })
24
+ export class JoistIfElement extends HTMLElement {
25
+ @attr()
26
+ accessor props = "";
27
+
28
+ @attr()
29
+ accessor attrs = "";
30
+
31
+ @attr()
32
+ accessor target = "";
33
+
34
+ connectedCallback(): void {
35
+ const attrBindings = this.#parseBinding(this.attrs);
36
+ const propBindings = this.#parseBinding(this.props);
37
+
38
+ let child = this.firstElementChild;
39
+
40
+ if (this.target) {
41
+ child = this.querySelector(this.target);
42
+ }
43
+
44
+ if (!child) {
45
+ throw new Error("j-bind must have a child element or defined target");
46
+ }
47
+
48
+ for (const attrValue of attrBindings) {
49
+ const token = new JAttrToken(attrValue);
50
+
51
+ this.#dispatch(token, (value) => {
52
+ if (value === true) {
53
+ child.setAttribute(token.mapTo, "");
54
+ } else if (value === false) {
55
+ child.removeAttribute(token.mapTo);
56
+ } else {
57
+ child.setAttribute(token.mapTo, String(value));
58
+ }
59
+ });
60
+ }
61
+
62
+ for (const propValue of propBindings) {
63
+ const token = new JAttrToken(propValue);
64
+
65
+ this.#dispatch(token, (value) => {
66
+ Reflect.set(child, token.mapTo, value);
67
+ });
68
+ }
69
+ }
70
+
71
+ #parseBinding(binding: string) {
72
+ return binding
73
+ .split(",")
74
+ .map((b) => b.trim())
75
+ .filter((b) => b);
76
+ }
77
+
78
+ #dispatch(token: JToken, write: (value: unknown) => void) {
79
+ this.dispatchEvent(
80
+ new JoistValueEvent(token, ({ newValue, oldValue }) => {
81
+ if (newValue === oldValue) {
82
+ return;
83
+ }
84
+
85
+ let valueToWrite = newValue;
86
+
87
+ if (typeof newValue === "object" && newValue !== null) {
88
+ valueToWrite = token.readTokenValueFrom(newValue);
89
+ }
90
+
91
+ if (token.isNegated) {
92
+ valueToWrite = !valueToWrite;
93
+ }
94
+
95
+ write(valueToWrite);
96
+ }),
97
+ );
98
+ }
99
+ }