@orieljs/framework 0.1.0-alpha.6 → 0.1.0-alpha.8
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/NOTICE +4 -0
- package/README.md +10 -1
- package/TRADEMARKS.md +33 -0
- package/dist/{chunk-POEP4B2M.js → chunk-7ZQZYDOC.js} +745 -105
- package/dist/chunk-7ZQZYDOC.js.map +1 -0
- package/dist/{chunk-I63ZFMX6.js → chunk-GVNLZZTN.js} +1 -1
- package/dist/chunk-GVNLZZTN.js.map +1 -0
- package/dist/{chunk-ZOEPKBVF.js → chunk-SPQUX3C7.js} +1347 -428
- package/dist/chunk-SPQUX3C7.js.map +1 -0
- package/dist/{chunk-MFTZQMZM.js → chunk-VOYW3R5J.js} +4 -4
- package/dist/chunk-VOYW3R5J.js.map +1 -0
- package/dist/contracts.d.ts +515 -17
- package/dist/contracts.js +1 -1
- package/dist/index.d-WQpfdIbG.d.ts +630 -0
- package/dist/index.d.ts +964 -152
- package/dist/index.js +4279 -866
- package/dist/index.js.map +1 -1
- package/dist/{jsx-runtime-CBZQYYDS.d.ts → jsx-runtime-6um4Mwfb.d.ts} +96 -5
- package/dist/jsx-runtime.d.ts +2 -2
- package/dist/jsx-runtime.js +3 -3
- package/dist/{testing-BM481Hdz.d.ts → testing-CKwfWJGn.d.ts} +180 -15
- package/dist/testing.d.ts +2 -2
- package/dist/testing.js +3 -3
- package/docs/authentication.md +487 -0
- package/docs/authorization.md +435 -0
- package/docs/cache.md +647 -0
- package/docs/collections.md +2270 -0
- package/docs/configuration.md +334 -0
- package/docs/console.md +651 -0
- package/docs/container.md +430 -0
- package/docs/controllers.md +406 -0
- package/docs/csrf.md +126 -0
- package/docs/database-testing.md +144 -0
- package/docs/database.md +236 -0
- package/docs/deployment.md +276 -0
- package/docs/eloquent-collections.md +218 -0
- package/docs/eloquent-factories.md +255 -0
- package/docs/eloquent-mutators.md +254 -0
- package/docs/eloquent-observers.md +145 -0
- package/docs/eloquent-relationships.md +557 -0
- package/docs/eloquent.md +494 -0
- package/docs/encryption.md +104 -0
- package/docs/errors.md +318 -0
- package/docs/events.md +336 -0
- package/docs/facades.md +278 -0
- package/docs/hashing.md +118 -0
- package/docs/helpers.md +1281 -0
- package/docs/http-tests.md +386 -0
- package/docs/inertia.md +485 -0
- package/docs/installation.md +209 -0
- package/docs/lifecycle.md +234 -0
- package/docs/live-tests.md +226 -0
- package/docs/live.md +910 -0
- package/docs/logging.md +255 -0
- package/docs/mail.md +619 -0
- package/docs/middleware.md +384 -0
- package/docs/migrations.md +416 -0
- package/docs/mocking.md +264 -0
- package/docs/navigation.json +273 -0
- package/docs/notifications.md +225 -0
- package/docs/pagination.md +247 -0
- package/docs/passwords.md +207 -0
- package/docs/providers.md +187 -0
- package/docs/queries.md +600 -0
- package/docs/queues.md +330 -0
- package/docs/requests.md +533 -0
- package/docs/responses.md +444 -0
- package/docs/routing.md +738 -0
- package/docs/scheduling.md +277 -0
- package/docs/seeding.md +122 -0
- package/docs/session.md +409 -0
- package/docs/starter-kits.md +396 -0
- package/docs/storage.md +192 -0
- package/docs/structure.md +264 -0
- package/docs/testing.md +165 -0
- package/docs/time-travel.md +138 -0
- package/docs/validation.md +1792 -0
- package/docs/verification.md +152 -0
- package/docs/views.md +566 -0
- package/docs/vite.md +322 -0
- package/package.json +34 -32
- package/dist/chunk-I63ZFMX6.js.map +0 -1
- package/dist/chunk-MFTZQMZM.js.map +0 -1
- package/dist/chunk-POEP4B2M.js.map +0 -1
- package/dist/chunk-ZOEPKBVF.js.map +0 -1
- package/dist/index.d-BJyJx6U1.d.ts +0 -319
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 `&` displays as
|
|
264
|
+
`&`. 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.
|