@zerotal/arch 1.7.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.
Files changed (160) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/LICENSE +21 -0
  3. package/README.md +67 -0
  4. package/api-surface.md +283 -0
  5. package/docs/about.md +676 -0
  6. package/docs/admin/actions.md +183 -0
  7. package/docs/admin/auth.md +89 -0
  8. package/docs/admin/dashboard.md +155 -0
  9. package/docs/admin/extending-ui.md +229 -0
  10. package/docs/admin/extending.md +205 -0
  11. package/docs/admin/forms.md +155 -0
  12. package/docs/admin/index.md +144 -0
  13. package/docs/admin/operations.md +224 -0
  14. package/docs/admin/references.md +174 -0
  15. package/docs/admin/resources.md +132 -0
  16. package/docs/admin/structure.md +158 -0
  17. package/docs/admin/tables.md +185 -0
  18. package/docs/admin/testing.md +138 -0
  19. package/docs/ai.md +373 -0
  20. package/docs/application.md +435 -0
  21. package/docs/arch.md +263 -0
  22. package/docs/assets.md +755 -0
  23. package/docs/audit.md +367 -0
  24. package/docs/authentication.md +1000 -0
  25. package/docs/authorization.md +529 -0
  26. package/docs/broadcasting/channels.md +170 -0
  27. package/docs/broadcasting/client.md +108 -0
  28. package/docs/broadcasting/events.md +190 -0
  29. package/docs/broadcasting/index.md +163 -0
  30. package/docs/broadcasting/references.md +84 -0
  31. package/docs/broadcasting/testing.md +132 -0
  32. package/docs/cache.md +387 -0
  33. package/docs/carbon.md +830 -0
  34. package/docs/changelog.md +465 -0
  35. package/docs/client/auth.md +113 -0
  36. package/docs/client/errors.md +139 -0
  37. package/docs/client/files.md +118 -0
  38. package/docs/client/index.md +128 -0
  39. package/docs/client/references.md +58 -0
  40. package/docs/client/requests.md +131 -0
  41. package/docs/client/resilience.md +141 -0
  42. package/docs/client/testing.md +146 -0
  43. package/docs/commands.md +450 -0
  44. package/docs/components.md +2427 -0
  45. package/docs/config-system.md +258 -0
  46. package/docs/container.md +596 -0
  47. package/docs/context.md +579 -0
  48. package/docs/contributing.md +164 -0
  49. package/docs/controllers.md +354 -0
  50. package/docs/conventions.md +466 -0
  51. package/docs/cookies.md +101 -0
  52. package/docs/csrf.md +206 -0
  53. package/docs/database.md +476 -0
  54. package/docs/deployment.md +434 -0
  55. package/docs/devtools.md +1013 -0
  56. package/docs/email-verification.md +320 -0
  57. package/docs/encryption.md +380 -0
  58. package/docs/errors.md +317 -0
  59. package/docs/events.md +420 -0
  60. package/docs/flow/components.md +846 -0
  61. package/docs/flow/decorators.md +458 -0
  62. package/docs/flow/events.md +420 -0
  63. package/docs/flow/forms.md +551 -0
  64. package/docs/flow/index.md +568 -0
  65. package/docs/flow/layouts.md +793 -0
  66. package/docs/flow/lifecycle.md +336 -0
  67. package/docs/flow/pagination.md +402 -0
  68. package/docs/flow/performance.md +262 -0
  69. package/docs/flow/references.md +317 -0
  70. package/docs/flow/routing.md +550 -0
  71. package/docs/flow/testing.md +447 -0
  72. package/docs/getting-started.md +286 -0
  73. package/docs/health.md +269 -0
  74. package/docs/helpers.md +423 -0
  75. package/docs/i18n.md +414 -0
  76. package/docs/index.md +65 -0
  77. package/docs/inertia/build.md +95 -0
  78. package/docs/inertia/devtools.md +197 -0
  79. package/docs/inertia/index.md +267 -0
  80. package/docs/inertia/middleware.md +81 -0
  81. package/docs/inertia/props.md +507 -0
  82. package/docs/inertia/references.md +144 -0
  83. package/docs/inertia/rendering.md +189 -0
  84. package/docs/inertia/ssr.md +102 -0
  85. package/docs/inspirations.md +257 -0
  86. package/docs/lifecycle.md +331 -0
  87. package/docs/lock.md +454 -0
  88. package/docs/logger.md +517 -0
  89. package/docs/media.md +582 -0
  90. package/docs/middleware.md +509 -0
  91. package/docs/migrations.md +529 -0
  92. package/docs/monitor.md +377 -0
  93. package/docs/notifications.md +848 -0
  94. package/docs/orm/casts.md +365 -0
  95. package/docs/orm/factories.md +231 -0
  96. package/docs/orm/index.md +663 -0
  97. package/docs/orm/lifecycle.md +421 -0
  98. package/docs/orm/queries.md +692 -0
  99. package/docs/orm/relationships.md +440 -0
  100. package/docs/orm/serialization.md +251 -0
  101. package/docs/package-development.md +536 -0
  102. package/docs/pagination.md +340 -0
  103. package/docs/password-reset.md +335 -0
  104. package/docs/providers.md +447 -0
  105. package/docs/query-builder.md +465 -0
  106. package/docs/queue.md +549 -0
  107. package/docs/rate-limiting.md +253 -0
  108. package/docs/responses.md +336 -0
  109. package/docs/roles-and-2fa.md +418 -0
  110. package/docs/routing.md +1008 -0
  111. package/docs/scaffolding.md +335 -0
  112. package/docs/scheduler.md +533 -0
  113. package/docs/seeding.md +229 -0
  114. package/docs/session.md +451 -0
  115. package/docs/social.md +524 -0
  116. package/docs/storage.md +622 -0
  117. package/docs/structure.md +230 -0
  118. package/docs/support-policy.md +114 -0
  119. package/docs/telemetry.md +505 -0
  120. package/docs/tenancy.md +608 -0
  121. package/docs/testing/browser.md +153 -0
  122. package/docs/testing/console.md +130 -0
  123. package/docs/testing/database.md +238 -0
  124. package/docs/testing/flow-browser.md +216 -0
  125. package/docs/testing/http.md +441 -0
  126. package/docs/testing/index.md +240 -0
  127. package/docs/testing/mocking.md +410 -0
  128. package/docs/upgrade.md +192 -0
  129. package/docs/validator.md +549 -0
  130. package/docs/view.md +453 -0
  131. package/package.json +60 -0
  132. package/src/bin/mcp.ts +73 -0
  133. package/src/config.ts +63 -0
  134. package/src/errors.ts +27 -0
  135. package/src/index.ts +56 -0
  136. package/src/install/ArchInstallCommand.ts +189 -0
  137. package/src/install/detect.ts +98 -0
  138. package/src/install/guidelines.ts +290 -0
  139. package/src/install/markers.ts +77 -0
  140. package/src/install/mcpConfig.ts +81 -0
  141. package/src/mcp/index.ts +27 -0
  142. package/src/mcp/jsonrpc.ts +133 -0
  143. package/src/mcp/server.ts +307 -0
  144. package/src/mcp/stdio.ts +111 -0
  145. package/src/mcp/types.ts +146 -0
  146. package/src/probe/ArchProbeCommand.ts +48 -0
  147. package/src/probe/sentinel.ts +13 -0
  148. package/src/probe/topics.ts +291 -0
  149. package/src/provider/ArchProvider.ts +101 -0
  150. package/src/tools/_probe.ts +164 -0
  151. package/src/tools/apiSurface.ts +241 -0
  152. package/src/tools/appInfo.ts +88 -0
  153. package/src/tools/baselines.ts +210 -0
  154. package/src/tools/context.ts +37 -0
  155. package/src/tools/doctor.ts +93 -0
  156. package/src/tools/index.ts +50 -0
  157. package/src/tools/logs.ts +265 -0
  158. package/src/tools/routes.ts +133 -0
  159. package/src/tools/schema.ts +119 -0
  160. package/src/tools/searchDocs.ts +345 -0
@@ -0,0 +1,458 @@
1
+ ---
2
+ title: Flow Decorators
3
+ description: The decorators that expose state and actions to the client: @expose, @locked, @computed, and friends.
4
+ ---
5
+
6
+ # Decorators
7
+
8
+ Decorators are the primary way to define a Flow component's public contract with the browser. They control what gets synced to the client, what the client can change, how state is persisted, and how the component communicates with the rest of the page.
9
+
10
+ All decorators are imported from `@zerotal/flow`.
11
+
12
+ ## The @expose decorator
13
+
14
+ Marks a property or method as part of the public contract with the browser.
15
+
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
+ - **On a method** — makes it callable from the browser over the WebSocket via event bindings like `onClick={this.save}`.
18
+
19
+ ```typescript
20
+ import { expose } from "@zerotal/flow";
21
+
22
+ export class CounterPage extends Component {
23
+ @expose count: number = 0;
24
+ @expose name: string = "";
25
+ @expose filter: string = "all";
26
+
27
+ @expose increment(): void {
28
+ this.count++;
29
+ }
30
+
31
+ @expose async save(): Promise<void> {
32
+ await Post.create({ title: this.title, body: this.body });
33
+ this.flash("Post saved.");
34
+ this.redirect("/posts");
35
+ }
36
+ }
37
+ ```
38
+
39
+ Only `@expose` properties are two-way: the client can push updates back to the server. If you only need the server to push state to the client, use `@locked` instead.
40
+
41
+ ## The @locked decorator
42
+
43
+ Sent to the client for display, but the client cannot mutate it. Included in the snapshot so it survives WebSocket round-trips without re-loading from the database.
44
+
45
+ A `value={this.x}` binding on a `@locked` property renders as a read-only display — not an editable field.
46
+
47
+ Use `@locked` for data loaded in `onMount()` that the server controls: model results, user info, computed totals, child props from the parent:
48
+
49
+ ```tsx
50
+ export class PostsPage extends Component {
51
+ @locked posts: Post[] = [];
52
+ @locked user: User | null = null;
53
+ @locked total: number = 0;
54
+
55
+ override async onMount() {
56
+ this.user = await User.findOrFail(this.userId);
57
+ this.posts = await Post.query()
58
+ .where("user_id", this.user.id)
59
+ .where("status", "published")
60
+ .orderBy("created_at", "desc")
61
+ .get();
62
+ this.total = this.posts.length;
63
+ }
64
+
65
+ override async render() {
66
+ return (
67
+ <div>
68
+ <h1>Posts by {this.user?.name}</h1>
69
+ <p>{this.total} posts</p>
70
+ <ul>
71
+ {this.posts.map((p) => (
72
+ <li key={String(p.id)}>{p.title}</li>
73
+ ))}
74
+ </ul>
75
+ </div>
76
+ );
77
+ }
78
+ }
79
+ ```
80
+
81
+ ## The @validate decorator
82
+
83
+ Attaches a validation rule to a property, built with the framework validator's fluent chain
84
+ (`@zerotal/validator`'s `RuleBuilder`). Picked up automatically when you call `this.validate()`
85
+ with no arguments, and used for [real-time validation](#real-time-validation) on update.
86
+
87
+ Combine with `@expose` for a property that is both two-way bound and validated:
88
+
89
+ ```typescript
90
+ @expose @validate((rule) => rule.required().email()) email: string = "";
91
+ @expose @validate((rule) => rule.required().min(8)) password: string = "";
92
+ @expose @validate((rule) => rule.required().min(2).max(50)) name: string = "";
93
+ @expose @validate((rule) => rule.required().in(["admin", "user", "guest"])) role: string = "user";
94
+ @expose @validate((rule) => rule.number().min(0)) age?: number;
95
+ ```
96
+
97
+ > Rules are written with the chain, not strings — `(rule) => rule.required().min(8)`, not
98
+ > `"required|min:8"`. `rule.required()` is shorthand for a required string; start from a typed
99
+ > builder (`rule.number()`, `rule.boolean()`, …) for other types. Pass a custom message to any rule,
100
+ > e.g. `rule.required("Please enter your name")`.
101
+
102
+ The validation runs when you call `this.validate()` inside an action:
103
+
104
+ ```typescript
105
+ @expose async register(): Promise<void> {
106
+ await this.validate(); // reads all @validate rules on the class
107
+
108
+ // Only reached if all fields are valid:
109
+ const user = await User.create({
110
+ name: this.name,
111
+ email: this.email,
112
+ password: this.password,
113
+ role: this.role,
114
+ });
115
+
116
+ this.redirect("/dashboard");
117
+ }
118
+ ```
119
+
120
+ You can also pass rules directly to `this.validate()` — they override the decorator-based rules:
121
+
122
+ ```typescript
123
+ @expose async update(): Promise<void> {
124
+ await this.validate({
125
+ email: (rule) => rule.required().email(),
126
+ password: (rule) => rule.string().optional().min(8),
127
+ });
128
+
129
+ await this.user.fill({ email: this.email }).save();
130
+ this.flash("Profile updated.");
131
+ }
132
+ ```
133
+
134
+ ### Real-time validation
135
+
136
+ When a `@validate` field is bound with `flow:model.live` (or `.blur`), each change is validated on
137
+ the server as it arrives. The field's error appears (and clears)
138
+ as the user edits, with no action call and without affecting any other field:
139
+
140
+ ```tsx
141
+ @expose @validate((rule) => rule.required().email()) email = "";
142
+
143
+ async render() {
144
+ return (
145
+ <div>
146
+ <input type="email" value={this.email} flow:model.live />
147
+ <span error={this.errors.email} />
148
+ </div>
149
+ );
150
+ }
151
+ ```
152
+
153
+ Only the changed field is validated, and cross-field rules (like `confirmed`) see the other fields'
154
+ current values. Fields bound with the default (deferred) `flow:model` validate when you call
155
+ `this.validate()` in an action instead.
156
+
157
+ See [Forms & Validation](/docs/flow/forms) for the full validation rules reference and the error display API.
158
+
159
+ ## The @url decorator
160
+
161
+ 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:
162
+
163
+ ```typescript
164
+ // Page and search stay in the URL: /posts?page=2&search=TypeScript
165
+ @url page: number = 1;
166
+ @url search: string = "";
167
+ @url status: string = "all";
168
+
169
+ // Custom query parameter name: /posts?q=TypeScript
170
+ @url({ as: "q" }) query: string = "";
171
+
172
+ // Push a new browser history entry on change (back button works)
173
+ @url({ history: "push" }) tab: string = "overview";
174
+
175
+ // Custom name AND push history
176
+ @url({ as: "p", history: "push" }) currentPage: number = 1;
177
+ ```
178
+
179
+ `@url` properties can also be read-only (`@locked`) if the server alone controls the URL parameter — though this is uncommon.
180
+
181
+ ## The @session decorator
182
+
183
+ Binds a property to the HTTP session, so the value survives a browser refresh. Requires `SessionMiddleware` to be active for the route.
184
+
185
+ The field reads and writes the session key of the same name — the same value a controller or another component sees:
186
+
187
+ ```typescript
188
+ @session userId: string = ""; // the session's `userId`
189
+ @session preferredTheme: string = "light";
190
+ ```
191
+
192
+ Pass options to change the key, or to keep it to this component:
193
+
194
+ ```typescript
195
+ // Read a differently-named key
196
+ @session({ key: "s" }) whatever: number = 0;
197
+
198
+ // Namespace it to this component → flow:Preferences:draft
199
+ @session({ scoped: true }) draft: string = "";
200
+ ```
201
+
202
+ Use `scoped: true` for working state that belongs to one page and shouldn't collide with anything else — a half-finished draft, a wizard step. Leave it off for values the rest of the app shares.
203
+
204
+ A `@session` field is **not** in the WebSocket snapshot: it is read from and written to the session on each request, so the browser never receives it and cannot write it. Add `@locked` when the client needs to read the value.
205
+
206
+ - It persists until the session expires or the user logs out.
207
+ - It doesn't count against snapshot size.
208
+ - It's the right tool for cross-page preferences, dismissal flags, and anything the user should see the same way on their next visit.
209
+
210
+ ## The @computed decorator
211
+
212
+ 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:
213
+
214
+ ```typescript
215
+ @computed get fullName(): string {
216
+ return `${this.firstName} ${this.lastName}`;
217
+ }
218
+
219
+ @computed get filteredPosts(): Post[] {
220
+ // Even if render() reads filteredPosts 3 times, this filter runs only once per render:
221
+ return this.posts.filter((p) => p.status === this.filter);
222
+ }
223
+
224
+ @computed get totalRevenue(): number {
225
+ return this.orders.reduce((sum, o) => sum + o.total, 0);
226
+ }
227
+
228
+ @computed get isOverBudget(): boolean {
229
+ return this.totalRevenue > this.budgetLimit;
230
+ }
231
+ ```
232
+
233
+ Use them freely in a template as a text child — `{this.fullName}` — where they render a **static, server-evaluated** value (memoized per render). Because a computed getter isn't stored in the snapshot, it re-evaluates and updates on the next server patch, not client-side — so it can't be bound reactively: `{this.total}` is fine, but `text={this.total}` (a client-reactive binding) is not. For a value that must update on the client without a round-trip, keep it in an `@expose` property (write it from an action or an `onUpdated` hook) rather than deriving it with `@computed`.
234
+
235
+ ## The @transient decorator
236
+
237
+ 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:
238
+
239
+ ```typescript
240
+ @transient isUploading: boolean = false;
241
+ @transient dropzoneActive: boolean = false;
242
+ @transient tempMessage: string = "";
243
+ @transient previewUrl: string = "";
244
+ ```
245
+
246
+ The pattern: start an upload, set `this.isUploading = true`, send a response. On the next round-trip the flag is already `false` again — you don't have to reset it manually.
247
+
248
+ ## The @renderless decorator
249
+
250
+ 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:
251
+
252
+ ```typescript
253
+ @expose @renderless async exportCsv(): Promise<void> {
254
+ const rows = await Report.all();
255
+ const csv = rows.map((r) => `${r.id},${r.name},${r.email}`).join("\n");
256
+ this.download("report.csv", csv, "text/csv;charset=utf-8");
257
+ }
258
+
259
+ @expose @renderless async triggerWebhook(): Promise<void> {
260
+ await fetch("https://hooks.example.com/notify", {
261
+ method: "POST",
262
+ body: JSON.stringify({ userId: this.userId }),
263
+ });
264
+ this.flash("Webhook sent.", "success");
265
+ }
266
+
267
+ @expose @renderless async archivePost(): Promise<void> {
268
+ await Post.where("id", this.postId).update({ status: "archived" });
269
+ this.redirect("/posts");
270
+ }
271
+ ```
272
+
273
+ Because the render cycle is skipped, `@renderless` actions are faster and cheaper for effects that don't produce UI output.
274
+
275
+ ## The @on decorator
276
+
277
+ Registers a method as a listener for cross-component events dispatched via `this.dispatch()`. The method is implicitly exposed — no separate `@expose` needed:
278
+
279
+ ```typescript
280
+ @on("post-created")
281
+ async handlePostCreated(data: { id: number; title: string }): Promise<void> {
282
+ this.posts = await Post.query().orderBy("created_at", "desc").limit(10).get();
283
+ this.flash(`"${data.title}" was published.`);
284
+ }
285
+
286
+ @on("user-updated")
287
+ async refreshUser(data: { userId: number }): Promise<void> {
288
+ this.user = await User.findOrFail(data.userId);
289
+ }
290
+
291
+ // Listen for real-time WebSocket broadcasts (see Events doc for channel formats)
292
+ @on("echo:orders,OrderPlaced")
293
+ async onOrderPlaced(payload: { id: number }): Promise<void> {
294
+ this.orderCount++;
295
+ this.flash("New order received!", "success");
296
+ }
297
+ ```
298
+
299
+ See [Events & Broadcasting](/docs/flow/events) for dispatch methods, targeting, broadcasting, and native event integration.
300
+
301
+ ## The @reactive decorator
302
+
303
+ For child components — marks a prop the parent can re-push whenever its value changes. The child re-renders with the new prop while keeping the rest of its own state intact.
304
+
305
+ Declare the reactive prop on the child:
306
+
307
+ ```tsx
308
+ export class PriceTag extends Component {
309
+ @reactive currency = "USD";
310
+ @reactive amount = 0;
311
+
312
+ @computed get formatted(): string {
313
+ return new Intl.NumberFormat("en-US", {
314
+ style: "currency",
315
+ currency: this.currency,
316
+ }).format(this.amount);
317
+ }
318
+
319
+ override async render() {
320
+ return <span class="price text-2xl font-bold">{this.formatted}</span>;
321
+ }
322
+ }
323
+ ```
324
+
325
+ Pass the prop from the parent:
326
+
327
+ ```tsx
328
+ // When this.currency changes, PriceTag re-renders automatically:
329
+ <PriceTag currency={this.currency} amount={this.subtotal} />
330
+ ```
331
+
332
+ Unlike `@locked`, `@reactive` props are **live** — the parent keeps them current on every round-trip. See [Layouts & Composition](/docs/flow/layouts#reactive-props) for the full pattern.
333
+
334
+ ## The @modelable decorator
335
+
336
+ 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:
337
+
338
+ ```tsx
339
+ export class StarRating extends Component {
340
+ @modelable rating: number = 0; // two-way bound to parent
341
+
342
+ @expose set(n: number): void {
343
+ this.rating = n; // flows up to the parent immediately
344
+ }
345
+
346
+ override async render() {
347
+ return (
348
+ <div class="flex gap-1">
349
+ {[1, 2, 3, 4, 5].map((n) => (
350
+ <button
351
+ key={String(n)}
352
+ onClick={() => this.set(n)}
353
+ class={n <= this.rating ? "text-yellow-400" : "text-gray-300"}
354
+ >
355
+
356
+ </button>
357
+ ))}
358
+ </div>
359
+ );
360
+ }
361
+ }
362
+
363
+ // Parent — this.productRating and StarRating.rating stay in sync both ways:
364
+ <StarRating value={this.productRating} />;
365
+ ```
366
+
367
+ See [Layouts & Composition](/docs/flow/layouts#two-way-props) for the full `@modelable` pattern with nested form controls.
368
+
369
+ ## Decorator combination reference
370
+
371
+ | Combination | Meaning |
372
+ | ----------------------------------------------- | --------------------------------------------------------------------- |
373
+ | `@expose prop` | Synced to client; client can mutate via `value=` or client expression |
374
+ | `@locked prop` | Synced to client for display only; client cannot change it |
375
+ | `@url prop` | Two-way, AND synced to the URL query string |
376
+ | `@url({ as: "q" }) prop` | URL-synced with a custom query parameter name |
377
+ | `@url({ history: "push" }) prop` | URL-synced; browser history entry pushed on each change |
378
+ | `@expose @validate((r) => r.required()…) prop` | Two-way, AND auto-validated by `this.validate()` + live on update |
379
+ | `@url @validate((r) => r.number().min(1)) page` | URL-synced page number with type validation |
380
+ | `@session prop` | Reads/writes the session key of the same name (not in snapshot) |
381
+ | `@session({ key: "s" }) prop` | Reads/writes the session key `s` |
382
+ | `@session({ scoped: true }) prop` | Namespaces the key to this component |
383
+ | `@transient prop` | Local-only; excluded from snapshot; reset on each round-trip |
384
+ | `@computed get prop()` | Derived; not in snapshot; memoized per render pass |
385
+ | `@expose method` | Callable from the browser via WebSocket |
386
+ | `@expose @renderless method` | Callable from browser; skips re-render cycle |
387
+ | `@on("event") method` | Listens for cross-component events (auto-exposed) |
388
+ | `@on("echo:channel,Event") method` | Listens for real-time server broadcasts |
389
+ | `@reactive prop` | Child prop; parent re-pushes on change, child re-renders |
390
+ | `@modelable prop` | Two-way child prop; writes from child flow back to parent |
391
+
392
+ ## Action helpers
393
+
394
+ These helpers are available inside any `@expose`d method:
395
+
396
+ ```typescript
397
+ // Flash a toast notification to the browser
398
+ this.flash("Saved successfully!"); // level defaults to "success"
399
+ this.flash("Could not connect.", "error");
400
+ this.flash("Check your inbox.", "info");
401
+ this.flash("Your session expires in 5 minutes.", "warning");
402
+
403
+ // Redirect after the action completes (sends a redirect effect to the browser)
404
+ this.redirect("/dashboard");
405
+ this.redirect(`/posts/${post.id}`);
406
+ this.redirectRoute("profile", { id: 1 }); // named route → /users/1 (extra keys become ?query)
407
+ this.redirectIntended("/dashboard"); // back to where AuthMiddleware intercepted, else the fallback
408
+
409
+ // Force onMount() to re-run this round-trip (useful for reloading stale data)
410
+ this.refresh();
411
+
412
+ // Update the document title in the browser tab
413
+ this.title("Edit post — My App");
414
+
415
+ // Run raw JavaScript in the browser after the DOM patch is applied
416
+ this.client("$refs.titleInput.focus()");
417
+ this.client("window.scrollTo({ top: 0, behavior: 'smooth' })");
418
+ this.client(`$dispatch('toast', { message: 'Done!' })`);
419
+
420
+ // Trigger a file download in the browser
421
+ this.download("report.csv", csvContent, "text/csv;charset=utf-8");
422
+ this.download("export.json", JSON.stringify(data, null, 2), "application/json");
423
+ ```
424
+
425
+ ## The errors API
426
+
427
+ `this.errors` is a typed proxy over the validation error bag. It is populated by `this.validate()` and `this.addError()`.
428
+
429
+ ```tsx
430
+ // Check if any validation errors exist
431
+ if (this.errors.any()) {
432
+ return; // stop processing
433
+ }
434
+
435
+ // Check a specific field
436
+ if (this.errors.has("email")) {
437
+ this.flash("Please fix the email address.", "error");
438
+ }
439
+
440
+ // In JSX — reactive; shows the first error message for the field or hides itself:
441
+ <span error={this.errors.email} class="text-sm text-red-500" />
442
+ <span error={this.errors.name} class="text-sm text-red-500" />
443
+
444
+ // Add manual errors (e.g., from a database constraint)
445
+ this.addError("email", "That email address is already taken.");
446
+ this.addError("slug", "This slug is already in use.");
447
+
448
+ // Clear validation state
449
+ this.resetValidation(); // clear all fields
450
+ this.resetValidation("email"); // clear one specific field
451
+ ```
452
+
453
+ The `error` JSX prop compiles to `flow:error="field"`. It reads from `this.errors.<field>` reactively — when the field has no error, the element is hidden; when it has one, the element shows the first message. No conditional rendering needed in the template.
454
+
455
+ ## Next steps
456
+
457
+ - [Flow overview](/docs/flow) — the guide's front page and the rest of the sections.
458
+ - [Reference](/docs/flow/references) — every decorator, prop, and directive in one table.