@orieljs/live 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 +188 -14
- package/TRADEMARKS.md +33 -0
- package/dist/index.d.ts +178 -12
- package/dist/index.js +891 -317
- package/dist/index.js.map +1 -1
- package/package.json +7 -6
package/NOTICE
CHANGED
|
@@ -7,3 +7,10 @@ This product includes software developed by the Oriel project
|
|
|
7
7
|
Oriel is licensed under the Apache License, Version 2.0 (see LICENSE).
|
|
8
8
|
The Oriel name and logo are not covered by that license; see TRADEMARKS.md
|
|
9
9
|
(https://github.com/orieljs/framework/blob/main/TRADEMARKS.md).
|
|
10
|
+
|
|
11
|
+
@orieljs/support (and @orieljs/framework, which bundles it) includes transliteration data from
|
|
12
|
+
voku/portable-ascii (https://github.com/voku/portable-ascii), Copyright (C) 2019 Lars Moelleken, MIT License.
|
|
13
|
+
|
|
14
|
+
@orieljs/notifications (and @orieljs/framework, which bundles it) includes the HTML and CSS of
|
|
15
|
+
Laravel's default notification mail (laravel/framework, https://github.com/laravel/framework),
|
|
16
|
+
Copyright (c) Taylor Otwell, MIT License.
|
package/README.md
CHANGED
|
@@ -6,8 +6,7 @@ Server-side Live components for Oriel: Livewire-style components written in TSX,
|
|
|
6
6
|
npm install @orieljs/framework
|
|
7
7
|
```
|
|
8
8
|
|
|
9
|
-
```tsx
|
|
10
|
-
// app/Live/Counter.tsx
|
|
9
|
+
```tsx title="app/Live/Counter.tsx"
|
|
11
10
|
import { Component } from '@orieljs/framework'
|
|
12
11
|
|
|
13
12
|
export default class Counter extends Component {
|
|
@@ -23,27 +22,90 @@ export default class Counter extends Component {
|
|
|
23
22
|
}
|
|
24
23
|
```
|
|
25
24
|
|
|
26
|
-
```ts
|
|
27
|
-
//
|
|
25
|
+
```ts title="routes/web.ts"
|
|
26
|
+
// @filename: app/Live/Counter.tsx
|
|
27
|
+
import { Component } from '@orieljs/framework'
|
|
28
|
+
export default class Counter extends Component { render() { return <p /> } }
|
|
29
|
+
// ---cut---
|
|
28
30
|
import { Route } from '@orieljs/framework'
|
|
29
31
|
import Counter from '../app/Live/Counter'
|
|
30
32
|
|
|
31
33
|
Route.get('/counter', Counter)
|
|
32
34
|
```
|
|
33
35
|
|
|
34
|
-
Embed a component in any view with `<Live component={Counter} />`. In the browser, components are driven by `@orieljs/live-client`.
|
|
36
|
+
Embed a component in any view with `<Live component={Counter} />`. In the browser, components are driven by `@orieljs/live-client`. Public fields are a component's state and its public methods are actions. `mount()` and `render()` may be async (`const user = await request().user<User>()`), and `await this.validate(rules)` returns the validated data, typed from the rules.
|
|
35
37
|
|
|
36
|
-
##
|
|
38
|
+
## Typed actions
|
|
39
|
+
|
|
40
|
+
`this.$action(method, ...arguments)` writes an action for an `o:` directive, typed against the method:
|
|
41
|
+
|
|
42
|
+
```tsx title="app/Live/Drafts.tsx"
|
|
43
|
+
import { Component } from '@orieljs/framework'
|
|
44
|
+
import { Post } from '../Models/Post'
|
|
45
|
+
|
|
46
|
+
export default class Drafts extends Component {
|
|
47
|
+
async publish(id: number) {
|
|
48
|
+
const post = await Post.findOrFail(id)
|
|
49
|
+
post.published_at = new Date()
|
|
50
|
+
await post.save()
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async render() {
|
|
54
|
+
const drafts = await Post.whereNull('published_at').get()
|
|
55
|
+
return (
|
|
56
|
+
<ul>
|
|
57
|
+
{drafts.map((post) => (
|
|
58
|
+
<li>
|
|
59
|
+
{post.title}
|
|
60
|
+
<button type="button" o:click={this.$action(this.publish, post.id)}>Publish</button>
|
|
61
|
+
</li>
|
|
62
|
+
))}
|
|
63
|
+
</ul>
|
|
64
|
+
)
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
For post 5 this renders `o:click="publish(5)"`, and since `publish(id: number)` takes a number,
|
|
70
|
+
`this.$action(this.publish, String(post.id))` is a type error. The arguments are written as JSON, so pass ids and
|
|
71
|
+
plain values (strings, numbers, booleans, `null`, arrays and plain objects): a model arrives as its attributes and a
|
|
72
|
+
date as its ISO string, so pass `post.id`, not `post`. The action contains double quotes, so use it as a TSX attribute
|
|
73
|
+
value, which escapes them, not inside an HTML string you write by hand. Rendering throws when the method isn't one of
|
|
74
|
+
the component's actions (a hook, or a `#private` or `@Locked` method). The types check what your pages send; a crafted
|
|
75
|
+
request can still send anything, so an action checks what it is given, as a controller does.
|
|
76
|
+
|
|
77
|
+
## Confirming an action
|
|
78
|
+
|
|
79
|
+
`o:confirm` asks before an action runs, as Livewire's `wire:confirm`:
|
|
37
80
|
|
|
38
81
|
```tsx
|
|
82
|
+
declare const tag: { id: number }
|
|
83
|
+
// ---cut---
|
|
84
|
+
<button type="button" class="btn btn-danger" o:click={`delete(${tag.id})`} o:confirm="Delete this tag?">
|
|
85
|
+
Delete
|
|
86
|
+
</button>
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
The browser's confirm dialog shows the message (an empty `o:confirm` asks "Are you sure?", and `\n` is a line break),
|
|
90
|
+
and the action runs only when it is accepted. `o:confirm-prompt="Type DELETE to confirm|DELETE"` (`o:confirm.prompt`
|
|
91
|
+
in plain HTML) asks with a prompt instead, and runs the action only when the answer is exactly the text after the `|`.
|
|
92
|
+
It works on any element with an `o:` action: `o:click`, `o:submit` on a form, `o:keydown-enter`, and the others. A
|
|
93
|
+
dismissed dialog stops the event, so the actions of the elements around it don't run either. With `-once`, a dismissed
|
|
94
|
+
dialog doesn't use the action up. `o:confirm` guards the actions of the element it sits on: on a form, put it on the
|
|
95
|
+
`<form>` with the `o:submit`. A button inside a form with its own `o:click` needs `type="button"` or `-prevent`
|
|
96
|
+
(`o:click-prevent`), or the browser still submits the form when the dialog is dismissed.
|
|
97
|
+
|
|
98
|
+
## Pagination
|
|
99
|
+
|
|
100
|
+
```tsx title="app/Live/Posts.tsx"
|
|
39
101
|
import { Component, Pagination, WithPagination } from '@orieljs/framework'
|
|
40
102
|
import { Post } from '../Models/Post'
|
|
41
103
|
|
|
42
104
|
export default class Posts extends WithPagination(Component) {
|
|
43
105
|
search = ''
|
|
44
106
|
|
|
45
|
-
updatedSearch() {
|
|
46
|
-
this.resetPage()
|
|
107
|
+
async updatedSearch() {
|
|
108
|
+
await this.resetPage()
|
|
47
109
|
}
|
|
48
110
|
|
|
49
111
|
async render() {
|
|
@@ -61,8 +123,81 @@ export default class Posts extends WithPagination(Component) {
|
|
|
61
123
|
|
|
62
124
|
`paginate()` in the component (in `render()`, in `mount()`, in actions and the update hooks, or in a `@Computed`
|
|
63
125
|
getter any of them reads) shows the component's `page`, and its links are built on the page's URL. The actions
|
|
64
|
-
`nextPage`, `previousPage`, `gotoPage(n)`, `setPage(n)` and `resetPage` move it
|
|
65
|
-
buttons with the kits' `btn` classes. The page starts at 1 and isn't kept
|
|
126
|
+
`nextPage`, `previousPage`, `gotoPage(n)`, `setPage(n)` and `resetPage` move it (they are async, so await them in your
|
|
127
|
+
own methods); `<Pagination>` renders them as buttons with the kits' `btn` classes. The page starts at 1 and isn't kept
|
|
128
|
+
in the address bar.
|
|
129
|
+
|
|
130
|
+
## File uploads
|
|
131
|
+
|
|
132
|
+
```tsx title="app/Live/ProfilePhoto.tsx"
|
|
133
|
+
import { Component, TemporaryUploadedFile, WithFileUploads } from '@orieljs/framework'
|
|
134
|
+
|
|
135
|
+
export default class ProfilePhoto extends WithFileUploads(Component) {
|
|
136
|
+
photo: TemporaryUploadedFile | null = null
|
|
137
|
+
|
|
138
|
+
async save() {
|
|
139
|
+
const { photo } = await this.validate({ photo: 'required|image|max:1024' })
|
|
140
|
+
await photo.store('photos')
|
|
141
|
+
this.reset('photo')
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
render() {
|
|
145
|
+
return (
|
|
146
|
+
<form o:submit="save">
|
|
147
|
+
<input type="file" o:model="photo" />
|
|
148
|
+
{this.photo?.isPreviewable() ? <img src={this.photo.temporaryUrl()} alt="Preview" /> : null}
|
|
149
|
+
<button type="submit">Save</button>
|
|
150
|
+
</form>
|
|
151
|
+
)
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
In a `WithFileUploads` component, `o:model` on a file input uploads the picked file at once, to a temporary
|
|
157
|
+
directory, and sets the property to a `TemporaryUploadedFile` (an array of them with `multiple`). Validate it with the
|
|
158
|
+
file rules, preview it with `temporaryUrl()` (a signed link, for images, audio and video), and `store()` it. In tests,
|
|
159
|
+
`Live.test()`'s `set('photo', file)` uploads a file the same way. Every upload must also pass
|
|
160
|
+
`live.temporary_file_upload.rules`, which allow 12 MB (`max:12288`), but a request body larger than
|
|
161
|
+
`http.max_body_size` (8 MB, for every request) gets a 413: raise it in `config/http.ts` to accept files up to the
|
|
162
|
+
rule's size.
|
|
163
|
+
|
|
164
|
+
## Lazy components
|
|
165
|
+
|
|
166
|
+
```tsx title="app/Live/PostCount.tsx"
|
|
167
|
+
import { Component } from '@orieljs/framework'
|
|
168
|
+
import { Post } from '../Models/Post'
|
|
169
|
+
|
|
170
|
+
export default class PostCount extends Component {
|
|
171
|
+
static override lazy = true
|
|
172
|
+
count = 0
|
|
173
|
+
|
|
174
|
+
async mount() {
|
|
175
|
+
this.count = await Post.count()
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
placeholder() {
|
|
179
|
+
return <p>Counting posts…</p>
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
render() {
|
|
183
|
+
return <p>{this.count} posts</p>
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
With `static override lazy = true`, each `<Live component={PostCount} />` (and the full page) renders the
|
|
189
|
+
placeholder first and mounts the component once it scrolls into view. `lazy` on one embed makes that embed lazy,
|
|
190
|
+
`lazy="on-load"` mounts it as soon as the page loads, and `lazy={false}` turns `static lazy` off. Without
|
|
191
|
+
`placeholder()`, the placeholder is the `live.lazy_placeholder` view, else an empty `<div>`; it needs the same root
|
|
192
|
+
element as `render()`.
|
|
193
|
+
|
|
194
|
+
## Navigating
|
|
195
|
+
|
|
196
|
+
`<a href="/posts" o:navigate>` shows the next page without a full reload: Live fetches it (from `mousedown`, or on
|
|
197
|
+
hover with `o:navigate-hover`), merges its `<head>`, swaps the `<body>` and starts the new page's components.
|
|
198
|
+
`this.redirect(url, { navigate: true })` does the same from an action. The new page's scripts run as on a full load,
|
|
199
|
+
except a `<head>` script the page already has, and a `<body>` script marked `data-navigate-once` that an earlier page
|
|
200
|
+
already ran.
|
|
66
201
|
|
|
67
202
|
## Middleware on updates
|
|
68
203
|
|
|
@@ -70,22 +205,61 @@ Each update re-runs the `auth`, `verified`, `guest` and `can` middleware of the
|
|
|
70
205
|
(Livewire's persistent middleware), with that route's parameters bound again, so `can:update,post` sees the post. A
|
|
71
206
|
signed-out tab gets a 401 and a user who lost the ability a 403, before the component's actions run. On a page behind
|
|
72
207
|
one of these middleware, a model deleted since the page rendered gets a 404 too; a page with none binds nothing on
|
|
73
|
-
updates. The list is `live.persistent_middleware
|
|
208
|
+
updates. The list is `live.persistent_middleware`, which a `config/live.ts` of your own can set.
|
|
74
209
|
|
|
75
210
|
## Flash data in updates
|
|
76
211
|
|
|
77
212
|
A status an action flashes (`session().flash('status', 'Saved.')`) shows in that update's response only, unless the
|
|
78
213
|
action redirects or navigates: then it is there on the next page. Component state is unaffected.
|
|
79
214
|
|
|
215
|
+
## Status messages announced again
|
|
216
|
+
|
|
217
|
+
Screen readers announce a status region (`role="status"`) when its text changes. When an action brings the same
|
|
218
|
+
message again ("Saved" after a second save), the page doesn't change and nothing is announced. Add `o:announce` to the
|
|
219
|
+
region and Live takes the message out and puts it back a moment later, so it is announced again:
|
|
220
|
+
|
|
221
|
+
```tsx title="app/Live/Settings.tsx"
|
|
222
|
+
import { Component } from '@orieljs/framework'
|
|
223
|
+
|
|
224
|
+
export default class Settings extends Component {
|
|
225
|
+
name = ''
|
|
226
|
+
#saved = false
|
|
227
|
+
|
|
228
|
+
save() {
|
|
229
|
+
this.#saved = true
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
render() {
|
|
233
|
+
return (
|
|
234
|
+
<form o:submit="save">
|
|
235
|
+
<input o:model="name" aria-label="Name" />
|
|
236
|
+
<button type="submit">Save</button>
|
|
237
|
+
<p role="status" o:announce>
|
|
238
|
+
{this.#saved ? 'Saved' : ''}
|
|
239
|
+
</p>
|
|
240
|
+
</form>
|
|
241
|
+
)
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
Only actions do this, and an `o:poll` that calls a method counts as one: an update that only syncs `o:model` values,
|
|
247
|
+
refreshes or delivers an event leaves the region alone. Keep a status for one response (a `#private` field, or a
|
|
248
|
+
flashed `status`), so that any other update clears it; a status kept in a public property is announced again by every
|
|
249
|
+
action until it changes, which in a polled component is every tick. Keep an `o:announce` region to text: its children
|
|
250
|
+
are taken out for 100 ms, so a button or input inside it would lose focus.
|
|
251
|
+
|
|
80
252
|
## Known limitations
|
|
81
253
|
|
|
82
|
-
- `o
|
|
83
|
-
-
|
|
254
|
+
- A DOM event whose name contains `-` can't be an `o:<event>` action, because `-` (and `.`) separate the modifiers (for example `o:model-live`).
|
|
255
|
+
- `temporaryUrl()` previews don't answer range requests, which Safari needs to play a `<video>`.
|
|
256
|
+
|
|
257
|
+
Documentation: https://orieljs.vercel.app/docs/live
|
|
84
258
|
|
|
85
259
|
Status: early alpha. APIs will change.
|
|
86
260
|
|
|
87
261
|
## License
|
|
88
262
|
|
|
89
|
-
Oriel is open-source software licensed under the Apache License 2.0 (Apache-2.0; see `LICENSE` and `NOTICE`). The Oriel name and logo are trademarks; see
|
|
263
|
+
Oriel is open-source software licensed under the Apache License 2.0 (Apache-2.0; see `LICENSE` and `NOTICE`). The Oriel name and logo are trademarks; see `TRADEMARKS.md`.
|
|
90
264
|
|
|
91
265
|
Website: https://orieljs.vercel.app
|
package/TRADEMARKS.md
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Oriel trademark policy
|
|
2
|
+
|
|
3
|
+
The Oriel code is open source under the Apache License 2.0. The **Oriel name and logo** are not: the license covers
|
|
4
|
+
the code only (section 6 of the license says so), and the name and logo stay with the project. This page explains
|
|
5
|
+
what you can and can't do with them. The goal is simple: people should always be able to tell what is the real Oriel
|
|
6
|
+
and what isn't.
|
|
7
|
+
|
|
8
|
+
## You can, without asking
|
|
9
|
+
|
|
10
|
+
- Use, change, ship and sell the code, as the license allows.
|
|
11
|
+
- Say that your app or package is "built with Oriel", "for Oriel" or "compatible with Oriel".
|
|
12
|
+
- Name a package `oriel-<something>` or `<something>-for-oriel` when it works with Oriel, as long as it is clear that
|
|
13
|
+
it is not official (for example, don't publish it under the `@orieljs` npm scope).
|
|
14
|
+
- Use the name and logo to write about Oriel, teach it, or link to it, in articles, talks, videos and courses.
|
|
15
|
+
- Use the unmodified logo to point to the project, as it is (no recolouring, cropping or
|
|
16
|
+
distortion).
|
|
17
|
+
|
|
18
|
+
## You can't, without written permission
|
|
19
|
+
|
|
20
|
+
- Call a fork, a modified copy or a competing product "Oriel", or anything likely to be confused with it.
|
|
21
|
+
- Use the name or logo as the name, logo or domain of your own product, company or service.
|
|
22
|
+
- Suggest that the Oriel project made, endorses or supports your work when it doesn't.
|
|
23
|
+
- Sell merchandise with the name or logo.
|
|
24
|
+
|
|
25
|
+
## Forks
|
|
26
|
+
|
|
27
|
+
Forks are welcome. Please give yours a different name and logo, keep the `LICENSE` and `NOTICE` files, and say that
|
|
28
|
+
it is based on Oriel.
|
|
29
|
+
|
|
30
|
+
## Questions
|
|
31
|
+
|
|
32
|
+
Open an issue at https://github.com/orieljs/framework or email meeeet.dev@gmail.com. If you're unsure whether a use
|
|
33
|
+
is fine, ask; the answer is usually yes.
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as _orieljs_contracts from '@orieljs/contracts';
|
|
2
|
-
import { Class, Rules, MessageBag, LiveEffects, Htmlable, LiveSnapshot, LiveManager, Application, HttpRequest, LiveUpdateRequest, LiveUpdateResponse, OrielError, ErrorKind, MiddlewareClass, Router } from '@orieljs/contracts';
|
|
2
|
+
import { Class, RouteName, RouteArguments, Rules, InferValidatedType, MessageBag, LiveEffects, Htmlable, LiveSnapshot, LiveManager, Application, HttpRequest, LiveUpdateRequest, LiveUpdateResponse, OrielError, ErrorKind, UploadedFile, MiddlewareClass, Router } from '@orieljs/contracts';
|
|
3
3
|
import { ServiceProvider } from '@orieljs/support';
|
|
4
4
|
|
|
5
5
|
/** Marks Live component classes (Symbol.for, so two copies of the package agree). */
|
|
@@ -13,8 +13,11 @@ interface QueuedDispatch {
|
|
|
13
13
|
}
|
|
14
14
|
/** The validation errors of a component (Laravel's MessageBag), for render(): `this.errors.first('title')`. */
|
|
15
15
|
declare class ErrorBag implements MessageBag {
|
|
16
|
+
#private;
|
|
16
17
|
private readonly messages;
|
|
17
18
|
constructor(messages: Record<string, string[]>);
|
|
19
|
+
/** Adds to the component's errors, as addError() does. */
|
|
20
|
+
add(key: string, message: string): void;
|
|
18
21
|
has(key: string): boolean;
|
|
19
22
|
first(key?: string): string | undefined;
|
|
20
23
|
get(key: string): string[];
|
|
@@ -45,6 +48,13 @@ declare abstract class Component {
|
|
|
45
48
|
static layout?: string;
|
|
46
49
|
/** The page title passed to the layout as `title`. */
|
|
47
50
|
static title?: string;
|
|
51
|
+
/**
|
|
52
|
+
* Livewire's #[Lazy]: every embed (and the full page) renders the placeholder first and mounts once the browser shows
|
|
53
|
+
* it. `<Live component={…} lazy={false} />` mounts one at once. The placeholder is the component's
|
|
54
|
+
* `placeholder(params)` method (TSX), else the `live.lazy_placeholder` view, else an empty `<div>`: give it the root
|
|
55
|
+
* element the component renders.
|
|
56
|
+
*/
|
|
57
|
+
static lazy?: boolean;
|
|
48
58
|
/** Static fallbacks for runtimes without decorators: { event: 'method' }, locked names, computed getter names. */
|
|
49
59
|
static listeners?: Record<string, string>;
|
|
50
60
|
static locked?: string[];
|
|
@@ -58,14 +68,19 @@ declare abstract class Component {
|
|
|
58
68
|
redirect(url: string, options?: {
|
|
59
69
|
navigate?: boolean;
|
|
60
70
|
}): void;
|
|
61
|
-
|
|
71
|
+
/**
|
|
72
|
+
* redirect() to a named route: `this.redirectRoute('posts.show', post, { navigate: true })`. Once `oriel
|
|
73
|
+
* route:types` has run, the name and the route's parameters are checked, as route()'s are.
|
|
74
|
+
*/
|
|
75
|
+
redirectRoute<N extends RouteName>(name: N, ...args: RouteArguments<N, [options?: {
|
|
62
76
|
navigate?: boolean;
|
|
63
|
-
}): void;
|
|
77
|
+
}]>): void;
|
|
64
78
|
/**
|
|
65
|
-
* Validates the public state. On success clears the error bag and returns the validated data
|
|
66
|
-
*
|
|
79
|
+
* Validates the public state. On success clears the error bag and returns the validated data, typed from the rules
|
|
80
|
+
* (`await this.validate({ title: 'required|string' })` is `{ title: string }`); on failure fills the error bag and
|
|
81
|
+
* throws (the rest of the action is skipped and the component re-renders with `errors`).
|
|
67
82
|
*/
|
|
68
|
-
validate<
|
|
83
|
+
validate<const R extends Rules>(rules: R, messages?: Record<string, string>, attributes?: Record<string, string>): Promise<InferValidatedType<R>>;
|
|
69
84
|
addError(key: string, message: string): void;
|
|
70
85
|
/** Clears every error, or only those of `keys`. */
|
|
71
86
|
resetErrorBag(...keys: string[]): void;
|
|
@@ -74,6 +89,13 @@ declare abstract class Component {
|
|
|
74
89
|
reset(...properties: string[]): void;
|
|
75
90
|
/** Keeps the current HTML: the response carries the new state but no `html` effect. */
|
|
76
91
|
skipRender(): void;
|
|
92
|
+
/**
|
|
93
|
+
* An action for an `o:` directive, typed against its method: `o:click={this.$action(this.delete, tag.id)}` writes
|
|
94
|
+
* `delete(5)`, and a string where `delete` takes a number is a type error. The arguments are written as JSON, the
|
|
95
|
+
* trailing `undefined` ones left out so the method's defaults apply. The method must be one of this component's
|
|
96
|
+
* actions (a public method of its class, not a hook and not @Locked); anything else throws while rendering.
|
|
97
|
+
*/
|
|
98
|
+
$action<P extends unknown[]>(method: (...params: P) => unknown, ...params: NoInfer<P>): string;
|
|
77
99
|
/** A full-page component as a route action: Route.get('/counter', Counter). Route parameters go to mount(). */
|
|
78
100
|
__invoke(): Promise<unknown>;
|
|
79
101
|
}
|
|
@@ -102,7 +124,10 @@ declare class Testable<C extends Component = Component> {
|
|
|
102
124
|
private readonly manager;
|
|
103
125
|
private readonly scope;
|
|
104
126
|
private constructor();
|
|
105
|
-
/**
|
|
127
|
+
/**
|
|
128
|
+
* Mounts `component` with `params`, as `<Live component={…} {...params} />` would. A lazy component mounts at once
|
|
129
|
+
* (Livewire's withoutLazyLoading()); `{ lazy: true }` renders its placeholder, and `call('__lazyLoad', …)` loads it.
|
|
130
|
+
*/
|
|
106
131
|
static create<C extends Component>(component: Class<C> | string, params?: Record<string, unknown>): Promise<Testable<C>>;
|
|
107
132
|
/** The component instance after the last request. */
|
|
108
133
|
instance(): C;
|
|
@@ -112,7 +137,14 @@ declare class Testable<C extends Component = Component> {
|
|
|
112
137
|
html(): string;
|
|
113
138
|
/** The effects of the last request. */
|
|
114
139
|
effects(): LiveEffects;
|
|
140
|
+
/**
|
|
141
|
+
* Sets public properties through an update. A file (a FakeFile) or an array of files is uploaded as `o:model` on a
|
|
142
|
+
* file input does it: the upload rules, the temporary disk, then a TemporaryUploadedFile (or an array) in the
|
|
143
|
+
* property, or the upload's errors on it (Livewire's).
|
|
144
|
+
*/
|
|
115
145
|
set(path: string | Record<string, unknown>, value?: unknown): Promise<this>;
|
|
146
|
+
/** Uploads `files` into the property `name` (an array when `multiple`), as the browser's upload calls do. */
|
|
147
|
+
upload(name: string, files: readonly Blob[], multiple?: boolean): Promise<this>;
|
|
116
148
|
call(method: string, ...params: unknown[]): Promise<this>;
|
|
117
149
|
toggle(path: string): Promise<this>;
|
|
118
150
|
/** Dispatches an event to this component, as the browser does for its listeners. */
|
|
@@ -125,6 +157,9 @@ declare class Testable<C extends Component = Component> {
|
|
|
125
157
|
assertDontSee(...values: string[]): this;
|
|
126
158
|
/** Each value appears in the rendered HTML as written (not escaped). */
|
|
127
159
|
assertSeeHtml(...values: string[]): this;
|
|
160
|
+
/** Each value, HTML-escaped, appears in the rendered text: the HTML with its tags stripped (Livewire's assertSeeText). */
|
|
161
|
+
assertSeeText(...values: string[]): this;
|
|
162
|
+
assertDontSeeText(...values: string[]): this;
|
|
128
163
|
assertDispatched(name: string, params?: Record<string, unknown>): this;
|
|
129
164
|
assertNotDispatched(name: string): this;
|
|
130
165
|
/** The error bag has errors for every key given (or any error at all with no keys). */
|
|
@@ -142,6 +177,11 @@ interface LiveProps {
|
|
|
142
177
|
component: Class<Component> | string;
|
|
143
178
|
/** Identifies a child inside its parent across re-renders (Livewire's :key / wire:key). */
|
|
144
179
|
'o:key'?: string | number;
|
|
180
|
+
/**
|
|
181
|
+
* Renders the placeholder and mounts the component once it scrolls into view (`'on-load'`: once the page starts);
|
|
182
|
+
* `false` mounts a `static lazy` component at once.
|
|
183
|
+
*/
|
|
184
|
+
lazy?: boolean | 'on-load';
|
|
145
185
|
[param: string]: unknown;
|
|
146
186
|
}
|
|
147
187
|
/** Blade's @livewire('counter', ['count' => 3], key('c1')): the component's first render, as trusted HTML. */
|
|
@@ -184,10 +224,25 @@ declare class LiveComponentManager implements LiveManager {
|
|
|
184
224
|
isLiveRequest(request: HttpRequest): boolean;
|
|
185
225
|
/** The first render: HTML with o:id, o:snapshot and o:effects on the root element. */
|
|
186
226
|
mount(component: string | Class<unknown>, params?: Record<string, unknown>, key?: string): Promise<string>;
|
|
187
|
-
|
|
227
|
+
/**
|
|
228
|
+
* Mounts a component: boot(), the params into its properties, mount(params), then its first render. A lazy one
|
|
229
|
+
* (`lazy` in the params, or `static lazy`) renders its placeholder instead and mounts when the browser loads it.
|
|
230
|
+
*/
|
|
231
|
+
mountComponent(component: string | Class<unknown>, embedParams?: Record<string, unknown>, existing?: Component): Promise<ComponentResult>;
|
|
232
|
+
/**
|
|
233
|
+
* A lazy component's first render (Livewire's): its placeholder, with the state it has before mount() and the mount
|
|
234
|
+
* params signed for the `__lazyLoad` call the browser makes when it shows it (`o:lazy`, or `o:lazy-on-load`).
|
|
235
|
+
*/
|
|
236
|
+
private mountPlaceholder;
|
|
188
237
|
/** Applies each component's updates and calls, re-renders it and returns its new snapshot and effects. */
|
|
189
238
|
update(request: LiveUpdateRequest): Promise<LiveUpdateResponse>;
|
|
190
239
|
updateComponent(snapshotJson: string, updates: Record<string, unknown>, calls: Call[]): Promise<ComponentResult>;
|
|
240
|
+
/**
|
|
241
|
+
* The mount params and placeholder tag of a `__lazyLoad` request, or undefined for any other update. The load comes
|
|
242
|
+
* alone (no updates, no other call: the browser sends it in a request of its own) and its payload must be the one
|
|
243
|
+
* signed for this component: anything else is refused as corrupt (a 403) before the component boots.
|
|
244
|
+
*/
|
|
245
|
+
private lazyLoad;
|
|
191
246
|
/**
|
|
192
247
|
* The route action of POST /oriel/live/update. Each component's update runs behind the persistent middleware of the
|
|
193
248
|
* route that rendered its page (Livewire's): `auth`, `verified`, `guest` and `can`, with the page route's parameters
|
|
@@ -206,7 +261,7 @@ declare class LiveComponentManager implements LiveManager {
|
|
|
206
261
|
/** Makes every computed getter (static `computed` too) cache its value for the rest of the request. */
|
|
207
262
|
private prepareComputed;
|
|
208
263
|
private applyUpdate;
|
|
209
|
-
/** Runs one call from the browser: a magic action, an event, or a public method. Returns its return value. */
|
|
264
|
+
/** Runs one call from the browser: a magic action, an event, an upload call, or a public method. Returns its return value. */
|
|
210
265
|
private call;
|
|
211
266
|
/**
|
|
212
267
|
* Renders the component (rendering/rendered hooks, nested children tracked) and checks for a single root. A
|
|
@@ -226,8 +281,9 @@ declare class LiveComponentManager implements LiveManager {
|
|
|
226
281
|
declare const UPDATE_URI = "/oriel/live/update";
|
|
227
282
|
/**
|
|
228
283
|
* Binds the LiveManager and the PaginationState that WithPagination components answer, merges the `live` config,
|
|
229
|
-
* registers POST /oriel/live/update (named `live.update`, in the `web` group: sessions and CSRF apply), the
|
|
230
|
-
* command, and the
|
|
284
|
+
* registers POST /oriel/live/update (named `live.update`, in the `web` group: sessions and CSRF apply), the upload and
|
|
285
|
+
* preview routes of WithFileUploads (`live.upload-file`, `live.preview-file`), the make:live command, and the
|
|
286
|
+
* components under app/Live.
|
|
231
287
|
*/
|
|
232
288
|
declare class LiveServiceProvider extends ServiceProvider {
|
|
233
289
|
register(): void;
|
|
@@ -277,6 +333,108 @@ declare class InvalidLiveRequestError extends LiveError {
|
|
|
277
333
|
readonly kind: ErrorKind;
|
|
278
334
|
readonly status: number;
|
|
279
335
|
}
|
|
336
|
+
/** An upload or preview URL whose signature is missing, wrong or expired (Livewire's 401). Nothing is read or stored. */
|
|
337
|
+
declare class InvalidUploadSignatureError extends LiveError {
|
|
338
|
+
readonly name: string;
|
|
339
|
+
readonly kind: ErrorKind;
|
|
340
|
+
readonly status: number;
|
|
341
|
+
constructor();
|
|
342
|
+
}
|
|
343
|
+
/** A preview of a temporary upload that is no longer on the disk (cleaned up, or stored and deleted). */
|
|
344
|
+
declare class TemporaryFileNotFoundError extends LiveError {
|
|
345
|
+
readonly name: string;
|
|
346
|
+
readonly kind: ErrorKind;
|
|
347
|
+
readonly status: number;
|
|
348
|
+
constructor(filename: string);
|
|
349
|
+
}
|
|
350
|
+
/** temporaryUrl() on an upload whose type has no preview (live.temporary_file_upload.preview_mimes). */
|
|
351
|
+
declare class FileNotPreviewableError extends LiveError {
|
|
352
|
+
readonly name: string;
|
|
353
|
+
constructor(extension: string);
|
|
354
|
+
}
|
|
355
|
+
/** The errors an upload came back with (`_uploadErrored`): the component re-renders with them on the property. */
|
|
356
|
+
declare class UploadValidationError extends LiveError {
|
|
357
|
+
readonly errors: Record<string, string[]>;
|
|
358
|
+
readonly name: string;
|
|
359
|
+
readonly kind: ErrorKind;
|
|
360
|
+
readonly status: number;
|
|
361
|
+
readonly errorBag: string;
|
|
362
|
+
constructor(errors: Record<string, string[]>);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Livewire's `temporary_file_upload` settings, read from config `live.temporary_file_upload.*` (each one falls back on
|
|
367
|
+
* its own): the disk (the default filesystem disk), the directory on it, the rules every upload must pass, the upload
|
|
368
|
+
* route's middleware, the types a preview is served for, how long an upload URL lasts (minutes), and whether a finished
|
|
369
|
+
* upload removes the temporary files older than a day.
|
|
370
|
+
*/
|
|
371
|
+
declare const TEMPORARY_UPLOAD_DEFAULTS: {
|
|
372
|
+
disk: string | null;
|
|
373
|
+
directory: string;
|
|
374
|
+
rules: string | string[];
|
|
375
|
+
middleware: string | string[];
|
|
376
|
+
preview_mimes: string[];
|
|
377
|
+
max_upload_time: number;
|
|
378
|
+
cleanup: boolean;
|
|
379
|
+
};
|
|
380
|
+
/** What a temporary upload is: its stored name on the temporary disk and what the browser sent (signed with it). */
|
|
381
|
+
interface TemporaryFileData {
|
|
382
|
+
/** A random 40-character name with the extension its bytes showed: `live-tmp/<filename>` on the temporary disk. */
|
|
383
|
+
filename: string;
|
|
384
|
+
/** The name the browser gave the file. */
|
|
385
|
+
name: string;
|
|
386
|
+
size: number;
|
|
387
|
+
/** The MIME type the browser claimed. */
|
|
388
|
+
type: string;
|
|
389
|
+
/** The extension of the upload's own type (its bytes, else a plain claimed extension), or ''. */
|
|
390
|
+
extension: string;
|
|
391
|
+
}
|
|
392
|
+
/**
|
|
393
|
+
* Livewire's TemporaryUploadedFile: a file the browser uploaded to the temporary directory before an action stores
|
|
394
|
+
* it. A WithFileUploads component's property holds one (or an array of them) once `o:model` on a file input has
|
|
395
|
+
* uploaded it: validate it like any upload (`'photo' => 'image|max:1024'`), preview it with `temporaryUrl()`, then
|
|
396
|
+
* `store()` it. It travels in the component's snapshot as its data, signed with the rest.
|
|
397
|
+
*/
|
|
398
|
+
declare class TemporaryUploadedFile implements UploadedFile {
|
|
399
|
+
#private;
|
|
400
|
+
readonly filename: string;
|
|
401
|
+
readonly clientName: string;
|
|
402
|
+
readonly mimeType: string;
|
|
403
|
+
readonly size: number;
|
|
404
|
+
constructor(data: TemporaryFileData);
|
|
405
|
+
/** What the snapshot carries. */
|
|
406
|
+
toData(): TemporaryFileData;
|
|
407
|
+
/** The file's path on the temporary disk. */
|
|
408
|
+
get path(): string;
|
|
409
|
+
/** The extension of the upload's own type, as FormUploadedFile's extension(): what putFile() names a stored copy by. */
|
|
410
|
+
extension(): string;
|
|
411
|
+
/** The extension of the name the browser gave the file, lower-cased ('' without one). */
|
|
412
|
+
clientExtension(): string;
|
|
413
|
+
/** A random 40-character name with extension(), as FormUploadedFile's. */
|
|
414
|
+
hashName(): string;
|
|
415
|
+
arrayBuffer(): Promise<ArrayBuffer>;
|
|
416
|
+
text(): Promise<string>;
|
|
417
|
+
/** The file's bytes from the temporary disk, opened on the first read. */
|
|
418
|
+
stream(): ReadableStream<Uint8Array>;
|
|
419
|
+
/** Stores a copy under a random name in `directory`, on `disk` (by default the temporary disk, as Livewire's). */
|
|
420
|
+
store(directory: string, disk?: string): Promise<string>;
|
|
421
|
+
storeAs(directory: string, name: string, disk?: string): Promise<string>;
|
|
422
|
+
exists(): Promise<boolean>;
|
|
423
|
+
/** Removes the temporary file (a stored copy stays). */
|
|
424
|
+
delete(): Promise<boolean>;
|
|
425
|
+
/** Whether temporaryUrl() serves this file: its type is in `live.temporary_file_upload.preview_mimes`. */
|
|
426
|
+
isPreviewable(): boolean;
|
|
427
|
+
/**
|
|
428
|
+
* A signed link to the file for `<img src>` or `<video src>` before it is stored. It lasts until the end of the hour
|
|
429
|
+
* at least 30 minutes away (Livewire's), so a re-render keeps the same link and the browser doesn't reload it.
|
|
430
|
+
*/
|
|
431
|
+
temporaryUrl(): string;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/** Where the browser POSTs a WithFileUploads component's files (Livewire's /livewire/upload-file), named live.upload-file. */
|
|
435
|
+
declare const UPLOAD_URI = "/oriel/live/upload-file";
|
|
436
|
+
/** Where temporaryUrl() links lead (Livewire's /livewire/preview-file/{filename}), named live.preview-file. */
|
|
437
|
+
declare const PREVIEW_URI = "/oriel/live/preview-file";
|
|
280
438
|
|
|
281
439
|
/** The route middleware an update re-applies by default (config live.persistent_middleware), by alias. */
|
|
282
440
|
declare const PERSISTENT_MIDDLEWARE: string[];
|
|
@@ -312,6 +470,14 @@ interface Paginated {
|
|
|
312
470
|
*/
|
|
313
471
|
declare function WithPagination<TBase extends ComponentConstructor>(Base: TBase): TBase & (abstract new (...args: any[]) => Paginated);
|
|
314
472
|
|
|
473
|
+
/**
|
|
474
|
+
* Livewire's WithFileUploads: `class Profile extends WithFileUploads(Component)`. A file input with `o:model="photo"`
|
|
475
|
+
* uploads the file to the temporary directory as soon as it is picked, then sets `photo` to a TemporaryUploadedFile
|
|
476
|
+
* (`<input type="file" multiple>` sets an array), through `updatingPhoto()`/`updatedPhoto()` like any update. Validate
|
|
477
|
+
* it with the file rules, preview it with `temporaryUrl()`, store it with `store()`.
|
|
478
|
+
*/
|
|
479
|
+
declare function WithFileUploads<TBase extends ComponentConstructor>(Base: TBase): TBase;
|
|
480
|
+
|
|
315
481
|
/**
|
|
316
482
|
* What <Pagination> reads from a paginator. Structural, since Live can't import Database: paginate()'s
|
|
317
483
|
* LengthAwarePaginator (with lastPage(), so page buttons) and simplePaginate()'s Paginator (Previous and Next only).
|
|
@@ -339,4 +505,4 @@ interface PaginationProps {
|
|
|
339
505
|
*/
|
|
340
506
|
declare function Pagination({ paginator, class: className }: PaginationProps): Htmlable;
|
|
341
507
|
|
|
342
|
-
export { Component, type ComponentResult, Computed, CorruptComponentPayloadError, ErrorBag, InvalidLiveRequestError, Live, LiveComponentManager, LiveComponentNotFoundError, LiveError, type LiveProps, LiveServiceProvider, Locked, MethodNotCallableError, On, PERSISTENT_MIDDLEWARE, type Paginated, Pagination, type PaginationProps, type PaginationSource, PendingEvent, type PersistentMiddleware, PropertyNotUpdatableError, TestRequestScope, Testable, UPDATE_URI, WithPagination, componentName, live, persistentMiddleware };
|
|
508
|
+
export { Component, type ComponentResult, Computed, CorruptComponentPayloadError, ErrorBag, FileNotPreviewableError, InvalidLiveRequestError, InvalidUploadSignatureError, Live, LiveComponentManager, LiveComponentNotFoundError, LiveError, type LiveProps, LiveServiceProvider, Locked, MethodNotCallableError, On, PERSISTENT_MIDDLEWARE, PREVIEW_URI, type Paginated, Pagination, type PaginationProps, type PaginationSource, PendingEvent, type PersistentMiddleware, PropertyNotUpdatableError, TEMPORARY_UPLOAD_DEFAULTS, type TemporaryFileData, TemporaryFileNotFoundError, TemporaryUploadedFile, TestRequestScope, Testable, UPDATE_URI, UPLOAD_URI, UploadValidationError, WithFileUploads, WithPagination, componentName, live, persistentMiddleware };
|