@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/inertia.md
ADDED
|
@@ -0,0 +1,485 @@
|
|
|
1
|
+
# Inertia
|
|
2
|
+
|
|
3
|
+
Inertia lets you write your pages as React, Vue, Svelte, Solid or Angular components, while your routes and controllers
|
|
4
|
+
stay on the server, with no API to build in between.
|
|
5
|
+
|
|
6
|
+
## Introduction
|
|
7
|
+
|
|
8
|
+
An Inertia page is a component in `resources/js/pages`. Your controller returns the page's name and its props; Inertia
|
|
9
|
+
renders the component with them, in the browser on later visits and, with server-side rendering, on the server for the
|
|
10
|
+
first one. Links and forms make requests as a single-page app does, and the server answers with the next page's props as
|
|
11
|
+
JSON.
|
|
12
|
+
|
|
13
|
+
Oriel follows `inertia-laravel` on the server and speaks the protocol of the official Inertia clients. Its adapters,
|
|
14
|
+
one per framework, add typed pages and Oriel's `route()`:
|
|
15
|
+
|
|
16
|
+
| Framework | Package | Starter kit |
|
|
17
|
+
| --- | --- | --- |
|
|
18
|
+
| React | `@orieljs/inertia-react` | `--inertia=react` |
|
|
19
|
+
| Vue | `@orieljs/inertia-vue` | `--inertia=vue` |
|
|
20
|
+
| Svelte | `@orieljs/inertia-svelte` | `--inertia=svelte` |
|
|
21
|
+
| Solid | `@orieljs/inertia-solid` | `--inertia=solid` |
|
|
22
|
+
| Angular | `@orieljs/inertia-angular` | `--inertia=angular` |
|
|
23
|
+
|
|
24
|
+
The React, Vue and Svelte adapters are the official Inertia adapters, re-exported with Oriel's additions. The Solid
|
|
25
|
+
and Angular adapters are Oriel's own, with the same API in each framework's idiom. A new app from an Inertia
|
|
26
|
+
[starter kit](starter-kits.md) has everything below set up.
|
|
27
|
+
|
|
28
|
+
## Server-side setup
|
|
29
|
+
|
|
30
|
+
### Creating responses
|
|
31
|
+
|
|
32
|
+
An action returns a page with `inertia()`, its component's name and its props:
|
|
33
|
+
|
|
34
|
+
```ts title="app/Http/Controllers/PostController.ts"
|
|
35
|
+
import { inertia } from '@orieljs/framework'
|
|
36
|
+
import { Post } from '../../Models/Post'
|
|
37
|
+
import { Controller } from './Controller'
|
|
38
|
+
|
|
39
|
+
export class PostController extends Controller {
|
|
40
|
+
/** Show the blog's posts. */
|
|
41
|
+
async index() {
|
|
42
|
+
return inertia('Posts/Index', {
|
|
43
|
+
posts: await Post.latest().get(),
|
|
44
|
+
})
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
The component's name is its path under `resources/js/pages`, without the extension: `Posts/Index` is
|
|
50
|
+
`resources/js/pages/Posts/Index.tsx` (or `.vue`, `.svelte`, `.component.ts`). Models in the props reach the page as
|
|
51
|
+
their JSON, so their `hidden` attributes stay on the server.
|
|
52
|
+
|
|
53
|
+
A page with no data of its own needs no controller: `Route.inertia()` routes a URI to it.
|
|
54
|
+
|
|
55
|
+
```ts title="routes/web.ts"
|
|
56
|
+
import { Route } from '@orieljs/framework'
|
|
57
|
+
|
|
58
|
+
Route.inertia('/about', 'About').name('about')
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### The root view
|
|
62
|
+
|
|
63
|
+
The first visit loads the root view, `resources/views/app.tsx`, which writes the HTML document around the page. It
|
|
64
|
+
places the page's data with `inertiaBody(page)`, the server-rendered head with `inertiaHead(page)`, and your scripts and
|
|
65
|
+
styles with [`vite()`](vite.md). A React app adds `viteReactRefresh()` before `vite()`:
|
|
66
|
+
|
|
67
|
+
```tsx title="resources/views/app.tsx"
|
|
68
|
+
import { inertiaBody, inertiaHead, vite, viteReactRefresh } from '@orieljs/framework'
|
|
69
|
+
import type { InertiaPage } from '@orieljs/framework/contracts'
|
|
70
|
+
|
|
71
|
+
export default function App({ page }: { page: InertiaPage }) {
|
|
72
|
+
return (
|
|
73
|
+
<html lang="en">
|
|
74
|
+
<head>
|
|
75
|
+
<meta charset="utf-8" />
|
|
76
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
77
|
+
{inertiaHead(page)}
|
|
78
|
+
{viteReactRefresh()}
|
|
79
|
+
{vite(['resources/css/app.css', 'resources/js/app.tsx'])}
|
|
80
|
+
</head>
|
|
81
|
+
<body>{inertiaBody(page)}</body>
|
|
82
|
+
</html>
|
|
83
|
+
)
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### The middleware
|
|
88
|
+
|
|
89
|
+
`HandleInertiaRequests`, in `app/Http/Middleware`, extends `InertiaMiddleware`, and `bootstrap/app.ts` appends it to the
|
|
90
|
+
`web` group. It answers in the Inertia protocol: a JSON page on Inertia requests, a 409 when your assets changed since
|
|
91
|
+
the page was loaded, and a 303 after a `PUT`, `PATCH` or `DELETE` that redirects. `node oriel inertia:middleware` writes
|
|
92
|
+
one for an app that hasn't.
|
|
93
|
+
|
|
94
|
+
Its `share()` gives every page some props, merged with the validation errors it shares itself:
|
|
95
|
+
|
|
96
|
+
```ts title="app/Http/Middleware/HandleInertiaRequests.ts"
|
|
97
|
+
import { Auth, config, InertiaMiddleware } from '@orieljs/framework'
|
|
98
|
+
import type { HttpRequest } from '@orieljs/framework/contracts'
|
|
99
|
+
import type { User } from '../../Models/User'
|
|
100
|
+
|
|
101
|
+
export class HandleInertiaRequests extends InertiaMiddleware {
|
|
102
|
+
/** The root view loaded on the first page visit: resources/views/app.tsx. */
|
|
103
|
+
protected override rootViewName = 'app'
|
|
104
|
+
|
|
105
|
+
/** Props shared with every page. */
|
|
106
|
+
override async share(request: HttpRequest) {
|
|
107
|
+
const user = await Auth.user<User>()
|
|
108
|
+
|
|
109
|
+
return {
|
|
110
|
+
...(await super.share(request)),
|
|
111
|
+
name: String(config('app.name')),
|
|
112
|
+
auth: { user: user === undefined ? null : { id: user.id, name: user.name, email: user.email } },
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
`version()` returns your assets' version: a hash of `app.asset_url` when you set one, else of
|
|
119
|
+
`public/build/manifest.json`. A page loaded with an older version is reloaded in full on its next visit.
|
|
120
|
+
|
|
121
|
+
### Shared data
|
|
122
|
+
|
|
123
|
+
Besides the middleware's `share()`, `Inertia.share()` shares props from anywhere in a request, such as a controller:
|
|
124
|
+
|
|
125
|
+
```ts
|
|
126
|
+
Inertia.share('appName', 'Blog')
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
`Inertia.flash()` stores data for the next page only, which reads it as `page.flash`, as session flash data works.
|
|
130
|
+
|
|
131
|
+
### Redirects
|
|
132
|
+
|
|
133
|
+
Redirect as anywhere else in your app: `redirect().route('posts.index')`, or `back()`. Inertia follows the redirect
|
|
134
|
+
and shows the next page. To leave the app, to an external site or a page that isn't Inertia's, `Inertia.location()`
|
|
135
|
+
makes the browser load the URL in full:
|
|
136
|
+
|
|
137
|
+
```ts
|
|
138
|
+
Route.get('/billing', () => Inertia.location('https://example.com/billing'))
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### Props: deferred, optional and merged
|
|
142
|
+
|
|
143
|
+
A prop can be a function, which runs only when the prop is sent. These helpers choose when that is:
|
|
144
|
+
|
|
145
|
+
| Helper | The prop is sent |
|
|
146
|
+
| --- | --- |
|
|
147
|
+
| `Inertia.defer(fn, group?)` | In a request the page makes right after it renders, so the page shows sooner; props of one group come together |
|
|
148
|
+
| `Inertia.optional(fn)` | Only in a partial reload that asks for it |
|
|
149
|
+
| `Inertia.always(value)` | In every response, even a partial reload that didn't ask for it |
|
|
150
|
+
| `Inertia.merge(value)`, `Inertia.deepMerge(value)` | And merged into the page's current value on a reload, such as the next page of a list |
|
|
151
|
+
| `Inertia.once(fn)` | Once: the client keeps it for the next pages |
|
|
152
|
+
| `Inertia.scroll(paginator)` | As a page of an infinite scroll, with its page numbers |
|
|
153
|
+
|
|
154
|
+
```ts title="app/Http/Controllers/DashboardController.ts"
|
|
155
|
+
import { Inertia, inertia } from '@orieljs/framework'
|
|
156
|
+
import { Post } from '../../Models/Post'
|
|
157
|
+
import { Controller } from './Controller'
|
|
158
|
+
|
|
159
|
+
export class DashboardController extends Controller {
|
|
160
|
+
async index() {
|
|
161
|
+
return inertia('Dashboard', {
|
|
162
|
+
latest: await Post.latest().limit(5).get(),
|
|
163
|
+
drafts: Inertia.defer(() => Post.whereNull('published_at').get()),
|
|
164
|
+
stats: Inertia.optional(() => Post.count()),
|
|
165
|
+
})
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
A partial reload asks the server for some props only, such as `router.reload({ only: ['stats'] })` in the page.
|
|
171
|
+
|
|
172
|
+
## Client-side setup
|
|
173
|
+
|
|
174
|
+
The browser entry, `resources/js/app.tsx` (`app.ts` in the Vue, Svelte and Angular kits), starts Inertia with your
|
|
175
|
+
pages' folder:
|
|
176
|
+
|
|
177
|
+
```tsx title="resources/js/app.tsx"
|
|
178
|
+
import { createInertiaApp } from '@orieljs/inertia-react'
|
|
179
|
+
|
|
180
|
+
const appName = import.meta.env.VITE_APP_NAME || 'Oriel'
|
|
181
|
+
|
|
182
|
+
createInertiaApp({
|
|
183
|
+
pages: './pages',
|
|
184
|
+
title: (title) => (title ? `${title} – ${appName}` : appName),
|
|
185
|
+
})
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
`title` formats every page's title. The `oriel()` [Vite](vite.md) plugin sets up each framework's own Vite plugin.
|
|
189
|
+
|
|
190
|
+
## Pages
|
|
191
|
+
|
|
192
|
+
A page component receives its props. They are typed: `node oriel inertia:types` reads the `inertia()` calls of your
|
|
193
|
+
controllers and routes and writes `resources/js/oriel/pages.d.ts`, so each page knows its props, and the props your
|
|
194
|
+
middleware shares. The kits' Vite plugin runs it when Vite starts, and again when your server code changes.
|
|
195
|
+
|
|
196
|
+
```tsx tab="React" title="resources/js/pages/Posts/Index.tsx"
|
|
197
|
+
import { Head, Link, route, type PageProps } from '@orieljs/inertia-react'
|
|
198
|
+
|
|
199
|
+
export default function Index({ posts }: PageProps<'Posts/Index'>) {
|
|
200
|
+
return (
|
|
201
|
+
<>
|
|
202
|
+
<Head title="Posts" />
|
|
203
|
+
<ul>
|
|
204
|
+
{posts.map((post) => (
|
|
205
|
+
<li key={post.id}>
|
|
206
|
+
<Link href={route('posts.show', post.slug)}>{post.title}</Link>
|
|
207
|
+
</li>
|
|
208
|
+
))}
|
|
209
|
+
</ul>
|
|
210
|
+
</>
|
|
211
|
+
)
|
|
212
|
+
}
|
|
213
|
+
```
|
|
214
|
+
```vue tab="Vue" title="resources/js/pages/Posts/Index.vue"
|
|
215
|
+
<script setup lang="ts">
|
|
216
|
+
import { Head, Link, route, usePage } from '@orieljs/inertia-vue'
|
|
217
|
+
|
|
218
|
+
const page = usePage<'Posts/Index'>()
|
|
219
|
+
</script>
|
|
220
|
+
|
|
221
|
+
<template>
|
|
222
|
+
<Head title="Posts" />
|
|
223
|
+
<ul>
|
|
224
|
+
<li v-for="post in page.props.posts" :key="post.id">
|
|
225
|
+
<Link :href="route('posts.show', post.slug)">{{ post.title }}</Link>
|
|
226
|
+
</li>
|
|
227
|
+
</ul>
|
|
228
|
+
</template>
|
|
229
|
+
```
|
|
230
|
+
```svelte tab="Svelte" title="resources/js/pages/Posts/Index.svelte"
|
|
231
|
+
<script lang="ts">
|
|
232
|
+
import { Link, route, type PageProps } from '@orieljs/inertia-svelte'
|
|
233
|
+
|
|
234
|
+
let { posts }: PageProps<'Posts/Index'> = $props()
|
|
235
|
+
</script>
|
|
236
|
+
|
|
237
|
+
<svelte:head>
|
|
238
|
+
<title>Posts</title>
|
|
239
|
+
</svelte:head>
|
|
240
|
+
|
|
241
|
+
<ul>
|
|
242
|
+
{#each posts as post (post.id)}
|
|
243
|
+
<li><Link href={route('posts.show', post.slug)}>{post.title}</Link></li>
|
|
244
|
+
{/each}
|
|
245
|
+
</ul>
|
|
246
|
+
```
|
|
247
|
+
```tsx tab="Solid" title="resources/js/pages/Posts/Index.tsx"
|
|
248
|
+
import { Head, Link, route, type PageProps } from '@orieljs/inertia-solid'
|
|
249
|
+
import { For } from 'solid-js'
|
|
250
|
+
|
|
251
|
+
export default function Index(props: PageProps<'Posts/Index'>) {
|
|
252
|
+
return (
|
|
253
|
+
<>
|
|
254
|
+
<Head title="Posts" />
|
|
255
|
+
<ul>
|
|
256
|
+
<For each={props.posts}>
|
|
257
|
+
{(post) => (
|
|
258
|
+
<li>
|
|
259
|
+
<Link href={route('posts.show', post.slug)}>{post.title}</Link>
|
|
260
|
+
</li>
|
|
261
|
+
)}
|
|
262
|
+
</For>
|
|
263
|
+
</ul>
|
|
264
|
+
</>
|
|
265
|
+
)
|
|
266
|
+
}
|
|
267
|
+
```
|
|
268
|
+
```ts tab="Angular" title="resources/js/pages/Posts/Index.component.ts"
|
|
269
|
+
import { Component, input } from '@angular/core'
|
|
270
|
+
import { Head, Link, route } from '@orieljs/inertia-angular'
|
|
271
|
+
|
|
272
|
+
@Component({
|
|
273
|
+
selector: 'posts-index',
|
|
274
|
+
imports: [Head, Link],
|
|
275
|
+
template: `
|
|
276
|
+
<o-head title="Posts" />
|
|
277
|
+
<ul>
|
|
278
|
+
@for (post of posts(); track post.id) {
|
|
279
|
+
<li><a oLink [href]="route('posts.show', post.slug)">{{ post.title }}</a></li>
|
|
280
|
+
}
|
|
281
|
+
</ul>
|
|
282
|
+
`,
|
|
283
|
+
})
|
|
284
|
+
export default class Index {
|
|
285
|
+
readonly posts = input.required<{ id: number; title: string; slug: string }[]>()
|
|
286
|
+
|
|
287
|
+
protected readonly route = route
|
|
288
|
+
}
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
> [!NOTE]
|
|
292
|
+
> A Vue page reads its props with `usePage<'Posts/Index'>()`, typed by the page's name, where Inertia's own Vue docs
|
|
293
|
+
> declare them with `defineProps`: Vue's compiler can't resolve the generated page types in
|
|
294
|
+
> `defineProps<PageProps<'Posts/Index'>>()`. An Angular page receives each prop as a signal input. Svelte has no
|
|
295
|
+
> Inertia `Head` component: the kits use `<svelte:head>`.
|
|
296
|
+
|
|
297
|
+
### Links and the route helper
|
|
298
|
+
|
|
299
|
+
`Link` (in Angular, the `oLink` directive on an `<a>`) visits a page without reloading the document. `route()` gives a
|
|
300
|
+
named route's URL, typed: once `node oriel route:types` has run, a wrong name or a missing parameter is a type error.
|
|
301
|
+
`isCurrentRoute('posts.*')` tells you whether the current page's route matches, for an active link. Both work during
|
|
302
|
+
server-side rendering, from the URL being rendered.
|
|
303
|
+
|
|
304
|
+
`router` makes visits from code: `router.visit(url)`, `router.post(url, data)`, `router.reload({ only: ['stats'] })`.
|
|
305
|
+
|
|
306
|
+
## Forms
|
|
307
|
+
|
|
308
|
+
`useForm()` holds a form's data, sends it, and gives back the validation errors, one per field, and whether the
|
|
309
|
+
request is in flight:
|
|
310
|
+
|
|
311
|
+
```tsx tab="React" title="resources/js/pages/Posts/Create.tsx"
|
|
312
|
+
import { route, useForm } from '@orieljs/inertia-react'
|
|
313
|
+
import type { FormEvent } from 'react'
|
|
314
|
+
|
|
315
|
+
export default function Create() {
|
|
316
|
+
const { data, setData, post, processing, errors } = useForm<{ title: string }>({ title: '' })
|
|
317
|
+
|
|
318
|
+
const submit = (event: FormEvent) => {
|
|
319
|
+
event.preventDefault()
|
|
320
|
+
post(route('posts.store'))
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
return (
|
|
324
|
+
<form onSubmit={submit}>
|
|
325
|
+
<input value={data.title} onChange={(event) => setData('title', event.target.value)} />
|
|
326
|
+
{errors.title && <p className="error">{errors.title}</p>}
|
|
327
|
+
|
|
328
|
+
<button type="submit" disabled={processing}>
|
|
329
|
+
Publish
|
|
330
|
+
</button>
|
|
331
|
+
</form>
|
|
332
|
+
)
|
|
333
|
+
}
|
|
334
|
+
```
|
|
335
|
+
```vue tab="Vue" title="resources/js/pages/Posts/Create.vue"
|
|
336
|
+
<script setup lang="ts">
|
|
337
|
+
import { route, useForm } from '@orieljs/inertia-vue'
|
|
338
|
+
|
|
339
|
+
const form = useForm({
|
|
340
|
+
title: '',
|
|
341
|
+
})
|
|
342
|
+
|
|
343
|
+
const submit = () => {
|
|
344
|
+
form.post(route('posts.store'))
|
|
345
|
+
}
|
|
346
|
+
</script>
|
|
347
|
+
|
|
348
|
+
<template>
|
|
349
|
+
<form @submit.prevent="submit">
|
|
350
|
+
<input v-model="form.title" />
|
|
351
|
+
<p v-if="form.errors.title" class="error">{{ form.errors.title }}</p>
|
|
352
|
+
|
|
353
|
+
<button type="submit" :disabled="form.processing">Publish</button>
|
|
354
|
+
</form>
|
|
355
|
+
</template>
|
|
356
|
+
```
|
|
357
|
+
```svelte tab="Svelte" title="resources/js/pages/Posts/Create.svelte"
|
|
358
|
+
<script lang="ts">
|
|
359
|
+
import { route, useForm } from '@orieljs/inertia-svelte'
|
|
360
|
+
|
|
361
|
+
const form = useForm({
|
|
362
|
+
title: '',
|
|
363
|
+
})
|
|
364
|
+
|
|
365
|
+
function submit(event: SubmitEvent) {
|
|
366
|
+
event.preventDefault()
|
|
367
|
+
form.post(route('posts.store'))
|
|
368
|
+
}
|
|
369
|
+
</script>
|
|
370
|
+
|
|
371
|
+
<form onsubmit={submit}>
|
|
372
|
+
<input bind:value={form.title} />
|
|
373
|
+
{#if form.errors.title}
|
|
374
|
+
<p class="error">{form.errors.title}</p>
|
|
375
|
+
{/if}
|
|
376
|
+
|
|
377
|
+
<button type="submit" disabled={form.processing}>Publish</button>
|
|
378
|
+
</form>
|
|
379
|
+
```
|
|
380
|
+
```tsx tab="Solid" title="resources/js/pages/Posts/Create.tsx"
|
|
381
|
+
import { route, useForm } from '@orieljs/inertia-solid'
|
|
382
|
+
import { Show } from 'solid-js'
|
|
383
|
+
|
|
384
|
+
export default function Create() {
|
|
385
|
+
const form = useForm<{ title: string }>({ title: '' })
|
|
386
|
+
|
|
387
|
+
const submit = (event: SubmitEvent) => {
|
|
388
|
+
event.preventDefault()
|
|
389
|
+
form.post(route('posts.store'))
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
return (
|
|
393
|
+
<form onSubmit={submit}>
|
|
394
|
+
<input value={form.data.title} onInput={(event) => form.setData('title', event.currentTarget.value)} />
|
|
395
|
+
<Show when={form.errors.title}>
|
|
396
|
+
<p class="error">{form.errors.title}</p>
|
|
397
|
+
</Show>
|
|
398
|
+
|
|
399
|
+
<button type="submit" disabled={form.processing}>
|
|
400
|
+
Publish
|
|
401
|
+
</button>
|
|
402
|
+
</form>
|
|
403
|
+
)
|
|
404
|
+
}
|
|
405
|
+
```
|
|
406
|
+
```ts tab="Angular" title="resources/js/pages/Posts/Create.component.ts"
|
|
407
|
+
import { Component } from '@angular/core'
|
|
408
|
+
import { route, useForm } from '@orieljs/inertia-angular'
|
|
409
|
+
|
|
410
|
+
@Component({
|
|
411
|
+
selector: 'posts-create',
|
|
412
|
+
template: `
|
|
413
|
+
<form (submit)="submit($event)">
|
|
414
|
+
<input #title [value]="form.data.title" (input)="form.setData('title', title.value)" />
|
|
415
|
+
@if (form.errors.title) {
|
|
416
|
+
<p class="error">{{ form.errors.title }}</p>
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
<button type="submit" [disabled]="form.processing">Publish</button>
|
|
420
|
+
</form>
|
|
421
|
+
`,
|
|
422
|
+
})
|
|
423
|
+
export default class Create {
|
|
424
|
+
protected readonly form = useForm<{ title: string }>({ title: '' })
|
|
425
|
+
|
|
426
|
+
protected submit(event: SubmitEvent): void {
|
|
427
|
+
event.preventDefault()
|
|
428
|
+
this.form.post(route('posts.store'))
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
A failed validation sends the user back to the page, with the errors (see
|
|
434
|
+
[Displaying errors in Inertia pages](validation.md#displaying-errors-in-inertia-pages)). Each adapter also has a `Form`
|
|
435
|
+
component (Angular's `form[oForm]`), which sends a plain HTML form's fields without `useForm()`.
|
|
436
|
+
|
|
437
|
+
### Precognition
|
|
438
|
+
|
|
439
|
+
On a route with the `precognitive` middleware, the React, Vue and Svelte forms can validate a field while the user fills
|
|
440
|
+
it in: give `useForm()` the method and the URL first, `useForm('post', route('posts.store'), { title: '' })`, then call
|
|
441
|
+
`form.validate('title')`; `Form` has a `validate()` too. The Solid and Angular forms don't offer Precognition yet.
|
|
442
|
+
[Precognition](validation.md#precognition) describes the server side, and why the action validates first.
|
|
443
|
+
|
|
444
|
+
## More Inertia features
|
|
445
|
+
|
|
446
|
+
Each adapter has Inertia's components and hooks for the props above, under Inertia's names (Angular's are directives):
|
|
447
|
+
|
|
448
|
+
| Feature | React, Vue, Svelte, Solid | Angular |
|
|
449
|
+
| --- | --- | --- |
|
|
450
|
+
| Deferred props, with a fallback while they load | `Deferred` | `oDeferred` directive |
|
|
451
|
+
| Loading props when an element scrolls into view | `WhenVisible` | `oWhenVisible` directive |
|
|
452
|
+
| Infinite scrolling of an `Inertia.scroll()` prop | `InfiniteScroll` | `<o-infinite-scroll>` |
|
|
453
|
+
| Reloading props on an interval | `usePoll()` | `usePoll()` |
|
|
454
|
+
| Prefetching pages before a click | `usePrefetch()`, `prefetch` on `Link` | `usePrefetch()`, `prefetch` on `oLink` |
|
|
455
|
+
| Keeping local state in the browser's history | `useRemember()` | `useRemember()` |
|
|
456
|
+
| The page's title and meta tags | `Head` (Svelte: `<svelte:head>`) | `<o-head>` |
|
|
457
|
+
|
|
458
|
+
They work as Inertia's documentation describes, at [inertiajs.com](https://inertiajs.com).
|
|
459
|
+
|
|
460
|
+
## Server-side rendering
|
|
461
|
+
|
|
462
|
+
The React, Vue, Svelte and Solid kits render the first visit on the server, so it arrives as HTML. `npm run build`
|
|
463
|
+
builds the server bundle from `resources/js/ssr.*` into `bootstrap/ssr`, and `node oriel inertia:start-ssr` runs it;
|
|
464
|
+
when no SSR server answers, the pages are rendered in the browser. [Inertia SSR](starter-kits.md#inertia-ssr) and
|
|
465
|
+
[The SSR server](deployment.md#the-ssr-server) cover running it. `inertia.ssr.enabled`, from `INERTIA_SSR_ENABLED`,
|
|
466
|
+
turns it off.
|
|
467
|
+
|
|
468
|
+
## History encryption
|
|
469
|
+
|
|
470
|
+
Inertia keeps each page's props in the browser's history, so the back button can show them again. To keep a signed-in
|
|
471
|
+
user's data out of it after they sign out, encrypt it: `INERTIA_ENCRYPT_HISTORY=true` encrypts every page's state, and
|
|
472
|
+
the `inertia.encrypt` middleware does it for some routes. `Inertia.clearHistory()` clears the encrypted history, such
|
|
473
|
+
as when a user signs out.
|
|
474
|
+
|
|
475
|
+
## Adapter differences
|
|
476
|
+
|
|
477
|
+
| | React, Vue, Svelte | Solid | Angular |
|
|
478
|
+
| --- | --- | --- | --- |
|
|
479
|
+
| Server-side rendering | Yes | Yes | No |
|
|
480
|
+
| Precognition | Yes | No | No |
|
|
481
|
+
| `useHttp()` | Yes | No | No |
|
|
482
|
+
|
|
483
|
+
The Angular adapter's other limits are in its package's README: track a form's array rows by `$index` or a stable id,
|
|
484
|
+
not by the item, since `useForm` copies the objects along a dotted path on each write; and once a page uses `<o-head>`,
|
|
485
|
+
a later `Title.setTitle()` is overwritten.
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
# Installation
|
|
2
|
+
|
|
3
|
+
Oriel is a full-stack TypeScript framework for Node and Bun, in the spirit of Laravel. Create an app with your
|
|
4
|
+
package manager and one of seven starter kits.
|
|
5
|
+
|
|
6
|
+
## Meet Oriel
|
|
7
|
+
|
|
8
|
+
Oriel gives a TypeScript app what Laravel gives a PHP one: routing, controllers and middleware, models and
|
|
9
|
+
migrations, validation, authentication, queues, mail and the scheduler, under Laravel's names. Your pages are Inertia
|
|
10
|
+
pages in React, Vue, Svelte, Solid or Angular, or Live components rendered on the server, and the same app can serve a
|
|
11
|
+
JSON API.
|
|
12
|
+
|
|
13
|
+
## Creating an Oriel application
|
|
14
|
+
|
|
15
|
+
### Requirements
|
|
16
|
+
|
|
17
|
+
You need **Node 22** or later, or **Bun 1.2** or later. Nothing is installed globally: the installer runs through
|
|
18
|
+
your package manager, and each app has its own `oriel` command. A new app keeps its data in SQLite, so you don't need
|
|
19
|
+
a database server to start.
|
|
20
|
+
|
|
21
|
+
### Creating an application
|
|
22
|
+
|
|
23
|
+
Create an app with your package manager's `create` command:
|
|
24
|
+
|
|
25
|
+
```sh
|
|
26
|
+
npm create @orieljs@latest blog
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
The same installer runs with pnpm, Bun and Yarn:
|
|
30
|
+
|
|
31
|
+
```sh
|
|
32
|
+
pnpm create @orieljs blog
|
|
33
|
+
bun create @orieljs blog
|
|
34
|
+
yarn create @orieljs blog
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
`npm create oriel-app` is the same installer under another name: both run `create-oriel-app` at the same version.
|
|
38
|
+
|
|
39
|
+
The installer asks for the app's name, unless you gave one, then for a stack (Inertia, Live or an API) and, for Inertia,
|
|
40
|
+
its framework, and last whether to install the dependencies and start a git repository. The name is the last part of the
|
|
41
|
+
folder you create the app in, and it must be a valid npm package name: lower case, with no spaces. When the folder isn't
|
|
42
|
+
empty, the installer asks before it removes the files in it.
|
|
43
|
+
|
|
44
|
+
### Choosing a starter kit
|
|
45
|
+
|
|
46
|
+
The kits with pages come with registration, login, email verification, password reset, a dashboard, and profile,
|
|
47
|
+
password and appearance settings. The API kit has registration, login, logout and password reset as JSON endpoints,
|
|
48
|
+
with personal access tokens and no pages. Pick one in the prompt, or name it with an option:
|
|
49
|
+
|
|
50
|
+
| Option | The kit |
|
|
51
|
+
| --- | --- |
|
|
52
|
+
| `--inertia=react` | Inertia pages in React (a bare `--inertia` means React) |
|
|
53
|
+
| `--inertia=vue`, `--inertia=svelte`, `--inertia=solid`, `--inertia=angular` | Inertia pages in Vue, Svelte, Solid or Angular |
|
|
54
|
+
| `--live` | Live: server-driven components written in TSX |
|
|
55
|
+
| `--api` | A JSON API with personal access tokens, and no pages |
|
|
56
|
+
|
|
57
|
+
[Starter kits](starter-kits.md) describes what each kit contains.
|
|
58
|
+
|
|
59
|
+
> [!NOTE]
|
|
60
|
+
> With npm, the options go after `--`, so npm passes them on to the installer:
|
|
61
|
+
> `npm create @orieljs@latest blog -- --inertia=vue`. pnpm, Bun and Yarn take them directly.
|
|
62
|
+
|
|
63
|
+
### Installer options
|
|
64
|
+
|
|
65
|
+
| Option | What it does |
|
|
66
|
+
| --- | --- |
|
|
67
|
+
| `--pm <npm\|pnpm\|yarn\|bun>` | The package manager to use (by default, the one running the installer) |
|
|
68
|
+
| `--install`, `--no-install` | Install the dependencies, set the app key, create the database and build the assets (default: yes) |
|
|
69
|
+
| `--git`, `--no-git` | Start a git repository with a first commit (default: yes) |
|
|
70
|
+
| `-f`, `--force` | Empty a folder that isn't empty, without asking (a `.git` folder stays) |
|
|
71
|
+
| `-y`, `--yes` | Ask nothing, and take the defaults for everything not given: `oriel-app`, React, install, git |
|
|
72
|
+
| `-h`, `--help` | List the options |
|
|
73
|
+
| `-v`, `--version` | Print the installer's version |
|
|
74
|
+
|
|
75
|
+
The installer asks its questions only in a terminal: when its input and output are a terminal, the `CI` variable isn't
|
|
76
|
+
set and you didn't pass `--yes`. In a script or on CI, pass the kit as an option or `--yes`; without either, the
|
|
77
|
+
installer stops with "No stack chosen: pass --inertia[=<framework>], --live or --api (or --yes for React).". A folder
|
|
78
|
+
that isn't empty needs `--force` there.
|
|
79
|
+
|
|
80
|
+
### What the installer does
|
|
81
|
+
|
|
82
|
+
It copies the kit into the new folder and writes the app's `package.json` for your package manager. It writes `.env`
|
|
83
|
+
from `.env.example`, with `APP_NAME` set to the app's name, and a CI workflow, `.github/workflows/ci.yml`, that
|
|
84
|
+
installs the app, migrates a fresh database, type-checks and tests the app, builds its assets, and checks that it
|
|
85
|
+
serves. The workflow runs on Node 22, Node 24 and Bun, or on Bun alone for an app made with Bun.
|
|
86
|
+
|
|
87
|
+
Unless you pass `--no-install`, it then runs the first commands for you:
|
|
88
|
+
|
|
89
|
+
```sh
|
|
90
|
+
npm install
|
|
91
|
+
node oriel key:generate
|
|
92
|
+
node oriel migrate --force
|
|
93
|
+
npm run build
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
`key:generate` writes your app's secret key, `APP_KEY`, into `.env`, and `migrate` creates the SQLite database,
|
|
97
|
+
`database/database.sqlite`, with the tables a new app needs: users, password reset tokens, sessions, cache, cache locks,
|
|
98
|
+
jobs, job batches, failed jobs, personal access tokens and notifications. The API kit has no assets, so it has no
|
|
99
|
+
`build` step.
|
|
100
|
+
|
|
101
|
+
Last, unless you pass `--no-git`, it runs `git init` and makes a first commit, "Initial commit". It skips this when
|
|
102
|
+
git isn't installed or the folder is inside a git repository already. When the commit fails, for example because
|
|
103
|
+
git has no `user.name` and `user.email` yet, the repository stays, with your files added but not committed.
|
|
104
|
+
|
|
105
|
+
### Package managers
|
|
106
|
+
|
|
107
|
+
The installer works with npm, pnpm, Yarn (Yarn 1, and Yarn 2 or later such as Yarn 4) and Bun. The app it writes
|
|
108
|
+
follows the one you chose:
|
|
109
|
+
|
|
110
|
+
- **pnpm:** `package.json` names pnpm and its version in `packageManager`, and `pnpm-workspace.yaml` lists the
|
|
111
|
+
dependencies allowed to run install scripts (before pnpm 10.26, a `pnpm` field in `package.json` does).
|
|
112
|
+
- **Yarn 2 or later:** `package.json` names Yarn and its version in `packageManager`, and `.yarnrc.yml` sets
|
|
113
|
+
`nodeLinker: node-modules`, since the app's tools don't run under Plug'n'Play. Yarn 1 needs neither.
|
|
114
|
+
- **Bun:** the app runs `bun oriel` wherever another app runs `node oriel`: in its scripts, in the commands the
|
|
115
|
+
installer runs and prints, and in its CI workflow, which runs on Bun only.
|
|
116
|
+
|
|
117
|
+
The commands in the kit's `README.md` are rewritten for your package manager too.
|
|
118
|
+
|
|
119
|
+
## Running your application
|
|
120
|
+
|
|
121
|
+
When the installer is done, it prints the commands to run while you work:
|
|
122
|
+
|
|
123
|
+
```sh
|
|
124
|
+
cd blog
|
|
125
|
+
npm run dev
|
|
126
|
+
node oriel serve --watch
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Run the last two in two terminals. In one, `npm run dev` starts Vite, which serves your pages' scripts and styles and
|
|
130
|
+
updates them in the browser as you edit them. In the other, `node oriel serve --watch` starts your app's server.
|
|
131
|
+
|
|
132
|
+
Your app is at [http://localhost:8000](http://localhost:8000). `--watch` restarts the server when a file it runs
|
|
133
|
+
changes: a TypeScript, JavaScript or JSON file in `app`, `bootstrap`, `config`, `lang`, `resources/lang`,
|
|
134
|
+
`resources/views` or `routes`, `.env` or another `.env.*` file (not `.env.example`), `package.json` or
|
|
135
|
+
`tsconfig.json`. Your pages' own scripts and styles, in `resources/js` and `resources/css`, are Vite's, and don't
|
|
136
|
+
restart it.
|
|
137
|
+
|
|
138
|
+
> [!NOTE]
|
|
139
|
+
> Vite reloads the page when a view, a route file or a Live component changes, often before the server has
|
|
140
|
+
> restarted, and it doesn't reload the page again once the new server is up. After `serve --watch` prints
|
|
141
|
+
> "Restarting server...", reload the page once more to see your change.
|
|
142
|
+
|
|
143
|
+
`oriel serve` listens on `127.0.0.1`, on port 8000 or the one the `PORT` variable names. `--host` and `--port` choose
|
|
144
|
+
others:
|
|
145
|
+
|
|
146
|
+
```sh
|
|
147
|
+
node oriel serve --watch --host=0.0.0.0 --port=3000
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
The API kit has no Vite config and no `dev` script: `node oriel serve --watch` is all it needs. An app made with Bun
|
|
151
|
+
runs `bun run dev` and `bun oriel serve --watch`.
|
|
152
|
+
|
|
153
|
+
If you passed `--no-install`, the printed commands start with the steps the installer skipped: `npm install`,
|
|
154
|
+
`node oriel key:generate`, `node oriel migrate` and `npm run build`.
|
|
155
|
+
|
|
156
|
+
> [!TIP]
|
|
157
|
+
> Every command the framework knows is listed by `node oriel list`, and `node oriel help <command>` explains one.
|
|
158
|
+
|
|
159
|
+
## Initial configuration
|
|
160
|
+
|
|
161
|
+
Your app's settings are in the `config` folder, one file per part of the framework: `config/app.ts`,
|
|
162
|
+
`config/database.ts`, `config/session.ts` and so on. A new app runs as it is, but you may want to look at
|
|
163
|
+
`config/app.ts`, which holds the app's name, URL and locale.
|
|
164
|
+
|
|
165
|
+
### Environment based configuration
|
|
166
|
+
|
|
167
|
+
The values that differ between your machine and your servers, such as the database's password or the mail server,
|
|
168
|
+
come from the `.env` file at the root of your app, which the config files read with `env()`. The installer writes
|
|
169
|
+
`.env` for you, and git ignores it: each machine has its own, and its secrets stay out of your repository.
|
|
170
|
+
[Configuration](configuration.md) explains the file and `env()`.
|
|
171
|
+
|
|
172
|
+
### Databases and migrations
|
|
173
|
+
|
|
174
|
+
A new app uses SQLite, in `database/database.sqlite`, which the installer created and migrated. To use PostgreSQL or
|
|
175
|
+
MySQL instead, install its driver, `pg` or `mysql2`:
|
|
176
|
+
|
|
177
|
+
```sh
|
|
178
|
+
npm install pg
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
then set the `DB_*` variables in `.env`:
|
|
182
|
+
|
|
183
|
+
```dotenv
|
|
184
|
+
DB_CONNECTION=pgsql
|
|
185
|
+
DB_HOST=127.0.0.1
|
|
186
|
+
DB_PORT=5432
|
|
187
|
+
DB_DATABASE=blog
|
|
188
|
+
DB_USERNAME=root
|
|
189
|
+
DB_PASSWORD=
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
Create the database on your server, then run your migrations:
|
|
193
|
+
|
|
194
|
+
```sh
|
|
195
|
+
node oriel migrate
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
For MySQL, `DB_CONNECTION` is `mysql` and the port is usually 3306.
|
|
199
|
+
|
|
200
|
+
## Next steps
|
|
201
|
+
|
|
202
|
+
Read how an app is laid out in [Directory structure](structure.md), and how it reads its settings in
|
|
203
|
+
[Configuration](configuration.md). Your app's routes live in `routes/web.ts`: [Routing](routing.md) is the place to
|
|
204
|
+
start writing your own code. When a request has slow work to do, such as sending mail or resizing images, hand it to a
|
|
205
|
+
worker with [Queues](queues.md). When your app is ready to go live, [Deployment](deployment.md) covers building and
|
|
206
|
+
running it on a server.
|
|
207
|
+
|
|
208
|
+
To learn how an app runs, read [Request lifecycle](lifecycle.md), [Service container](container.md) and
|
|
209
|
+
[Facades](facades.md).
|