@orieljs/live 0.1.0-alpha.4 → 0.1.0-alpha.6
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 +3 -0
- package/README.md +100 -1
- package/dist/index.d.ts +76 -10
- package/dist/index.js +457 -226
- package/dist/index.js.map +1 -1
- package/package.json +5 -5
package/NOTICE
CHANGED
|
@@ -7,3 +7,6 @@ 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.
|
package/README.md
CHANGED
|
@@ -33,7 +33,106 @@ Route.get('/counter', Counter)
|
|
|
33
33
|
|
|
34
34
|
Embed a component in any view with `<Live component={Counter} />`. In the browser, components are driven by `@orieljs/live-client`.
|
|
35
35
|
|
|
36
|
-
|
|
36
|
+
## Typed actions
|
|
37
|
+
|
|
38
|
+
`this.$action(method, ...arguments)` writes an action for an `o:` directive, typed against the method:
|
|
39
|
+
|
|
40
|
+
```tsx
|
|
41
|
+
<button type="button" o:click={this.$action(this.revokeAdmin, user.id)}>
|
|
42
|
+
Revoke admin
|
|
43
|
+
</button>
|
|
44
|
+
```
|
|
45
|
+
|
|
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
|
|
48
|
+
plain values (strings, numbers, booleans, `null`, arrays and plain objects): a model arrives as its attributes and a
|
|
49
|
+
date as its ISO string, so pass `post.id`, not `post`. The action contains double quotes, so use it as a TSX attribute
|
|
50
|
+
value, which escapes them, not inside an HTML string you write by hand. Rendering throws when the method isn't one of
|
|
51
|
+
the component's actions (a hook, or a `#private` or `@Locked` method). The types check what your pages send; a crafted
|
|
52
|
+
request can still send anything, so an action checks what it is given, as a controller does.
|
|
53
|
+
|
|
54
|
+
## Confirming an action
|
|
55
|
+
|
|
56
|
+
`o:confirm` asks before an action runs, as Livewire's `wire:confirm`:
|
|
57
|
+
|
|
58
|
+
```tsx
|
|
59
|
+
<button type="button" class="btn btn-danger" o:click={`delete(${tag.id})`} o:confirm="Delete this tag?">
|
|
60
|
+
Delete
|
|
61
|
+
</button>
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
The browser's confirm dialog shows the message (an empty `o:confirm` asks "Are you sure?", and `\n` is a line break),
|
|
65
|
+
and the action runs only when it is accepted. `o:confirm-prompt="Type DELETE to confirm|DELETE"` (`o:confirm.prompt`
|
|
66
|
+
in plain HTML) asks with a prompt instead, and runs the action only when the answer is exactly the text after the `|`.
|
|
67
|
+
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.
|
|
72
|
+
|
|
73
|
+
## Pagination
|
|
74
|
+
|
|
75
|
+
```tsx
|
|
76
|
+
import { Component, Pagination, WithPagination } from '@orieljs/framework'
|
|
77
|
+
import { Post } from '../Models/Post'
|
|
78
|
+
|
|
79
|
+
export default class Posts extends WithPagination(Component) {
|
|
80
|
+
search = ''
|
|
81
|
+
|
|
82
|
+
updatedSearch() {
|
|
83
|
+
this.resetPage()
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async render() {
|
|
87
|
+
const posts = await Post.whereLike('title', `%${this.search}%`).latest().paginate(10)
|
|
88
|
+
return (
|
|
89
|
+
<div>
|
|
90
|
+
<input o:model-live="search" aria-label="Search" />
|
|
91
|
+
{posts.items().map((post) => <p>{post.title}</p>)}
|
|
92
|
+
<Pagination paginator={posts} class="flex gap-2" />
|
|
93
|
+
</div>
|
|
94
|
+
)
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
`paginate()` in the component (in `render()`, in `mount()`, in actions and the update hooks, or in a `@Computed`
|
|
100
|
+
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.
|
|
103
|
+
|
|
104
|
+
## Middleware on updates
|
|
105
|
+
|
|
106
|
+
Each update re-runs the `auth`, `verified`, `guest` and `can` middleware of the route that rendered the page
|
|
107
|
+
(Livewire's persistent middleware), with that route's parameters bound again, so `can:update,post` sees the post. A
|
|
108
|
+
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
|
+
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`.
|
|
111
|
+
|
|
112
|
+
## Flash data in updates
|
|
113
|
+
|
|
114
|
+
A status an action flashes (`session().flash('status', 'Saved.')`) shows in that update's response only, unless the
|
|
115
|
+
action redirects or navigates: then it is there on the next page. Component state is unaffected.
|
|
116
|
+
|
|
117
|
+
## Status messages announced again
|
|
118
|
+
|
|
119
|
+
Screen readers announce a status region (`role="status"`) when its text changes. When an action brings the same
|
|
120
|
+
message again ("Saved" after a second save), the page doesn't change and nothing is announced. Add `o:announce` to the
|
|
121
|
+
region and Live takes the message out and puts it back a moment later, so it is announced again:
|
|
122
|
+
|
|
123
|
+
```tsx
|
|
124
|
+
<p role="status" o:announce>
|
|
125
|
+
{this.#saved ? 'Saved' : ''}
|
|
126
|
+
</p>
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Only actions do this, and an `o:poll` that calls a method counts as one: an update that only syncs `o:model` values,
|
|
130
|
+
refreshes or delivers an event leaves the region alone. Keep a status for one response (a `#private` field, or a
|
|
131
|
+
flashed `status`), so that any other update clears it; a status kept in a public property is announced again by every
|
|
132
|
+
action until it changes, which in a polled component is every tick. Keep an `o:announce` region to text: its children
|
|
133
|
+
are taken out for 100 ms, so a button or input inside it would lose focus.
|
|
134
|
+
|
|
135
|
+
## Known limitations
|
|
37
136
|
|
|
38
137
|
- `o:navigate` does not run scripts in the new page.
|
|
39
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`).
|
package/dist/index.d.ts
CHANGED
|
@@ -74,6 +74,13 @@ declare abstract class Component {
|
|
|
74
74
|
reset(...properties: string[]): void;
|
|
75
75
|
/** Keeps the current HTML: the response carries the new state but no `html` effect. */
|
|
76
76
|
skipRender(): void;
|
|
77
|
+
/**
|
|
78
|
+
* An action for an `o:` directive, typed against its method: `o:click={this.$action(this.delete, tag.id)}` writes
|
|
79
|
+
* `delete(5)`, and a string where `delete` takes a number is a type error. The arguments are written as JSON, the
|
|
80
|
+
* trailing `undefined` ones left out so the method's defaults apply. The method must be one of this component's
|
|
81
|
+
* actions (a public method of its class, not a hook and not @Locked); anything else throws while rendering.
|
|
82
|
+
*/
|
|
83
|
+
$action<P extends unknown[]>(method: (...params: P) => unknown, ...params: NoInfer<P>): string;
|
|
77
84
|
/** A full-page component as a route action: Route.get('/counter', Counter). Route parameters go to mount(). */
|
|
78
85
|
__invoke(): Promise<unknown>;
|
|
79
86
|
}
|
|
@@ -125,6 +132,9 @@ declare class Testable<C extends Component = Component> {
|
|
|
125
132
|
assertDontSee(...values: string[]): this;
|
|
126
133
|
/** Each value appears in the rendered HTML as written (not escaped). */
|
|
127
134
|
assertSeeHtml(...values: string[]): this;
|
|
135
|
+
/** Each value, HTML-escaped, appears in the rendered text: the HTML with its tags stripped (Livewire's assertSeeText). */
|
|
136
|
+
assertSeeText(...values: string[]): this;
|
|
137
|
+
assertDontSeeText(...values: string[]): this;
|
|
128
138
|
assertDispatched(name: string, params?: Record<string, unknown>): this;
|
|
129
139
|
assertNotDispatched(name: string): this;
|
|
130
140
|
/** The error bag has errors for every key given (or any error at all with no keys). */
|
|
@@ -189,9 +199,11 @@ declare class LiveComponentManager implements LiveManager {
|
|
|
189
199
|
update(request: LiveUpdateRequest): Promise<LiveUpdateResponse>;
|
|
190
200
|
updateComponent(snapshotJson: string, updates: Record<string, unknown>, calls: Call[]): Promise<ComponentResult>;
|
|
191
201
|
/**
|
|
192
|
-
* The route action of POST /oriel/live/update. Each component's update runs behind the
|
|
193
|
-
* that rendered its page (Livewire's
|
|
194
|
-
*
|
|
202
|
+
* The route action of POST /oriel/live/update. Each component's update runs behind the persistent middleware of the
|
|
203
|
+
* route that rendered its page (Livewire's): `auth`, `verified`, `guest` and `can`, with the page route's parameters
|
|
204
|
+
* bound again. A signed-out tab gets a 401, a revoked ability a 403 and a deleted model a 404, before the component
|
|
205
|
+
* hydrates. When a middleware answers instead (a redirect), that is the response. A key first flashed during the
|
|
206
|
+
* update shows in this response only, unless the update redirects.
|
|
195
207
|
*/
|
|
196
208
|
handle(request: HttpRequest): Promise<LiveUpdateResponse | Response>;
|
|
197
209
|
/** A full-page component (Route.get('/counter', Counter)): mounted with the route parameters, inside the layout. */
|
|
@@ -206,8 +218,14 @@ declare class LiveComponentManager implements LiveManager {
|
|
|
206
218
|
private applyUpdate;
|
|
207
219
|
/** Runs one call from the browser: a magic action, an event, or a public method. Returns its return value. */
|
|
208
220
|
private call;
|
|
209
|
-
/**
|
|
221
|
+
/**
|
|
222
|
+
* Renders the component (rendering/rendered hooks, nested children tracked) and checks for a single root. A
|
|
223
|
+
* WithPagination component's page and page URL are what paginate() reads while it renders, as in its mount() and in
|
|
224
|
+
* its update's hooks and actions.
|
|
225
|
+
*/
|
|
210
226
|
private render;
|
|
227
|
+
/** The absolute URL of the page a memo path belongs to, on the current request's origin ('/' + path without one). */
|
|
228
|
+
private pageUrl;
|
|
211
229
|
/** Dispatches, redirects: the effects every response carries. */
|
|
212
230
|
private effects;
|
|
213
231
|
/** dehydrate() hook, then the state as snapshot data, the error bag, and the checksum over both. */
|
|
@@ -217,8 +235,9 @@ declare class LiveComponentManager implements LiveManager {
|
|
|
217
235
|
/** The URI the client runtime posts updates to. */
|
|
218
236
|
declare const UPDATE_URI = "/oriel/live/update";
|
|
219
237
|
/**
|
|
220
|
-
* Binds the LiveManager
|
|
221
|
-
* group: sessions and CSRF apply), the make:live
|
|
238
|
+
* 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.
|
|
222
241
|
*/
|
|
223
242
|
declare class LiveServiceProvider extends ServiceProvider {
|
|
224
243
|
register(): void;
|
|
@@ -277,10 +296,57 @@ interface PersistentMiddleware {
|
|
|
277
296
|
readonly parameters: readonly string[];
|
|
278
297
|
}
|
|
279
298
|
/**
|
|
280
|
-
* Livewire's persistent middleware: the
|
|
281
|
-
*
|
|
282
|
-
* A path no route matches has none.
|
|
299
|
+
* Livewire's persistent middleware: the middleware among `persistent` (aliases) that the page route (the component's
|
|
300
|
+
* memo path and method) ran, in the order an update runs them. A path no route matches has none.
|
|
283
301
|
*/
|
|
284
302
|
declare function persistentMiddleware(router: Router, persistent: readonly string[], method: string, path: string): PersistentMiddleware[];
|
|
285
303
|
|
|
286
|
-
|
|
304
|
+
type ComponentConstructor = abstract new (...args: any[]) => Component;
|
|
305
|
+
/** What WithPagination() adds to a component: its page and the actions that move it. */
|
|
306
|
+
interface Paginated {
|
|
307
|
+
/** The current page. The browser may set it; Live reads it as a page number. */
|
|
308
|
+
page: number;
|
|
309
|
+
nextPage(): Promise<void>;
|
|
310
|
+
previousPage(): Promise<void>;
|
|
311
|
+
gotoPage(page: number | string): Promise<void>;
|
|
312
|
+
resetPage(): Promise<void>;
|
|
313
|
+
setPage(page: number | string): Promise<void>;
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* Livewire's WithPagination: `class Posts extends WithPagination(Component)`. The public `page` (1 to start; there
|
|
317
|
+
* is no address-bar sync) is what `paginate()` shows in the component (in `render()`, `mount()`, `hydrate()`, the
|
|
318
|
+
* update hooks and actions, or in a `@Computed` getter any of them reads), and its page links are built on the
|
|
319
|
+
* page's URL. The actions move it: `o:click="nextPage"`, `previousPage`, `gotoPage(3)`, `setPage(3)`,
|
|
320
|
+
* `resetPage` (call it from `updatedSearch()` so a new search starts on page 1). `updatingPage(page)` and
|
|
321
|
+
* `updatedPage(page)` run around every change. One paginator per component: other page names read the request.
|
|
322
|
+
*/
|
|
323
|
+
declare function WithPagination<TBase extends ComponentConstructor>(Base: TBase): TBase & (abstract new (...args: any[]) => Paginated);
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* What <Pagination> reads from a paginator. Structural, since Live can't import Database: paginate()'s
|
|
327
|
+
* LengthAwarePaginator (with lastPage(), so page buttons) and simplePaginate()'s Paginator (Previous and Next only).
|
|
328
|
+
*/
|
|
329
|
+
interface PaginationSource {
|
|
330
|
+
currentPage(): number;
|
|
331
|
+
lastPage?(): number;
|
|
332
|
+
hasMorePages(): boolean;
|
|
333
|
+
onFirstPage(): boolean;
|
|
334
|
+
/** Page buttons on each side of the current one (the paginator's onEachSide(), 3 by default). */
|
|
335
|
+
readonly onEachSideCount?: number;
|
|
336
|
+
/** Not cursorPaginate(): its pages are cursors, which a component's page number can't follow. */
|
|
337
|
+
readonly nextCursor?: never;
|
|
338
|
+
}
|
|
339
|
+
interface PaginationProps {
|
|
340
|
+
paginator: PaginationSource;
|
|
341
|
+
/** Classes for the <nav>, such as its layout ("flex gap-2"); in your file, so Tailwind generates them. */
|
|
342
|
+
class?: string;
|
|
343
|
+
}
|
|
344
|
+
/**
|
|
345
|
+
* Livewire's pagination links for a WithPagination component: `<Pagination paginator={posts} />` in render(). A
|
|
346
|
+
* <nav aria-label="Pagination"> with « Previous, the page buttons and Next » as Live actions (previousPage,
|
|
347
|
+
* gotoPage(n), nextPage), the current page marked aria-current="page", the ends disabled, the kits' theme classes
|
|
348
|
+
* (btn, btn-secondary). Nothing when everything fits on one page. The address bar is left alone.
|
|
349
|
+
*/
|
|
350
|
+
declare function Pagination({ paginator, class: className }: PaginationProps): Htmlable;
|
|
351
|
+
|
|
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 };
|