@orieljs/live 0.1.0-alpha.6 → 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 CHANGED
@@ -10,3 +10,7 @@ The Oriel name and logo are not covered by that license; see TRADEMARKS.md
10
10
 
11
11
  @orieljs/support (and @orieljs/framework, which bundles it) includes transliteration data from
12
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,28 +22,52 @@ export default class Counter extends Component {
23
22
  }
24
23
  ```
25
24
 
26
- ```ts
27
- // routes/web.ts
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
37
39
 
38
40
  `this.$action(method, ...arguments)` writes an action for an `o:` directive, typed against the method:
39
41
 
40
- ```tsx
41
- <button type="button" o:click={this.$action(this.revokeAdmin, user.id)}>
42
- Revoke admin
43
- </button>
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
+ }
44
67
  ```
45
68
 
46
- This renders `o:click="revokeAdmin(5)"`, and when `revokeAdmin(id: number)` takes a number,
47
- `this.$action(this.revokeAdmin, String(user.id))` is a type error. The arguments are written as JSON, so pass ids and
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
48
71
  plain values (strings, numbers, booleans, `null`, arrays and plain objects): a model arrives as its attributes and a
49
72
  date as its ISO string, so pass `post.id`, not `post`. The action contains double quotes, so use it as a TSX attribute
50
73
  value, which escapes them, not inside an HTML string you write by hand. Rendering throws when the method isn't one of
@@ -56,6 +79,8 @@ request can still send anything, so an action checks what it is given, as a cont
56
79
  `o:confirm` asks before an action runs, as Livewire's `wire:confirm`:
57
80
 
58
81
  ```tsx
82
+ declare const tag: { id: number }
83
+ // ---cut---
59
84
  <button type="button" class="btn btn-danger" o:click={`delete(${tag.id})`} o:confirm="Delete this tag?">
60
85
  Delete
61
86
  </button>
@@ -65,22 +90,22 @@ The browser's confirm dialog shows the message (an empty `o:confirm` asks "Are y
65
90
  and the action runs only when it is accepted. `o:confirm-prompt="Type DELETE to confirm|DELETE"` (`o:confirm.prompt`
66
91
  in plain HTML) asks with a prompt instead, and runs the action only when the answer is exactly the text after the `|`.
67
92
  It works on any element with an `o:` action: `o:click`, `o:submit` on a form, `o:keydown-enter`, and the others. A
68
- dismissed dialog stops the event, so the actions of the elements around it don't run either. `o:confirm` guards the
69
- actions of the element it sits on: on a form, put it on the `<form>` with the `o:submit`. A button inside a form with
70
- its own `o:click` needs `type="button"` or `-prevent` (`o:click-prevent`), or the browser still submits the form when
71
- the dialog is dismissed.
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.
72
97
 
73
98
  ## Pagination
74
99
 
75
- ```tsx
100
+ ```tsx title="app/Live/Posts.tsx"
76
101
  import { Component, Pagination, WithPagination } from '@orieljs/framework'
77
102
  import { Post } from '../Models/Post'
78
103
 
79
104
  export default class Posts extends WithPagination(Component) {
80
105
  search = ''
81
106
 
82
- updatedSearch() {
83
- this.resetPage()
107
+ async updatedSearch() {
108
+ await this.resetPage()
84
109
  }
85
110
 
86
111
  async render() {
@@ -98,8 +123,81 @@ export default class Posts extends WithPagination(Component) {
98
123
 
99
124
  `paginate()` in the component (in `render()`, in `mount()`, in actions and the update hooks, or in a `@Computed`
100
125
  getter any of them reads) shows the component's `page`, and its links are built on the page's URL. The actions
101
- `nextPage`, `previousPage`, `gotoPage(n)`, `setPage(n)` and `resetPage` move it; `<Pagination>` renders them as
102
- buttons with the kits' `btn` classes. The page starts at 1 and isn't kept in the address bar.
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.
103
201
 
104
202
  ## Middleware on updates
105
203
 
@@ -107,7 +205,7 @@ Each update re-runs the `auth`, `verified`, `guest` and `can` middleware of the
107
205
  (Livewire's persistent middleware), with that route's parameters bound again, so `can:update,post` sees the post. A
108
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
109
207
  one of these middleware, a model deleted since the page rendered gets a 404 too; a page with none binds nothing on
110
- updates. The list is `live.persistent_middleware` in `config/live.ts`.
208
+ updates. The list is `live.persistent_middleware`, which a `config/live.ts` of your own can set.
111
209
 
112
210
  ## Flash data in updates
113
211
 
@@ -120,10 +218,29 @@ Screen readers announce a status region (`role="status"`) when its text changes.
120
218
  message again ("Saved" after a second save), the page doesn't change and nothing is announced. Add `o:announce` to the
121
219
  region and Live takes the message out and puts it back a moment later, so it is announced again:
122
220
 
123
- ```tsx
124
- <p role="status" o:announce>
125
- {this.#saved ? 'Saved' : ''}
126
- </p>
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
+ }
127
244
  ```
128
245
 
129
246
  Only actions do this, and an `o:poll` that calls a method counts as one: an update that only syncs `o:model` values,
@@ -134,13 +251,15 @@ are taken out for 100 ms, so a button or input inside it would lose focus.
134
251
 
135
252
  ## Known limitations
136
253
 
137
- - `o:navigate` does not run scripts in the new page.
138
- - A DOM event whose name contains `-` can't be written as `o:<event>` in TSX, because modifiers are `-` segments (for example `o:model-live`).
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
139
258
 
140
259
  Status: early alpha. APIs will change.
141
260
 
142
261
  ## License
143
262
 
144
- 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`](https://github.com/orieljs/framework/blob/main/TRADEMARKS.md).
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`.
145
264
 
146
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
- redirectRoute(name: string, parameters?: unknown, options?: {
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; on failure fills the
66
- * error bag and throws (the rest of the action is skipped and the component re-renders with `errors`).
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<T = Record<string, unknown>>(rules: Rules, messages?: Record<string, string>, attributes?: Record<string, string>): Promise<T>;
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;
@@ -109,7 +124,10 @@ declare class Testable<C extends Component = Component> {
109
124
  private readonly manager;
110
125
  private readonly scope;
111
126
  private constructor();
112
- /** Mounts `component` with `params`, as `<Live component={…} {...params} />` would. */
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
+ */
113
131
  static create<C extends Component>(component: Class<C> | string, params?: Record<string, unknown>): Promise<Testable<C>>;
114
132
  /** The component instance after the last request. */
115
133
  instance(): C;
@@ -119,7 +137,14 @@ declare class Testable<C extends Component = Component> {
119
137
  html(): string;
120
138
  /** The effects of the last request. */
121
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
+ */
122
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>;
123
148
  call(method: string, ...params: unknown[]): Promise<this>;
124
149
  toggle(path: string): Promise<this>;
125
150
  /** Dispatches an event to this component, as the browser does for its listeners. */
@@ -152,6 +177,11 @@ interface LiveProps {
152
177
  component: Class<Component> | string;
153
178
  /** Identifies a child inside its parent across re-renders (Livewire's :key / wire:key). */
154
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';
155
185
  [param: string]: unknown;
156
186
  }
157
187
  /** Blade's @livewire('counter', ['count' => 3], key('c1')): the component's first render, as trusted HTML. */
@@ -194,10 +224,25 @@ declare class LiveComponentManager implements LiveManager {
194
224
  isLiveRequest(request: HttpRequest): boolean;
195
225
  /** The first render: HTML with o:id, o:snapshot and o:effects on the root element. */
196
226
  mount(component: string | Class<unknown>, params?: Record<string, unknown>, key?: string): Promise<string>;
197
- mountComponent(component: string | Class<unknown>, params?: Record<string, unknown>, existing?: Component): Promise<ComponentResult>;
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;
198
237
  /** Applies each component's updates and calls, re-renders it and returns its new snapshot and effects. */
199
238
  update(request: LiveUpdateRequest): Promise<LiveUpdateResponse>;
200
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;
201
246
  /**
202
247
  * The route action of POST /oriel/live/update. Each component's update runs behind the persistent middleware of the
203
248
  * route that rendered its page (Livewire's): `auth`, `verified`, `guest` and `can`, with the page route's parameters
@@ -216,7 +261,7 @@ declare class LiveComponentManager implements LiveManager {
216
261
  /** Makes every computed getter (static `computed` too) cache its value for the rest of the request. */
217
262
  private prepareComputed;
218
263
  private applyUpdate;
219
- /** 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. */
220
265
  private call;
221
266
  /**
222
267
  * Renders the component (rendering/rendered hooks, nested children tracked) and checks for a single root. A
@@ -236,8 +281,9 @@ declare class LiveComponentManager implements LiveManager {
236
281
  declare const UPDATE_URI = "/oriel/live/update";
237
282
  /**
238
283
  * Binds the LiveManager and the PaginationState that WithPagination components answer, merges the `live` config,
239
- * registers POST /oriel/live/update (named `live.update`, in the `web` group: sessions and CSRF apply), the make:live
240
- * command, and the components under app/Live.
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.
241
287
  */
242
288
  declare class LiveServiceProvider extends ServiceProvider {
243
289
  register(): void;
@@ -287,6 +333,108 @@ declare class InvalidLiveRequestError extends LiveError {
287
333
  readonly kind: ErrorKind;
288
334
  readonly status: number;
289
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";
290
438
 
291
439
  /** The route middleware an update re-applies by default (config live.persistent_middleware), by alias. */
292
440
  declare const PERSISTENT_MIDDLEWARE: string[];
@@ -322,6 +470,14 @@ interface Paginated {
322
470
  */
323
471
  declare function WithPagination<TBase extends ComponentConstructor>(Base: TBase): TBase & (abstract new (...args: any[]) => Paginated);
324
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
+
325
481
  /**
326
482
  * What <Pagination> reads from a paginator. Structural, since Live can't import Database: paginate()'s
327
483
  * LengthAwarePaginator (with lastPage(), so page buttons) and simplePaginate()'s Paginator (Previous and Next only).
@@ -349,4 +505,4 @@ interface PaginationProps {
349
505
  */
350
506
  declare function Pagination({ paginator, class: className }: PaginationProps): Htmlable;
351
507
 
352
- 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 };