@orieljs/live 0.1.0-alpha.3 → 0.1.0-alpha.5

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 ADDED
@@ -0,0 +1,9 @@
1
+ Oriel
2
+ Copyright 2026 meeeet-dev and Oriel contributors
3
+
4
+ This product includes software developed by the Oriel project
5
+ (https://orieljs.vercel.app, https://github.com/orieljs).
6
+
7
+ Oriel is licensed under the Apache License, Version 2.0 (see LICENSE).
8
+ The Oriel name and logo are not covered by that license; see TRADEMARKS.md
9
+ (https://github.com/orieljs/framework/blob/main/TRADEMARKS.md).
package/README.md CHANGED
@@ -33,7 +33,51 @@ 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
- Known limitations:
36
+ ## Pagination
37
+
38
+ ```tsx
39
+ import { Component, Pagination, WithPagination } from '@orieljs/framework'
40
+ import { Post } from '../Models/Post'
41
+
42
+ export default class Posts extends WithPagination(Component) {
43
+ search = ''
44
+
45
+ updatedSearch() {
46
+ this.resetPage()
47
+ }
48
+
49
+ async render() {
50
+ const posts = await Post.whereLike('title', `%${this.search}%`).latest().paginate(10)
51
+ return (
52
+ <div>
53
+ <input o:model-live="search" aria-label="Search" />
54
+ {posts.items().map((post) => <p>{post.title}</p>)}
55
+ <Pagination paginator={posts} class="flex gap-2" />
56
+ </div>
57
+ )
58
+ }
59
+ }
60
+ ```
61
+
62
+ `paginate()` in the component (in `render()`, in `mount()`, in actions and the update hooks, or in a `@Computed`
63
+ 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; `<Pagination>` renders them as
65
+ buttons with the kits' `btn` classes. The page starts at 1 and isn't kept in the address bar.
66
+
67
+ ## Middleware on updates
68
+
69
+ Each update re-runs the `auth`, `verified`, `guest` and `can` middleware of the route that rendered the page
70
+ (Livewire's persistent middleware), with that route's parameters bound again, so `can:update,post` sees the post. A
71
+ 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
+ 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` in `config/live.ts`.
74
+
75
+ ## Flash data in updates
76
+
77
+ A status an action flashes (`session().flash('status', 'Saved.')`) shows in that update's response only, unless the
78
+ action redirects or navigates: then it is there on the next page. Component state is unaffected.
79
+
80
+ ## Known limitations
37
81
 
38
82
  - `o:navigate` does not run scripts in the new page.
39
83
  - 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
@@ -1,4 +1,5 @@
1
- import { Class, Rules, MessageBag, LiveEffects, Htmlable, LiveSnapshot, LiveManager, Application, HttpRequest, LiveUpdateRequest, LiveUpdateResponse, OrielError, ErrorKind } from '@orieljs/contracts';
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
3
  import { ServiceProvider } from '@orieljs/support';
3
4
 
4
5
  /** Marks Live component classes (Symbol.for, so two copies of the package agree). */
@@ -84,13 +85,22 @@ declare function Locked(_target: unknown, context: ClassFieldDecoratorContext |
84
85
  /** @Computed: a getter cached for the rest of the request, never part of the component's state. */
85
86
  declare function Computed<T>(getter: (this: object) => T, context: ClassGetterDecoratorContext): (this: object) => T;
86
87
 
88
+ /**
89
+ * Runs Live.test()'s mount and each update. The testing package binds one for every test, so they run in a request
90
+ * scope like the test's HTTP requests (a request, the carried session, actingAs users) and what they change carries
91
+ * on to the next request. Without one they run in the current scope.
92
+ */
93
+ type TestRequestScope = <T>(run: () => Promise<T>) => Promise<T>;
94
+ declare const TestRequestScope: _orieljs_contracts.Token<TestRequestScope>;
87
95
  /**
88
96
  * Livewire's Livewire::test(): drives one component through the real mount → snapshot → checksum → update pipeline
89
- * (no HTTP). Actions are async (`await counter.call('increment')`); assertions are sync and return `this`.
97
+ * (no HTTP), inside the test's request scope. Actions are async (`await counter.call('increment')`); assertions are
98
+ * sync and return `this`.
90
99
  */
91
100
  declare class Testable<C extends Component = Component> {
92
101
  #private;
93
102
  private readonly manager;
103
+ private readonly scope;
94
104
  private constructor();
95
105
  /** Mounts `component` with `params`, as `<Live component={…} {...params} />` would. */
96
106
  static create<C extends Component>(component: Class<C> | string, params?: Record<string, unknown>): Promise<Testable<C>>;
@@ -178,8 +188,14 @@ declare class LiveComponentManager implements LiveManager {
178
188
  /** Applies each component's updates and calls, re-renders it and returns its new snapshot and effects. */
179
189
  update(request: LiveUpdateRequest): Promise<LiveUpdateResponse>;
180
190
  updateComponent(snapshotJson: string, updates: Record<string, unknown>, calls: Call[]): Promise<ComponentResult>;
181
- /** The route action of POST /oriel/live/update. */
182
- handle(request: HttpRequest): Promise<LiveUpdateResponse>;
191
+ /**
192
+ * The route action of POST /oriel/live/update. Each component's update runs behind the persistent middleware of the
193
+ * route that rendered its page (Livewire's): `auth`, `verified`, `guest` and `can`, with the page route's parameters
194
+ * bound again. A signed-out tab gets a 401, a revoked ability a 403 and a deleted model a 404, before the component
195
+ * hydrates. When a middleware answers instead (a redirect), that is the response. A key first flashed during the
196
+ * update shows in this response only, unless the update redirects.
197
+ */
198
+ handle(request: HttpRequest): Promise<LiveUpdateResponse | Response>;
183
199
  /** A full-page component (Route.get('/counter', Counter)): mounted with the route parameters, inside the layout. */
184
200
  page(component: Component): Promise<unknown>;
185
201
  /** Parses and checks a snapshot: a bad checksum throws before anything is hydrated. */
@@ -192,8 +208,14 @@ declare class LiveComponentManager implements LiveManager {
192
208
  private applyUpdate;
193
209
  /** Runs one call from the browser: a magic action, an event, or a public method. Returns its return value. */
194
210
  private call;
195
- /** Renders the component (rendering/rendered hooks, nested children tracked) and checks for a single root. */
211
+ /**
212
+ * Renders the component (rendering/rendered hooks, nested children tracked) and checks for a single root. A
213
+ * WithPagination component's page and page URL are what paginate() reads while it renders, as in its mount() and in
214
+ * its update's hooks and actions.
215
+ */
196
216
  private render;
217
+ /** The absolute URL of the page a memo path belongs to, on the current request's origin ('/' + path without one). */
218
+ private pageUrl;
197
219
  /** Dispatches, redirects: the effects every response carries. */
198
220
  private effects;
199
221
  /** dehydrate() hook, then the state as snapshot data, the error bag, and the checksum over both. */
@@ -203,8 +225,9 @@ declare class LiveComponentManager implements LiveManager {
203
225
  /** The URI the client runtime posts updates to. */
204
226
  declare const UPDATE_URI = "/oriel/live/update";
205
227
  /**
206
- * Binds the LiveManager, merges the `live` config, registers POST /oriel/live/update (named `live.update`, in the `web`
207
- * group: sessions and CSRF apply), the make:live command, and the components under app/Live.
228
+ * 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 make:live
230
+ * command, and the components under app/Live.
208
231
  */
209
232
  declare class LiveServiceProvider extends ServiceProvider {
210
233
  register(): void;
@@ -255,4 +278,65 @@ declare class InvalidLiveRequestError extends LiveError {
255
278
  readonly status: number;
256
279
  }
257
280
 
258
- export { Component, type ComponentResult, Computed, CorruptComponentPayloadError, ErrorBag, InvalidLiveRequestError, Live, LiveComponentManager, LiveComponentNotFoundError, LiveError, type LiveProps, LiveServiceProvider, Locked, MethodNotCallableError, On, PendingEvent, PropertyNotUpdatableError, Testable, UPDATE_URI, componentName, live };
281
+ /** The route middleware an update re-applies by default (config live.persistent_middleware), by alias. */
282
+ declare const PERSISTENT_MIDDLEWARE: string[];
283
+ /** A route middleware after alias and group expansion: 'auth:web' → { cls: Authenticate, parameters: ['web'] }. */
284
+ interface PersistentMiddleware {
285
+ readonly cls: MiddlewareClass;
286
+ readonly parameters: readonly string[];
287
+ }
288
+ /**
289
+ * Livewire's persistent middleware: the middleware among `persistent` (aliases) that the page route (the component's
290
+ * memo path and method) ran, in the order an update runs them. A path no route matches has none.
291
+ */
292
+ declare function persistentMiddleware(router: Router, persistent: readonly string[], method: string, path: string): PersistentMiddleware[];
293
+
294
+ type ComponentConstructor = abstract new (...args: any[]) => Component;
295
+ /** What WithPagination() adds to a component: its page and the actions that move it. */
296
+ interface Paginated {
297
+ /** The current page. The browser may set it; Live reads it as a page number. */
298
+ page: number;
299
+ nextPage(): Promise<void>;
300
+ previousPage(): Promise<void>;
301
+ gotoPage(page: number | string): Promise<void>;
302
+ resetPage(): Promise<void>;
303
+ setPage(page: number | string): Promise<void>;
304
+ }
305
+ /**
306
+ * Livewire's WithPagination: `class Posts extends WithPagination(Component)`. The public `page` (1 to start; there
307
+ * is no address-bar sync) is what `paginate()` shows in the component (in `render()`, `mount()`, `hydrate()`, the
308
+ * update hooks and actions, or in a `@Computed` getter any of them reads), and its page links are built on the
309
+ * page's URL. The actions move it: `o:click="nextPage"`, `previousPage`, `gotoPage(3)`, `setPage(3)`,
310
+ * `resetPage` (call it from `updatedSearch()` so a new search starts on page 1). `updatingPage(page)` and
311
+ * `updatedPage(page)` run around every change. One paginator per component: other page names read the request.
312
+ */
313
+ declare function WithPagination<TBase extends ComponentConstructor>(Base: TBase): TBase & (abstract new (...args: any[]) => Paginated);
314
+
315
+ /**
316
+ * What <Pagination> reads from a paginator. Structural, since Live can't import Database: paginate()'s
317
+ * LengthAwarePaginator (with lastPage(), so page buttons) and simplePaginate()'s Paginator (Previous and Next only).
318
+ */
319
+ interface PaginationSource {
320
+ currentPage(): number;
321
+ lastPage?(): number;
322
+ hasMorePages(): boolean;
323
+ onFirstPage(): boolean;
324
+ /** Page buttons on each side of the current one (the paginator's onEachSide(), 3 by default). */
325
+ readonly onEachSideCount?: number;
326
+ /** Not cursorPaginate(): its pages are cursors, which a component's page number can't follow. */
327
+ readonly nextCursor?: never;
328
+ }
329
+ interface PaginationProps {
330
+ paginator: PaginationSource;
331
+ /** Classes for the <nav>, such as its layout ("flex gap-2"); in your file, so Tailwind generates them. */
332
+ class?: string;
333
+ }
334
+ /**
335
+ * Livewire's pagination links for a WithPagination component: `<Pagination paginator={posts} />` in render(). A
336
+ * <nav aria-label="Pagination"> with « Previous, the page buttons and Next » as Live actions (previousPage,
337
+ * gotoPage(n), nextPage), the current page marked aria-current="page", the ends disabled, the kits' theme classes
338
+ * (btn, btn-secondary). Nothing when everything fits on one page. The address bar is left alone.
339
+ */
340
+ declare function Pagination({ paginator, class: className }: PaginationProps): Htmlable;
341
+
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 };