@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.
- package/NOTICE +7 -0
- package/README.md +10 -1
- package/TRADEMARKS.md +33 -0
- package/dist/{chunk-WNJP46B7.js → chunk-4MWG3B7G.js} +1785 -626
- package/dist/chunk-4MWG3B7G.js.map +1 -0
- package/dist/chunk-7ZQZYDOC.js +3253 -0
- package/dist/chunk-7ZQZYDOC.js.map +1 -0
- package/dist/{chunk-BSQKOLSK.js → chunk-GVNLZZTN.js} +1 -1
- package/dist/chunk-GVNLZZTN.js.map +1 -0
- package/dist/{chunk-A5XRLEWM.js → chunk-VOYW3R5J.js} +4 -4
- package/dist/chunk-VOYW3R5J.js.map +1 -0
- package/dist/contracts.d.ts +535 -17
- package/dist/contracts.js +1 -1
- package/dist/index.d-WQpfdIbG.d.ts +630 -0
- package/dist/index.d.ts +2202 -5952
- package/dist/index.js +4900 -944
- package/dist/index.js.map +1 -1
- package/dist/{jsx-runtime-WKTl3EEw.d.ts → jsx-runtime-6um4Mwfb.d.ts} +97 -51
- package/dist/jsx-runtime.d.ts +2 -2
- package/dist/jsx-runtime.js +3 -3
- package/dist/{testing-BErengYY.d.ts → testing-CKwfWJGn.d.ts} +313 -994
- package/dist/testing.d.ts +2 -2
- package/dist/testing.js +19 -5
- 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-A5XRLEWM.js.map +0 -1
- package/dist/chunk-BSQKOLSK.js.map +0 -1
- package/dist/chunk-PM5YTG57.js +0 -880
- package/dist/chunk-PM5YTG57.js.map +0 -1
- package/dist/chunk-WNJP46B7.js.map +0 -1
- package/dist/index.d-DOlTeG-8.d.ts +0 -99
package/docs/live.md
ADDED
|
@@ -0,0 +1,910 @@
|
|
|
1
|
+
# Live
|
|
2
|
+
|
|
3
|
+
Live components are classes on the server that render TSX and answer the browser's clicks and input. You write no
|
|
4
|
+
client code: Live sends each action to the server and swaps in the new HTML.
|
|
5
|
+
|
|
6
|
+
## Introduction
|
|
7
|
+
|
|
8
|
+
A Live component keeps its state in public fields and its actions in public methods. Its page carries the state,
|
|
9
|
+
signed; when the user clicks a button with `o:click="increment"`, the browser sends the state and the call, the
|
|
10
|
+
server runs `increment()` and renders the component again, and the browser patches the page with the new HTML.
|
|
11
|
+
|
|
12
|
+
Live follows Livewire 3: its components, directives, hooks and events work as Livewire's do, with `o:` in place of
|
|
13
|
+
`wire:`. The Live starter kit (`npm create @orieljs@latest blog -- --live`) is built with it, and is a good place to see
|
|
14
|
+
whole pages.
|
|
15
|
+
|
|
16
|
+
Live is part of `@orieljs/framework`. Import everything from there: `@orieljs/live` is published on its own too, but a
|
|
17
|
+
separate copy doesn't share the framework's per-request state.
|
|
18
|
+
|
|
19
|
+
### The client script
|
|
20
|
+
|
|
21
|
+
In the browser, `@orieljs/live-client` starts the components on the page. The Live kit starts it in
|
|
22
|
+
`resources/js/app.ts`, which the layout loads with `vite()`:
|
|
23
|
+
|
|
24
|
+
```ts title="resources/js/app.ts"
|
|
25
|
+
import { Live } from '@orieljs/live-client'
|
|
26
|
+
|
|
27
|
+
Live.start()
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Updates go to `POST /oriel/live/update`, a route in the `web` middleware group, so your sessions, the signed-in user
|
|
31
|
+
and CSRF protection apply to them. The client sends the CSRF token from the `XSRF-TOKEN` cookie.
|
|
32
|
+
|
|
33
|
+
## Components
|
|
34
|
+
|
|
35
|
+
### Creating components
|
|
36
|
+
|
|
37
|
+
`make:live` writes a component in `app/Live`, and `--test` a test for it in `tests/Feature/Live`:
|
|
38
|
+
|
|
39
|
+
```sh
|
|
40
|
+
node oriel make:live Counter
|
|
41
|
+
|
|
42
|
+
node oriel make:live Posts/CreatePost --test
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
A component is the default export of its file. Give it state and actions:
|
|
46
|
+
|
|
47
|
+
```tsx title="app/Live/Counter.tsx"
|
|
48
|
+
import { Component } from '@orieljs/framework'
|
|
49
|
+
|
|
50
|
+
export default class Counter extends Component {
|
|
51
|
+
count = 0
|
|
52
|
+
|
|
53
|
+
increment() {
|
|
54
|
+
this.count++
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
decrement() {
|
|
58
|
+
this.count--
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
render() {
|
|
62
|
+
return (
|
|
63
|
+
<div>
|
|
64
|
+
<h1>{this.count}</h1>
|
|
65
|
+
|
|
66
|
+
<button type="button" o:click="increment">+</button>
|
|
67
|
+
<button type="button" o:click="decrement">-</button>
|
|
68
|
+
</div>
|
|
69
|
+
)
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
`render()` returns the component's HTML, with one root element. It can be async, to read the database.
|
|
75
|
+
|
|
76
|
+
Oriel registers the components in `app/Live` at boot, by their path: `app/Live/Posts/CreatePost.tsx` is named
|
|
77
|
+
`posts.create-post`. `Live.component('counter', Counter)` registers one that lives elsewhere.
|
|
78
|
+
|
|
79
|
+
### Rendering components
|
|
80
|
+
|
|
81
|
+
`<Live>` renders a component in any view, or in another component:
|
|
82
|
+
|
|
83
|
+
```tsx
|
|
84
|
+
// @filename: app/Live/Counter.tsx
|
|
85
|
+
import { Component } from '@orieljs/framework'
|
|
86
|
+
export default class Counter extends Component {
|
|
87
|
+
count = 0
|
|
88
|
+
render() {
|
|
89
|
+
return <p />
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
// ---cut---
|
|
93
|
+
import Counter from './app/Live/Counter'
|
|
94
|
+
|
|
95
|
+
const page = (
|
|
96
|
+
<main>
|
|
97
|
+
<Live component={Counter} />
|
|
98
|
+
</main>
|
|
99
|
+
)
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
`component` also takes a registered name, `<Live component="counter" />`. Outside TSX, `await live(Counter)` gives the
|
|
103
|
+
component's first render: `(await live(Counter)).toHtml()` is its HTML.
|
|
104
|
+
|
|
105
|
+
#### Passing parameters
|
|
106
|
+
|
|
107
|
+
The other props are the component's mount parameters. Each one whose name is a field of the component sets that field,
|
|
108
|
+
and `mount()` receives them all:
|
|
109
|
+
|
|
110
|
+
```tsx
|
|
111
|
+
// @filename: app/Live/Counter.tsx
|
|
112
|
+
import { Component } from '@orieljs/framework'
|
|
113
|
+
export default class Counter extends Component {
|
|
114
|
+
count = 0
|
|
115
|
+
render() {
|
|
116
|
+
return <p />
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
// ---cut---
|
|
120
|
+
import Counter from './app/Live/Counter'
|
|
121
|
+
|
|
122
|
+
const page = <Live component={Counter} count={3} />
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### Full-page components
|
|
126
|
+
|
|
127
|
+
A component can be a whole page: give it to a route as its action.
|
|
128
|
+
|
|
129
|
+
```ts title="routes/web.ts"
|
|
130
|
+
// @filename: app/Live/Counter.tsx
|
|
131
|
+
import { Component } from '@orieljs/framework'
|
|
132
|
+
export default class Counter extends Component {
|
|
133
|
+
render() {
|
|
134
|
+
return <p />
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
// ---cut---
|
|
138
|
+
import { Route } from '@orieljs/framework'
|
|
139
|
+
import Counter from '../app/Live/Counter'
|
|
140
|
+
|
|
141
|
+
Route.get('/counter', Counter)
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
The page is the component inside a layout view, `components.layouts.app` unless `live.layout` names another. The
|
|
145
|
+
layout receives the component's HTML as `children`, and `static title` as `title`. `static layout` picks a layout for
|
|
146
|
+
one component:
|
|
147
|
+
|
|
148
|
+
```tsx title="app/Live/Auth/Login.tsx"
|
|
149
|
+
import { Component } from '@orieljs/framework'
|
|
150
|
+
|
|
151
|
+
export default class Login extends Component {
|
|
152
|
+
static override layout = 'components.layouts.auth'
|
|
153
|
+
static override title = 'Log in'
|
|
154
|
+
|
|
155
|
+
render() {
|
|
156
|
+
return <form>...</form>
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
The route's parameters are mount parameters. A parameter named after a model is [bound](routing.md#implicit-binding)
|
|
162
|
+
first, so `post` is the `Post`:
|
|
163
|
+
|
|
164
|
+
```tsx title="app/Live/ShowPost.tsx"
|
|
165
|
+
import { Component } from '@orieljs/framework'
|
|
166
|
+
import type { Post } from '../Models/Post'
|
|
167
|
+
|
|
168
|
+
export default class ShowPost extends Component {
|
|
169
|
+
post!: Post
|
|
170
|
+
|
|
171
|
+
render() {
|
|
172
|
+
return (
|
|
173
|
+
<article>
|
|
174
|
+
<h1>{this.post.title}</h1>
|
|
175
|
+
</article>
|
|
176
|
+
)
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
```ts title="routes/web.ts"
|
|
182
|
+
// @filename: app/Live/ShowPost.tsx
|
|
183
|
+
import { Component } from '@orieljs/framework'
|
|
184
|
+
export default class ShowPost extends Component {
|
|
185
|
+
render() {
|
|
186
|
+
return <p />
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
// ---cut---
|
|
190
|
+
import { Route } from '@orieljs/framework'
|
|
191
|
+
import ShowPost from '../app/Live/ShowPost'
|
|
192
|
+
|
|
193
|
+
Route.get('/posts/{post}', ShowPost).name('posts.show')
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
## Properties
|
|
197
|
+
|
|
198
|
+
A component's fields are its state. The page keeps them between requests, and `render()` reads them:
|
|
199
|
+
|
|
200
|
+
```tsx title="app/Live/TodoList.tsx"
|
|
201
|
+
import { Component } from '@orieljs/framework'
|
|
202
|
+
|
|
203
|
+
export default class TodoList extends Component {
|
|
204
|
+
todos: string[] = []
|
|
205
|
+
todo = ''
|
|
206
|
+
|
|
207
|
+
add() {
|
|
208
|
+
this.todos.push(this.todo)
|
|
209
|
+
this.reset('todo')
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
render() {
|
|
213
|
+
return (
|
|
214
|
+
<form o:submit="add">
|
|
215
|
+
<input type="text" o:model="todo" aria-label="Todo" />
|
|
216
|
+
<button type="submit">Add</button>
|
|
217
|
+
|
|
218
|
+
<ul>
|
|
219
|
+
{this.todos.map((todo) => (
|
|
220
|
+
<li>{todo}</li>
|
|
221
|
+
))}
|
|
222
|
+
</ul>
|
|
223
|
+
</form>
|
|
224
|
+
)
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
`reset('todo')` puts a field back to its first value, and `reset()` all of them.
|
|
230
|
+
|
|
231
|
+
A field holds a string, number, boolean, `null`, `Date`, an array or a plain object of those, a saved model, a
|
|
232
|
+
collection or an uploaded file. A model is kept as its class and key, and read from the database again on each update.
|
|
233
|
+
Anything else, such as an instance of your own class, throws when the component renders, naming the field.
|
|
234
|
+
|
|
235
|
+
> [!WARNING]
|
|
236
|
+
> The page carries the state as JSON. It is signed, so a changed value is refused, but anyone can read it: keep
|
|
237
|
+
> secrets out of fields. A `#private` field isn't state: it starts again on each request.
|
|
238
|
+
|
|
239
|
+
### Locked properties
|
|
240
|
+
|
|
241
|
+
The browser can set a field: an input with `o:model`, or a crafted request. `@Locked` stops it, for a field your code
|
|
242
|
+
sets and the browser must not change, such as an ID:
|
|
243
|
+
|
|
244
|
+
```tsx title="app/Live/EditPost.tsx"
|
|
245
|
+
import { Component, Locked } from '@orieljs/framework'
|
|
246
|
+
|
|
247
|
+
export default class EditPost extends Component {
|
|
248
|
+
@Locked
|
|
249
|
+
id = 0
|
|
250
|
+
|
|
251
|
+
render() {
|
|
252
|
+
return <div>{this.id}</div>
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
A field whose name starts with `_`, and a field holding a model or a collection, can't be set from the browser either.
|
|
258
|
+
To edit a model's attributes, copy them to fields and save them in an action.
|
|
259
|
+
|
|
260
|
+
### Computed properties
|
|
261
|
+
|
|
262
|
+
`@Computed` caches a getter for the rest of the request. It isn't state, and isn't sent to the browser:
|
|
263
|
+
|
|
264
|
+
```tsx title="app/Live/ShowUser.tsx"
|
|
265
|
+
import { Component, Computed } from '@orieljs/framework'
|
|
266
|
+
import { User } from '../Models/User'
|
|
267
|
+
|
|
268
|
+
export default class ShowUser extends Component {
|
|
269
|
+
userId = 1
|
|
270
|
+
|
|
271
|
+
@Computed
|
|
272
|
+
get user() {
|
|
273
|
+
return User.findOrFail(this.userId)
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
async render() {
|
|
277
|
+
const user = await this.user
|
|
278
|
+
|
|
279
|
+
return <h1>{user.name}</h1>
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
Decorators work in your app, under Bun and Node, and in your tests: a new app's `vitest.config.ts` has the
|
|
285
|
+
`decorators()` plugin that lowers them for Node. Without decorators, `static locked = ['id']`,
|
|
286
|
+
`static computed = ['user']` and `static listeners = { 'post-created': 'updatePostList' }` do the same.
|
|
287
|
+
|
|
288
|
+
## Actions
|
|
289
|
+
|
|
290
|
+
A public method is an action, which `o:click`, `o:submit` or any other `o:<event>` calls:
|
|
291
|
+
|
|
292
|
+
```tsx
|
|
293
|
+
<button type="button" o:click="save">Save</button>
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
Hooks, getters, `@Locked` methods, and methods whose name starts with `_` or `#` aren't actions.
|
|
297
|
+
|
|
298
|
+
> [!WARNING]
|
|
299
|
+
> Every action can be called by anyone who can see the page, with any arguments. Check that the user may do what the
|
|
300
|
+
> action does, as you would in a controller.
|
|
301
|
+
|
|
302
|
+
### Passing parameters
|
|
303
|
+
|
|
304
|
+
The call is written as the method's name, with its arguments in JSON (single quotes work too, as in `'Hello'`):
|
|
305
|
+
|
|
306
|
+
```tsx
|
|
307
|
+
declare const post: { id: number }
|
|
308
|
+
// ---cut---
|
|
309
|
+
<button type="button" o:click={`delete(${post.id})`}>Delete</button>
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
`this.$action()` writes it for you, typed against the method:
|
|
313
|
+
|
|
314
|
+
```tsx title="app/Live/Drafts.tsx"
|
|
315
|
+
import { Component } from '@orieljs/framework'
|
|
316
|
+
import { Post } from '../Models/Post'
|
|
317
|
+
|
|
318
|
+
export default class Drafts extends Component {
|
|
319
|
+
async publish(id: number) {
|
|
320
|
+
const post = await Post.findOrFail(id)
|
|
321
|
+
post.published_at = new Date()
|
|
322
|
+
await post.save()
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
async render() {
|
|
326
|
+
const drafts = await Post.whereNull('published_at').get()
|
|
327
|
+
|
|
328
|
+
return (
|
|
329
|
+
<ul>
|
|
330
|
+
{drafts.map((post) => (
|
|
331
|
+
<li>
|
|
332
|
+
{post.title}
|
|
333
|
+
<button type="button" o:click={this.$action(this.publish, post.id)}>
|
|
334
|
+
Publish
|
|
335
|
+
</button>
|
|
336
|
+
</li>
|
|
337
|
+
))}
|
|
338
|
+
</ul>
|
|
339
|
+
)
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
For post 5, this renders `o:click="publish(5)"`; `this.$action(this.publish, String(post.id))` is a type error, since
|
|
345
|
+
`publish()` takes a number. Pass IDs and plain values: a model arrives as its attributes, and a date as its ISO string.
|
|
346
|
+
Rendering throws when the method isn't one of the component's actions.
|
|
347
|
+
|
|
348
|
+
> [!NOTE]
|
|
349
|
+
> Livewire writes the call in Blade, `wire:click="publish({{ $post->id }})"`. `$action()` is Oriel's: TSX can check
|
|
350
|
+
> the method and its arguments.
|
|
351
|
+
|
|
352
|
+
In TSX, the `o:` attributes are type-checked: [Views](views.md#live-components) shows what TypeScript catches.
|
|
353
|
+
|
|
354
|
+
### Event modifiers
|
|
355
|
+
|
|
356
|
+
Modifiers follow the event, joined by `-`:
|
|
357
|
+
|
|
358
|
+
```tsx
|
|
359
|
+
const form = (
|
|
360
|
+
<form o:submit="save">
|
|
361
|
+
<input type="text" o:keydown-enter="search" aria-label="Search" />
|
|
362
|
+
|
|
363
|
+
<a href="#" o:click-prevent="toggle">
|
|
364
|
+
Toggle
|
|
365
|
+
</a>
|
|
366
|
+
</form>
|
|
367
|
+
)
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
| Modifier | What it does |
|
|
371
|
+
| --- | --- |
|
|
372
|
+
| `prevent` | Calls `preventDefault()` |
|
|
373
|
+
| `stop` | Calls `stopPropagation()` |
|
|
374
|
+
| `self` | Runs only when the event's target is the element itself |
|
|
375
|
+
| `once` | Runs once |
|
|
376
|
+
| `window`, `document` | Listens on the window, or the document |
|
|
377
|
+
| `passive`, `capture` | Accepted, and change nothing: Live listens once for each type of event |
|
|
378
|
+
| A key, such as `enter`, `escape`, `space`, `tab` or `down` | On a key event, runs only for that key |
|
|
379
|
+
|
|
380
|
+
`o:submit` always prevents the form's own submission.
|
|
381
|
+
|
|
382
|
+
> [!NOTE]
|
|
383
|
+
> In TSX, modifiers are joined by `-`, as in `o:click-prevent`; in plain HTML, Livewire's `.` works too
|
|
384
|
+
> (`o:click.prevent`). A DOM event whose name has a `-` in it can't be written as an `o:` action.
|
|
385
|
+
|
|
386
|
+
### Magic actions
|
|
387
|
+
|
|
388
|
+
| Action | What it does |
|
|
389
|
+
| --- | --- |
|
|
390
|
+
| `$refresh` | Renders the component again |
|
|
391
|
+
| `$set('title', 'Hello')` | Sets a field |
|
|
392
|
+
| `$toggle('open')` | Flips a boolean field |
|
|
393
|
+
| `$dispatch('post-created', { 'id': 1 })` | Dispatches an event, in the browser |
|
|
394
|
+
| `$commit` | Sends the pending `o:model` values |
|
|
395
|
+
|
|
396
|
+
```tsx
|
|
397
|
+
<button type="button" o:click="$toggle('open')">Menu</button>
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
### Confirming an action
|
|
401
|
+
|
|
402
|
+
`o:confirm` asks before the action runs:
|
|
403
|
+
|
|
404
|
+
```tsx
|
|
405
|
+
declare const tag: { id: number }
|
|
406
|
+
// ---cut---
|
|
407
|
+
<button type="button" o:click={`delete(${tag.id})`} o:confirm="Delete this tag?">
|
|
408
|
+
Delete
|
|
409
|
+
</button>
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
The browser's confirm dialog shows the message; an empty `o:confirm` asks "Are you sure?". `o:confirm-prompt` asks
|
|
413
|
+
for a typed answer, and runs the action only when it matches the text after the `|`:
|
|
414
|
+
|
|
415
|
+
```tsx
|
|
416
|
+
<button type="button" o:click="destroy" o:confirm-prompt="Type DELETE to confirm|DELETE">
|
|
417
|
+
Delete account
|
|
418
|
+
</button>
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
A dismissed dialog stops the event, so the actions around the element don't run either, and doesn't use up a `-once`
|
|
422
|
+
action. On a form, put `o:confirm` on the `<form>` with its `o:submit`. A button inside a form with its own `o:click`
|
|
423
|
+
needs `type="button"`, or the browser submits the form when the dialog is dismissed.
|
|
424
|
+
|
|
425
|
+
## Data binding
|
|
426
|
+
|
|
427
|
+
`o:model` binds an input to a field. The value is sent with the component's next request:
|
|
428
|
+
|
|
429
|
+
```tsx
|
|
430
|
+
const input = <input type="text" o:model="title" aria-label="Title" />
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
| Directive | Sends the value |
|
|
434
|
+
| --- | --- |
|
|
435
|
+
| `o:model` | With the next action |
|
|
436
|
+
| `o:model-live` | As you type, after 150 ms without a key press |
|
|
437
|
+
| `o:model-live-debounce-500ms` | As you type, after 500 ms |
|
|
438
|
+
| `o:model-blur` | When the input loses focus |
|
|
439
|
+
| `o:model-change` | When the input's value changes (Livewire's `.lazy`, which works too) |
|
|
440
|
+
| `o:model-number` | With the next action, as a number |
|
|
441
|
+
|
|
442
|
+
Checkboxes, radio buttons and selects are bound the same way. A checkbox gives `true` or `false`, or with a field that
|
|
443
|
+
is an array, the values of the checked boxes.
|
|
444
|
+
|
|
445
|
+
A dotted name binds a nested value: `o:model="form.title"`.
|
|
446
|
+
|
|
447
|
+
## Lifecycle hooks
|
|
448
|
+
|
|
449
|
+
| Hook | Runs |
|
|
450
|
+
| --- | --- |
|
|
451
|
+
| `boot()` | On every request, first |
|
|
452
|
+
| `mount(params)` | Once, when the component is first rendered |
|
|
453
|
+
| `hydrate()` | On every update, before anything else changes |
|
|
454
|
+
| `updating(name, value)`, `updated(name, value)` | Before and after the browser sets a field |
|
|
455
|
+
| `updatingTitle(value, key)`, `updatedTitle(value, key)` | Before and after the browser sets `title` |
|
|
456
|
+
| `rendering()`, `rendered(html)` | Before and after `render()` |
|
|
457
|
+
| `dehydrate()` | On every request, last |
|
|
458
|
+
|
|
459
|
+
A hook can be async. `updatedTitle()` is named after the field in StudlyCase, and on a nested name, such as
|
|
460
|
+
`form.title`, `updatedForm()` runs with `key` set to `'title'`.
|
|
461
|
+
|
|
462
|
+
```tsx title="app/Live/Search.tsx"
|
|
463
|
+
import { Component } from '@orieljs/framework'
|
|
464
|
+
|
|
465
|
+
export default class Search extends Component {
|
|
466
|
+
search = ''
|
|
467
|
+
|
|
468
|
+
updatedSearch(value: string) {
|
|
469
|
+
this.search = value.trim()
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
render() {
|
|
473
|
+
return <input type="search" o:model-live="search" aria-label="Search" />
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
```
|
|
477
|
+
|
|
478
|
+
> [!NOTE]
|
|
479
|
+
> Livewire's `booted()` and `exception()` hooks aren't called in Oriel.
|
|
480
|
+
|
|
481
|
+
## Validation
|
|
482
|
+
|
|
483
|
+
`this.validate()` checks fields against rules, and returns the valid values, typed from the rules. A failed
|
|
484
|
+
validation stops the action, and the component renders with its errors in `this.errors`:
|
|
485
|
+
|
|
486
|
+
```tsx title="app/Live/CreatePost.tsx"
|
|
487
|
+
import { Component, request, Str } from '@orieljs/framework'
|
|
488
|
+
import type { User } from '../Models/User'
|
|
489
|
+
|
|
490
|
+
export default class CreatePost extends Component {
|
|
491
|
+
title = ''
|
|
492
|
+
body = ''
|
|
493
|
+
|
|
494
|
+
async save() {
|
|
495
|
+
const data = await this.validate({
|
|
496
|
+
title: 'required|string|max:255',
|
|
497
|
+
body: 'required|string',
|
|
498
|
+
})
|
|
499
|
+
|
|
500
|
+
const user = (await request().user<User>())!
|
|
501
|
+
|
|
502
|
+
await user.posts().create({ ...data, slug: Str.slug(data.title) })
|
|
503
|
+
|
|
504
|
+
this.redirect('/posts')
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
render() {
|
|
508
|
+
return (
|
|
509
|
+
<form o:submit="save">
|
|
510
|
+
<input type="text" o:model="title" aria-label="Title" />
|
|
511
|
+
{this.errors.has('title') && <p class="error">{this.errors.first('title')}</p>}
|
|
512
|
+
|
|
513
|
+
<textarea o:model="body" aria-label="Body" />
|
|
514
|
+
{this.errors.has('body') && <p class="error">{this.errors.first('body')}</p>}
|
|
515
|
+
|
|
516
|
+
<button type="submit">Save</button>
|
|
517
|
+
</form>
|
|
518
|
+
)
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
```
|
|
522
|
+
|
|
523
|
+
`addError(key, message)` adds an error of your own, and `resetErrorBag()` clears them, or those of the keys you give.
|
|
524
|
+
[Validation](validation.md) lists the rules.
|
|
525
|
+
|
|
526
|
+
A component's fields aren't a request's input, so an empty one is `''`, not `null`: `nullable|integer` passes an
|
|
527
|
+
empty `age` and returns `''`. Start such a field at `null` (`age: number | null = null`) when it is optional.
|
|
528
|
+
|
|
529
|
+
## Events
|
|
530
|
+
|
|
531
|
+
`this.dispatch()` sends an event. Any component on the page with an `@On` method for it runs the method, with the
|
|
532
|
+
event's parameters:
|
|
533
|
+
|
|
534
|
+
```tsx title="app/Live/CreatePost.tsx"
|
|
535
|
+
import { Component } from '@orieljs/framework'
|
|
536
|
+
|
|
537
|
+
export default class CreatePost extends Component {
|
|
538
|
+
title = ''
|
|
539
|
+
|
|
540
|
+
save() {
|
|
541
|
+
// ...
|
|
542
|
+
|
|
543
|
+
this.dispatch('post-created', { title: this.title })
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
render() {
|
|
547
|
+
return <form o:submit="save">...</form>
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
```
|
|
551
|
+
|
|
552
|
+
```tsx title="app/Live/Dashboard.tsx"
|
|
553
|
+
import { Component, On } from '@orieljs/framework'
|
|
554
|
+
|
|
555
|
+
export default class Dashboard extends Component {
|
|
556
|
+
status = ''
|
|
557
|
+
|
|
558
|
+
@On('post-created')
|
|
559
|
+
updatePostList({ title }: Record<string, unknown>) {
|
|
560
|
+
this.status = `Created ${String(title)}`
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
render() {
|
|
564
|
+
return <p>{this.status}</p>
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
```
|
|
568
|
+
|
|
569
|
+
`dispatch(...).to(Dashboard)` sends it only to the components of that class, and `dispatch(...).self()` only to the
|
|
570
|
+
component that sent it. A component listening for an event re-renders after its method runs.
|
|
571
|
+
|
|
572
|
+
Events are browser events on `window`, so your scripts can send and receive them:
|
|
573
|
+
|
|
574
|
+
```ts
|
|
575
|
+
import { Live } from '@orieljs/live-client'
|
|
576
|
+
|
|
577
|
+
Live.dispatch('post-created', { title: 'Hello' })
|
|
578
|
+
|
|
579
|
+
Live.on('post-created', ({ title }) => {
|
|
580
|
+
console.log(title)
|
|
581
|
+
})
|
|
582
|
+
```
|
|
583
|
+
|
|
584
|
+
## Loading states
|
|
585
|
+
|
|
586
|
+
`o:loading` shows an element while the component's request runs:
|
|
587
|
+
|
|
588
|
+
```tsx
|
|
589
|
+
const form = (
|
|
590
|
+
<form o:submit="save">
|
|
591
|
+
<button type="submit">Save</button>
|
|
592
|
+
|
|
593
|
+
<span o:loading>Saving...</span>
|
|
594
|
+
</form>
|
|
595
|
+
)
|
|
596
|
+
```
|
|
597
|
+
|
|
598
|
+
| Directive | While the request runs |
|
|
599
|
+
| --- | --- |
|
|
600
|
+
| `o:loading` | Shows the element, which is hidden otherwise |
|
|
601
|
+
| `o:loading-remove` | Hides the element |
|
|
602
|
+
| `o:loading-class="opacity-50"` | Adds the classes (`o:loading-class-remove` removes them) |
|
|
603
|
+
| `o:loading-attr="disabled"` | Sets the attribute |
|
|
604
|
+
| `o:loading-delay` | Waits 200 ms first, so a quick request shows nothing |
|
|
605
|
+
|
|
606
|
+
`o:target` limits it to some actions or fields: `o:target="save"`, or `o:target="save,title"`.
|
|
607
|
+
|
|
608
|
+
### Polling
|
|
609
|
+
|
|
610
|
+
`o:poll` re-renders the component every 2.5 seconds, or calls a method:
|
|
611
|
+
|
|
612
|
+
```tsx
|
|
613
|
+
const stats = (
|
|
614
|
+
<div o:poll-15s="refreshStats">
|
|
615
|
+
<p>Visitors today</p>
|
|
616
|
+
</div>
|
|
617
|
+
)
|
|
618
|
+
```
|
|
619
|
+
|
|
620
|
+
`o:poll-500ms` and `o:poll-15s` change the interval. Polling pauses while the browser tab is in the background, unless
|
|
621
|
+
you add `-keep-alive`, and with `-visible` it runs only while the element is in view.
|
|
622
|
+
|
|
623
|
+
### Offline state
|
|
624
|
+
|
|
625
|
+
`o:offline` shows an element while the browser is offline, and takes the same `-remove`, `-class` and `-attr` forms as
|
|
626
|
+
`o:loading`:
|
|
627
|
+
|
|
628
|
+
```tsx
|
|
629
|
+
<p o:offline>You are offline.</p>
|
|
630
|
+
```
|
|
631
|
+
|
|
632
|
+
## Nesting components
|
|
633
|
+
|
|
634
|
+
A component can render other components with `<Live>`. Give each child in a list an `o:key`, so Live knows which is
|
|
635
|
+
which when the list changes:
|
|
636
|
+
|
|
637
|
+
```tsx
|
|
638
|
+
// @filename: app/Live/TodoItem.tsx
|
|
639
|
+
import { Component } from '@orieljs/framework'
|
|
640
|
+
export default class TodoItem extends Component {
|
|
641
|
+
render() {
|
|
642
|
+
return <li />
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
// ---cut---
|
|
646
|
+
import TodoItem from './app/Live/TodoItem'
|
|
647
|
+
|
|
648
|
+
declare const todos: { id: number; title: string }[]
|
|
649
|
+
|
|
650
|
+
const list = (
|
|
651
|
+
<ul>
|
|
652
|
+
{todos.map((todo) => (
|
|
653
|
+
<Live component={TodoItem} o:key={todo.id} todo={todo} />
|
|
654
|
+
))}
|
|
655
|
+
</ul>
|
|
656
|
+
)
|
|
657
|
+
```
|
|
658
|
+
|
|
659
|
+
A child keeps its own state. When the parent renders again, a child that is already on the page isn't mounted again:
|
|
660
|
+
send it new values with an event.
|
|
661
|
+
|
|
662
|
+
## Lazy loading
|
|
663
|
+
|
|
664
|
+
A slow component can render a placeholder first, and load once it scrolls into view:
|
|
665
|
+
|
|
666
|
+
```tsx title="app/Live/PostCount.tsx"
|
|
667
|
+
import { Component } from '@orieljs/framework'
|
|
668
|
+
import { Post } from '../Models/Post'
|
|
669
|
+
|
|
670
|
+
export default class PostCount extends Component {
|
|
671
|
+
static override lazy = true
|
|
672
|
+
count = 0
|
|
673
|
+
|
|
674
|
+
async mount() {
|
|
675
|
+
this.count = await Post.count()
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
placeholder() {
|
|
679
|
+
return <p>Counting posts...</p>
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
render() {
|
|
683
|
+
return <p>{this.count} posts</p>
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
```
|
|
687
|
+
|
|
688
|
+
`lazy` on one `<Live>` makes that one lazy, `lazy="on-load"` loads it as soon as the page has loaded, and
|
|
689
|
+
`lazy={false}` turns `static lazy` off. Without `placeholder()`, the placeholder is the `live.lazy_placeholder` view,
|
|
690
|
+
or an empty `<div>`. Give the placeholder the same root element as `render()`.
|
|
691
|
+
|
|
692
|
+
## Pagination
|
|
693
|
+
|
|
694
|
+
`WithPagination` gives a component a page, which `paginate()` uses:
|
|
695
|
+
|
|
696
|
+
```tsx title="app/Live/Posts.tsx"
|
|
697
|
+
import { Component, Pagination, WithPagination } from '@orieljs/framework'
|
|
698
|
+
import { Post } from '../Models/Post'
|
|
699
|
+
|
|
700
|
+
export default class Posts extends WithPagination(Component) {
|
|
701
|
+
search = ''
|
|
702
|
+
|
|
703
|
+
async updatedSearch() {
|
|
704
|
+
await this.resetPage()
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
async render() {
|
|
708
|
+
const posts = await Post.whereLike('title', `%${this.search}%`).latest().paginate(10)
|
|
709
|
+
|
|
710
|
+
return (
|
|
711
|
+
<div>
|
|
712
|
+
<input type="search" o:model-live="search" aria-label="Search" />
|
|
713
|
+
|
|
714
|
+
{posts.items().map((post) => (
|
|
715
|
+
<p>{post.title}</p>
|
|
716
|
+
))}
|
|
717
|
+
|
|
718
|
+
<Pagination paginator={posts} />
|
|
719
|
+
</div>
|
|
720
|
+
)
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
```
|
|
724
|
+
|
|
725
|
+
`<Pagination>` renders the page buttons. The actions `nextPage`, `previousPage`, `gotoPage(n)`, `setPage(n)` and
|
|
726
|
+
`resetPage` move the page; they are async, so `await` them in your own methods. The page starts at 1.
|
|
727
|
+
|
|
728
|
+
> [!NOTE]
|
|
729
|
+
> Livewire keeps the page in the URL's query string. Oriel keeps it in the component: a reload starts at page 1.
|
|
730
|
+
|
|
731
|
+
## File uploads
|
|
732
|
+
|
|
733
|
+
In a `WithFileUploads` component, `o:model` on a file input uploads the file as soon as it is picked, and sets the
|
|
734
|
+
field to a `TemporaryUploadedFile`:
|
|
735
|
+
|
|
736
|
+
```tsx title="app/Live/ProfilePhoto.tsx"
|
|
737
|
+
import { Component, TemporaryUploadedFile, WithFileUploads } from '@orieljs/framework'
|
|
738
|
+
|
|
739
|
+
export default class ProfilePhoto extends WithFileUploads(Component) {
|
|
740
|
+
photo: TemporaryUploadedFile | null = null
|
|
741
|
+
|
|
742
|
+
async save() {
|
|
743
|
+
const { photo } = await this.validate({ photo: 'required|image|max:1024' })
|
|
744
|
+
|
|
745
|
+
await photo.store('photos')
|
|
746
|
+
|
|
747
|
+
this.reset('photo')
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
render() {
|
|
751
|
+
return (
|
|
752
|
+
<form o:submit="save">
|
|
753
|
+
<input type="file" o:model="photo" aria-label="Photo" />
|
|
754
|
+
|
|
755
|
+
{this.photo?.isPreviewable() && <img src={this.photo.temporaryUrl()} alt="Preview" />}
|
|
756
|
+
|
|
757
|
+
<button type="submit">Save</button>
|
|
758
|
+
</form>
|
|
759
|
+
)
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
```
|
|
763
|
+
|
|
764
|
+
With `multiple`, the field is an array of files. A temporary file has the methods of an [uploaded
|
|
765
|
+
file](requests.md#files), such as `store()` and `storeAs()`, and `temporaryUrl()`, a signed link that previews an image,
|
|
766
|
+
audio or video.
|
|
767
|
+
|
|
768
|
+
Temporary files go in `live-tmp` on your default disk, and [Storage](storage.md#live-uploads) explains how they are
|
|
769
|
+
cleaned up. `o:loading` shows while a file uploads, and these browser events follow it, on the input:
|
|
770
|
+
`live-upload-start`, `live-upload-progress` (with `detail.progress`, from 0 to 100), `live-upload-finish`,
|
|
771
|
+
`live-upload-error` and `live-upload-cancel`. Their names have a `-`, so listen for them with `addEventListener()`
|
|
772
|
+
rather than an `o:` directive.
|
|
773
|
+
|
|
774
|
+
> [!WARNING]
|
|
775
|
+
> Every upload must also pass `live.temporary_file_upload.rules`, which allow 12 MB. A request body larger than
|
|
776
|
+
> `http.max_body_size`, 8 MB, gets a 413, and the field gets "The photo failed to upload.". To accept files up to the
|
|
777
|
+
> rule's size, raise `max_body_size` in `config/http.ts`.
|
|
778
|
+
|
|
779
|
+
## Navigating
|
|
780
|
+
|
|
781
|
+
`o:navigate` on a link shows the next page without a full reload. Live fetches the page, merges its `<head>`, swaps
|
|
782
|
+
the `<body>` and starts its components:
|
|
783
|
+
|
|
784
|
+
```tsx
|
|
785
|
+
const nav = (
|
|
786
|
+
<nav>
|
|
787
|
+
<a href="/dashboard" o:navigate>
|
|
788
|
+
Dashboard
|
|
789
|
+
</a>
|
|
790
|
+
<a href="/posts" o:navigate-hover>
|
|
791
|
+
Posts
|
|
792
|
+
</a>
|
|
793
|
+
</nav>
|
|
794
|
+
)
|
|
795
|
+
```
|
|
796
|
+
|
|
797
|
+
The fetch starts on `mousedown`, or with `-hover`, when the pointer rests on the link. The new page's scripts run,
|
|
798
|
+
except a `<head>` script the page already has, and a `<body>` script marked `data-navigate-once` that an earlier page
|
|
799
|
+
ran. Unlike a full load, an inline script runs as soon as it is inserted, without waiting for an external script above
|
|
800
|
+
it; write the URLs in `<head>` absolute or from the root (`/build/...`).
|
|
801
|
+
|
|
802
|
+
Some pages load in full instead: one that a redirect takes to another origin, and one whose tracked assets changed.
|
|
803
|
+
The tags `vite()` writes carry `data-navigate-track`, so after a deploy, the first page with the new build's files
|
|
804
|
+
loads in full, rather than run the new bundle beside the old one.
|
|
805
|
+
|
|
806
|
+
> [!WARNING]
|
|
807
|
+
> Under a Content Security Policy with a nonce per request, as `vite().useCspNonce()` makes, the scripts of a page
|
|
808
|
+
> reached with `o:navigate` carry that page's nonce, which the current page's policy doesn't allow, so they are
|
|
809
|
+
> blocked. In such an app, use `o:navigate` only for links to pages without scripts of their own.
|
|
810
|
+
|
|
811
|
+
### Redirects
|
|
812
|
+
|
|
813
|
+
An action redirects with `this.redirect(url)`, or `this.redirectRoute(name, params)` for a named route. Add
|
|
814
|
+
`{ navigate: true }` to go there as `o:navigate` does:
|
|
815
|
+
|
|
816
|
+
```ts
|
|
817
|
+
declare const component: Component
|
|
818
|
+
// ---cut---
|
|
819
|
+
component.redirect('/posts', { navigate: true })
|
|
820
|
+
```
|
|
821
|
+
|
|
822
|
+
A message an action flashes, `session().flash('status', 'Saved.')`, is in that update's response only, unless the
|
|
823
|
+
action redirects: then it is there on the next page.
|
|
824
|
+
|
|
825
|
+
## Middleware on updates
|
|
826
|
+
|
|
827
|
+
Each update runs the `auth`, `verified`, `guest` and `can` middleware of the route that rendered the page again, with
|
|
828
|
+
that route's parameters, before the component's actions. A user who signed out in another tab gets a 401, and one who
|
|
829
|
+
lost an ability a 403. The list is `live.persistent_middleware`.
|
|
830
|
+
|
|
831
|
+
## Announcing status messages
|
|
832
|
+
|
|
833
|
+
Screen readers announce a status region, `role="status"`, when its text changes. When an action brings the same
|
|
834
|
+
message again, such as "Saved" after a second save, nothing changes and nothing is announced. `o:announce` makes Live
|
|
835
|
+
take the message out and put it back, so it is announced again:
|
|
836
|
+
|
|
837
|
+
```tsx title="app/Live/Settings.tsx"
|
|
838
|
+
import { Component } from '@orieljs/framework'
|
|
839
|
+
|
|
840
|
+
export default class Settings extends Component {
|
|
841
|
+
name = ''
|
|
842
|
+
#saved = false
|
|
843
|
+
|
|
844
|
+
save() {
|
|
845
|
+
this.#saved = true
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
render() {
|
|
849
|
+
return (
|
|
850
|
+
<form o:submit="save">
|
|
851
|
+
<input type="text" o:model="name" aria-label="Name" />
|
|
852
|
+
<button type="submit">Save</button>
|
|
853
|
+
|
|
854
|
+
<p role="status" o:announce>
|
|
855
|
+
{this.#saved ? 'Saved' : ''}
|
|
856
|
+
</p>
|
|
857
|
+
</form>
|
|
858
|
+
)
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
```
|
|
862
|
+
|
|
863
|
+
Only actions do this: an update that only sends `o:model` values, refreshes or delivers an event leaves the region
|
|
864
|
+
alone. Keep a status for one response, in a `#private` field or a flashed message, so the next update clears it. Keep
|
|
865
|
+
the region to text: its children are taken out for 100 ms, so a button in it would lose focus.
|
|
866
|
+
|
|
867
|
+
## The JavaScript API
|
|
868
|
+
|
|
869
|
+
`window.Live` is the client, once `Live.start()` has run:
|
|
870
|
+
|
|
871
|
+
| Method | What it does |
|
|
872
|
+
| --- | --- |
|
|
873
|
+
| `Live.find(id)`, `Live.first()`, `Live.all()`, `Live.getByName(name)` | Finds components on the page |
|
|
874
|
+
| `Live.dispatch(name, params)`, `Live.dispatchTo(component, name, params)` | Dispatches an event |
|
|
875
|
+
| `Live.on(name, callback)` | Listens for an event; returns the function that stops listening |
|
|
876
|
+
| `Live.navigate(url)` | Goes to a page as `o:navigate` does |
|
|
877
|
+
| `Live.settled()` | Resolves once no request or upload is running |
|
|
878
|
+
|
|
879
|
+
A component has `get(name)`, `set(name, value)`, which sends the value with the next request, `call(method, params)`,
|
|
880
|
+
with the arguments in an array, `call('publish', [5])`, which resolves with the action's return value, and `commit()`.
|
|
881
|
+
|
|
882
|
+
## Configuration
|
|
883
|
+
|
|
884
|
+
Live's settings are under `live`. A new app has no `config/live.ts`; add one to change them:
|
|
885
|
+
|
|
886
|
+
| Key | Default | What it is |
|
|
887
|
+
| --- | --- | --- |
|
|
888
|
+
| `class_path` | `app/Live` | The folder whose components are registered at boot |
|
|
889
|
+
| `layout` | `components.layouts.app` | The layout of full-page components |
|
|
890
|
+
| `lazy_placeholder` | none | The view lazy components show before they load |
|
|
891
|
+
| `persistent_middleware` | `auth`, `verified`, `guest`, `can` | The page route's middleware each update runs again |
|
|
892
|
+
| `temporary_file_upload.disk` | Your default disk | Where uploads wait until they are stored |
|
|
893
|
+
| `temporary_file_upload.directory` | `live-tmp` | The folder on that disk |
|
|
894
|
+
| `temporary_file_upload.rules` | `required`, `file`, `max:12288` | The rules every upload must pass |
|
|
895
|
+
| `temporary_file_upload.middleware` | `throttle:60,1` | The upload route's middleware |
|
|
896
|
+
| `temporary_file_upload.max_upload_time` | `5` | Minutes a signed upload URL stays valid |
|
|
897
|
+
| `temporary_file_upload.preview_mimes` | Images, audio and video | The types `temporaryUrl()` previews |
|
|
898
|
+
| `temporary_file_upload.cleanup` | `true` | Deletes temporary files older than a day |
|
|
899
|
+
|
|
900
|
+
## Testing
|
|
901
|
+
|
|
902
|
+
`Live.test(Counter)` mounts a component in a test, calls its actions and checks its HTML and state. [Live
|
|
903
|
+
tests](live-tests.md) covers it.
|
|
904
|
+
|
|
905
|
+
## Differences from Livewire
|
|
906
|
+
|
|
907
|
+
> [!NOTE]
|
|
908
|
+
> Live has no Alpine.js, `wire:stream`, `#[Url]` query-string binding, `#[Reactive]`, `#[Modelable]`, form objects or
|
|
909
|
+
> JavaScript hooks (`Livewire.hook()`). `temporaryUrl()` previews don't answer range requests, which Safari needs to
|
|
910
|
+
> play a `<video>`.
|