@zerotal/arch 1.7.3 → 1.7.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +34 -2
- package/README.md +4 -2
- package/api-surface.md +4 -4
- package/docs/about.md +16 -16
- package/docs/admin/actions.md +9 -9
- package/docs/admin/auth.md +2 -2
- package/docs/admin/dashboard.md +5 -5
- package/docs/admin/extending-ui.md +12 -12
- package/docs/admin/extending.md +16 -7
- package/docs/admin/forms.md +4 -4
- package/docs/admin/index.md +2 -2
- package/docs/admin/operations.md +9 -9
- package/docs/admin/resources.md +3 -3
- package/docs/admin/structure.md +6 -6
- package/docs/admin/tables.md +8 -8
- package/docs/admin/testing.md +4 -4
- package/docs/ai.md +118 -11
- package/docs/application.md +12 -12
- package/docs/arch.md +25 -3
- package/docs/assets.md +11 -11
- package/docs/audit.md +13 -13
- package/docs/authentication.md +39 -39
- package/docs/authorization.md +13 -13
- package/docs/broadcasting/channels.md +2 -2
- package/docs/broadcasting/client.md +2 -2
- package/docs/broadcasting/events.md +9 -10
- package/docs/broadcasting/index.md +1 -1
- package/docs/broadcasting/testing.md +5 -5
- package/docs/cache.md +15 -13
- package/docs/carbon.md +35 -35
- package/docs/changelog.md +165 -1
- package/docs/client/index.md +277 -70
- package/docs/commands.md +4 -4
- package/docs/components.md +116 -115
- package/docs/config-system.md +4 -4
- package/docs/container.md +27 -27
- package/docs/context.md +27 -27
- package/docs/contributing.md +46 -0
- package/docs/controllers.md +10 -10
- package/docs/conventions.md +23 -15
- package/docs/cookies.md +3 -3
- package/docs/csrf.md +6 -6
- package/docs/database.md +22 -17
- package/docs/deployment.md +2 -2
- package/docs/devtools.md +16 -16
- package/docs/email-verification.md +8 -8
- package/docs/encryption.md +7 -7
- package/docs/errors.md +6 -6
- package/docs/events.md +12 -11
- package/docs/flow/components.md +32 -32
- package/docs/flow/decorators.md +18 -18
- package/docs/flow/events.md +16 -16
- package/docs/flow/forms.md +17 -17
- package/docs/flow/icons.md +199 -0
- package/docs/flow/index.md +20 -20
- package/docs/flow/layouts.md +35 -35
- package/docs/flow/lifecycle.md +16 -16
- package/docs/flow/models.md +9 -9
- package/docs/flow/pagination.md +8 -8
- package/docs/flow/performance.md +7 -7
- package/docs/flow/references.md +3 -3
- package/docs/flow/routing.md +26 -26
- package/docs/flow/testing.md +20 -20
- package/docs/getting-started.md +18 -13
- package/docs/health.md +17 -8
- package/docs/helpers.md +17 -17
- package/docs/i18n.md +10 -10
- package/docs/inertia/devtools.md +4 -4
- package/docs/inertia/index.md +12 -7
- package/docs/inertia/props.md +22 -22
- package/docs/inertia/rendering.md +86 -7
- package/docs/inertia/ssr.md +2 -2
- package/docs/lifecycle.md +2 -2
- package/docs/lock.md +11 -11
- package/docs/logger.md +12 -12
- package/docs/media.md +22 -22
- package/docs/middleware.md +17 -17
- package/docs/migrations.md +13 -13
- package/docs/monitor.md +6 -6
- package/docs/notifications.md +24 -24
- package/docs/orm/casts.md +14 -14
- package/docs/orm/factories.md +8 -8
- package/docs/orm/index.md +22 -20
- package/docs/orm/lifecycle.md +13 -13
- package/docs/orm/queries.md +28 -28
- package/docs/orm/relationships.md +16 -16
- package/docs/orm/serialization.md +11 -11
- package/docs/package-development.md +10 -10
- package/docs/pagination.md +14 -14
- package/docs/password-reset.md +6 -6
- package/docs/providers.md +11 -11
- package/docs/query-builder.md +18 -18
- package/docs/queue.md +14 -12
- package/docs/rate-limiting.md +6 -6
- package/docs/responses.md +14 -14
- package/docs/roles-and-2fa.md +9 -9
- package/docs/routing.md +44 -44
- package/docs/scaffolding.md +1 -1
- package/docs/scheduler.md +10 -10
- package/docs/seeding.md +6 -6
- package/docs/session.md +16 -14
- package/docs/social.md +14 -14
- package/docs/storage.md +25 -13
- package/docs/structure.md +2 -2
- package/docs/support-policy.md +36 -8
- package/docs/telemetry.md +12 -12
- package/docs/tenancy.md +34 -40
- package/docs/testing/browser.md +6 -6
- package/docs/testing/console.md +3 -3
- package/docs/testing/database.md +10 -10
- package/docs/testing/flow-browser.md +6 -6
- package/docs/testing/http.md +12 -12
- package/docs/testing/index.md +3 -3
- package/docs/testing/mocking.md +8 -8
- package/docs/upgrade.md +1 -1
- package/docs/validator.md +19 -19
- package/docs/view.md +14 -14
- package/package.json +4 -4
- package/src/index.ts +25 -1
- package/src/tools/searchDocs.ts +7 -0
- package/docs/client/auth.md +0 -113
- package/docs/client/errors.md +0 -139
- package/docs/client/files.md +0 -118
- package/docs/client/references.md +0 -58
- package/docs/client/requests.md +0 -131
- package/docs/client/resilience.md +0 -141
- package/docs/client/testing.md +0 -146
package/docs/errors.md
CHANGED
|
@@ -123,7 +123,7 @@ export class QuotaExceededError extends HttpError {
|
|
|
123
123
|
}
|
|
124
124
|
```
|
|
125
125
|
|
|
126
|
-
```typescript
|
|
126
|
+
```typescript fragment
|
|
127
127
|
// in a controller
|
|
128
128
|
throw new PaymentFailedError(charge.id);
|
|
129
129
|
```
|
|
@@ -135,7 +135,7 @@ then register it in `bootstrap/app.ts` with `app.withExceptionHandler(Handler)`.
|
|
|
135
135
|
The framework calls `report()` first, then `render()`, for every unhandled
|
|
136
136
|
exception:
|
|
137
137
|
|
|
138
|
-
```typescript
|
|
138
|
+
```typescript fragment
|
|
139
139
|
// app/exceptions/Handler.ts
|
|
140
140
|
import { ExceptionHandler, NotFoundError, ForbiddenError } from "zerotal";
|
|
141
141
|
import type { HttpContext } from "zerotal";
|
|
@@ -169,7 +169,7 @@ export class Handler extends ExceptionHandler {
|
|
|
169
169
|
}
|
|
170
170
|
```
|
|
171
171
|
|
|
172
|
-
```typescript
|
|
172
|
+
```typescript fragment
|
|
173
173
|
// bootstrap/app.ts
|
|
174
174
|
import { Application } from "zerotal";
|
|
175
175
|
import providers from "./providers.ts";
|
|
@@ -193,7 +193,7 @@ reach `console.error`.
|
|
|
193
193
|
To silence your own classes, list them on the `dontReport` array instead of
|
|
194
194
|
filtering by hand:
|
|
195
195
|
|
|
196
|
-
```typescript
|
|
196
|
+
```typescript fragment
|
|
197
197
|
// app/exceptions/Handler.ts
|
|
198
198
|
import { ExceptionHandler, ForbiddenError } from "zerotal";
|
|
199
199
|
import { PaymentFailedError } from "./PaymentFailedError.ts";
|
|
@@ -227,7 +227,7 @@ The validation system throws automatically — you don't catch anything. It thro
|
|
|
227
227
|
`ValidationJsonError` (a 422 JSON body for API clients), and both flow straight
|
|
228
228
|
through the handler:
|
|
229
229
|
|
|
230
|
-
```typescript
|
|
230
|
+
```typescript fragment
|
|
231
231
|
// in a controller
|
|
232
232
|
// Browser → 303 redirect back with flashed errors
|
|
233
233
|
// API → 422 JSON: { message: 'Validation failed', errors: {...} }
|
|
@@ -241,7 +241,7 @@ See the [Validator](/docs/validator) guide for details.
|
|
|
241
241
|
Every `ZerotalError` carries a machine-readable `code` string alongside the HTTP
|
|
242
242
|
`status`. Use it on the client to distinguish errors that share a status:
|
|
243
243
|
|
|
244
|
-
```typescript
|
|
244
|
+
```typescript fragment
|
|
245
245
|
// the API client receives:
|
|
246
246
|
// { "message": "Payment failed for charge ch_xxx", "code": "E_PAYMENT_FAILED" }
|
|
247
247
|
|
package/docs/events.md
CHANGED
|
@@ -36,7 +36,7 @@ never use framework events to drive business logic.
|
|
|
36
36
|
If you want the full side-by-side, here it is — but you can skip it and come back
|
|
37
37
|
once you've read the section for the one you need:
|
|
38
38
|
|
|
39
|
-
|
|
|
39
|
+
| Aspect | Framework events | Application events |
|
|
40
40
|
| ------------- | --------------------------------- | --------------------------- |
|
|
41
41
|
| Purpose | Observing the framework | Reacting to your domain |
|
|
42
42
|
| Who emits | The framework | Your code |
|
|
@@ -53,8 +53,9 @@ talks through the bus instead.
|
|
|
53
53
|
|
|
54
54
|
### API
|
|
55
55
|
|
|
56
|
-
```typescript
|
|
57
|
-
import { FrameworkEvents
|
|
56
|
+
```typescript fragment
|
|
57
|
+
import { FrameworkEvents } from "zerotal";
|
|
58
|
+
import { QueryExecuted } from "@zerotal/orm";
|
|
58
59
|
|
|
59
60
|
// Subscribe — returns an unsubscribe function.
|
|
60
61
|
const off = FrameworkEvents.on(QueryExecuted, (event) => {
|
|
@@ -87,7 +88,7 @@ dispatch a queue job) from outside the handler.
|
|
|
87
88
|
Subscribe in `onBooted`, keep the unsubscribe functions, and release them in
|
|
88
89
|
`onStopping` so handlers never leak between boots or test suites:
|
|
89
90
|
|
|
90
|
-
```typescript
|
|
91
|
+
```typescript fragment
|
|
91
92
|
import { ServiceProvider, FrameworkEvents, RequestFailed } from "zerotal";
|
|
92
93
|
|
|
93
94
|
export class MetricsProvider extends ServiceProvider {
|
|
@@ -185,7 +186,7 @@ the emitting packages knowing you exist.
|
|
|
185
186
|
Some events carry `ctx`; for those that do not (mail/cache/queue happen outside the
|
|
186
187
|
HTTP frame of reference), resolve the current request lazily:
|
|
187
188
|
|
|
188
|
-
```typescript
|
|
189
|
+
```typescript fragment
|
|
189
190
|
import { FrameworkEvents, RequestContext } from "zerotal";
|
|
190
191
|
import { CacheQueried } from "@zerotal/cache";
|
|
191
192
|
|
|
@@ -204,7 +205,7 @@ app should react to. Events are plain classes; listeners are classes with a
|
|
|
204
205
|
|
|
205
206
|
### Define an event and a listener
|
|
206
207
|
|
|
207
|
-
```typescript
|
|
208
|
+
```typescript fragment
|
|
208
209
|
// app/events/UserRegistered.ts
|
|
209
210
|
export class UserRegistered {
|
|
210
211
|
constructor(
|
|
@@ -227,7 +228,7 @@ export class SendWelcomeEmail {
|
|
|
227
228
|
|
|
228
229
|
### Register and emit
|
|
229
230
|
|
|
230
|
-
```typescript
|
|
231
|
+
```typescript fragment
|
|
231
232
|
// usually a provider's onBooted(), or bootstrap code
|
|
232
233
|
import { Events } from "zerotal";
|
|
233
234
|
import { UserRegistered } from "./app/events/UserRegistered.ts";
|
|
@@ -266,7 +267,7 @@ You usually don't call `Events.on(...)` yourself. A listener placed in `app/list
|
|
|
266
267
|
declares the event(s) it handles via `static listens`, and the framework binds it on the
|
|
267
268
|
emitter at boot — no registration code required.
|
|
268
269
|
|
|
269
|
-
```typescript
|
|
270
|
+
```typescript fragment
|
|
270
271
|
// app/listeners/SendWelcomeEmail.ts
|
|
271
272
|
import { UserRegistered } from "../events/UserRegistered.ts";
|
|
272
273
|
|
|
@@ -284,7 +285,7 @@ export class SendWelcomeEmail {
|
|
|
284
285
|
`static listens` accepts an array — the listener binds to each event and `handle()`
|
|
285
286
|
receives whichever one fired:
|
|
286
287
|
|
|
287
|
-
```typescript
|
|
288
|
+
```typescript fragment
|
|
288
289
|
// app/listeners/GrantWelcomeBonus.ts
|
|
289
290
|
import { UserRegistered } from "../events/UserRegistered.ts";
|
|
290
291
|
import { UserReactivated } from "../events/UserReactivated.ts";
|
|
@@ -309,7 +310,7 @@ job, so `emit()` returns without waiting for the work to finish. Set `queue` to
|
|
|
309
310
|
the default queue, or to a queue name. Two optional fields tune the retry behaviour of the
|
|
310
311
|
dispatched job:
|
|
311
312
|
|
|
312
|
-
```typescript
|
|
313
|
+
```typescript fragment
|
|
313
314
|
// app/listeners/SendWelcomeEmail.ts
|
|
314
315
|
import { UserRegistered } from "../events/UserRegistered.ts";
|
|
315
316
|
|
|
@@ -360,7 +361,7 @@ authorization, and the client side.
|
|
|
360
361
|
|
|
361
362
|
Both buses expose a reset so one test never leaks subscriptions into the next:
|
|
362
363
|
|
|
363
|
-
```typescript
|
|
364
|
+
```typescript fragment
|
|
364
365
|
import { Events, FrameworkEvents } from "zerotal";
|
|
365
366
|
|
|
366
367
|
// Remove every application listener
|
package/docs/flow/components.md
CHANGED
|
@@ -7,7 +7,7 @@ description: The component library that ships with Flow — forms, overlays, tab
|
|
|
7
7
|
|
|
8
8
|
Flow ships a library of ready-made native components and unstyled headless primitives. All are imported from `@zerotal/flow`.
|
|
9
9
|
|
|
10
|
-
> **Tip** — See them live: the
|
|
10
|
+
> **Tip** — See them live: [the component gallery](/showcase/flow/components) shows modals, drawers, tabs and menus driven by a real server, and [the flow-ui kit](/showcase/flow/ui-kit) walks the styled set. The [full catalogue](/docs/components) documents every component with props.
|
|
11
11
|
|
|
12
12
|
## Navigation
|
|
13
13
|
|
|
@@ -54,7 +54,7 @@ cached either way.
|
|
|
54
54
|
|
|
55
55
|
`data-current` matches by **prefix** — a link to `/posts` stays active on `/posts/42` — which is what you want for a section parent. For an index link that should be active only on its own exact URL (an "Overview" tab that shouldn't light up on the section's sub-pages), add `exact`:
|
|
56
56
|
|
|
57
|
-
```tsx
|
|
57
|
+
```tsx fragment
|
|
58
58
|
<Link href="/dashboard" exact class="data-[current]:font-bold">
|
|
59
59
|
Overview
|
|
60
60
|
</Link>
|
|
@@ -72,7 +72,7 @@ Some links aren't really going anywhere, though: a sort header, a filter chip, a
|
|
|
72
72
|
tab strip partway down a long page. Jumping to the top for those loses the
|
|
73
73
|
control the user was just looking at. `preserveScroll` leaves the viewport alone:
|
|
74
74
|
|
|
75
|
-
```tsx
|
|
75
|
+
```tsx fragment
|
|
76
76
|
<Link href={this.currentUrl({ query: { sort: "title" } })} preserveScroll>
|
|
77
77
|
Title
|
|
78
78
|
</Link>
|
|
@@ -101,7 +101,7 @@ import { Head } from "@zerotal/flow";
|
|
|
101
101
|
|
|
102
102
|
Shorthand for `<Head><title>…</title></Head>`. Supports interpolation:
|
|
103
103
|
|
|
104
|
-
```tsx
|
|
104
|
+
```tsx fragment
|
|
105
105
|
import { Title } from "@zerotal/flow";
|
|
106
106
|
|
|
107
107
|
<Title>{`${this.unreadCount} unread — Inbox`}</Title>;
|
|
@@ -113,7 +113,7 @@ import { Title } from "@zerotal/flow";
|
|
|
113
113
|
|
|
114
114
|
Preserve an element — and its live state (a playing `<audio>`, an embedded widget) — across `navigate` page visits. Give it a stable `name`; on navigation the bridge re-uses the existing DOM node instead of replacing it. Best placed in your layout:
|
|
115
115
|
|
|
116
|
-
```tsx
|
|
116
|
+
```tsx fragment
|
|
117
117
|
import { Persist } from "@zerotal/flow";
|
|
118
118
|
|
|
119
119
|
{
|
|
@@ -132,7 +132,7 @@ Elements inside `<Persist>` also survive server-patch morphs (they carry `flow:i
|
|
|
132
132
|
|
|
133
133
|
A dialog that bundles reactive visibility, a backdrop, a panel, a transition, a close button, and Escape-to-close — all wired to one boolean prop. Clicking the backdrop, the × button, or pressing Escape sets that prop back to `false`:
|
|
134
134
|
|
|
135
|
-
```tsx
|
|
135
|
+
```tsx fragment
|
|
136
136
|
import { Modal } from "@zerotal/flow";
|
|
137
137
|
|
|
138
138
|
export class ContactsPage extends Component {
|
|
@@ -175,7 +175,7 @@ export class ContactsPage extends Component {
|
|
|
175
175
|
|
|
176
176
|
A slide-over panel — the edge-anchored sibling of `<Modal>`. Same binding and close model (backdrop, × and Escape all close it client-side with no round-trip; focus-trapped), but slides in from an edge:
|
|
177
177
|
|
|
178
|
-
```tsx
|
|
178
|
+
```tsx fragment
|
|
179
179
|
import { Drawer } from "@zerotal/flow";
|
|
180
180
|
|
|
181
181
|
<button onClick={() => (this.cartOpen = true)}>Cart ({this.cartCount})</button>
|
|
@@ -219,7 +219,7 @@ Levels `success` / `error` / `warning` / `info` map to distinct colors. Click a
|
|
|
219
219
|
|
|
220
220
|
A dismissible inline alert. `variant` sets the palette and ARIA role (error/warning announce assertively). Dismissal is client-only — no round-trip:
|
|
221
221
|
|
|
222
|
-
```tsx
|
|
222
|
+
```tsx fragment
|
|
223
223
|
import { Alert } from "@zerotal/flow";
|
|
224
224
|
|
|
225
225
|
<Alert variant="success" dismissible>
|
|
@@ -237,7 +237,7 @@ import { Alert } from "@zerotal/flow";
|
|
|
237
237
|
|
|
238
238
|
Shows its children only while a server action is in flight — and, so a fast action never flashes a spinner, loading **indicators wait out a short delay** (~200ms) by default: an action that finishes inside that window shows nothing at all. This applies to the whole loading family — `<Loading>`, `showOnLoading`, `hideOnLoading`, and `loadingClass`. Only `loadingAttr` (e.g. `loadingAttr="disabled"`) is applied immediately, so a submit button still guards against a double-click even on a sub-100ms action. The `delay` prop is now the default behaviour and kept only for clarity/back-compat:
|
|
239
239
|
|
|
240
|
-
```tsx
|
|
240
|
+
```tsx fragment
|
|
241
241
|
import { Loading } from "@zerotal/flow";
|
|
242
242
|
|
|
243
243
|
<button onClick={this.save}>Save</button>
|
|
@@ -262,7 +262,7 @@ import { Loading } from "@zerotal/flow";
|
|
|
262
262
|
|
|
263
263
|
Renders the component's entire validation error bag as a list. Hidden when there are none:
|
|
264
264
|
|
|
265
|
-
```tsx
|
|
265
|
+
```tsx fragment
|
|
266
266
|
import { Errors } from "@zerotal/flow";
|
|
267
267
|
|
|
268
268
|
<Errors /> {/* every current error */}
|
|
@@ -273,7 +273,7 @@ import { Errors } from "@zerotal/flow";
|
|
|
273
273
|
|
|
274
274
|
A single field's first error message as a self-hiding `<span>`. Equivalent to `<span error={this.errors.field} />`:
|
|
275
275
|
|
|
276
|
-
```tsx
|
|
276
|
+
```tsx fragment
|
|
277
277
|
import { ErrorMessage } from "@zerotal/flow";
|
|
278
278
|
|
|
279
279
|
<input value={this.form.email} />
|
|
@@ -286,7 +286,7 @@ Contains a failure in a nested component so it costs that component rather than
|
|
|
286
286
|
one, a child that throws while mounting or rendering takes the whole response with it — one broken
|
|
287
287
|
widget blanks the dashboard.
|
|
288
288
|
|
|
289
|
-
```tsx
|
|
289
|
+
```tsx fragment
|
|
290
290
|
import { ErrorBoundary } from "@zerotal/flow";
|
|
291
291
|
|
|
292
292
|
<ErrorBoundary fallback={<p class="text-sm text-red-600">Sales data unavailable.</p>}>
|
|
@@ -297,7 +297,7 @@ import { ErrorBoundary } from "@zerotal/flow";
|
|
|
297
297
|
`fallback` may be a function, which receives the thrown error. `onError` reports it (for logging or
|
|
298
298
|
an error tracker) without changing what renders:
|
|
299
299
|
|
|
300
|
-
```tsx
|
|
300
|
+
```tsx fragment
|
|
301
301
|
<ErrorBoundary fallback={(e) => <p>{(e as Error).message}</p>} onError={(e) => Log.error(e)}>
|
|
302
302
|
<RiskyWidget />
|
|
303
303
|
</ErrorBoundary>
|
|
@@ -317,7 +317,7 @@ affect the other.
|
|
|
317
317
|
|
|
318
318
|
A data table with URL-driven sortable headers. Clicking a sortable header navigates to `?sortBy=key&sortDir=asc|desc`. Pair with `@url sortBy`/`@url sortDir` and sort the rows server-side in `render()`:
|
|
319
319
|
|
|
320
|
-
```tsx
|
|
320
|
+
```tsx fragment
|
|
321
321
|
import { Table, Pager } from "@zerotal/flow";
|
|
322
322
|
|
|
323
323
|
export class UsersPage extends Component.using(Pagination) {
|
|
@@ -362,7 +362,7 @@ export class UsersPage extends Component.using(Pagination) {
|
|
|
362
362
|
|
|
363
363
|
Renders a Prev / numbered / Next pager from either paginator — `Model.paginate()` or the in-memory `paginate()` helper. Links are `navigate` anchors to `?page=N`, so they pair with `@url page` automatically. (The `Pagination` export is the page-state mixin; `<Pager>` is the links UI.)
|
|
364
364
|
|
|
365
|
-
```tsx
|
|
365
|
+
```tsx fragment
|
|
366
366
|
import { Pager } from "@zerotal/flow";
|
|
367
367
|
|
|
368
368
|
const users = await User.paginate(this.perPage);
|
|
@@ -375,7 +375,7 @@ const users = await User.paginate(this.perPage);
|
|
|
375
375
|
|
|
376
376
|
A sentinel element that calls a server action when it scrolls into view. Pass `show` to stop loading when there are no more items:
|
|
377
377
|
|
|
378
|
-
```tsx
|
|
378
|
+
```tsx fragment
|
|
379
379
|
import { InfiniteScroll } from "@zerotal/flow";
|
|
380
380
|
|
|
381
381
|
@expose async loadMore(): Promise<void> {
|
|
@@ -396,7 +396,7 @@ elements; spacers above and below hold the scrollbar at the size the full collec
|
|
|
396
396
|
As the viewport moves, `onWindow` is called with `(start, count)` and your action loads that slice.
|
|
397
397
|
The collection never has to reach the client in full.
|
|
398
398
|
|
|
399
|
-
```tsx
|
|
399
|
+
```tsx fragment
|
|
400
400
|
import { Virtualize } from "@zerotal/flow";
|
|
401
401
|
|
|
402
402
|
@expose rows: Row[] = [];
|
|
@@ -440,7 +440,7 @@ fetch latency.
|
|
|
440
440
|
|
|
441
441
|
A click-to-open menu, entirely client-side (no round-trip). Fully keyboard-navigable: Down/Up/Enter opens (focusing first/last item), arrow keys + Home/End move between items, Escape closes and returns focus to the trigger, click-outside dismisses:
|
|
442
442
|
|
|
443
|
-
```tsx
|
|
443
|
+
```tsx fragment
|
|
444
444
|
import { Dropdown } from "@zerotal/flow";
|
|
445
445
|
|
|
446
446
|
<Dropdown label="Options" align="right">
|
|
@@ -457,7 +457,7 @@ import { Dropdown } from "@zerotal/flow";
|
|
|
457
457
|
|
|
458
458
|
Client-side tabbed panels. Pass `items`, each with a `label` and the `content` to show when selected:
|
|
459
459
|
|
|
460
|
-
```tsx
|
|
460
|
+
```tsx fragment
|
|
461
461
|
import { Tabs } from "@zerotal/flow";
|
|
462
462
|
|
|
463
463
|
<Tabs
|
|
@@ -485,7 +485,7 @@ import { Tabs } from "@zerotal/flow";
|
|
|
485
485
|
|
|
486
486
|
A dropzone bound to an `@expose` property. Choosing a file POSTs the bytes to `/__flow/upload` over HTTP, shows live upload progress, and resolves to a signed `TemporaryUploadedFile` reference. Compose the `FileUploads` mixin for the `removeUpload` action:
|
|
487
487
|
|
|
488
|
-
```tsx
|
|
488
|
+
```tsx fragment
|
|
489
489
|
import { Component, expose, FileUpload, FileUploads, TemporaryUploadedFile } from "@zerotal/flow";
|
|
490
490
|
|
|
491
491
|
export class AvatarPage extends Component.using(FileUploads) {
|
|
@@ -525,7 +525,7 @@ export class AvatarPage extends Component.using(FileUploads) {
|
|
|
525
525
|
|
|
526
526
|
For multiple files, use `multiple`:
|
|
527
527
|
|
|
528
|
-
```tsx
|
|
528
|
+
```tsx fragment
|
|
529
529
|
@expose photos: TemporaryUploadedFile[] = [];
|
|
530
530
|
|
|
531
531
|
<FileUpload bind={this.photos} multiple accept="image/*" />
|
|
@@ -538,7 +538,7 @@ For multiple files, use `multiple`:
|
|
|
538
538
|
|
|
539
539
|
Common Alpine plugins are bundled and exposed as props:
|
|
540
540
|
|
|
541
|
-
```tsx
|
|
541
|
+
```tsx fragment
|
|
542
542
|
{/* Input masking */}
|
|
543
543
|
<input mask="(999) 999-9999" value={this.phone} live />
|
|
544
544
|
<input mask="9999 9999 9999 9999" value={this.cardNumber} live />
|
|
@@ -576,7 +576,7 @@ Unstyled, fully accessible interactive primitives that expose state through `dat
|
|
|
576
576
|
|
|
577
577
|
An accessible on/off toggle (`role="switch"`, keyboard-operable). Style the on-state with `data-[checked]:…`:
|
|
578
578
|
|
|
579
|
-
```tsx
|
|
579
|
+
```tsx fragment
|
|
580
580
|
import { Switch } from "@zerotal/flow";
|
|
581
581
|
|
|
582
582
|
<Switch
|
|
@@ -593,7 +593,7 @@ The Switch button is a Tailwind `group`, so the inner knob reacts with `group-da
|
|
|
593
593
|
|
|
594
594
|
An accessible checkbox (`role="checkbox"`) bound to a boolean. Style the checked state with `data-[checked]:…`:
|
|
595
595
|
|
|
596
|
-
```tsx
|
|
596
|
+
```tsx fragment
|
|
597
597
|
import { Checkbox } from "@zerotal/flow";
|
|
598
598
|
|
|
599
599
|
<Checkbox bind={this.agree} class="h-5 w-5 rounded border border-gray-700 data-[checked]:bg-indigo-600 data-[checked]:border-indigo-600">
|
|
@@ -608,7 +608,7 @@ import { Checkbox } from "@zerotal/flow";
|
|
|
608
608
|
|
|
609
609
|
A styled native `<select>`. Fully accessible out of the box. Use `<Listbox>` only when you need custom option markup:
|
|
610
610
|
|
|
611
|
-
```tsx
|
|
611
|
+
```tsx fragment
|
|
612
612
|
import { Select } from "@zerotal/flow";
|
|
613
613
|
|
|
614
614
|
<Select
|
|
@@ -627,7 +627,7 @@ import { Select } from "@zerotal/flow";
|
|
|
627
627
|
|
|
628
628
|
`role="radiogroup"` with arrow-key roving. Style the selected option with `data-[checked]:…`:
|
|
629
629
|
|
|
630
|
-
```tsx
|
|
630
|
+
```tsx fragment
|
|
631
631
|
import { RadioGroup } from "@zerotal/flow";
|
|
632
632
|
|
|
633
633
|
<RadioGroup
|
|
@@ -645,7 +645,7 @@ import { RadioGroup } from "@zerotal/flow";
|
|
|
645
645
|
|
|
646
646
|
A fully keyboard-navigable custom select. Arrow keys, Home/End, Enter/Escape, `aria-activedescendant`. `multiple` makes the value an array:
|
|
647
647
|
|
|
648
|
-
```tsx
|
|
648
|
+
```tsx fragment
|
|
649
649
|
import { Listbox } from "@zerotal/flow";
|
|
650
650
|
|
|
651
651
|
<Listbox
|
|
@@ -662,7 +662,7 @@ States: `data-[selected]`, `data-[active]`, `data-[open]`.
|
|
|
662
662
|
|
|
663
663
|
An autocomplete input + filtered dropdown list. Two modes:
|
|
664
664
|
|
|
665
|
-
```tsx
|
|
665
|
+
```tsx fragment
|
|
666
666
|
import { Combobox } from "@zerotal/flow";
|
|
667
667
|
|
|
668
668
|
{
|
|
@@ -744,7 +744,7 @@ import { Popover } from "@zerotal/flow";
|
|
|
744
744
|
|
|
745
745
|
Accessibility glue around a single control. `<Field>` wires `for` / `id` / `aria-describedby` between the label, the control, the description, and the error — keeping screen-reader semantics correct without hand-wiring IDs:
|
|
746
746
|
|
|
747
|
-
```tsx
|
|
747
|
+
```tsx fragment
|
|
748
748
|
import { Field } from "@zerotal/flow";
|
|
749
749
|
|
|
750
750
|
<Field label="Email" description="We'll never share it." error={this.errors.email}>
|
|
@@ -758,7 +758,7 @@ All three props (`label`, `description`, `error`) are optional. You can also com
|
|
|
758
758
|
|
|
759
759
|
Group related fields. A native `<fieldset disabled>` cascades the disabled state to every control inside:
|
|
760
760
|
|
|
761
|
-
```tsx
|
|
761
|
+
```tsx fragment
|
|
762
762
|
import { Fieldset } from "@zerotal/flow";
|
|
763
763
|
|
|
764
764
|
<Fieldset legend="Billing address" disabled={this.saving}>
|
|
@@ -775,7 +775,7 @@ import { Fieldset } from "@zerotal/flow";
|
|
|
775
775
|
|
|
776
776
|
Shows `content` on hover/focus of its children, wired with `aria-describedby` and `role="tooltip"`. Client-only:
|
|
777
777
|
|
|
778
|
-
```tsx
|
|
778
|
+
```tsx fragment
|
|
779
779
|
import { Tooltip } from "@zerotal/flow";
|
|
780
780
|
|
|
781
781
|
<Tooltip content="Copy link to clipboard" placement="top">
|
|
@@ -825,7 +825,7 @@ ZT_FLOW_CSP_SAFE=true
|
|
|
825
825
|
|
|
826
826
|
When on, the client runtime swaps Alpine's evaluator for an eval-free interpreter, the bridge avoids `new Function`, and the AOT compiler emits only CSP-safe expressions:
|
|
827
827
|
|
|
828
|
-
```tsx
|
|
828
|
+
```tsx fragment
|
|
829
829
|
<button onClick={() => this.count++}>+</button>
|
|
830
830
|
// standard mode → flow:click="() => $flow.count++"
|
|
831
831
|
// CSP mode → flow:click="$flow.count++"
|
package/docs/flow/decorators.md
CHANGED
|
@@ -16,7 +16,7 @@ Marks a property or method as part of the public contract with the browser.
|
|
|
16
16
|
- **On a property** — the value is included in the signed snapshot and synced to the client on every patch. The client can update it via a bound `value`/`checked` attribute or a client expression like `onClick={() => this.count++}`.
|
|
17
17
|
- **On a method** — makes it callable from the browser over the WebSocket via event bindings like `onClick={this.save}`.
|
|
18
18
|
|
|
19
|
-
```typescript
|
|
19
|
+
```typescript fragment
|
|
20
20
|
import { expose } from "@zerotal/flow";
|
|
21
21
|
|
|
22
22
|
export class CounterPage extends Component {
|
|
@@ -48,7 +48,7 @@ A `value={this.x}` binding on a `@locked` property renders as a read-only displa
|
|
|
48
48
|
|
|
49
49
|
Use `@locked` for anything the server owns: a record the route resolved, results loaded in `onMount()`, computed totals, child props from the parent:
|
|
50
50
|
|
|
51
|
-
```tsx
|
|
51
|
+
```tsx fragment
|
|
52
52
|
export class PostsPage extends Component {
|
|
53
53
|
@locked user!: User; // /users/:user — the record, already found
|
|
54
54
|
@locked posts: Post[] = [];
|
|
@@ -87,7 +87,7 @@ with no arguments, and used for [real-time validation](#real-time-validation) on
|
|
|
87
87
|
|
|
88
88
|
Combine with `@expose` for a property that is both two-way bound and validated:
|
|
89
89
|
|
|
90
|
-
```typescript
|
|
90
|
+
```typescript fragment
|
|
91
91
|
@expose @validate((rule) => rule.required().email()) email: string = "";
|
|
92
92
|
@expose @validate((rule) => rule.required().min(8)) password: string = "";
|
|
93
93
|
@expose @validate((rule) => rule.required().min(2).max(50)) name: string = "";
|
|
@@ -102,7 +102,7 @@ Combine with `@expose` for a property that is both two-way bound and validated:
|
|
|
102
102
|
|
|
103
103
|
The validation runs when you call `this.validate()` inside an action:
|
|
104
104
|
|
|
105
|
-
```typescript
|
|
105
|
+
```typescript fragment
|
|
106
106
|
@expose async register(): Promise<void> {
|
|
107
107
|
await this.validate(); // reads all @validate rules on the class
|
|
108
108
|
|
|
@@ -120,7 +120,7 @@ The validation runs when you call `this.validate()` inside an action:
|
|
|
120
120
|
|
|
121
121
|
You can also pass rules directly to `this.validate()` — they override the decorator-based rules:
|
|
122
122
|
|
|
123
|
-
```typescript
|
|
123
|
+
```typescript fragment
|
|
124
124
|
@expose async update(): Promise<void> {
|
|
125
125
|
await this.validate({
|
|
126
126
|
email: (rule) => rule.required().email(),
|
|
@@ -142,7 +142,7 @@ as the user edits, with no action call and without affecting any other field.
|
|
|
142
142
|
> form is not writable in TSX: the `.` in an attribute name is a parse error (`TS1003`), so
|
|
143
143
|
> copying it out of the emitted HTML into a component will not build.
|
|
144
144
|
|
|
145
|
-
```tsx
|
|
145
|
+
```tsx fragment
|
|
146
146
|
@expose @validate((rule) => rule.required().email()) email = "";
|
|
147
147
|
|
|
148
148
|
async render() {
|
|
@@ -165,7 +165,7 @@ See [Forms & Validation](/docs/flow/forms) for the full validation rules referen
|
|
|
165
165
|
|
|
166
166
|
Syncs a property to the browser URL query string. Initialised from the query string on the initial page load; updated in the URL on every patch. The URL becomes shareable and bookmark-friendly automatically:
|
|
167
167
|
|
|
168
|
-
```typescript
|
|
168
|
+
```typescript fragment
|
|
169
169
|
// Page and search stay in the URL: /posts?page=2&search=TypeScript
|
|
170
170
|
@url page: number = 1;
|
|
171
171
|
@url search: string = "";
|
|
@@ -189,14 +189,14 @@ Binds a property to the HTTP session, so the value survives a browser refresh. R
|
|
|
189
189
|
|
|
190
190
|
The field reads and writes the session key of the same name — the same value a controller or another component sees:
|
|
191
191
|
|
|
192
|
-
```typescript
|
|
192
|
+
```typescript fragment
|
|
193
193
|
@session userId: string = ""; // the session's `userId`
|
|
194
194
|
@session preferredTheme: string = "light";
|
|
195
195
|
```
|
|
196
196
|
|
|
197
197
|
Pass options to change the key, or to keep it to this component:
|
|
198
198
|
|
|
199
|
-
```typescript
|
|
199
|
+
```typescript fragment
|
|
200
200
|
// Read a differently-named key
|
|
201
201
|
@session({ key: "s" }) whatever: number = 0;
|
|
202
202
|
|
|
@@ -216,7 +216,7 @@ A `@session` field is **not** in the WebSocket snapshot: it is read from and wri
|
|
|
216
216
|
|
|
217
217
|
A getter derived from other state. Not stored in the snapshot — recomputed on every render pass. The result is **memoized for the duration of a single render**, so an expensive getter read multiple times in the same template runs only once:
|
|
218
218
|
|
|
219
|
-
```typescript
|
|
219
|
+
```typescript fragment
|
|
220
220
|
@computed get fullName(): string {
|
|
221
221
|
return `${this.firstName} ${this.lastName}`;
|
|
222
222
|
}
|
|
@@ -241,7 +241,7 @@ Use them freely in a template as a text child — `{this.fullName}` — where th
|
|
|
241
241
|
|
|
242
242
|
Excludes a property from the snapshot entirely. Reset to its class-level default on every WebSocket round-trip. Use for ephemeral UI state that shouldn't persist between server calls:
|
|
243
243
|
|
|
244
|
-
```typescript
|
|
244
|
+
```typescript fragment
|
|
245
245
|
@transient isUploading: boolean = false;
|
|
246
246
|
@transient dropzoneActive: boolean = false;
|
|
247
247
|
@transient tempMessage: string = "";
|
|
@@ -254,7 +254,7 @@ The pattern: start an upload, set `this.isUploading = true`, send a response. On
|
|
|
254
254
|
|
|
255
255
|
An `@expose`d method that runs on the server but **skips the re-render cycle**. Use for side-effects that don't change the UI: file downloads, external API calls, jobs that just need to fire:
|
|
256
256
|
|
|
257
|
-
```typescript
|
|
257
|
+
```typescript fragment
|
|
258
258
|
@expose @renderless async exportCsv(): Promise<void> {
|
|
259
259
|
const rows = await Report.all();
|
|
260
260
|
const csv = rows.map((r) => `${r.id},${r.name},${r.email}`).join("\n");
|
|
@@ -282,7 +282,7 @@ Because the render cycle is skipped, `@renderless` actions are faster and cheape
|
|
|
282
282
|
|
|
283
283
|
Registers a method as a listener for cross-component events dispatched via `this.dispatch()`. The method is implicitly exposed — no separate `@expose` needed:
|
|
284
284
|
|
|
285
|
-
```typescript
|
|
285
|
+
```typescript fragment
|
|
286
286
|
@on("post-created")
|
|
287
287
|
async handlePostCreated(data: { id: number; title: string }): Promise<void> {
|
|
288
288
|
this.posts = await Post.query().orderBy("created_at", "desc").limit(10).get();
|
|
@@ -319,7 +319,7 @@ For child components — marks a prop the parent can re-push whenever its value
|
|
|
319
319
|
|
|
320
320
|
Declare the reactive prop on the child:
|
|
321
321
|
|
|
322
|
-
```tsx
|
|
322
|
+
```tsx fragment
|
|
323
323
|
export class PriceTag extends Component {
|
|
324
324
|
@reactive currency = "USD";
|
|
325
325
|
@reactive amount = 0;
|
|
@@ -339,7 +339,7 @@ export class PriceTag extends Component {
|
|
|
339
339
|
|
|
340
340
|
Pass the prop from the parent:
|
|
341
341
|
|
|
342
|
-
```tsx
|
|
342
|
+
```tsx fragment
|
|
343
343
|
// When this.currency changes, PriceTag re-renders automatically:
|
|
344
344
|
<PriceTag currency={this.currency} amount={this.subtotal} />
|
|
345
345
|
```
|
|
@@ -350,7 +350,7 @@ Unlike `@locked`, `@reactive` props are **live** — the parent keeps them curre
|
|
|
350
350
|
|
|
351
351
|
A reactive prop that **also syncs back to the parent** (two-way). The parent property and the child prop stay in lock-step. Use for reusable input/control components that need to write a value back to their parent:
|
|
352
352
|
|
|
353
|
-
```tsx
|
|
353
|
+
```tsx fragment
|
|
354
354
|
export class StarRating extends Component {
|
|
355
355
|
@modelable rating: number = 0; // two-way bound to parent
|
|
356
356
|
|
|
@@ -409,7 +409,7 @@ See [Layouts & Composition](/docs/flow/layouts#two-way-props) for the full `@mod
|
|
|
409
409
|
|
|
410
410
|
These helpers are available inside any `@expose`d method:
|
|
411
411
|
|
|
412
|
-
```typescript
|
|
412
|
+
```typescript fragment
|
|
413
413
|
// Flash a toast notification to the browser
|
|
414
414
|
this.flash("Saved successfully!"); // level defaults to "success"
|
|
415
415
|
this.flash("Could not connect.", "error");
|
|
@@ -442,7 +442,7 @@ this.download("export.json", JSON.stringify(data, null, 2), "application/json");
|
|
|
442
442
|
|
|
443
443
|
`this.errors` is a typed proxy over the validation error bag. It is populated by `this.validate()` and `this.addError()`.
|
|
444
444
|
|
|
445
|
-
```tsx
|
|
445
|
+
```tsx fragment
|
|
446
446
|
// Check if any validation errors exist
|
|
447
447
|
if (this.errors.any()) {
|
|
448
448
|
return; // stop processing
|