@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.
- package/README.md +172 -144
- package/package.json +1 -2
- package/src/lib/define.ts +1 -1
- package/src/lib/elements/{props.element.test.ts → bind.element.test.ts} +50 -7
- package/src/lib/elements/bind.element.ts +99 -0
- package/src/lib/elements/for.element.test.ts +8 -9
- package/src/lib/elements/scope.ts +1 -1
- package/src/lib/elements/value.element.test.ts +1 -1
- package/src/lib/elements/value.element.ts +2 -2
- package/target/lib/define.d.ts +1 -1
- package/target/lib/define.js +1 -1
- package/target/lib/define.js.map +1 -1
- package/target/lib/elements/{props.element.d.ts → bind.element.d.ts} +3 -2
- package/target/lib/elements/bind.element.js +115 -0
- package/target/lib/elements/bind.element.js.map +1 -0
- package/target/lib/elements/bind.element.test.d.ts +1 -0
- package/target/lib/elements/bind.element.test.js +90 -0
- package/target/lib/elements/bind.element.test.js.map +1 -0
- package/target/lib/elements/for.element.test.js +7 -8
- package/target/lib/elements/for.element.test.js.map +1 -1
- package/target/lib/elements/scope.js +1 -1
- package/target/lib/elements/scope.js.map +1 -1
- package/target/lib/elements/value.element.d.ts +1 -1
- package/target/lib/elements/value.element.js +1 -1
- package/target/lib/elements/value.element.js.map +1 -1
- package/target/lib/elements/value.element.test.js +1 -1
- package/src/lib/elements/props.element.ts +0 -76
- package/target/lib/elements/props.element.js +0 -90
- package/target/lib/elements/props.element.js.map +0 -1
- package/target/lib/elements/props.element.test.d.ts +0 -1
- package/target/lib/elements/props.element.test.js +0 -53
- 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
|
|
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
|
-
|
|
66
|
+
## Built-in Template Elements
|
|
42
67
|
|
|
43
|
-
|
|
68
|
+
Joist provides several built-in template elements for common templating needs:
|
|
44
69
|
|
|
45
|
-
|
|
46
|
-
- Bubbles through the DOM tree
|
|
47
|
-
- Carries both the token and update mechanism
|
|
70
|
+
### Value Display (`j-val`)
|
|
48
71
|
|
|
49
|
-
|
|
72
|
+
Displays a bound value as text content:
|
|
50
73
|
|
|
51
|
-
|
|
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
|
-
|
|
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
|
-
-
|
|
92
|
-
-
|
|
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
|
-
|
|
132
|
+
#### Binding Syntax
|
|
98
133
|
|
|
99
|
-
|
|
134
|
+
The binding syntax follows the format `target:source` where:
|
|
100
135
|
|
|
101
|
-
-
|
|
102
|
-
-
|
|
136
|
+
- `target` is the property/attribute name to bind to
|
|
137
|
+
- `source` is the value to bind from
|
|
103
138
|
|
|
104
139
|
```html
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
<
|
|
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
|
-
|
|
110
|
-
<input type="text" $.value="userName">
|
|
161
|
+
#### Targeting Specific Elements
|
|
111
162
|
|
|
112
|
-
|
|
113
|
-
<my-element $.data="complexObject">
|
|
163
|
+
You can target a specific child element using the `target` attribute:
|
|
114
164
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
<
|
|
118
|
-
<
|
|
119
|
-
</j-
|
|
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
|
-
<
|
|
130
|
-
<
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
</
|
|
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
|
|
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-
|
|
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-
|
|
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:
|
|
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
|
-
<
|
|
256
|
-
<j-
|
|
257
|
-
|
|
258
|
-
|
|
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-
|
|
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
|
|
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
|
+
"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,4 +1,4 @@
|
|
|
1
|
-
import "./
|
|
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
|
|
31
|
-
</j-
|
|
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-
|
|
51
|
+
<j-bind attrs="href:href" target="#test">
|
|
52
52
|
<a>Default</a>
|
|
53
|
-
<a id="test"
|
|
54
|
-
</j-
|
|
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
|
+
}
|