@orieljs/framework 0.1.0-alpha.5 → 0.1.0-alpha.7

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 (87) hide show
  1. package/NOTICE +7 -0
  2. package/README.md +10 -1
  3. package/TRADEMARKS.md +33 -0
  4. package/dist/{chunk-WNJP46B7.js → chunk-4MWG3B7G.js} +1785 -626
  5. package/dist/chunk-4MWG3B7G.js.map +1 -0
  6. package/dist/chunk-7ZQZYDOC.js +3253 -0
  7. package/dist/chunk-7ZQZYDOC.js.map +1 -0
  8. package/dist/{chunk-BSQKOLSK.js → chunk-GVNLZZTN.js} +1 -1
  9. package/dist/chunk-GVNLZZTN.js.map +1 -0
  10. package/dist/{chunk-A5XRLEWM.js → chunk-VOYW3R5J.js} +4 -4
  11. package/dist/chunk-VOYW3R5J.js.map +1 -0
  12. package/dist/contracts.d.ts +535 -17
  13. package/dist/contracts.js +1 -1
  14. package/dist/index.d-WQpfdIbG.d.ts +630 -0
  15. package/dist/index.d.ts +2202 -5952
  16. package/dist/index.js +4900 -944
  17. package/dist/index.js.map +1 -1
  18. package/dist/{jsx-runtime-WKTl3EEw.d.ts → jsx-runtime-6um4Mwfb.d.ts} +97 -51
  19. package/dist/jsx-runtime.d.ts +2 -2
  20. package/dist/jsx-runtime.js +3 -3
  21. package/dist/{testing-BErengYY.d.ts → testing-CKwfWJGn.d.ts} +313 -994
  22. package/dist/testing.d.ts +2 -2
  23. package/dist/testing.js +19 -5
  24. package/docs/authentication.md +487 -0
  25. package/docs/authorization.md +435 -0
  26. package/docs/cache.md +647 -0
  27. package/docs/collections.md +2270 -0
  28. package/docs/configuration.md +334 -0
  29. package/docs/console.md +651 -0
  30. package/docs/container.md +430 -0
  31. package/docs/controllers.md +406 -0
  32. package/docs/csrf.md +126 -0
  33. package/docs/database-testing.md +144 -0
  34. package/docs/database.md +236 -0
  35. package/docs/deployment.md +276 -0
  36. package/docs/eloquent-collections.md +218 -0
  37. package/docs/eloquent-factories.md +255 -0
  38. package/docs/eloquent-mutators.md +254 -0
  39. package/docs/eloquent-observers.md +145 -0
  40. package/docs/eloquent-relationships.md +557 -0
  41. package/docs/eloquent.md +494 -0
  42. package/docs/encryption.md +104 -0
  43. package/docs/errors.md +318 -0
  44. package/docs/events.md +336 -0
  45. package/docs/facades.md +278 -0
  46. package/docs/hashing.md +118 -0
  47. package/docs/helpers.md +1281 -0
  48. package/docs/http-tests.md +386 -0
  49. package/docs/inertia.md +485 -0
  50. package/docs/installation.md +209 -0
  51. package/docs/lifecycle.md +234 -0
  52. package/docs/live-tests.md +226 -0
  53. package/docs/live.md +910 -0
  54. package/docs/logging.md +255 -0
  55. package/docs/mail.md +619 -0
  56. package/docs/middleware.md +384 -0
  57. package/docs/migrations.md +416 -0
  58. package/docs/mocking.md +264 -0
  59. package/docs/navigation.json +273 -0
  60. package/docs/notifications.md +225 -0
  61. package/docs/pagination.md +247 -0
  62. package/docs/passwords.md +207 -0
  63. package/docs/providers.md +187 -0
  64. package/docs/queries.md +600 -0
  65. package/docs/queues.md +330 -0
  66. package/docs/requests.md +533 -0
  67. package/docs/responses.md +444 -0
  68. package/docs/routing.md +738 -0
  69. package/docs/scheduling.md +277 -0
  70. package/docs/seeding.md +122 -0
  71. package/docs/session.md +409 -0
  72. package/docs/starter-kits.md +396 -0
  73. package/docs/storage.md +192 -0
  74. package/docs/structure.md +264 -0
  75. package/docs/testing.md +165 -0
  76. package/docs/time-travel.md +138 -0
  77. package/docs/validation.md +1792 -0
  78. package/docs/verification.md +152 -0
  79. package/docs/views.md +566 -0
  80. package/docs/vite.md +322 -0
  81. package/package.json +34 -32
  82. package/dist/chunk-A5XRLEWM.js.map +0 -1
  83. package/dist/chunk-BSQKOLSK.js.map +0 -1
  84. package/dist/chunk-PM5YTG57.js +0 -880
  85. package/dist/chunk-PM5YTG57.js.map +0 -1
  86. package/dist/chunk-WNJP46B7.js.map +0 -1
  87. package/dist/index.d-DOlTeG-8.d.ts +0 -99
package/docs/views.md ADDED
@@ -0,0 +1,566 @@
1
+ # Views
2
+
3
+ Views hold your pages' HTML. Each is a TSX file in `resources/views` whose component takes the view's data as props
4
+ and is rendered to HTML on the server.
5
+
6
+ ## Introduction
7
+
8
+ A view keeps your markup apart from the code that answers the request. It is a `.tsx` file that default-exports a
9
+ component, a function that receives the view's data and returns markup:
10
+
11
+ ```tsx title="resources/views/greeting.tsx"
12
+ export default function Greeting({ name }: { name: string }) {
13
+ return (
14
+ <html>
15
+ <body>
16
+ <h1>Hello, {name}</h1>
17
+ </body>
18
+ </html>
19
+ )
20
+ }
21
+ ```
22
+
23
+ As the file is `resources/views/greeting.tsx`, the `view()` helper returns it by the name `greeting`:
24
+
25
+ ```ts
26
+ Route.get('/', () => view('greeting', { name: 'James' }))
27
+ ```
28
+
29
+ > [!NOTE]
30
+ > Laravel's views are Blade templates, a template language that compiles to PHP. Oriel's are TSX, the same
31
+ > JavaScript expressions and components as the rest of your code, so TypeScript checks your views along with your
32
+ > routes and models. The sections below show how Blade's echoes, directives, components and layouts are written in
33
+ > TSX.
34
+
35
+ ### The JSX runtime
36
+
37
+ A new app's `tsconfig.json` points TSX at Oriel's JSX runtime:
38
+
39
+ ```json
40
+ {
41
+ "compilerOptions": {
42
+ "jsx": "react-jsx",
43
+ "jsxImportSource": "@orieljs/framework"
44
+ }
45
+ }
46
+ ```
47
+
48
+ There is no React here. An element such as `<h1>` is a plain description of the markup, and Oriel renders it to
49
+ HTML when the view renders. Nothing of a view runs in the browser: event handler props such as
50
+ `onClick={() => …}` are dropped, and a component has no state between renders. Node runs your TSX through `tsx`,
51
+ which the `oriel` command loads, and Bun runs it itself.
52
+
53
+ ### Writing views in React, Vue, Svelte, Solid or Angular
54
+
55
+ For pages written in a JavaScript framework, the Inertia starter kits pair your routes with React, Vue, Svelte, Solid or
56
+ Angular pages. Their root view, `resources/views/app.tsx`, is a TSX view like any other. [Inertia](inertia.md) covers
57
+ them.
58
+
59
+ ## Creating and rendering views
60
+
61
+ Create a view by adding a file to `resources/views`. Its default export must be a function, or rendering it fails with
62
+ "View [name] (path) must default-export a component function.". Return it from a route or a controller with the
63
+ `view()` helper, or with the `View` facade:
64
+
65
+ ```ts
66
+ const greeting = view('greeting', { name: 'James' })
67
+
68
+ const same = View.make('greeting', { name: 'James' })
69
+ ```
70
+
71
+ The first argument is the view's name, the file's path in `resources/views` without its extension. The second is its
72
+ data, which the component receives as its props.
73
+
74
+ A name finds the first file with the extension `.tsx`, `.ts`, `.jsx`, `.js` or `.mjs`, in that order. The folders to
75
+ search are `paths` in `config/view.ts`, `resources/views` in a new app. A view that doesn't exist is an error, "View
76
+ [name] not found.", and the request ends with a 500.
77
+
78
+ Oriel has no `make:view` command: a view is a file you create.
79
+
80
+ ### Nested view directories
81
+
82
+ Views can sit in folders of `resources/views`. A dot in the name is a folder, so `resources/views/admin/profile.tsx`
83
+ is `admin.profile`:
84
+
85
+ ```ts
86
+ declare const data: Record<string, unknown>
87
+ // ---cut---
88
+ const profile = view('admin.profile', data)
89
+ ```
90
+
91
+ As dots separate the folders, a folder whose name contains a dot can't be reached.
92
+
93
+ ### Creating the first available view
94
+
95
+ `View.first()` returns the first view of a list that exists, which suits views an app or a package lets you
96
+ override:
97
+
98
+ ```ts
99
+ declare const data: Record<string, unknown>
100
+ // ---cut---
101
+ const admin = View.first(['custom.admin', 'admin'], data)
102
+ ```
103
+
104
+ When none exists, the error names them all.
105
+
106
+ ### Determining if a view exists
107
+
108
+ `View.exists()` returns `true` when a view of that name exists:
109
+
110
+ ```ts
111
+ if (View.exists('admin.profile')) {
112
+ // ...
113
+ }
114
+ ```
115
+
116
+ ## Passing data to views
117
+
118
+ The data you give `view()` becomes the component's props:
119
+
120
+ ```ts
121
+ const res = view('greeting', { name: 'Victoria' })
122
+ ```
123
+
124
+ `with()` adds data to a view, a value or an object of values at a time, and returns the view so you can chain it:
125
+
126
+ ```ts
127
+ const res = view('greeting').with('name', 'Victoria').with('occupation', 'Astronaut')
128
+ ```
129
+
130
+ #### Typing a view's data
131
+
132
+ A view's component declares the type of its props, and TypeScript checks the component's code against it. `view()`
133
+ takes any data, as a view's name is a string that TypeScript can't follow to its file. To have the data checked,
134
+ export the props' type from the view and check the data with `satisfies`:
135
+
136
+ ```ts title="routes/web.ts"
137
+ // @filename: resources/views/greeting.tsx
138
+ export interface GreetingProps {
139
+ name: string
140
+ }
141
+ // ---cut---
142
+ import { Route, view } from '@orieljs/framework'
143
+ import type { GreetingProps } from '../resources/views/greeting'
144
+
145
+ Route.get('/', () => view('greeting', { name: 'James' } satisfies GreetingProps))
146
+ ```
147
+
148
+ ### Sharing data with all views
149
+
150
+ `View.share()` gives a value to every view your app renders. Call it in a service provider's `boot()`:
151
+
152
+ ```ts title="app/Providers/AppServiceProvider.ts"
153
+ import { ServiceProvider, View } from '@orieljs/framework'
154
+
155
+ export class AppServiceProvider extends ServiceProvider {
156
+ override boot(): void {
157
+ View.share('key', 'value')
158
+ }
159
+ }
160
+ ```
161
+
162
+ Every view receives `key` as a prop, unless its own data has a `key`, which wins. `share()` also takes an object of
163
+ values.
164
+
165
+ > [!WARNING]
166
+ > Shared data belongs to the app, not to a request: every request's views see the same values. Share what doesn't
167
+ > change from one user to the next, and give a view the current user's data with a [view composer](#view-composers).
168
+
169
+ ## View composers
170
+
171
+ A view composer is a function that runs each time a view renders, before the view does, and gives it data. When
172
+ several routes return the same view and it always needs the same data, a composer fetches the data in one place.
173
+ Register composers in a service provider's `boot()`:
174
+
175
+ ```ts title="app/Providers/AppServiceProvider.ts"
176
+ import { Auth, ServiceProvider, View } from '@orieljs/framework'
177
+ import { User } from '../Models/User'
178
+
179
+ export class AppServiceProvider extends ServiceProvider {
180
+ override boot(): void {
181
+ View.composer('profile', async (view) => {
182
+ view.with('count', await User.query().count())
183
+ })
184
+
185
+ View.composer('dashboard', async (view) => {
186
+ view.with('user', await Auth.user())
187
+ })
188
+ }
189
+ }
190
+ ```
191
+
192
+ A composer receives the view, whose `with()` adds data, and may be async: the view waits for it. It runs during the
193
+ request, so it can read the request, the session and the signed-in user.
194
+
195
+ `composer()` takes a function. A composer with more logic can be a class of your own, which the function calls. Oriel
196
+ has no folder for them; `app/View/Composers` keeps them together:
197
+
198
+ ```ts title="app/View/Composers/ProfileComposer.ts"
199
+ import type { View } from '@orieljs/framework/contracts'
200
+ import { User } from '../../Models/User'
201
+
202
+ export class ProfileComposer {
203
+ async compose(view: View): Promise<void> {
204
+ view.with('count', await User.query().count())
205
+ }
206
+ }
207
+ ```
208
+
209
+ ```ts title="app/Providers/AppServiceProvider.ts"
210
+ // @filename: app/View/Composers/ProfileComposer.ts
211
+ export declare class ProfileComposer {
212
+ compose(view: import('@orieljs/framework/contracts').View): Promise<void>
213
+ }
214
+ // ---cut---
215
+ import { ServiceProvider, View } from '@orieljs/framework'
216
+ import { ProfileComposer } from '../View/Composers/ProfileComposer'
217
+
218
+ export class AppServiceProvider extends ServiceProvider {
219
+ override boot(): void {
220
+ View.composer('profile', (view) => new ProfileComposer().compose(view))
221
+ }
222
+ }
223
+ ```
224
+
225
+ Composers run for views rendered by name: those `view()` and `View.make()` return, error pages and mail views. A
226
+ component that a view imports is a function the view calls, not a view, so composers don't run for it.
227
+
228
+ #### Attaching a composer to multiple views
229
+
230
+ `composer()` takes a list of names, and `*` matches any part of a name:
231
+
232
+ ```ts
233
+ View.composer(['profile', 'dashboard'], (view) => {
234
+ // ...
235
+ })
236
+
237
+ View.composer('admin.*', (view) => {
238
+ // ...
239
+ })
240
+
241
+ View.composer('*', (view) => {
242
+ // ...
243
+ })
244
+ ```
245
+
246
+ Laravel's view creators, which run as a view is made rather than as it renders, aren't in Oriel: use a composer.
247
+
248
+ ## Displaying data
249
+
250
+ Put any expression in braces to display it:
251
+
252
+ ```tsx title="resources/views/welcome.tsx"
253
+ export default function Welcome({ name }: { name: string }) {
254
+ return (
255
+ <p>
256
+ Hello, {name}. The current UNIX timestamp is {Math.floor(Date.now() / 1000)}.
257
+ </p>
258
+ )
259
+ }
260
+ ```
261
+
262
+ Text is escaped as it is rendered: `&`, `<`, `>`, `"` and `'` become entities, in content and in attribute values
263
+ alike, so a value can't add markup to your page. Entities in your data are escaped again, so `&amp;` displays as
264
+ `&amp;`. The `e()` helper escapes a string the same way.
265
+
266
+ #### Displaying unescaped data
267
+
268
+ `raw()` marks HTML you trust, which is rendered as it is:
269
+
270
+ ```tsx
271
+ declare const html: string
272
+ // ---cut---
273
+ const article = <article>{raw(html)}</article>
274
+ ```
275
+
276
+ > [!WARNING]
277
+ > Never pass the input of your users to `raw()`: it would let them add scripts to your pages.
278
+
279
+ The text inside `<script>` and `<style>` is escaped too, so inline code goes through `raw()`:
280
+
281
+ ```tsx
282
+ const script = <script>{raw("document.documentElement.classList.add('js')")}</script>
283
+ ```
284
+
285
+ #### Passing data to your scripts
286
+
287
+ An object or an array in an attribute is written as JSON, and escaped like any other value. A script reads it back
288
+ with `JSON.parse()`:
289
+
290
+ ```tsx
291
+ declare const user: User
292
+ // ---cut---
293
+ const element = <div id="profile" data-user={user}></div>
294
+ ```
295
+
296
+ A model is written without its hidden attributes, as in a JSON response.
297
+
298
+ ## Conditionals and loops
299
+
300
+ TSX has no directives: conditions and loops are JavaScript expressions. `&&` and the ternary operator choose what to
301
+ show, and `map()` repeats markup:
302
+
303
+ ```tsx title="resources/views/posts/index.tsx"
304
+ import type { Collection } from '@orieljs/framework'
305
+ import type { Post } from '../../../app/Models/Post'
306
+
307
+ export default function Index({ posts }: { posts: Collection<Post> }) {
308
+ return (
309
+ <main>
310
+ {posts.isEmpty() ? <p>No posts yet.</p> : <h1>{posts.count()} posts</h1>}
311
+
312
+ <ul>
313
+ {posts.map((post) => (
314
+ <li>{post.title}</li>
315
+ ))}
316
+ </ul>
317
+ </main>
318
+ )
319
+ }
320
+ ```
321
+
322
+ `null`, `undefined`, `true` and `false` render nothing, and `0` renders as `0`. A list renders each of its items, and
323
+ so does a collection.
324
+
325
+ Blade's other directives are function calls in a view: `await Auth.check()` for `@auth`,
326
+ `app().environment('local')` for `@env`, `session('status')` for `@session`. A view renders during the request, so
327
+ `request()`, `session()`, `config()` and the other helpers work in it.
328
+
329
+ ### Attributes
330
+
331
+ Attributes take values as in HTML, with a few conveniences:
332
+
333
+ - `class` takes a string, a list, or an object whose keys are kept when their value is truthy, as Blade's `@class`
334
+ does: `class={['btn', { 'btn-primary': primary }]}`. `className` is the same attribute.
335
+ - `style` takes a string or an object: `style={{ marginTop: 4 }}` renders `margin-top:4px`.
336
+ - `true` writes a boolean attribute such as `disabled`, and `false`, `null` and `undefined` leave it out. `aria-*`
337
+ and `data-*` attributes write `true` and `false` as text.
338
+ - `htmlFor` is written `for`, and `for` works too.
339
+
340
+ An `<html>` element is preceded by `<!DOCTYPE html>`, as TSX has no way to write one.
341
+
342
+ ## Components and layouts
343
+
344
+ ### Components
345
+
346
+ A component is a function that takes props and returns markup. Write it in a file of its own and import it into
347
+ the views that use it. `children` holds what you put between its tags:
348
+
349
+ ```tsx title="resources/views/components/alert.tsx"
350
+ import type { Renderable } from '@orieljs/framework'
351
+
352
+ export default function Alert({ type = 'info', children }: { type?: 'info' | 'danger'; children?: Renderable }) {
353
+ return (
354
+ <div class={['alert', `alert-${type}`]} role="alert">
355
+ {children}
356
+ </div>
357
+ )
358
+ }
359
+ ```
360
+
361
+ ```tsx
362
+ declare const Alert: (props: { type?: 'info' | 'danger'; children?: Renderable }) => Renderable
363
+ // ---cut---
364
+ const alert = <Alert type="danger">Your session has expired.</Alert>
365
+ ```
366
+
367
+ A component can be async: Oriel waits for it while it renders, so a component can query the database itself.
368
+
369
+ ### Layouts
370
+
371
+ Most pages share a layout. A layout is a component that takes the page as its `children`. The Live starter kit keeps
372
+ its layouts in `resources/views/components/layouts`:
373
+
374
+ ```tsx title="resources/views/components/layout.tsx"
375
+ import type { Renderable } from '@orieljs/framework'
376
+
377
+ export default function Layout({ title = 'Todo Manager', children }: { title?: string; children?: Renderable }) {
378
+ return (
379
+ <html lang="en">
380
+ <head>
381
+ <title>{title}</title>
382
+ </head>
383
+ <body>
384
+ <h1>Todos</h1>
385
+ <hr />
386
+ {children}
387
+ </body>
388
+ </html>
389
+ )
390
+ }
391
+ ```
392
+
393
+ A view wraps its markup in the layout, and passes the title as a prop:
394
+
395
+ ```tsx title="resources/views/tasks.tsx"
396
+ // @filename: resources/views/components/layout.tsx
397
+ declare const Layout: (props: { title?: string; children?: import('@orieljs/framework').Renderable }) => import('@orieljs/framework').Renderable
398
+ export default Layout
399
+ // ---cut---
400
+ import type { Collection } from '@orieljs/framework'
401
+ import type { Post } from '../../app/Models/Post'
402
+ import Layout from './components/layout'
403
+
404
+ export default function Tasks({ tasks }: { tasks: Collection<Post> }) {
405
+ return (
406
+ <Layout title="Custom Title">
407
+ {tasks.map((task) => (
408
+ <div>{task.title}</div>
409
+ ))}
410
+ </Layout>
411
+ )
412
+ }
413
+ ```
414
+
415
+ ```ts
416
+ Route.get('/tasks', async () => view('tasks', { tasks: await Post.all() }))
417
+ ```
418
+
419
+ > [!NOTE]
420
+ > Laravel builds layouts with Blade components (`<x-layout>`, `$slot`) or with template inheritance (`@extends`,
421
+ > `@section`, `@yield`). Oriel has neither: TSX components already do both jobs. A slot is a prop, the default slot
422
+ > is `children`, and a named slot is a prop whose value is markup, such as `sidebar={<Nav />}`.
423
+
424
+ ### Including subviews
425
+
426
+ To reuse markup, import the component that holds it. A view can also render another view by name, as a child:
427
+
428
+ ```tsx
429
+ const page = <div>{view('shared.errors')}</div>
430
+ ```
431
+
432
+ The view renders with its own data only: unlike Blade's `@include`, it doesn't receive the data of the view around it,
433
+ so pass what it needs, as in `view('shared.errors', { title })`. Shared data and composers apply to it.
434
+
435
+ ## Forms
436
+
437
+ ### CSRF field
438
+
439
+ A form that posts to a route in the `web` group must send the CSRF token. `csrfField()` writes the hidden field (see
440
+ [CSRF protection](csrf.md)):
441
+
442
+ ```tsx
443
+ const form = (
444
+ <form method="post" action="/profile">
445
+ {csrfField()}
446
+ ...
447
+ </form>
448
+ )
449
+ ```
450
+
451
+ ### Method field
452
+
453
+ HTML forms can't send `PUT`, `PATCH` or `DELETE`. `methodField()` writes the hidden `_method` field that spoofs them
454
+ (see [form method spoofing](routing.md#form-method-spoofing)):
455
+
456
+ ```tsx
457
+ const form = (
458
+ <form action="/foo/bar" method="post">
459
+ {csrfField()}
460
+ {methodField('PUT')}
461
+ ...
462
+ </form>
463
+ )
464
+ ```
465
+
466
+ ### Validation errors
467
+
468
+ After a failed [validation](validation.md), `errors()` returns the messages, by field, and `old()` the input the form
469
+ sent:
470
+
471
+ ```tsx title="resources/views/post/create.tsx"
472
+ import { csrfField, errors, old } from '@orieljs/framework'
473
+
474
+ export default function Create() {
475
+ const error = errors().first('title')
476
+
477
+ return (
478
+ <form method="post" action="/posts">
479
+ {csrfField()}
480
+
481
+ <label for="title">Post Title</label>
482
+ <input id="title" type="text" name="title" value={old('title', '')} class={{ 'is-invalid': error !== undefined }} />
483
+
484
+ {error && <div class="alert alert-danger">{error}</div>}
485
+ </form>
486
+ )
487
+ }
488
+ ```
489
+
490
+ `errors().has('title')` tells you whether a field has messages, `get('title')` returns them all, and `all()` every
491
+ message of every field. On a page with several forms, `errors('login')` reads the `login` error bag.
492
+
493
+ ## Rendering TSX without a view file
494
+
495
+ `View.renderToString()` renders TSX, a view or text to an HTML string, without a view file:
496
+
497
+ ```tsx
498
+ declare const Alert: (props: { children?: Renderable }) => Renderable
499
+ // ---cut---
500
+ const html = await View.renderToString(<Alert>Saved.</Alert>)
501
+ ```
502
+
503
+ `renderToStream()` renders to a stream instead, which sends each part as soon as it is ready, while an async
504
+ component waits for its data:
505
+
506
+ ```tsx
507
+ declare const Page: () => Promise<Renderable>
508
+ // ---cut---
509
+ const res = response().stream(renderToStream(<Page />), 200, { 'Content-Type': 'text/html; charset=utf-8' })
510
+ ```
511
+
512
+ ## Error views
513
+
514
+ When Oriel answers an error with an HTML page, it looks for a view named after the status:
515
+ `resources/views/errors/404.tsx` for a 404, then `errors/4xx.tsx` for any client error and `errors/5xx.tsx` for any
516
+ server error. The view receives the `status`, the `message` and the `exception`:
517
+
518
+ ```tsx title="resources/views/errors/404.tsx"
519
+ export default function NotFound({ message }: { status: number; message: string }) {
520
+ return (
521
+ <html lang="en">
522
+ <body>
523
+ <h1>{message}</h1>
524
+ </body>
525
+ </html>
526
+ )
527
+ }
528
+ ```
529
+
530
+ [Error handling](errors.md) covers when they are used. Mail views are views as well: `make:mail` writes one under
531
+ `resources/views/mail`, as [Mail](mail.md) explains.
532
+
533
+ ## Live components
534
+
535
+ Live components render with the same TSX, and their `o:` attributes, the directives that wire an element to the
536
+ component, are type-checked. An `o:` name that is neither a directive nor a DOM event is a type error, and so is a
537
+ directive's value of the wrong type:
538
+
539
+ ```tsx
540
+ // @ts-expect-error: o:clik is neither a directive nor an event
541
+ const typo = <button o:clik="save">Save</button>
542
+
543
+ // @ts-expect-error: o:model takes the name of a property
544
+ const value = <input o:model={5} />
545
+
546
+ const form = (
547
+ <form o:submit="save">
548
+ <input o:model-live="title" />
549
+ <a href="/posts" o:navigate>
550
+ Posts
551
+ </a>
552
+ </form>
553
+ )
554
+ ```
555
+
556
+ TypeScript checks a name with a `-` only when the type lists it. The listed modifier forms, such as `o:model-live`, are
557
+ checked; an unlisted one, such as the duration in `o:model-live-debounce-500ms` or an event's modifier in
558
+ `o:keydown-enter`, is accepted as written, and so is a misspelt one. A custom event isn't a DOM event, so spread it onto
559
+ the element, with `-window` for one a component dispatches, which fires on `window`:
560
+ `<div {...{ 'o:saved-window': '$refresh' }} />`. [Live](live.md) covers the directives.
561
+
562
+ ## Optimizing views
563
+
564
+ A view has no compile step of its own. Each view file is imported the first time it renders, and the runtime keeps
565
+ the module for the life of the process, so Laravel's `view:cache` and `view:clear` aren't needed. A changed view
566
+ shows once the server restarts: `node oriel serve --watch` restarts it when a file in `resources/views` changes.