@jay-framework/jay-stack-cli 0.15.5 → 0.16.0
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/agent-kit-template/{INSTRUCTIONS.md → designer/INSTRUCTIONS.md} +11 -8
- package/agent-kit-template/{jay-html-syntax.md → designer/jay-html-components.md} +89 -158
- package/agent-kit-template/designer/jay-html-styling.md +97 -0
- package/agent-kit-template/designer/jay-html-syntax.md +44 -0
- package/agent-kit-template/designer/jay-html-template-syntax.md +203 -0
- package/agent-kit-template/developer/INSTRUCTIONS.md +34 -0
- package/agent-kit-template/developer/cli-commands.md +228 -0
- package/agent-kit-template/developer/component-data.md +109 -0
- package/agent-kit-template/developer/component-refs.md +117 -0
- package/agent-kit-template/developer/component-state.md +140 -0
- package/agent-kit-template/developer/configuration.md +76 -0
- package/agent-kit-template/developer/dev-server-service.md +126 -0
- package/agent-kit-template/developer/page-components.md +103 -0
- package/agent-kit-template/developer/page-contracts.md +114 -0
- package/agent-kit-template/developer/project-structure.md +242 -0
- package/agent-kit-template/developer/render-results.md +112 -0
- package/agent-kit-template/developer/routing.md +175 -0
- package/agent-kit-template/developer/seo-guide.md +93 -0
- package/agent-kit-template/plugin/INSTRUCTIONS.md +43 -0
- package/agent-kit-template/plugin/actions-guide.md +184 -0
- package/agent-kit-template/plugin/component-context.md +103 -0
- package/agent-kit-template/plugin/component-data.md +109 -0
- package/agent-kit-template/plugin/component-refs.md +117 -0
- package/agent-kit-template/plugin/component-state.md +140 -0
- package/agent-kit-template/plugin/component-structure.md +174 -0
- package/agent-kit-template/plugin/contracts-guide.md +193 -0
- package/agent-kit-template/plugin/dev-server-service.md +137 -0
- package/agent-kit-template/plugin/plugin-routes.md +146 -0
- package/agent-kit-template/plugin/plugin-structure.md +210 -0
- package/agent-kit-template/plugin/render-results.md +112 -0
- package/agent-kit-template/plugin/seo-guide.md +93 -0
- package/agent-kit-template/plugin/services-guide.md +116 -0
- package/agent-kit-template/plugin/validation.md +101 -0
- package/dist/index.js +791 -60
- package/package.json +10 -10
- /package/agent-kit-template/{cli-commands.md → designer/cli-commands.md} +0 -0
- /package/agent-kit-template/{contracts-and-plugins.md → designer/contracts-and-plugins.md} +0 -0
- /package/agent-kit-template/{project-structure.md → designer/project-structure.md} +0 -0
- /package/agent-kit-template/{routing.md → designer/routing.md} +0 -0
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# Immutable Data and Patching
|
|
2
|
+
|
|
3
|
+
In Jay, ViewState data is immutable. Never mutate objects directly — use signals and JSON Patch for updates.
|
|
4
|
+
|
|
5
|
+
## Immutable Data Model
|
|
6
|
+
|
|
7
|
+
ViewState objects passed to the render function are immutable snapshots. The framework compares old and new snapshots to determine what changed in the DOM.
|
|
8
|
+
|
|
9
|
+
```typescript
|
|
10
|
+
// WRONG — never mutate directly
|
|
11
|
+
viewState.items.push(newItem);
|
|
12
|
+
viewState.count = 5;
|
|
13
|
+
|
|
14
|
+
// RIGHT — return new values from signals
|
|
15
|
+
const [count, setCount] = createSignal(0);
|
|
16
|
+
setCount(5);
|
|
17
|
+
|
|
18
|
+
return { render: () => ({ count: count() }) };
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## JSON Patch for Complex Updates
|
|
22
|
+
|
|
23
|
+
For objects with many fields, use `createPatchableSignal` with JSON Patch operations instead of replacing the entire object:
|
|
24
|
+
|
|
25
|
+
```typescript
|
|
26
|
+
import { createPatchableSignal } from '@jay-framework/component';
|
|
27
|
+
import { REPLACE, ADD, REMOVE } from '@jay-framework/json-patch';
|
|
28
|
+
|
|
29
|
+
const [state, setState, patchState] = createPatchableSignal({
|
|
30
|
+
title: 'Product',
|
|
31
|
+
price: 29.99,
|
|
32
|
+
tags: ['sale', 'featured'],
|
|
33
|
+
details: { color: 'red', size: 'M' },
|
|
34
|
+
});
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### Patch Operations
|
|
38
|
+
|
|
39
|
+
**REPLACE** — Update an existing value:
|
|
40
|
+
|
|
41
|
+
```typescript
|
|
42
|
+
patchState({ op: REPLACE, path: ['price'], value: 19.99 });
|
|
43
|
+
patchState({ op: REPLACE, path: ['details', 'color'], value: 'blue' });
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
**ADD** — Add a new field or array item:
|
|
47
|
+
|
|
48
|
+
```typescript
|
|
49
|
+
patchState({ op: ADD, path: ['tags', 1], value: 'new-tag' }); // Insert at index 1
|
|
50
|
+
patchState({ op: ADD, path: ['details', 'weight'], value: '500g' });
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
**REMOVE** — Remove a field or array item:
|
|
54
|
+
|
|
55
|
+
```typescript
|
|
56
|
+
patchState({ op: REMOVE, path: ['tags', 0] }); // Remove first tag
|
|
57
|
+
patchState({ op: REMOVE, path: ['details', 'size'] });
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
**MOVE** — Move a value from one path to another:
|
|
61
|
+
|
|
62
|
+
```typescript
|
|
63
|
+
import { MOVE } from '@jay-framework/json-patch';
|
|
64
|
+
|
|
65
|
+
patchState({ op: MOVE, from: ['tags', 0], path: ['tags', 2] }); // Reorder array item
|
|
66
|
+
patchState({ op: MOVE, from: ['details', 'color'], path: ['primaryColor'] }); // Relocate field
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### Multiple Patches
|
|
70
|
+
|
|
71
|
+
Apply multiple patches at once — the framework batches them into a single update:
|
|
72
|
+
|
|
73
|
+
```typescript
|
|
74
|
+
patchState(
|
|
75
|
+
{ op: REPLACE, path: ['price'], value: 19.99 },
|
|
76
|
+
{ op: REPLACE, path: ['details', 'color'], value: 'blue' },
|
|
77
|
+
);
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### When to Use Patch vs Set
|
|
81
|
+
|
|
82
|
+
- **Simple values** (number, string, boolean): use `setSignal(newValue)`
|
|
83
|
+
- **Objects with few fields**: use `setSignal({ ...old, field: newValue })`
|
|
84
|
+
- **Complex nested objects**: use `patchState` for surgical updates
|
|
85
|
+
- **Arrays with identity tracking**: use `patchState` with ADD/REMOVE
|
|
86
|
+
|
|
87
|
+
## createDerivedArray (Map Hook)
|
|
88
|
+
|
|
89
|
+
Transform an array reactively with smart caching. Only remaps items that actually changed:
|
|
90
|
+
|
|
91
|
+
```typescript
|
|
92
|
+
import { createDerivedArray } from '@jay-framework/component';
|
|
93
|
+
|
|
94
|
+
const displayProducts = createDerivedArray(
|
|
95
|
+
() => products(),
|
|
96
|
+
(item, index, length) => ({
|
|
97
|
+
label: `${item().name} - ${formatPrice(item().price)}`,
|
|
98
|
+
position: `${index() + 1} of ${length()}`,
|
|
99
|
+
}),
|
|
100
|
+
);
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Key behavior:
|
|
104
|
+
|
|
105
|
+
- If an item's object identity hasn't changed, the cached mapped result is reused
|
|
106
|
+
- `index()` and `length()` are tracked — if you don't call them, changes to index/length won't trigger a remap
|
|
107
|
+
- Returns a `Getter<U[]>` — read with `displayProducts()`
|
|
108
|
+
|
|
109
|
+
See [component-state.md](component-state.md) for the full hook reference.
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# Component Refs
|
|
2
|
+
|
|
3
|
+
Refs provide access to DOM elements declared as `interactive` in the contract. They are the second parameter of the interactive constructor.
|
|
4
|
+
|
|
5
|
+
## Single Refs
|
|
6
|
+
|
|
7
|
+
A ref maps to one DOM element:
|
|
8
|
+
|
|
9
|
+
```yaml
|
|
10
|
+
# Contract
|
|
11
|
+
- tag: addToCart
|
|
12
|
+
type: interactive
|
|
13
|
+
elementType: HTMLButtonElement
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
```typescript
|
|
17
|
+
// Component
|
|
18
|
+
.withInteractive(function MyComp(props, refs) {
|
|
19
|
+
refs.addToCart.onClick(() => {
|
|
20
|
+
// handle click
|
|
21
|
+
});
|
|
22
|
+
})
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
### Ref Methods
|
|
26
|
+
|
|
27
|
+
Refs provide type-safe access to the DOM element:
|
|
28
|
+
|
|
29
|
+
```typescript
|
|
30
|
+
refs.submitButton.onClick(() => {
|
|
31
|
+
/* ... */
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
// exec$ gives direct access to the element and current ViewState
|
|
35
|
+
refs.submitButton.exec$((element, viewState) => {
|
|
36
|
+
element.disabled = viewState.isSubmitting;
|
|
37
|
+
});
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Collection Refs
|
|
41
|
+
|
|
42
|
+
When an interactive tag is inside a `repeated` sub-contract, the ref becomes a collection. In jay-html, collection refs use the `$` suffix:
|
|
43
|
+
|
|
44
|
+
```html
|
|
45
|
+
<div forEach="items" trackBy="id">
|
|
46
|
+
<button ref="itemButton$">Click</button>
|
|
47
|
+
</div>
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
The `$` is stripped from the name in the contract and component code:
|
|
51
|
+
|
|
52
|
+
```yaml
|
|
53
|
+
# Contract
|
|
54
|
+
- tag: items
|
|
55
|
+
type: sub-contract
|
|
56
|
+
repeated: true
|
|
57
|
+
trackBy: id
|
|
58
|
+
tags:
|
|
59
|
+
- tag: id
|
|
60
|
+
type: data
|
|
61
|
+
dataType: string
|
|
62
|
+
- tag: itemButton
|
|
63
|
+
type: interactive
|
|
64
|
+
elementType: HTMLButtonElement
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### Collection Ref Methods
|
|
68
|
+
|
|
69
|
+
```typescript
|
|
70
|
+
// Map over all items in the collection
|
|
71
|
+
const labels = refs.itemButton.map((proxy, viewState, coordinate) => {
|
|
72
|
+
return viewState.name;
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// Find a specific item
|
|
76
|
+
const target = refs.itemButton.find((viewState) => viewState.id === 'target-id');
|
|
77
|
+
|
|
78
|
+
// Find by coordinate
|
|
79
|
+
const target = refs.itemButton.find((viewState, coordinate) =>
|
|
80
|
+
sameCoordinate(coordinate, ['item-2', 'itemButton']),
|
|
81
|
+
);
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Element Types
|
|
85
|
+
|
|
86
|
+
Common element types for interactive tags:
|
|
87
|
+
|
|
88
|
+
| Element Type | Use For |
|
|
89
|
+
| --------------------- | --------------------- |
|
|
90
|
+
| `HTMLButtonElement` | Buttons, clickable |
|
|
91
|
+
| `HTMLAnchorElement` | Links |
|
|
92
|
+
| `HTMLInputElement` | Text inputs, checkbox |
|
|
93
|
+
| `HTMLSelectElement` | Dropdowns |
|
|
94
|
+
| `HTMLTextAreaElement` | Multi-line text |
|
|
95
|
+
| `HTMLFormElement` | Forms |
|
|
96
|
+
| `HTMLDivElement` | Generic containers |
|
|
97
|
+
|
|
98
|
+
Multiple element types (when the same ref may bind to different elements):
|
|
99
|
+
|
|
100
|
+
```yaml
|
|
101
|
+
- tag: trigger
|
|
102
|
+
type: interactive
|
|
103
|
+
elementType: HTMLButtonElement | HTMLAnchorElement
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## Data + Interactive
|
|
107
|
+
|
|
108
|
+
A tag can be both data and interactive:
|
|
109
|
+
|
|
110
|
+
```yaml
|
|
111
|
+
- tag: quantityInput
|
|
112
|
+
type: [data, interactive]
|
|
113
|
+
dataType: number
|
|
114
|
+
elementType: HTMLInputElement
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
This generates both a ViewState field and a ref.
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# Component State Hooks
|
|
2
|
+
|
|
3
|
+
All hooks are used inside the interactive phase (the `withInteractive` constructor function). They provide reactive state management for client-side behavior.
|
|
4
|
+
|
|
5
|
+
## createSignal
|
|
6
|
+
|
|
7
|
+
Creates a reactive getter/setter pair:
|
|
8
|
+
|
|
9
|
+
```typescript
|
|
10
|
+
import { createSignal } from '@jay-framework/component';
|
|
11
|
+
|
|
12
|
+
const [count, setCount] = createSignal(0);
|
|
13
|
+
|
|
14
|
+
// Read
|
|
15
|
+
count(); // 0
|
|
16
|
+
|
|
17
|
+
// Write
|
|
18
|
+
setCount(5); // set to 5
|
|
19
|
+
setCount((n) => n + 1); // increment
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Can initialize from a getter (reactive dependency):
|
|
23
|
+
|
|
24
|
+
```typescript
|
|
25
|
+
const [label, setLabel] = createSignal(() => 'Hello ' + props.name());
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## createPatchableSignal
|
|
29
|
+
|
|
30
|
+
Creates a signal with JSON Patch support for fine-grained updates to complex objects:
|
|
31
|
+
|
|
32
|
+
```typescript
|
|
33
|
+
import { createPatchableSignal } from '@jay-framework/component';
|
|
34
|
+
import { REPLACE } from '@jay-framework/json-patch';
|
|
35
|
+
|
|
36
|
+
const [data, setData, patchData] = createPatchableSignal({
|
|
37
|
+
label: 'Hello',
|
|
38
|
+
count: 0,
|
|
39
|
+
nested: { value: 42 },
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
// Patch a specific field
|
|
43
|
+
patchData({ op: REPLACE, path: ['label'], value: 'Updated' });
|
|
44
|
+
|
|
45
|
+
// Patch nested field
|
|
46
|
+
patchData({ op: REPLACE, path: ['nested', 'value'], value: 99 });
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
See [component-data.md](component-data.md) for more on immutable data and patching.
|
|
50
|
+
|
|
51
|
+
## createMemo
|
|
52
|
+
|
|
53
|
+
Creates a memoized computed value that recalculates only when dependencies change:
|
|
54
|
+
|
|
55
|
+
```typescript
|
|
56
|
+
import { createMemo } from '@jay-framework/component';
|
|
57
|
+
|
|
58
|
+
const fullName = createMemo(() => `${firstName()} ${lastName()}`);
|
|
59
|
+
|
|
60
|
+
// Read
|
|
61
|
+
fullName(); // recomputes only when firstName() or lastName() change
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
With initial value:
|
|
65
|
+
|
|
66
|
+
```typescript
|
|
67
|
+
const total = createMemo((prev) => prev + latestValue(), 0);
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## createEffect
|
|
71
|
+
|
|
72
|
+
Registers a side effect that runs on mount and when dependencies change. Optional cleanup function:
|
|
73
|
+
|
|
74
|
+
```typescript
|
|
75
|
+
import { createEffect } from '@jay-framework/component';
|
|
76
|
+
|
|
77
|
+
createEffect(() => {
|
|
78
|
+
const handler = () => setWindowWidth(window.innerWidth);
|
|
79
|
+
window.addEventListener('resize', handler);
|
|
80
|
+
return () => window.removeEventListener('resize', handler); // cleanup
|
|
81
|
+
});
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Effects track reactive dependencies automatically:
|
|
85
|
+
|
|
86
|
+
```typescript
|
|
87
|
+
createEffect(() => {
|
|
88
|
+
document.title = `${count()} items`; // reruns when count() changes
|
|
89
|
+
});
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## createDerivedArray
|
|
93
|
+
|
|
94
|
+
Efficiently maps an array with smart caching. Only remaps items that actually changed:
|
|
95
|
+
|
|
96
|
+
```typescript
|
|
97
|
+
import { createDerivedArray } from '@jay-framework/component';
|
|
98
|
+
|
|
99
|
+
const displayItems = createDerivedArray(
|
|
100
|
+
() => products(),
|
|
101
|
+
(item, index, length) => ({
|
|
102
|
+
name: item().name,
|
|
103
|
+
displayPrice: formatPrice(item().price),
|
|
104
|
+
isLast: index() === length() - 1,
|
|
105
|
+
}),
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
// Read the mapped array
|
|
109
|
+
displayItems();
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Key optimizations:
|
|
113
|
+
|
|
114
|
+
- Reuses mapped items when the source item hasn't changed
|
|
115
|
+
- Only tracks `index()` and `length()` if you actually call them
|
|
116
|
+
- Uses object identity (not deep equality) for cache hits
|
|
117
|
+
|
|
118
|
+
## createEvent
|
|
119
|
+
|
|
120
|
+
Creates an event emitter for component-to-parent communication:
|
|
121
|
+
|
|
122
|
+
```typescript
|
|
123
|
+
import { createEvent } from '@jay-framework/component';
|
|
124
|
+
|
|
125
|
+
const onChange = createEvent<{ value: number }>((emitter) => {
|
|
126
|
+
emitter.emit({ value: count() });
|
|
127
|
+
});
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## useReactive
|
|
131
|
+
|
|
132
|
+
Gets the current reactive context for advanced use cases:
|
|
133
|
+
|
|
134
|
+
```typescript
|
|
135
|
+
import { useReactive } from '@jay-framework/component';
|
|
136
|
+
|
|
137
|
+
const reactive = useReactive();
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Most components won't need this — prefer the higher-level hooks above.
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
# Component Structure
|
|
2
|
+
|
|
3
|
+
Full-stack components use `makeJayStackComponent` with a fluent builder API and three rendering phases.
|
|
4
|
+
|
|
5
|
+
## Basic Structure
|
|
6
|
+
|
|
7
|
+
```typescript
|
|
8
|
+
import { makeJayStackComponent, phaseOutput } from '@jay-framework/fullstack-component';
|
|
9
|
+
import type { MyContract } from './my-contract.generated';
|
|
10
|
+
|
|
11
|
+
export const myComponent = makeJayStackComponent<MyContract>()
|
|
12
|
+
.withSlowlyRender(async (props) => {
|
|
13
|
+
const data = await fetchStaticData();
|
|
14
|
+
return phaseOutput(
|
|
15
|
+
{ title: data.title, description: data.description }, // ViewState
|
|
16
|
+
{ productId: data.id }, // CarryForward
|
|
17
|
+
);
|
|
18
|
+
})
|
|
19
|
+
.withFastRender(async (props) => {
|
|
20
|
+
return phaseOutput({ price: await getPrice(), inStock: true }, {});
|
|
21
|
+
})
|
|
22
|
+
.withInteractive(function MyComponent(props, refs) {
|
|
23
|
+
// Client-side hooks here
|
|
24
|
+
return { render: () => ({}) };
|
|
25
|
+
});
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Builder API
|
|
29
|
+
|
|
30
|
+
### `.withProps<T>()`
|
|
31
|
+
|
|
32
|
+
Declare the props type (must match contract `props`):
|
|
33
|
+
|
|
34
|
+
```typescript
|
|
35
|
+
makeJayStackComponent<MyContract>().withProps<{ productId: string; currency?: string }>();
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### `.withServices(...markers)`
|
|
39
|
+
|
|
40
|
+
Inject server-side services:
|
|
41
|
+
|
|
42
|
+
```typescript
|
|
43
|
+
.withServices(DATABASE_SERVICE, CACHE_SERVICE)
|
|
44
|
+
.withSlowlyRender(async (props, db, cache) => {
|
|
45
|
+
// db and cache are injected
|
|
46
|
+
})
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### `.withContexts(...markers)`
|
|
50
|
+
|
|
51
|
+
Consume client-side contexts in the interactive phase:
|
|
52
|
+
|
|
53
|
+
```typescript
|
|
54
|
+
.withContexts(CART_CONTEXT)
|
|
55
|
+
.withInteractive(function MyComp(props, refs, cartCtx) {
|
|
56
|
+
// cartCtx available in interactive phase
|
|
57
|
+
})
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### `.withLoadParams(fn)`
|
|
61
|
+
|
|
62
|
+
Generate URL params for SSG (static site generation). Returns an async iterable of param arrays:
|
|
63
|
+
|
|
64
|
+
```typescript
|
|
65
|
+
.withLoadParams(async function* (db) {
|
|
66
|
+
const products = await db.getAllProducts();
|
|
67
|
+
yield products.map(p => ({ slug: p.slug }));
|
|
68
|
+
})
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### `.withSlowlyRender(fn)` — Build-time rendering
|
|
72
|
+
|
|
73
|
+
Runs during SSG. Has access to props and services. Returns `phaseOutput(viewState, carryForward)`.
|
|
74
|
+
|
|
75
|
+
CarryForward data is passed to the fast phase but not included in the ViewState.
|
|
76
|
+
|
|
77
|
+
```typescript
|
|
78
|
+
.withSlowlyRender(async (props, db) => {
|
|
79
|
+
const product = await db.getProduct(props.slug);
|
|
80
|
+
if (!product) return notFound('Product not found');
|
|
81
|
+
return phaseOutput(
|
|
82
|
+
{ title: product.name, description: product.desc },
|
|
83
|
+
{ productId: product.id }, // Available in fast phase
|
|
84
|
+
);
|
|
85
|
+
})
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### `.withFastRender(fn)` — Request-time rendering
|
|
89
|
+
|
|
90
|
+
Runs on each request. Receives props (including `query` for query parameters) and carry-forward from slow phase.
|
|
91
|
+
|
|
92
|
+
```typescript
|
|
93
|
+
.withFastRender(async (props, db) => {
|
|
94
|
+
const price = await db.getPrice(props.carryForward.productId);
|
|
95
|
+
return phaseOutput({ price, inStock: price > 0 }, {});
|
|
96
|
+
})
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### `.withClientDefaults(fn)` — Default client ViewState
|
|
100
|
+
|
|
101
|
+
Provides default values for client-side ViewState before hydration:
|
|
102
|
+
|
|
103
|
+
```typescript
|
|
104
|
+
.withClientDefaults(() => ({
|
|
105
|
+
quantity: 1,
|
|
106
|
+
selectedVariant: 'default',
|
|
107
|
+
}))
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### `.withInteractive(ComponentConstructor)` — Client-side logic
|
|
111
|
+
|
|
112
|
+
The interactive phase runs in the browser. Use hooks here (see component-state.md):
|
|
113
|
+
|
|
114
|
+
```typescript
|
|
115
|
+
.withInteractive(function ProductPage(props, refs) {
|
|
116
|
+
const [quantity, setQuantity] = createSignal(1);
|
|
117
|
+
|
|
118
|
+
refs.addToCart.onClick(() => {
|
|
119
|
+
addToCartAction({ productId: props.productId, quantity: quantity() });
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
render: () => ({
|
|
124
|
+
quantity: quantity(),
|
|
125
|
+
}),
|
|
126
|
+
};
|
|
127
|
+
})
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## Render Return Types
|
|
131
|
+
|
|
132
|
+
Each phase can return:
|
|
133
|
+
|
|
134
|
+
- `phaseOutput(viewState, carryForward)` — success
|
|
135
|
+
- `notFound()`, `badRequest()`, `unauthorized()`, `forbidden()` — client errors
|
|
136
|
+
- `serverError5xx(status, message)` — server errors
|
|
137
|
+
- `redirect3xx(status, location)` — redirects
|
|
138
|
+
|
|
139
|
+
See [render-results.md](render-results.md) for details.
|
|
140
|
+
|
|
141
|
+
## Props and Contract Alignment
|
|
142
|
+
|
|
143
|
+
The component's props type must match the contract's `props` section:
|
|
144
|
+
|
|
145
|
+
```yaml
|
|
146
|
+
# Contract
|
|
147
|
+
props:
|
|
148
|
+
- name: productId
|
|
149
|
+
type: string
|
|
150
|
+
required: true
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
```typescript
|
|
154
|
+
// Component
|
|
155
|
+
makeJayStackComponent<MyContract>().withProps<{ productId: string }>();
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
## Params and Contract Alignment
|
|
159
|
+
|
|
160
|
+
If the contract has `params`, the component should use `withLoadParams` and the route must have matching dynamic segments:
|
|
161
|
+
|
|
162
|
+
```yaml
|
|
163
|
+
# Contract
|
|
164
|
+
params:
|
|
165
|
+
slug: string
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
```typescript
|
|
169
|
+
// Component
|
|
170
|
+
.withLoadParams(async function* (db) {
|
|
171
|
+
const items = await db.getAll();
|
|
172
|
+
yield items.map(i => ({ slug: i.slug }));
|
|
173
|
+
})
|
|
174
|
+
```
|