@joist/templating 4.2.4-next.1 → 4.2.4-next.10
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 +43 -164
- package/package.json +1 -1
- package/src/lib/bind.test.ts +72 -0
- package/src/lib/bind.ts +32 -9
- package/src/lib/define.ts +1 -0
- package/src/lib/elements/async.element.test.ts +90 -0
- package/src/lib/elements/async.element.ts +13 -2
- package/src/lib/elements/bind.element.test.ts +2 -2
- package/src/lib/elements/bind.element.ts +13 -12
- package/src/lib/elements/for.element.ts +16 -9
- package/src/lib/elements/if.element.test.ts +210 -0
- package/src/lib/elements/if.element.ts +16 -11
- package/src/lib/elements/scope.element.test.ts +32 -0
- package/src/lib/elements/scope.element.ts +19 -0
- package/src/lib/elements/value.element.test.ts +28 -0
- package/src/lib/elements/value.element.ts +9 -11
- package/src/lib/events.ts +10 -5
- package/src/lib/expression.test.ts +204 -0
- package/src/lib/expression.ts +179 -0
- package/target/lib/bind.d.ts +5 -1
- package/target/lib/bind.js +22 -8
- package/target/lib/bind.js.map +1 -1
- package/target/lib/bind.test.js +76 -0
- package/target/lib/bind.test.js.map +1 -0
- package/target/lib/define.d.ts +1 -0
- package/target/lib/define.js +1 -0
- package/target/lib/define.js.map +1 -1
- package/target/lib/elements/async.element.js +11 -2
- package/target/lib/elements/async.element.js.map +1 -1
- package/target/lib/elements/async.element.test.js +76 -0
- package/target/lib/elements/async.element.test.js.map +1 -1
- package/target/lib/elements/bind.element.d.ts +8 -3
- package/target/lib/elements/bind.element.js +10 -13
- package/target/lib/elements/bind.element.js.map +1 -1
- package/target/lib/elements/bind.element.test.js +2 -2
- package/target/lib/elements/bind.element.test.js.map +1 -1
- package/target/lib/elements/for.element.d.ts +1 -1
- package/target/lib/elements/for.element.js +13 -7
- package/target/lib/elements/for.element.js.map +1 -1
- package/target/lib/elements/if.element.js +12 -12
- package/target/lib/elements/if.element.js.map +1 -1
- package/target/lib/elements/if.element.test.js +184 -0
- package/target/lib/elements/if.element.test.js.map +1 -1
- package/target/lib/elements/scope.element.d.ts +8 -0
- package/target/lib/elements/scope.element.js +38 -0
- package/target/lib/elements/scope.element.js.map +1 -0
- package/target/lib/elements/scope.element.test.d.ts +2 -0
- package/target/lib/elements/scope.element.test.js +25 -0
- package/target/lib/elements/scope.element.test.js.map +1 -0
- package/target/lib/elements/value.element.js +7 -11
- package/target/lib/elements/value.element.js.map +1 -1
- package/target/lib/elements/value.element.test.js +24 -0
- package/target/lib/elements/value.element.test.js.map +1 -1
- package/target/lib/events.d.ts +8 -4
- package/target/lib/events.js +3 -3
- package/target/lib/events.js.map +1 -1
- package/target/lib/expression.d.ts +13 -0
- package/target/lib/expression.js +87 -0
- package/target/lib/expression.js.map +1 -0
- package/target/lib/expression.test.d.ts +1 -0
- package/target/lib/expression.test.js +171 -0
- package/target/lib/expression.test.js.map +1 -0
- package/src/lib/elements/scope.ts +0 -39
- package/src/lib/token.test.ts +0 -74
- package/src/lib/token.ts +0 -34
- package/target/lib/elements/scope.d.ts +0 -13
- package/target/lib/elements/scope.js +0 -56
- package/target/lib/elements/scope.js.map +0 -1
- package/target/lib/token.d.ts +0 -8
- package/target/lib/token.js +0 -27
- package/target/lib/token.js.map +0 -1
- package/target/lib/token.test.js +0 -56
- package/target/lib/token.test.js.map +0 -1
- /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
|
|
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(
|
|
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
|
|
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
|
@@ -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
|
@@ -1,28 +1,51 @@
|
|
|
1
|
-
import { instanceMetadataStore, observe } from "@joist/observable";
|
|
1
|
+
import { instanceMetadataStore, observe, ObserveOpts } from "@joist/observable";
|
|
2
2
|
|
|
3
|
-
export
|
|
3
|
+
export interface BindOpts<This, Value> extends ObserveOpts<This, Value> {
|
|
4
|
+
/**
|
|
5
|
+
* Trigger bindings on every change cycle, regardless of value,
|
|
6
|
+
* newValue and oldValue will be the same in that case
|
|
7
|
+
**/
|
|
8
|
+
alwaysUpdate?: boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function bind<This extends HTMLElement, Value>(opts: BindOpts<This, Value> = {}) {
|
|
4
12
|
return function bindDecorator(
|
|
5
13
|
base: ClassAccessorDecoratorTarget<This, Value>,
|
|
6
14
|
ctx: ClassAccessorDecoratorContext<This, Value>,
|
|
7
15
|
): ClassAccessorDecoratorResult<This, Value> {
|
|
8
|
-
const internalObserve = observe(
|
|
16
|
+
const internalObserve = observe(opts)(base, ctx);
|
|
9
17
|
|
|
10
18
|
return {
|
|
11
19
|
init(value) {
|
|
12
20
|
this.addEventListener("joist::value", (e) => {
|
|
13
|
-
if (e.
|
|
21
|
+
if (e.expression.bindTo === ctx.name) {
|
|
14
22
|
const instanceMeta = instanceMetadataStore.read<This>(this);
|
|
15
23
|
|
|
16
24
|
e.stopPropagation();
|
|
17
25
|
|
|
18
|
-
e.update({
|
|
26
|
+
e.update({
|
|
27
|
+
oldValue: null,
|
|
28
|
+
newValue: ctx.access.get(this),
|
|
29
|
+
alwaysUpdate: opts.alwaysUpdate,
|
|
30
|
+
firstChange: true,
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const name = ctx.name as keyof This;
|
|
19
34
|
|
|
20
35
|
instanceMeta.bindings.add((changes) => {
|
|
21
|
-
const
|
|
22
|
-
|
|
36
|
+
const change = changes.get(name);
|
|
37
|
+
|
|
38
|
+
if (change) {
|
|
39
|
+
e.update({ ...change, alwaysUpdate: opts.alwaysUpdate, firstChange: false });
|
|
40
|
+
} else if (opts.alwaysUpdate) {
|
|
41
|
+
const value = ctx.access.get(this);
|
|
23
42
|
|
|
24
|
-
|
|
25
|
-
|
|
43
|
+
e.update({
|
|
44
|
+
oldValue: value,
|
|
45
|
+
newValue: value,
|
|
46
|
+
alwaysUpdate: opts.alwaysUpdate,
|
|
47
|
+
firstChange: false,
|
|
48
|
+
});
|
|
26
49
|
}
|
|
27
50
|
});
|
|
28
51
|
}
|
package/src/lib/define.ts
CHANGED
|
@@ -88,3 +88,93 @@ it("should handle state transitions", async () => {
|
|
|
88
88
|
await new Promise((resolve) => setTimeout(resolve, 150));
|
|
89
89
|
assert.equal(element.textContent?.trim(), "Success!");
|
|
90
90
|
});
|
|
91
|
+
|
|
92
|
+
it("should show loading template when AsyncState is loading", () => {
|
|
93
|
+
const element = fixtureSync(html`
|
|
94
|
+
<div
|
|
95
|
+
@joist::value=${(e: JoistValueEvent) => {
|
|
96
|
+
e.update({ oldValue: null, newValue: { status: "loading" } });
|
|
97
|
+
}}
|
|
98
|
+
>
|
|
99
|
+
<j-async bind="test">
|
|
100
|
+
<template loading>Loading...</template>
|
|
101
|
+
<template success>Success!</template>
|
|
102
|
+
<template error>Error!</template>
|
|
103
|
+
</j-async>
|
|
104
|
+
</div>
|
|
105
|
+
`);
|
|
106
|
+
|
|
107
|
+
assert.equal(element.textContent?.trim(), "Loading...");
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("should show success template when AsyncState is success", () => {
|
|
111
|
+
const element = fixtureSync(html`
|
|
112
|
+
<div
|
|
113
|
+
@joist::value=${(e: JoistValueEvent) => {
|
|
114
|
+
e.update({ oldValue: null, newValue: { status: "success", data: "test data" } });
|
|
115
|
+
}}
|
|
116
|
+
>
|
|
117
|
+
<j-async bind="test">
|
|
118
|
+
<template loading>Loading...</template>
|
|
119
|
+
<template success>Success!</template>
|
|
120
|
+
<template error>Error!</template>
|
|
121
|
+
</j-async>
|
|
122
|
+
</div>
|
|
123
|
+
`);
|
|
124
|
+
|
|
125
|
+
assert.equal(element.textContent?.trim(), "Success!");
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("should show error template when AsyncState is error", () => {
|
|
129
|
+
const element = fixtureSync(html`
|
|
130
|
+
<div
|
|
131
|
+
@joist::value=${(e: JoistValueEvent) => {
|
|
132
|
+
e.update({ oldValue: null, newValue: { status: "error", error: "test error" } });
|
|
133
|
+
}}
|
|
134
|
+
>
|
|
135
|
+
<j-async bind="test">
|
|
136
|
+
<template loading>Loading...</template>
|
|
137
|
+
<template success>Success!</template>
|
|
138
|
+
<template error>Error!</template>
|
|
139
|
+
</j-async>
|
|
140
|
+
</div>
|
|
141
|
+
`);
|
|
142
|
+
|
|
143
|
+
assert.equal(element.textContent?.trim(), "Error!");
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it("should handle AsyncState transitions", () => {
|
|
147
|
+
const element = fixtureSync(html`
|
|
148
|
+
<div
|
|
149
|
+
@joist::value=${(e: JoistValueEvent) => {
|
|
150
|
+
// Initial state
|
|
151
|
+
e.update({ oldValue: null, newValue: { status: "loading" } });
|
|
152
|
+
|
|
153
|
+
// Simulate state transition after a short delay
|
|
154
|
+
setTimeout(() => {
|
|
155
|
+
e.update({
|
|
156
|
+
oldValue: { status: "loading" },
|
|
157
|
+
newValue: { status: "success", data: "test data" },
|
|
158
|
+
});
|
|
159
|
+
}, 100);
|
|
160
|
+
}}
|
|
161
|
+
>
|
|
162
|
+
<j-async bind="test">
|
|
163
|
+
<template loading>Loading...</template>
|
|
164
|
+
<template success>Success!</template>
|
|
165
|
+
<template error>Error!</template>
|
|
166
|
+
</j-async>
|
|
167
|
+
</div>
|
|
168
|
+
`);
|
|
169
|
+
|
|
170
|
+
// Initially should show loading
|
|
171
|
+
assert.equal(element.textContent?.trim(), "Loading...");
|
|
172
|
+
|
|
173
|
+
// Wait for state transition
|
|
174
|
+
return new Promise((resolve) => {
|
|
175
|
+
setTimeout(() => {
|
|
176
|
+
assert.equal(element.textContent?.trim(), "Success!");
|
|
177
|
+
resolve(undefined);
|
|
178
|
+
}, 150);
|
|
179
|
+
});
|
|
180
|
+
});
|
|
@@ -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 {
|
|
5
|
+
import { JExpression } from "../expression.js";
|
|
6
6
|
|
|
7
7
|
declare global {
|
|
8
8
|
interface HTMLElementTagNameMap {
|
|
@@ -52,13 +52,15 @@ export class JoistAsyncElement extends HTMLElement {
|
|
|
52
52
|
success: templates.find((t) => t.hasAttribute("success")),
|
|
53
53
|
};
|
|
54
54
|
|
|
55
|
-
const token = new
|
|
55
|
+
const token = new JExpression(this.bind);
|
|
56
56
|
|
|
57
57
|
this.dispatchEvent(
|
|
58
58
|
new JoistValueEvent(token, ({ newValue, oldValue }) => {
|
|
59
59
|
if (newValue !== oldValue) {
|
|
60
60
|
if (newValue instanceof Promise) {
|
|
61
61
|
this.#handlePromise(newValue);
|
|
62
|
+
} else if (this.#isAsyncState(newValue)) {
|
|
63
|
+
this.#handleState(newValue);
|
|
62
64
|
} else {
|
|
63
65
|
console.warn("j-async bind value must be a Promise or AsyncState");
|
|
64
66
|
}
|
|
@@ -67,6 +69,15 @@ export class JoistAsyncElement extends HTMLElement {
|
|
|
67
69
|
);
|
|
68
70
|
}
|
|
69
71
|
|
|
72
|
+
#isAsyncState(value: unknown): value is AsyncState {
|
|
73
|
+
return (
|
|
74
|
+
typeof value === "object" &&
|
|
75
|
+
value !== null &&
|
|
76
|
+
"status" in value &&
|
|
77
|
+
(value.status === "loading" || value.status === "error" || value.status === "success")
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
70
81
|
async #handlePromise(promise: Promise<unknown>): Promise<void> {
|
|
71
82
|
try {
|
|
72
83
|
this.#handleState({ status: "loading" });
|
|
@@ -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.
|
|
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.
|
|
19
|
+
if (e.expression.bindTo === "target") {
|
|
20
20
|
e.update({
|
|
21
21
|
oldValue: null,
|
|
22
22
|
newValue: {
|
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
import { attr, element, css, html } from "@joist/element";
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
import { JToken } from "../token.js";
|
|
3
|
+
import { JExpression } from "../expression.js";
|
|
5
4
|
import { JoistValueEvent } from "../events.js";
|
|
6
5
|
|
|
7
|
-
|
|
6
|
+
declare global {
|
|
7
|
+
interface HTMLElementTagNameMap {
|
|
8
|
+
"j-bind": JoistBindElement;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export class JAttrToken extends JExpression {
|
|
8
13
|
mapTo: string;
|
|
9
14
|
|
|
10
15
|
constructor(binding: string) {
|
|
@@ -21,7 +26,7 @@ export class JAttrToken extends JToken {
|
|
|
21
26
|
// prettier-ignore
|
|
22
27
|
shadowDom: [css`:host{display: contents;}`, html`<slot></slot>`],
|
|
23
28
|
})
|
|
24
|
-
export class
|
|
29
|
+
export class JoistBindElement extends HTMLElement {
|
|
25
30
|
@attr()
|
|
26
31
|
accessor props = "";
|
|
27
32
|
|
|
@@ -75,18 +80,14 @@ export class JoistIfElement extends HTMLElement {
|
|
|
75
80
|
.filter((b) => b);
|
|
76
81
|
}
|
|
77
82
|
|
|
78
|
-
#dispatch(token:
|
|
83
|
+
#dispatch(token: JExpression, write: (value: unknown) => void) {
|
|
79
84
|
this.dispatchEvent(
|
|
80
|
-
new JoistValueEvent(token, ({ newValue, oldValue }) => {
|
|
81
|
-
if (newValue === oldValue) {
|
|
85
|
+
new JoistValueEvent(token, ({ newValue, oldValue, alwaysUpdate }) => {
|
|
86
|
+
if (newValue === oldValue && !alwaysUpdate) {
|
|
82
87
|
return;
|
|
83
88
|
}
|
|
84
89
|
|
|
85
|
-
let valueToWrite = newValue;
|
|
86
|
-
|
|
87
|
-
if (typeof newValue === "object" && newValue !== null) {
|
|
88
|
-
valueToWrite = token.readTokenValueFrom(newValue);
|
|
89
|
-
}
|
|
90
|
+
let valueToWrite = token.evaluate(newValue);
|
|
90
91
|
|
|
91
92
|
if (token.isNegated) {
|
|
92
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 {
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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;
|
|
125
|
+
const keyProperty = this.key;
|
|
122
126
|
|
|
123
127
|
let index = 0;
|
|
124
128
|
|
|
125
129
|
for (const value of this.#items) {
|
|
126
|
-
|
|
127
|
-
|
|
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 =
|
|
148
|
+
scope.key = key;
|
|
142
149
|
scope.each = { position: index + 1, index, value };
|
|
143
150
|
}
|
|
144
151
|
|