@querykitjs/web 4.0.0 → 4.1.0
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/README.en.md +101 -15
- package/README.md +102 -16
- package/dist/angular/index.cjs +2 -0
- package/dist/angular/index.cjs.map +1 -0
- package/dist/angular/index.d.cts +70 -0
- package/dist/angular/index.d.ts +70 -0
- package/dist/angular/index.js +2 -0
- package/dist/angular/index.js.map +1 -0
- package/dist/index.cjs +1 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +48 -4
- package/dist/index.d.ts +48 -4
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/react/index.cjs +1 -1
- package/dist/react/index.cjs.map +1 -1
- package/dist/react/index.js +1 -1
- package/dist/react/index.js.map +1 -1
- package/dist/{registry-DOVi_zCU.d.cts → registry-CEq4xFui.d.cts} +46 -44
- package/dist/{registry-DOVi_zCU.d.ts → registry-CEq4xFui.d.ts} +46 -44
- package/dist/vue/index.cjs +2 -0
- package/dist/vue/index.cjs.map +1 -0
- package/dist/vue/index.d.cts +64 -0
- package/dist/vue/index.d.ts +64 -0
- package/dist/vue/index.js +2 -0
- package/dist/vue/index.js.map +1 -0
- package/package.json +67 -6
- package/skills/querykit-web/SKILL.md +63 -3
package/README.en.md
CHANGED
|
@@ -46,12 +46,12 @@ function UsersPage() {
|
|
|
46
46
|
- **Registry** — configure `adapter` + defaults once; `resource<T>()` gives entity-typed, adapter-aware builders/filters/schema/keys/parsers.
|
|
47
47
|
- **Two filter styles** — `f.eq()...` builder (operators typed per field) AND object/array (`{key, operation, value}`). Fully compatible with legacy `IFilter[]`.
|
|
48
48
|
- **3 pagination modes** — offset (`list`), infinite (`infinite`), cursor/keyset (`cursor`); each has its own `parse*` to camelCase the meta.
|
|
49
|
-
- **Adapter-aware `with`** — only **wire-safe** (JSON) per-relation options are suggested for `mongoose`/`drizzle-pg`/`drizzle-sqlite`/`prisma-pg`; function/SQL forms are never offered.
|
|
49
|
+
- **Adapter-aware `with`** — only **wire-safe** (JSON) per-relation options are suggested for `mongoose`/`drizzle-pg`/`drizzle-sqlite`/`prisma-pg`; function/SQL forms are never offered. `mongodb` has no `with` — load relations with a second request.
|
|
50
50
|
- **Declarative schema** — `resource.schema` / `defineListSchema` maps URL params → filter; kills the per-page hand-building.
|
|
51
|
-
- **React hooks** — `useListParams` (react-router inside)
|
|
51
|
+
- **React hooks, Vue composables and Angular signals** — `useListParams` (react-router / vue-router inside) and `useListParamsBase` (router-agnostic) in `/react` and `/vue`; `injectListParams` and `injectListParamsBase` in `/angular`. `createListState` — the same logic, framework-free.
|
|
52
52
|
- **SSR** — `resource.fromSearchParams(sp, {schema})` builds a payload straight from `URLSearchParams`, no hook.
|
|
53
53
|
- **Query keys** — `resource.keys` gives stable keys for TanStack/SWR.
|
|
54
|
-
- **Zero dependency** — no axios/react-query/dayjs. `react` (+ `react-router` for the hook) are optional peers.
|
|
54
|
+
- **Zero dependency** — no axios/react-query/dayjs. `react` (+ `react-router` for the hook) `vue` (+ `vue-router`) and `@angular/core` (+ `@angular/router`, `rxjs`) are optional peers.
|
|
55
55
|
|
|
56
56
|
## Install
|
|
57
57
|
|
|
@@ -59,17 +59,24 @@ function UsersPage() {
|
|
|
59
59
|
bun add @querykitjs/web
|
|
60
60
|
# React hook: react (peer). `useListParams` also peers react-router.
|
|
61
61
|
# The router-agnostic `useListParamsBase` needs no react-router.
|
|
62
|
+
# Vue composable: vue (peer). `useListParams` also peers vue-router.
|
|
63
|
+
# Angular: @angular/core and rxjs (peers). `injectListParams` also peers @angular/router.
|
|
62
64
|
```
|
|
63
65
|
|
|
64
66
|
### Requirements
|
|
65
67
|
|
|
66
|
-
|
|
|
67
|
-
|
|
|
68
|
-
| Node.js
|
|
69
|
-
| `react` (optional peer)
|
|
70
|
-
| `react-router` (optional peer, `useListParams` only)
|
|
68
|
+
| | Version | Tested (`bun run test:matrix`) |
|
|
69
|
+
| ---------------------------------------------------------- | -------------------- | ------------------------------ |
|
|
70
|
+
| Node.js | `>=22.12` | 24 |
|
|
71
|
+
| `react` (optional peer) | `^18.3 \|\| ^19.2.7` | 18.3.1, 19.3.1 |
|
|
72
|
+
| `react-router` (optional peer, `useListParams` only) | `^7.0 \|\| ^8.0` | 7.18.5, 8.5.1 |
|
|
73
|
+
| `vue` (optional peer) | `^3.5` | 3.5.0, 3.5.44 |
|
|
74
|
+
| `vue-router` (optional peer, `useListParams` only) | `^4.6 \|\| ^5` | 4.6.4, 5.4.0 |
|
|
75
|
+
| `@angular/core` (optional peer) | `^21 \|\| ^22` | 21.2.26, 22.2.4 |
|
|
76
|
+
| `@angular/router` (optional peer, `injectListParams` only) | `^21 \|\| ^22` | 21.2.26, 22.2.4 |
|
|
77
|
+
| `rxjs` (optional peer, Angular's own) | `^7.8` | 7.8.2 |
|
|
71
78
|
|
|
72
|
-
Node 20 is no longer supported (`engines`: `>=22.12`). Peer ranges cover the last two majors; both ends are tested.
|
|
79
|
+
Node 20 is no longer supported (`engines`: `>=22.12`). Peer ranges cover the last two majors; both ends are tested. Vue has one supported major: its range starts at 3.5, the oldest minor vue-router 4.6 accepts.
|
|
73
80
|
|
|
74
81
|
**`react-router-dom` → `react-router`.** `useListParams` now imports from `react-router` (v7 and v8 both export `useSearchParams`). A project that has `react-router-dom` installs `react-router` too — on v7 it is already a dependency of `react-router-dom`: `npm i react-router`.
|
|
75
82
|
|
|
@@ -89,7 +96,7 @@ Use (A) when you want defaults in one place, autocomplete, adapter `with`, query
|
|
|
89
96
|
|
|
90
97
|
```ts
|
|
91
98
|
export const qk = createRegistry({
|
|
92
|
-
adapter: "mongoose", // "mongoose" | "drizzle-pg" | "drizzle-sqlite" | "prisma-pg"
|
|
99
|
+
adapter: "mongoose", // "mongoose" | "mongodb" | "drizzle-pg" | "drizzle-sqlite" | "prisma-pg"
|
|
93
100
|
defaults: {
|
|
94
101
|
perPage: 20, // list (offset) page size
|
|
95
102
|
limit: 20, // infinite + cursor page size
|
|
@@ -254,11 +261,12 @@ const { meta } = users.parseList(res.data); // meta.totalCount === null, meta.to
|
|
|
254
261
|
|
|
255
262
|
The registry types `with` per adapter — **serializable options only**:
|
|
256
263
|
|
|
257
|
-
| adapter | `with[rel]` intellisense | Hidden (not offered)
|
|
258
|
-
| ------------------------------- | ------------------------------------------------------------------ |
|
|
259
|
-
| `mongoose` | `boolean \| { select?, populate?, match?, options? }` | —
|
|
260
|
-
| `
|
|
261
|
-
| `
|
|
264
|
+
| adapter | `with[rel]` intellisense | Hidden (not offered) |
|
|
265
|
+
| ------------------------------- | ------------------------------------------------------------------ | --------------------------------------------------------- |
|
|
266
|
+
| `mongoose` | `boolean \| { select?, populate?, match?, options? }` | — |
|
|
267
|
+
| `mongodb` | no `with` — load relations with a second request | any `with` (TS error; the server answers `forbidden-key`) |
|
|
268
|
+
| `drizzle-pg` / `drizzle-sqlite` | `boolean \| { columns?, with?, limit?, offset? }` | `where:(f,op)=>…`, `orderBy:(f,op)=>…`, raw `sql` |
|
|
269
|
+
| `prisma-pg` | `boolean \| { select?, include?, where?, orderBy?, take?, skip? }` | function forms |
|
|
262
270
|
|
|
263
271
|
```ts
|
|
264
272
|
// adapter: "mongoose"
|
|
@@ -338,6 +346,79 @@ import { useListParamsBase } from "@querykitjs/web/react";
|
|
|
338
346
|
const { params, setPage } = useListParamsBase(users, { schema: usersSchema, searchParams, setSearchParams });
|
|
339
347
|
```
|
|
340
348
|
|
|
349
|
+
## Vue composable
|
|
350
|
+
|
|
351
|
+
```ts
|
|
352
|
+
import { useListParams } from "@querykitjs/web/vue"; // uses vue-router internally
|
|
353
|
+
import { useQuery } from "@tanstack/vue-query";
|
|
354
|
+
import { computed } from "vue";
|
|
355
|
+
|
|
356
|
+
const users = qk.resource<IUser>("users");
|
|
357
|
+
const usersSchema = users.schema({ name: { operation: "%_%", trim: true }, status: { operation: "=" } });
|
|
358
|
+
|
|
359
|
+
// inside <script setup>
|
|
360
|
+
const { params, clamped, setParam, setPage, setSort, reset } = useListParams(users, { schema: usersSchema });
|
|
361
|
+
|
|
362
|
+
const q = useQuery({
|
|
363
|
+
queryKey: computed(() => users.keys.list(params.value)),
|
|
364
|
+
queryFn: async () => users.parseList((await axios.post("/users/list", params.value)).data),
|
|
365
|
+
});
|
|
366
|
+
// setParam("status", "active") → router.push({ query }) (a new history entry), page reset
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
The same reads, clamping and setters as the React hook — `params`, `clamped` and `searchParams` are computed refs.
|
|
370
|
+
A repeated key (`?status=a&status=b`) is kept; `?q` (no `=`) reads as an empty value. Works under SSR (Nuxt): no `window` access.
|
|
371
|
+
|
|
372
|
+
**Router-agnostic** — pass `searchParams` (a ref, a getter or a value) to the base composable:
|
|
373
|
+
|
|
374
|
+
```ts
|
|
375
|
+
import { useListParamsBase } from "@querykitjs/web/vue";
|
|
376
|
+
const { params, setPage } = useListParamsBase(users, { schema: usersSchema, searchParams: () => currentSearchParams.value, setSearchParams });
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
## Angular
|
|
380
|
+
|
|
381
|
+
```ts
|
|
382
|
+
import { Component, computed } from "@angular/core";
|
|
383
|
+
import { injectListParams } from "@querykitjs/web/angular"; // uses the Angular Router
|
|
384
|
+
|
|
385
|
+
const users = qk.resource<IUser>("users");
|
|
386
|
+
const usersSchema = users.schema({ name: { operation: "%_%", trim: true }, status: { operation: "=" } });
|
|
387
|
+
|
|
388
|
+
@Component({
|
|
389
|
+
selector: "app-users",
|
|
390
|
+
template: `<button (click)="list.setPage(2)">{{ list.params().page }}</button>`,
|
|
391
|
+
})
|
|
392
|
+
export class UsersPage {
|
|
393
|
+
readonly list = injectListParams(users, { schema: usersSchema });
|
|
394
|
+
readonly queryKey = computed(() => users.keys.list(this.list.params()));
|
|
395
|
+
// list.setParam("status", "active") → router.navigate (a new history entry), page reset
|
|
396
|
+
}
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
The same reads, clamping and setters as the React and Vue versions — `params`, `clamped` and `searchParams` are signals. Call it in an injection context (a field initializer or a constructor); elsewhere Angular throws `NG0203`. A plain ESM entry: no decorator and no Angular package format, it builds with `@angular/build`.
|
|
400
|
+
|
|
401
|
+
**Router-agnostic** — pass your own `searchParams` signal to the base function:
|
|
402
|
+
|
|
403
|
+
```ts
|
|
404
|
+
import { injectListParamsBase } from "@querykitjs/web/angular";
|
|
405
|
+
const searchParams = signal(new URLSearchParams());
|
|
406
|
+
const list = injectListParamsBase(users, { schema: usersSchema, searchParams, setSearchParams: next => searchParams.set(next) });
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
## Any framework — `createListState`
|
|
410
|
+
|
|
411
|
+
The logic behind all of them, framework-free (Svelte, Solid, vanilla). Pure: it reads search params and returns the next ones; you navigate.
|
|
412
|
+
|
|
413
|
+
```ts
|
|
414
|
+
import { createListState } from "@querykitjs/web";
|
|
415
|
+
|
|
416
|
+
const list = createListState(users, { schema: usersSchema });
|
|
417
|
+
const { params, clamped } = list.read(new URLSearchParams(location.search));
|
|
418
|
+
const next = list.actions(new URLSearchParams(location.search)).setPage(2); // a new URLSearchParams
|
|
419
|
+
history.pushState(null, "", `?${next}`);
|
|
420
|
+
```
|
|
421
|
+
|
|
341
422
|
## SSR — `fromSearchParams` (no hook)
|
|
342
423
|
|
|
343
424
|
For server components / `getServerSideProps` — a payload straight from the URL:
|
|
@@ -387,6 +468,8 @@ const cur = users.parseCursor(curRes.data); // cur.meta.nextCursor, cur.meta.has
|
|
|
387
468
|
|
|
388
469
|
### TanStack `useQuery` (offset)
|
|
389
470
|
|
|
471
|
+
> Ready-made options: [`@querykitjs/tanstack-query`](../tanstack-query/README.en.md) builds these objects for you — `listOptions`, `infiniteOptions`, `cursorOptions` (key, fetch, meta parsing, page params).
|
|
472
|
+
|
|
390
473
|
```tsx
|
|
391
474
|
import { useQuery } from "@tanstack/react-query";
|
|
392
475
|
const users = qk.resource<IUser>("users");
|
|
@@ -585,6 +668,9 @@ const { data, meta } = users.parseList(await res.json());
|
|
|
585
668
|
| `encodeSort/decodeSort` | `[{key,direction}]` ↔ `"-createdAt,id"` (multi-field) |
|
|
586
669
|
| `useListParams(resource, {schema})` (`/react`) | URL-sync hook (react-router) |
|
|
587
670
|
| `useListParamsBase(resource, {schema, searchParams, setSearchParams})` (`/react`) | router-agnostic hook |
|
|
671
|
+
| `useListParams` / `useListParamsBase` (`/vue`) | the same as Vue composables (computed refs) |
|
|
672
|
+
| `injectListParams` / `injectListParamsBase` (`/angular`) | the same as Angular signals |
|
|
673
|
+
| `createListState(resource, {schema, url?, with?})` | framework-free `read(sp)` / `actions(sp)` |
|
|
588
674
|
|
|
589
675
|
## Development
|
|
590
676
|
|
package/README.md
CHANGED
|
@@ -46,12 +46,12 @@ function UsersPage() {
|
|
|
46
46
|
- **Registry** — `adapter` + default'larni bir joyda sozlang, `resource<T>()` entity'ga tipli, adapter-aware builder/filter/schema/keys/parser beradi.
|
|
47
47
|
- **Ikkala filter uslubi** — `f.eq()...` builder (maydon tipiga qarab operator) VA object/massiv (`{key, operation, value}`). Legacy `IFilter[]` bilan to'liq mos.
|
|
48
48
|
- **3 pagination** — offset (`list`), infinite (`infinite`), cursor/keyset (`cursor`); har birining meta'si alohida `parse*` bilan camelCase'ga o'giriladi.
|
|
49
|
-
- **Adapter-aware `with`** — `mongoose`/`drizzle-pg`/`drizzle-sqlite`/`prisma-pg` uchun faqat **wire-safe** (JSON) relation opsiyalari intellisense'
|
|
49
|
+
- **Adapter-aware `with`** — `mongoose`/`drizzle-pg`/`drizzle-sqlite`/`prisma-pg` uchun faqat **wire-safe** (JSON) relation opsiyalari intellisense'da; funksiya/SQL formalari ko'rsatilmaydi. `mongodb` da `with` yo'q — relation'larni ikkinchi so'rov bilan yuklang.
|
|
50
50
|
- **Declarative schema** — `resource.schema` / `defineListSchema` bilan URL param → filter; har sahifadagi qo'lda qurishni yo'q qiladi.
|
|
51
|
-
- **React hook** — `useListParams` (`react-router` ichida)
|
|
51
|
+
- **React hook, Vue composable va Angular signal** — `useListParams` (`react-router` / `vue-router` ichida) va `useListParamsBase` (router-agnostik) `/react` va `/vue` da; `injectListParams` va `injectListParamsBase` `/angular` da. `createListState` — shu mantiq, framework'siz.
|
|
52
52
|
- **SSR** — `resource.fromSearchParams(sp, {schema})` hook'siz, `URLSearchParams`'dan to'g'ridan-to'g'ri payload.
|
|
53
53
|
- **Query keys** — `resource.keys` TanStack/SWR uchun barqaror kalitlar.
|
|
54
|
-
- **Zero dependency** — axios/react-query/dayjs YO'Q. `react` (+ hook uchun `react-router`) ixtiyoriy peer.
|
|
54
|
+
- **Zero dependency** — axios/react-query/dayjs YO'Q. `react` (+ hook uchun `react-router`) `vue` (+ `vue-router`) va `@angular/core` (+ `@angular/router`, `rxjs`) ixtiyoriy peer.
|
|
55
55
|
|
|
56
56
|
## O'rnatish
|
|
57
57
|
|
|
@@ -59,17 +59,24 @@ function UsersPage() {
|
|
|
59
59
|
bun add @querykitjs/web
|
|
60
60
|
# React hook uchun: react (peer). `useListParams` uchun qo'shimcha: react-router (peer).
|
|
61
61
|
# Router-agnostik `useListParamsBase` react-router talab qilmaydi.
|
|
62
|
+
# Vue composable uchun: vue (peer). `useListParams` uchun qo'shimcha: vue-router (peer).
|
|
63
|
+
# Angular uchun: @angular/core va rxjs (peer). `injectListParams` uchun qo'shimcha: @angular/router (peer).
|
|
62
64
|
```
|
|
63
65
|
|
|
64
66
|
### Talablar
|
|
65
67
|
|
|
66
|
-
|
|
|
67
|
-
|
|
|
68
|
-
| Node.js
|
|
69
|
-
| `react` (ixtiyoriy peer)
|
|
70
|
-
| `react-router` (ixtiyoriy peer, faqat `useListParams`)
|
|
68
|
+
| | Versiya | Tekshiriladi (`bun run test:matrix`) |
|
|
69
|
+
| ------------------------------------------------------------ | -------------------- | ------------------------------------ |
|
|
70
|
+
| Node.js | `>=22.12` | 24 |
|
|
71
|
+
| `react` (ixtiyoriy peer) | `^18.3 \|\| ^19.2.7` | 18.3.1, 19.3.1 |
|
|
72
|
+
| `react-router` (ixtiyoriy peer, faqat `useListParams`) | `^7.0 \|\| ^8.0` | 7.18.5, 8.5.1 |
|
|
73
|
+
| `vue` (ixtiyoriy peer) | `^3.5` | 3.5.0, 3.5.44 |
|
|
74
|
+
| `vue-router` (ixtiyoriy peer, faqat `useListParams`) | `^4.6 \|\| ^5` | 4.6.4, 5.4.0 |
|
|
75
|
+
| `@angular/core` (ixtiyoriy peer) | `^21 \|\| ^22` | 21.2.26, 22.2.4 |
|
|
76
|
+
| `@angular/router` (ixtiyoriy peer, faqat `injectListParams`) | `^21 \|\| ^22` | 21.2.26, 22.2.4 |
|
|
77
|
+
| `rxjs` (ixtiyoriy peer, Angular'ning o'ziniki) | `^7.8` | 7.8.2 |
|
|
71
78
|
|
|
72
|
-
Node 20 endi qo'llab-quvvatlanmaydi (`engines`: `>=22.12`). Peer diapazoni — oxirgi ikki major; ikkala cheti testda.
|
|
79
|
+
Node 20 endi qo'llab-quvvatlanmaydi (`engines`: `>=22.12`). Peer diapazoni — oxirgi ikki major; ikkala cheti testda. Vue'ning bitta qo'llab-quvvatlanadigan major'i bor: diapazon 3.5 dan — vue-router 4.6 qabul qiladigan eng eski minor.
|
|
73
80
|
|
|
74
81
|
**`react-router-dom` → `react-router`.** `useListParams` endi `react-router`dan import qiladi (v7 va v8 ikkalasi ham `useSearchParams` eksport qiladi). `react-router-dom` o'rnatilgan loyiha `react-router` ni ham o'rnatadi — v7 da u allaqachon `react-router-dom`ning bog'liqligi: `npm i react-router`.
|
|
75
82
|
|
|
@@ -89,7 +96,7 @@ import { buildListParams, f } from "@querykitjs/web";
|
|
|
89
96
|
|
|
90
97
|
```ts
|
|
91
98
|
export const qk = createRegistry({
|
|
92
|
-
adapter: "mongoose", // "mongoose" | "drizzle-pg" | "drizzle-sqlite" | "prisma-pg"
|
|
99
|
+
adapter: "mongoose", // "mongoose" | "mongodb" | "drizzle-pg" | "drizzle-sqlite" | "prisma-pg"
|
|
93
100
|
defaults: {
|
|
94
101
|
perPage: 20, // list (offset) sahifa hajmi
|
|
95
102
|
limit: 20, // infinite + cursor hajmi
|
|
@@ -254,11 +261,12 @@ const { meta } = users.parseList(res.data); // meta.totalCount === null, meta.to
|
|
|
254
261
|
|
|
255
262
|
Registry `with`ni adapter bo'yicha tiplaydi — **faqat serializable** opsiyalar:
|
|
256
263
|
|
|
257
|
-
| adapter | `with[rel]` intellisense | Ko'rsatilmaydi
|
|
258
|
-
| ------------------------------- | ------------------------------------------------------------------ |
|
|
259
|
-
| `mongoose` | `boolean \| { select?, populate?, match?, options? }` | —
|
|
260
|
-
| `
|
|
261
|
-
| `
|
|
264
|
+
| adapter | `with[rel]` intellisense | Ko'rsatilmaydi |
|
|
265
|
+
| ------------------------------- | ------------------------------------------------------------------ | --------------------------------------------------- |
|
|
266
|
+
| `mongoose` | `boolean \| { select?, populate?, match?, options? }` | — |
|
|
267
|
+
| `mongodb` | `with` yo'q — relation'larni ikkinchi so'rov bilan yuklang | har qanday `with` (TS xato; server `forbidden-key`) |
|
|
268
|
+
| `drizzle-pg` / `drizzle-sqlite` | `boolean \| { columns?, with?, limit?, offset? }` | `where:(f,op)=>…`, `orderBy:(f,op)=>…`, raw `sql` |
|
|
269
|
+
| `prisma-pg` | `boolean \| { select?, include?, where?, orderBy?, take?, skip? }` | funksiya formalar |
|
|
262
270
|
|
|
263
271
|
```ts
|
|
264
272
|
// adapter: "mongoose"
|
|
@@ -267,7 +275,7 @@ users.list({ with: { author: { select: "name email" } } }); // ✓ `select`
|
|
|
267
275
|
users.list({ with: { author: { columns: { name: true } } } }); // ✓ `columns`; `where`-fn yashirin
|
|
268
276
|
```
|
|
269
277
|
|
|
270
|
-
Runtime o'zgarmaydi (pass-through) — bu sof DX. Funksiya/SQL
|
|
278
|
+
Runtime o'zgarmaydi (pass-through) — bu sof DX. Funksiya/SQL formalarini JSON'da yubora olmaganingiz uchun ular umuman taklif qilinmaydi.
|
|
271
279
|
|
|
272
280
|
## Declarative list schema — URL ↔ filter
|
|
273
281
|
|
|
@@ -338,6 +346,79 @@ import { useListParamsBase } from "@querykitjs/web/react";
|
|
|
338
346
|
const { params, setPage } = useListParamsBase(users, { schema: usersSchema, searchParams, setSearchParams });
|
|
339
347
|
```
|
|
340
348
|
|
|
349
|
+
## Vue composable
|
|
350
|
+
|
|
351
|
+
```ts
|
|
352
|
+
import { useListParams } from "@querykitjs/web/vue"; // ichida vue-router
|
|
353
|
+
import { useQuery } from "@tanstack/vue-query";
|
|
354
|
+
import { computed } from "vue";
|
|
355
|
+
|
|
356
|
+
const users = qk.resource<IUser>("users");
|
|
357
|
+
const usersSchema = users.schema({ name: { operation: "%_%", trim: true }, status: { operation: "=" } });
|
|
358
|
+
|
|
359
|
+
// <script setup> ichida
|
|
360
|
+
const { params, clamped, setParam, setPage, setSort, reset } = useListParams(users, { schema: usersSchema });
|
|
361
|
+
|
|
362
|
+
const q = useQuery({
|
|
363
|
+
queryKey: computed(() => users.keys.list(params.value)),
|
|
364
|
+
queryFn: async () => users.parseList((await axios.post("/users/list", params.value)).data),
|
|
365
|
+
});
|
|
366
|
+
// setParam("status", "active") → router.push({ query }) (tarixda yangi yozuv), page reset
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
React hook bilan bir xil o'qish, clamp va setterlar — `params`, `clamped` va `searchParams` computed ref.
|
|
370
|
+
Takroriy kalit (`?status=a&status=b`) saqlanadi; `?q` (`=` siz) bo'sh qiymat sifatida o'qiladi. SSR (Nuxt) da ishlaydi: `window` ga murojaat yo'q.
|
|
371
|
+
|
|
372
|
+
**Router-agnostik** — base composable'ga `searchParams` (ref, getter yoki qiymat) bering:
|
|
373
|
+
|
|
374
|
+
```ts
|
|
375
|
+
import { useListParamsBase } from "@querykitjs/web/vue";
|
|
376
|
+
const { params, setPage } = useListParamsBase(users, { schema: usersSchema, searchParams: () => currentSearchParams.value, setSearchParams });
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
## Angular
|
|
380
|
+
|
|
381
|
+
```ts
|
|
382
|
+
import { Component, computed } from "@angular/core";
|
|
383
|
+
import { injectListParams } from "@querykitjs/web/angular"; // ichida Angular Router
|
|
384
|
+
|
|
385
|
+
const users = qk.resource<IUser>("users");
|
|
386
|
+
const usersSchema = users.schema({ name: { operation: "%_%", trim: true }, status: { operation: "=" } });
|
|
387
|
+
|
|
388
|
+
@Component({
|
|
389
|
+
selector: "app-users",
|
|
390
|
+
template: `<button (click)="list.setPage(2)">{{ list.params().page }}</button>`,
|
|
391
|
+
})
|
|
392
|
+
export class UsersPage {
|
|
393
|
+
readonly list = injectListParams(users, { schema: usersSchema });
|
|
394
|
+
readonly queryKey = computed(() => users.keys.list(this.list.params()));
|
|
395
|
+
// list.setParam("status", "active") → router.navigate (tarixda yangi yozuv), page reset
|
|
396
|
+
}
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
React va Vue bilan bir xil o'qish, clamp va setterlar — `params`, `clamped` va `searchParams` signal. Injection context'da chaqiring (field initializer yoki constructor); boshqa joyda Angular `NG0203` xatosini beradi. Oddiy ESM entry: decorator ham, Angular package format ham yo'q, `@angular/build` bilan build bo'ladi.
|
|
400
|
+
|
|
401
|
+
**Router-agnostik** — base funksiyaga o'z `searchParams` signal'ingizni bering:
|
|
402
|
+
|
|
403
|
+
```ts
|
|
404
|
+
import { injectListParamsBase } from "@querykitjs/web/angular";
|
|
405
|
+
const searchParams = signal(new URLSearchParams());
|
|
406
|
+
const list = injectListParamsBase(users, { schema: usersSchema, searchParams, setSearchParams: next => searchParams.set(next) });
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
## Istalgan framework — `createListState`
|
|
410
|
+
|
|
411
|
+
Ularning hammasi ortidagi mantiq, framework'siz (Svelte, Solid, vanilla). Toza: search params'ni o'qiydi va keyingisini qaytaradi; navigatsiya sizda.
|
|
412
|
+
|
|
413
|
+
```ts
|
|
414
|
+
import { createListState } from "@querykitjs/web";
|
|
415
|
+
|
|
416
|
+
const list = createListState(users, { schema: usersSchema });
|
|
417
|
+
const { params, clamped } = list.read(new URLSearchParams(location.search));
|
|
418
|
+
const next = list.actions(new URLSearchParams(location.search)).setPage(2); // yangi URLSearchParams
|
|
419
|
+
history.pushState(null, "", `?${next}`);
|
|
420
|
+
```
|
|
421
|
+
|
|
341
422
|
## SSR — `fromSearchParams` (hook'siz)
|
|
342
423
|
|
|
343
424
|
Server component / `getServerSideProps` uchun — URL'дан to'g'ridan-to'g'ri payload:
|
|
@@ -387,6 +468,8 @@ const cur = users.parseCursor(curRes.data); // cur.meta.nextCursor, cur.meta.has
|
|
|
387
468
|
|
|
388
469
|
### TanStack `useQuery` (offset)
|
|
389
470
|
|
|
471
|
+
> Tayyor options: [`@querykitjs/tanstack-query`](../tanstack-query/README.md) shu obyektlarni o'zi quradi — `listOptions`, `infiniteOptions`, `cursorOptions` (kalit, fetch, meta parse, sahifa parametrlari).
|
|
472
|
+
|
|
390
473
|
```tsx
|
|
391
474
|
import { useQuery } from "@tanstack/react-query";
|
|
392
475
|
const users = qk.resource<IUser>("users");
|
|
@@ -585,6 +668,9 @@ const { data, meta } = users.parseList(await res.json());
|
|
|
585
668
|
| `encodeSort/decodeSort` | `[{key,direction}]` ↔ `"-createdAt,id"` (multi-field) |
|
|
586
669
|
| `useListParams(resource, {schema})` (`/react`) | URL-sync hook (react-router) |
|
|
587
670
|
| `useListParamsBase(resource, {schema, searchParams, setSearchParams})` (`/react`) | router-agnostik hook |
|
|
671
|
+
| `useListParams` / `useListParamsBase` (`/vue`) | Vue composable sifatida xuddi shu (computed ref) |
|
|
672
|
+
| `injectListParams` / `injectListParamsBase` (`/angular`) | Angular signal sifatida xuddi shu |
|
|
673
|
+
| `createListState(resource, {schema, url?, with?})` | framework'siz `read(sp)` / `actions(sp)` |
|
|
588
674
|
|
|
589
675
|
## Ishlab chiqish
|
|
590
676
|
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
'use strict';var core=require('@angular/core'),rxjsInterop=require('@angular/core/rxjs-interop'),router=require('@angular/router');require('@querykitjs/core');function P(a){let e={};for(let s of new Set(a.keys())){let t=a.getAll(s);e[s]=t.length>1?t:t[0]??"";}return e}var p={pageParam:"page",sizeParam:"size",sortParam:"sortType"};function A(a){return a.map(e=>typeof e=="string"?e.trim():e&&e.key?(e.direction==="desc"?"-":"")+e.key:"").filter(Boolean).join(",")}function l(a){return new URLSearchParams(a)}function g(a,e,s,t={}){let r={...p,...t},n=l(a);return n.delete(r.pageParam),s==null||s===""?n.delete(e):n.set(e,s),n}function S(a,e,s={}){let t={...p,...s},r=l(a);return r.set(t.pageParam,String(e)),r}function L(a,e,s={}){let t={...p,...s},r=l(a);return r.delete(t.pageParam),r.set(t.sizeParam,String(e)),r}function h(a,e,s={}){let t={...p,...s},r=l(a);r.delete(t.pageParam);let n=A(e);return n?r.set(t.sortParam,n):r.delete(t.sortParam),r}function y(a,e=[]){let s=new URLSearchParams;for(let t of e){let r=a.get(t);r!==null&&s.set(t,r);}return s}function U(a,e){let{schema:s,url:t,with:r}=e;return {read(n){let o=a.fromSearchParams(n,{schema:s,url:t,with:r});return {params:o,clamped:o.clamped===true}},actions(n){return {setParam:(o,c)=>g(n,o,c,t),setPage:o=>S(n,o,t),setSize:o=>L(n,o,t),setSort:o=>h(n,o,t),reset:o=>y(n,o)}}}}function f(a,e){let{schema:s,searchParams:t,setSearchParams:r,url:n,with:o}=e,c=U(a,{schema:s,url:n,with:o}),d=core.computed(()=>c.read(t())),m=()=>c.actions(t());return {params:core.computed(()=>d().params),clamped:core.computed(()=>d().clamped),searchParams:t,setParam:(i,x)=>r(m().setParam(i,x)),setPage:i=>r(m().setPage(i)),setSize:i=>r(m().setSize(i)),setSort:i=>r(m().setSort(i)),reset:i=>r(m().reset(i))}}function T(a){return new URLSearchParams(a.keys.flatMap(e=>a.getAll(e).map(s=>[e,s])))}function w(a,e){let s=core.inject(router.ActivatedRoute),t=core.inject(router.Router),r=rxjsInterop.toSignal(s.queryParamMap,{requireSync:true});return f(a,{...e,searchParams:core.computed(()=>T(r())),setSearchParams:n=>{t.navigate([],{relativeTo:s,queryParams:P(n)});}})}exports.injectListParams=w;exports.injectListParamsBase=f;//# sourceMappingURL=index.cjs.map
|
|
2
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/route-query.ts","../../src/url.ts","../../src/list-state.ts","../../src/angular/inject-list-params-base.ts","../../src/angular/inject-list-params.ts"],"names":["searchParamsToQuery","searchParams","query","key","values","URL_DEFAULTS","encodeSort","sort","i","clone","setParam","name","value","url","cfg","next","setPage","page","setSize","size","setSort","encoded","resetParams","keep","createListState","resource","options","schema","withRel","params","injectListParamsBase","setSearchParams","state","read","computed","actions","paramMapToSearchParams","paramMap","injectListParams","route","inject","ActivatedRoute","router","Router","toSignal"],"mappings":"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","file":"index.cjs","sourcesContent":["/**\n^ * A route query as vue-router gives it: v4 has `string | null | (string | null)[]` per key,\n * v5 always an array. `null` is a key without `=` (`?q`).\n */\nexport type RouteQuery = Record<string, string | null | (string | null)[] | undefined>;\n\n/** Route query → search params: a repeated key keeps every value; `null` becomes an empty value. */\nexport function queryToSearchParams(query: RouteQuery): URLSearchParams {\n const searchParams = new URLSearchParams();\n for (const [key, value] of Object.entries(query)) {\n for (const item of Array.isArray(value) ? value : [value]) {\n if (item !== undefined) searchParams.append(key, item ?? \"\");\n }\n }\n return searchParams;\n}\n\n/** Search params → route query for `router.push({ query })`: one value → string, a repeated key → array. */\nexport function searchParamsToQuery(searchParams: URLSearchParams): Record<string, string | string[]> {\n const query: Record<string, string | string[]> = {};\n for (const key of new Set(searchParams.keys())) {\n const values = searchParams.getAll(key);\n query[key] = values.length > 1 ? values : (values[0] ?? \"\");\n }\n return query;\n}\n","import { DEFAULT_MAX_OFFSET, DEFAULT_MAX_PER_PAGE, DEFAULT_MAX_SORT_FIELDS, DEFAULT_PER_PAGE } from \"@querykitjs/core\";\n\nimport { buildListParams, clampInt } from \"./query\";\nimport type { FieldDescriptor, ListSchema } from \"./schema\";\nimport type { FieldCondition, Filter, FilterNode, FilterValue, ListParams, ListPayload, Sort, SortDirection, SortInput } from \"./types\";\n\n/** URL param nomlari (sahifa/o'lcham/sort) — sozlanadigan. */\nexport interface UrlConfig {\n pageParam?: string;\n sizeParam?: string;\n sortParam?: string;\n}\n\nconst URL_DEFAULTS: Required<UrlConfig> = {\n pageParam: \"page\",\n sizeParam: \"size\",\n sortParam: \"sortType\",\n};\n\n/**\n * `\"-createdAt,id,-appNumber\"` → `[{ key, direction }, …]` — comma-separated,\n * order preserved, `-` prefix = `desc` (else `asc`). Empty → `undefined`.\n */\nexport function decodeSort(value: string | null | undefined): Sort | undefined {\n if (!value) return undefined;\n const items = value\n .split(\",\")\n .map(s => s.trim())\n .filter(Boolean)\n .map(tok => {\n const desc = tok.startsWith(\"-\");\n return { key: desc ? tok.slice(1) : tok, direction: (desc ? \"desc\" : \"asc\") as SortDirection };\n })\n .filter(i => i.key); // drop empty keys (e.g. a lone \"-\")\n return items.length ? items : undefined;\n}\n\n/** `[{ key, direction }, …]` (or `\"-field\"` shorthand strings) → `\"-createdAt,id,-appNumber\"` (empty → `\"\"`). */\nexport function encodeSort(sort: SortInput): string {\n return sort\n .map(i => (typeof i === \"string\" ? i.trim() : i && i.key ? (i.direction === \"desc\" ? \"-\" : \"\") + i.key : \"\"))\n .filter(Boolean)\n .join(\",\");\n}\n\n/**\n * Schema + searchParams → filter. Applies `range`/`between`/`search`/`split`/\n * `default`/`trim`; empty/absent params are dropped. Returns a flat\n * `FieldCondition[]` (implicit AND), or a tree when a `search` OR-group is\n * present (`{ and: [orGroup, …conditions] }`).\n */\nexport function schemaToFilter(schema: ListSchema, searchParams: URLSearchParams): Filter {\n const conditions: FieldCondition[] = [];\n const groups: FilterNode[] = [];\n\n for (const [param, d] of Object.entries(schema) as [string, FieldDescriptor][]) {\n const key = d.key ?? param;\n\n // range → two inclusive conditions (>= , <=)\n if (d.range) {\n const from = searchParams.get(d.range[0]);\n const to = searchParams.get(d.range[1]);\n if (from) conditions.push(cond(key, \">=\", from));\n if (to) conditions.push(cond(key, \"<=\", to));\n continue;\n }\n\n // between → one condition with a 2-tuple value\n if (d.between) {\n const from = searchParams.get(d.between[0]);\n const to = searchParams.get(d.between[1]);\n if (from && to) conditions.push(cond(key, \"between\", [from, to]));\n continue;\n }\n\n // read value, falling back to `default` only when the param is ABSENT\n // (an empty `?x=` means the user cleared the filter → prune, don't default).\n const raw = searchParams.get(param);\n let value: FilterValue | undefined = raw !== null ? raw : d.default;\n if (typeof value === \"string\" && d.trim) value = value.trim();\n if (value === undefined || value === null || value === \"\") continue;\n if (Array.isArray(value) && value.length === 0) continue;\n\n // search → OR of `contains` across the listed fields (needs a string)\n if (d.search && typeof value === \"string\") {\n groups.push({ or: d.search.map(field => cond(field, \"%_%\", value as string)) });\n continue;\n }\n\n // split → comma-string → array (for `in`/`notIn`); guarded against empties\n if (d.split && typeof value === \"string\") {\n const delimiter = d.split === true ? \",\" : d.split;\n const parts = value\n .split(delimiter)\n .map(s => s.trim())\n .filter(Boolean);\n if (parts.length === 0) continue;\n value = parts;\n }\n\n conditions.push(cond(key, d.operation ?? \"=\", value));\n }\n\n return groups.length === 0 ? conditions : { and: [...groups, ...conditions] };\n}\n\nfunction cond(key: string, operation: FieldCondition[\"operation\"], value: FilterValue): FieldCondition {\n return { key, operation, value };\n}\n\n/**\n * The bounds the URL readers clamp to — the builder's own (`createRegistry`\n * `defaults`); an absent one is the core default.\n */\nexport interface UrlLimits {\n perPage?: number;\n maxPerPage?: number;\n maxOffset?: number;\n maxSortFields?: number;\n}\n\n/** {@link ListParams} read from a URL. `clamped` — the URL's page or sort was cut to the limits. */\nexport type UrlListParams = ListParams & { clamped: boolean };\n\n/**\n * A list payload built from a URL. `clamped` is non-enumerable: readable in code,\n * never serialized, so the wire payload is unchanged.\n */\nexport type UrlListPayload = ListPayload & { readonly clamped?: boolean };\n\n/** Marks a payload with a non-enumerable `clamped` flag. */\nexport function withClamped(payload: ListPayload, clamped: boolean): UrlListPayload {\n return Object.defineProperty(payload, \"clamped\", { value: clamped, enumerable: false });\n}\n\n/** The last page the builder accepts: `(page - 1) × perPage ≤ maxOffset`. */\nfunction lastPage(perPage: number | undefined, limits: UrlLimits): number {\n const resolved = clampInt(perPage, limits.perPage ?? DEFAULT_PER_PAGE, limits.maxPerPage ?? DEFAULT_MAX_PER_PAGE);\n return Math.floor((limits.maxOffset ?? DEFAULT_MAX_OFFSET) / resolved) + 1;\n}\n\n/**\n * searchParams → normalizatsiyalanmagan {@link ListParams} (filter/sort/page/perPage).\n * The URL is user input: a page past `maxOffset` becomes the last allowed page and a\n * sort past `maxSortFields` keeps its first fields, with `clamped: true` — so a\n * hand-edited `?page=9999` never throws during render. The builders still throw\n * for a direct call.\n */\nexport function readListParams(schema: ListSchema, searchParams: URLSearchParams, url: UrlConfig = {}, limits: UrlLimits = {}): UrlListParams {\n const cfg = { ...URL_DEFAULTS, ...url };\n const size = searchParams.get(cfg.sizeParam);\n const perPage = size ? Number(size) : undefined;\n const page = Math.trunc(Number(searchParams.get(cfg.pageParam))) || 1;\n const sort = decodeSort(searchParams.get(cfg.sortParam));\n const maxPage = lastPage(perPage, limits);\n const maxSortFields = limits.maxSortFields ?? DEFAULT_MAX_SORT_FIELDS;\n const isPageClamped = page > maxPage;\n const isSortClamped = sort !== undefined && sort.length > maxSortFields;\n return {\n filter: schemaToFilter(schema, searchParams),\n sort: isSortClamped ? sort.slice(0, maxSortFields) : sort,\n page: isPageClamped ? maxPage : page,\n perPage,\n clamped: isPageClamped || isSortClamped,\n };\n}\n\n/** searchParams → to'liq, yuborishga tayyor {@link ListPayload} (core default limits; clamped like {@link readListParams}). */\nexport function searchParamsToPayload(schema: ListSchema, searchParams: URLSearchParams, url?: UrlConfig): UrlListPayload {\n const params = readListParams(schema, searchParams, url);\n return withClamped(buildListParams(params), params.clamped);\n}\n\n/* ------------------------- URL yozish (immutable) ------------------------- */\n/* Har biri YANGI URLSearchParams qaytaradi; filter/sort o'zgarsa `page` reset. */\n\nfunction clone(searchParams: URLSearchParams): URLSearchParams {\n return new URLSearchParams(searchParams);\n}\n\n/** Filter param'ini o'rnatadi/o'chiradi va `page`ni reset qiladi. */\nexport function setParam(searchParams: URLSearchParams, name: string, value: string | null | undefined, url: UrlConfig = {}): URLSearchParams {\n const cfg = { ...URL_DEFAULTS, ...url };\n const next = clone(searchParams);\n next.delete(cfg.pageParam);\n if (value === null || value === undefined || value === \"\") next.delete(name);\n else next.set(name, value);\n return next;\n}\n\n/** Sahifani o'rnatadi (`page` reset qilinmaydi). */\nexport function setPage(searchParams: URLSearchParams, page: number, url: UrlConfig = {}): URLSearchParams {\n const cfg = { ...URL_DEFAULTS, ...url };\n const next = clone(searchParams);\n next.set(cfg.pageParam, String(page));\n return next;\n}\n\n/** Sahifa o'lchamini o'rnatadi va `page`ni reset qiladi. */\nexport function setSize(searchParams: URLSearchParams, size: number, url: UrlConfig = {}): URLSearchParams {\n const cfg = { ...URL_DEFAULTS, ...url };\n const next = clone(searchParams);\n next.delete(cfg.pageParam);\n next.set(cfg.sizeParam, String(size));\n return next;\n}\n\n/** Sortni o'rnatadi (`[{ key, direction }, …]`) va `page`ni reset qiladi. */\nexport function setSort(searchParams: URLSearchParams, sort: SortInput, url: UrlConfig = {}): URLSearchParams {\n const cfg = { ...URL_DEFAULTS, ...url };\n const next = clone(searchParams);\n next.delete(cfg.pageParam);\n const encoded = encodeSort(sort);\n if (encoded) next.set(cfg.sortParam, encoded);\n else next.delete(cfg.sortParam);\n return next;\n}\n\n/** Barcha list param'larini tozalaydi (ixtiyoriy saqlanadigan kalitlar bilan). */\nexport function resetParams(searchParams: URLSearchParams, keep: string[] = []): URLSearchParams {\n const next = new URLSearchParams();\n for (const key of keep) {\n const value = searchParams.get(key);\n if (value !== null) next.set(key, value);\n }\n return next;\n}\n","import type { AdapterName, WithFor } from \"./adapter\";\nimport type { Resource } from \"./registry\";\nimport type { ListSchema } from \"./schema\";\nimport type { ListPayload, SortInput } from \"./types\";\nimport type { UrlConfig } from \"./url\";\nimport { resetParams, setPage, setParam, setSize, setSort } from \"./url\";\n\nexport interface ListStateOptions<A extends AdapterName> {\n /** URL↔filter schema ({@link defineListSchema} / `resource.schema`). */\n schema: ListSchema;\n /** URL param names (page/size/sortType) — optional. */\n url?: UrlConfig;\n /** Fixed relations to load on every request (not URL-driven). */\n with?: WithFor<A>;\n}\n\nexport interface ListStateRead {\n /** Ready-to-send list payload (built from the URL via the resource's defaults). */\n params: ListPayload;\n /** The URL's page or sort was past the limits and was clamped (`?page=9999` → the last allowed page). */\n clamped: boolean;\n}\n\n/** URL setters: each returns the NEXT search params and never mutates the given ones. */\nexport interface ListStateActions {\n /** Set/clear a filter param (resets `page`). */\n setParam: (name: string, value: string | null | undefined) => URLSearchParams;\n setPage: (page: number) => URLSearchParams;\n /** Resets `page`. */\n setSize: (size: number) => URLSearchParams;\n /** Resets `page`; an empty sort clears the param. */\n setSort: (sort: SortInput) => URLSearchParams;\n /** Clears every param except `keep`. */\n reset: (keep?: string[]) => URLSearchParams;\n}\n\nexport interface ListState {\n read: (searchParams: URLSearchParams) => ListStateRead;\n actions: (searchParams: URLSearchParams) => ListStateActions;\n}\n\n/**\n * Framework-free list URL state — the logic behind `useListParams` in `@querykitjs/web/react`\n * and `@querykitjs/web/vue`, for any other framework (Svelte, Solid, vanilla). Pure: it reads\n * search params and returns the next ones; navigating is the caller's job.\n *\n * @example\n * ```ts\n * const list = createListState(users, { schema: usersSchema });\n * const { params, clamped } = list.read(new URLSearchParams(location.search));\n * history.pushState(null, \"\", `?${list.actions(new URLSearchParams(location.search)).setPage(2)}`);\n * ```\n */\nexport function createListState<T, A extends AdapterName>(resource: Resource<T, A>, options: ListStateOptions<A>): ListState {\n const { schema, url, with: withRel } = options;\n return {\n read(searchParams) {\n const params = resource.fromSearchParams(searchParams, { schema, url, with: withRel });\n return { params, clamped: params.clamped === true };\n },\n actions(searchParams) {\n return {\n setParam: (name, value) => setParam(searchParams, name, value, url),\n setPage: page => setPage(searchParams, page, url),\n setSize: size => setSize(searchParams, size, url),\n setSort: sort => setSort(searchParams, sort, url),\n reset: keep => resetParams(searchParams, keep),\n };\n },\n };\n}\n","import { computed, type Signal } from \"@angular/core\";\n\nimport type { AdapterName, WithFor } from \"../adapter\";\nimport { createListState } from \"../list-state\";\nimport type { Resource } from \"../registry\";\nimport type { ListSchema } from \"../schema\";\nimport type { ListPayload, SortInput } from \"../types\";\nimport type { UrlConfig } from \"../url\";\n\nexport interface InjectListParamsBaseOptions<A extends AdapterName> {\n /** URL↔filter schema ({@link defineListSchema} / `resource.schema`). */\n schema: ListSchema;\n /** Current URL search params as a signal (from your router). */\n searchParams: Signal<URLSearchParams>;\n /** Apply new search params (`(next) => void`). */\n setSearchParams: (next: URLSearchParams) => void;\n /** URL param names (page/size/sortType) — optional. */\n url?: UrlConfig;\n /** Fixed relations to load on every request (not URL-driven). */\n with?: WithFor<A>;\n}\n\nexport interface InjectListParamsResult {\n /** Ready-to-send list payload (built from the URL via the resource's defaults). */\n params: Signal<ListPayload>;\n /**\n * The URL's page or sort was past the limits and was clamped (`?page=9999` → the last\n * allowed page). The app may rewrite the URL (`setPage(params().page)`).\n */\n clamped: Signal<boolean>;\n searchParams: Signal<URLSearchParams>;\n /** Set/clear a filter param (resets `page`). */\n setParam: (name: string, value: string | null | undefined) => void;\n setPage: (page: number) => void;\n /** Resets `page`. */\n setSize: (size: number) => void;\n /** Resets `page`; an empty sort clears the param. */\n setSort: (sort: SortInput) => void;\n /** Clears every param except `keep`. */\n reset: (keep?: string[]) => void;\n}\n\n/**\n * Router-agnostic list URL state as signals — the Angular twin of the React and Vue\n * `useListParamsBase`: the same reads, clamping and setters. Bring your own\n * `searchParams` signal and `setSearchParams`.\n */\nexport function injectListParamsBase<T, A extends AdapterName>(resource: Resource<T, A>, options: InjectListParamsBaseOptions<A>): InjectListParamsResult {\n const { schema, searchParams, setSearchParams, url, with: withRel } = options;\n const state = createListState(resource, { schema, url, with: withRel });\n const read = computed(() => state.read(searchParams()));\n const actions = () => state.actions(searchParams());\n\n return {\n params: computed(() => read().params),\n clamped: computed(() => read().clamped),\n searchParams,\n setParam: (name, value) => setSearchParams(actions().setParam(name, value)),\n setPage: page => setSearchParams(actions().setPage(page)),\n setSize: size => setSearchParams(actions().setSize(size)),\n setSort: sort => setSearchParams(actions().setSort(sort)),\n reset: keep => setSearchParams(actions().reset(keep)),\n };\n}\n","import { computed, inject } from \"@angular/core\";\nimport { toSignal } from \"@angular/core/rxjs-interop\";\nimport { ActivatedRoute, type ParamMap, Router } from \"@angular/router\";\n\nimport type { AdapterName, WithFor } from \"../adapter\";\nimport type { Resource } from \"../registry\";\nimport { searchParamsToQuery } from \"../route-query\";\nimport type { ListSchema } from \"../schema\";\nimport type { UrlConfig } from \"../url\";\nimport { injectListParamsBase, type InjectListParamsResult } from \"./inject-list-params-base\";\n\nexport interface InjectListParamsOptions<A extends AdapterName> {\n /** URL↔filter schema ({@link defineListSchema} / `resource.schema`). */\n schema: ListSchema;\n /** URL param names (page/size/sortType) — optional. */\n url?: UrlConfig;\n /** Fixed relations to load on every request (not URL-driven). */\n with?: WithFor<A>;\n}\n\nfunction paramMapToSearchParams(paramMap: ParamMap): URLSearchParams {\n return new URLSearchParams(paramMap.keys.flatMap(key => paramMap.getAll(key).map(value => [key, value])));\n}\n\n/**\n * List URL state as signals, with the Angular **Router** (`ActivatedRoute` + `Router`).\n * Reads filters/sort/page from the query params via the schema, builds a ready-to-send\n * payload through the `resource`, and returns URL setters that `router.navigate` (a new\n * history entry; filter/sort/size changes reset `page`). Does **not** fetch. Call it in an\n * injection context (a field initializer or a constructor); elsewhere Angular throws `NG0203`.\n *\n * @example\n * ```ts\n * @Component({ template: `<button (click)=\"list.setPage(2)\">2</button>` })\n * class UsersPage {\n * readonly list = injectListParams(users, { schema: usersSchema });\n * readonly payload = computed(() => this.list.params());\n * }\n * ```\n */\nexport function injectListParams<T, A extends AdapterName>(resource: Resource<T, A>, options: InjectListParamsOptions<A>): InjectListParamsResult {\n const route = inject(ActivatedRoute);\n const router = inject(Router);\n // `queryParamMap` replays its current value on subscribe, so the signal is never empty.\n const paramMap = toSignal(route.queryParamMap, { requireSync: true });\n return injectListParamsBase(resource, {\n ...options,\n searchParams: computed(() => paramMapToSearchParams(paramMap())),\n setSearchParams: next => void router.navigate([], { relativeTo: route, queryParams: searchParamsToQuery(next) }),\n });\n}\n"]}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { AdapterName, ListSchema, UrlConfig, WithFor, ListPayload, SortInput, Resource } from '../index.cjs';
|
|
2
|
+
import { Signal } from '@angular/core';
|
|
3
|
+
import '@querykitjs/core';
|
|
4
|
+
|
|
5
|
+
interface InjectListParamsBaseOptions<A extends AdapterName> {
|
|
6
|
+
/** URL↔filter schema ({@link defineListSchema} / `resource.schema`). */
|
|
7
|
+
schema: ListSchema;
|
|
8
|
+
/** Current URL search params as a signal (from your router). */
|
|
9
|
+
searchParams: Signal<URLSearchParams>;
|
|
10
|
+
/** Apply new search params (`(next) => void`). */
|
|
11
|
+
setSearchParams: (next: URLSearchParams) => void;
|
|
12
|
+
/** URL param names (page/size/sortType) — optional. */
|
|
13
|
+
url?: UrlConfig;
|
|
14
|
+
/** Fixed relations to load on every request (not URL-driven). */
|
|
15
|
+
with?: WithFor<A>;
|
|
16
|
+
}
|
|
17
|
+
interface InjectListParamsResult {
|
|
18
|
+
/** Ready-to-send list payload (built from the URL via the resource's defaults). */
|
|
19
|
+
params: Signal<ListPayload>;
|
|
20
|
+
/**
|
|
21
|
+
* The URL's page or sort was past the limits and was clamped (`?page=9999` → the last
|
|
22
|
+
* allowed page). The app may rewrite the URL (`setPage(params().page)`).
|
|
23
|
+
*/
|
|
24
|
+
clamped: Signal<boolean>;
|
|
25
|
+
searchParams: Signal<URLSearchParams>;
|
|
26
|
+
/** Set/clear a filter param (resets `page`). */
|
|
27
|
+
setParam: (name: string, value: string | null | undefined) => void;
|
|
28
|
+
setPage: (page: number) => void;
|
|
29
|
+
/** Resets `page`. */
|
|
30
|
+
setSize: (size: number) => void;
|
|
31
|
+
/** Resets `page`; an empty sort clears the param. */
|
|
32
|
+
setSort: (sort: SortInput) => void;
|
|
33
|
+
/** Clears every param except `keep`. */
|
|
34
|
+
reset: (keep?: string[]) => void;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Router-agnostic list URL state as signals — the Angular twin of the React and Vue
|
|
38
|
+
* `useListParamsBase`: the same reads, clamping and setters. Bring your own
|
|
39
|
+
* `searchParams` signal and `setSearchParams`.
|
|
40
|
+
*/
|
|
41
|
+
declare function injectListParamsBase<T, A extends AdapterName>(resource: Resource<T, A>, options: InjectListParamsBaseOptions<A>): InjectListParamsResult;
|
|
42
|
+
|
|
43
|
+
interface InjectListParamsOptions<A extends AdapterName> {
|
|
44
|
+
/** URL↔filter schema ({@link defineListSchema} / `resource.schema`). */
|
|
45
|
+
schema: ListSchema;
|
|
46
|
+
/** URL param names (page/size/sortType) — optional. */
|
|
47
|
+
url?: UrlConfig;
|
|
48
|
+
/** Fixed relations to load on every request (not URL-driven). */
|
|
49
|
+
with?: WithFor<A>;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* List URL state as signals, with the Angular **Router** (`ActivatedRoute` + `Router`).
|
|
53
|
+
* Reads filters/sort/page from the query params via the schema, builds a ready-to-send
|
|
54
|
+
* payload through the `resource`, and returns URL setters that `router.navigate` (a new
|
|
55
|
+
* history entry; filter/sort/size changes reset `page`). Does **not** fetch. Call it in an
|
|
56
|
+
* injection context (a field initializer or a constructor); elsewhere Angular throws `NG0203`.
|
|
57
|
+
*
|
|
58
|
+
* @example
|
|
59
|
+
* ```ts
|
|
60
|
+
* @Component({ template: `<button (click)="list.setPage(2)">2</button>` })
|
|
61
|
+
* class UsersPage {
|
|
62
|
+
* readonly list = injectListParams(users, { schema: usersSchema });
|
|
63
|
+
* readonly payload = computed(() => this.list.params());
|
|
64
|
+
* }
|
|
65
|
+
* ```
|
|
66
|
+
*/
|
|
67
|
+
declare function injectListParams<T, A extends AdapterName>(resource: Resource<T, A>, options: InjectListParamsOptions<A>): InjectListParamsResult;
|
|
68
|
+
|
|
69
|
+
export { injectListParams, injectListParamsBase };
|
|
70
|
+
export type { InjectListParamsBaseOptions, InjectListParamsOptions, InjectListParamsResult };
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { AdapterName, ListSchema, UrlConfig, WithFor, ListPayload, SortInput, Resource } from '../index.js';
|
|
2
|
+
import { Signal } from '@angular/core';
|
|
3
|
+
import '@querykitjs/core';
|
|
4
|
+
|
|
5
|
+
interface InjectListParamsBaseOptions<A extends AdapterName> {
|
|
6
|
+
/** URL↔filter schema ({@link defineListSchema} / `resource.schema`). */
|
|
7
|
+
schema: ListSchema;
|
|
8
|
+
/** Current URL search params as a signal (from your router). */
|
|
9
|
+
searchParams: Signal<URLSearchParams>;
|
|
10
|
+
/** Apply new search params (`(next) => void`). */
|
|
11
|
+
setSearchParams: (next: URLSearchParams) => void;
|
|
12
|
+
/** URL param names (page/size/sortType) — optional. */
|
|
13
|
+
url?: UrlConfig;
|
|
14
|
+
/** Fixed relations to load on every request (not URL-driven). */
|
|
15
|
+
with?: WithFor<A>;
|
|
16
|
+
}
|
|
17
|
+
interface InjectListParamsResult {
|
|
18
|
+
/** Ready-to-send list payload (built from the URL via the resource's defaults). */
|
|
19
|
+
params: Signal<ListPayload>;
|
|
20
|
+
/**
|
|
21
|
+
* The URL's page or sort was past the limits and was clamped (`?page=9999` → the last
|
|
22
|
+
* allowed page). The app may rewrite the URL (`setPage(params().page)`).
|
|
23
|
+
*/
|
|
24
|
+
clamped: Signal<boolean>;
|
|
25
|
+
searchParams: Signal<URLSearchParams>;
|
|
26
|
+
/** Set/clear a filter param (resets `page`). */
|
|
27
|
+
setParam: (name: string, value: string | null | undefined) => void;
|
|
28
|
+
setPage: (page: number) => void;
|
|
29
|
+
/** Resets `page`. */
|
|
30
|
+
setSize: (size: number) => void;
|
|
31
|
+
/** Resets `page`; an empty sort clears the param. */
|
|
32
|
+
setSort: (sort: SortInput) => void;
|
|
33
|
+
/** Clears every param except `keep`. */
|
|
34
|
+
reset: (keep?: string[]) => void;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Router-agnostic list URL state as signals — the Angular twin of the React and Vue
|
|
38
|
+
* `useListParamsBase`: the same reads, clamping and setters. Bring your own
|
|
39
|
+
* `searchParams` signal and `setSearchParams`.
|
|
40
|
+
*/
|
|
41
|
+
declare function injectListParamsBase<T, A extends AdapterName>(resource: Resource<T, A>, options: InjectListParamsBaseOptions<A>): InjectListParamsResult;
|
|
42
|
+
|
|
43
|
+
interface InjectListParamsOptions<A extends AdapterName> {
|
|
44
|
+
/** URL↔filter schema ({@link defineListSchema} / `resource.schema`). */
|
|
45
|
+
schema: ListSchema;
|
|
46
|
+
/** URL param names (page/size/sortType) — optional. */
|
|
47
|
+
url?: UrlConfig;
|
|
48
|
+
/** Fixed relations to load on every request (not URL-driven). */
|
|
49
|
+
with?: WithFor<A>;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* List URL state as signals, with the Angular **Router** (`ActivatedRoute` + `Router`).
|
|
53
|
+
* Reads filters/sort/page from the query params via the schema, builds a ready-to-send
|
|
54
|
+
* payload through the `resource`, and returns URL setters that `router.navigate` (a new
|
|
55
|
+
* history entry; filter/sort/size changes reset `page`). Does **not** fetch. Call it in an
|
|
56
|
+
* injection context (a field initializer or a constructor); elsewhere Angular throws `NG0203`.
|
|
57
|
+
*
|
|
58
|
+
* @example
|
|
59
|
+
* ```ts
|
|
60
|
+
* @Component({ template: `<button (click)="list.setPage(2)">2</button>` })
|
|
61
|
+
* class UsersPage {
|
|
62
|
+
* readonly list = injectListParams(users, { schema: usersSchema });
|
|
63
|
+
* readonly payload = computed(() => this.list.params());
|
|
64
|
+
* }
|
|
65
|
+
* ```
|
|
66
|
+
*/
|
|
67
|
+
declare function injectListParams<T, A extends AdapterName>(resource: Resource<T, A>, options: InjectListParamsOptions<A>): InjectListParamsResult;
|
|
68
|
+
|
|
69
|
+
export { injectListParams, injectListParamsBase };
|
|
70
|
+
export type { InjectListParamsBaseOptions, InjectListParamsOptions, InjectListParamsResult };
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import {computed,inject}from'@angular/core';import {toSignal}from'@angular/core/rxjs-interop';import {ActivatedRoute,Router}from'@angular/router';import'@querykitjs/core';function P(a){let e={};for(let s of new Set(a.keys())){let t=a.getAll(s);e[s]=t.length>1?t:t[0]??"";}return e}var p={pageParam:"page",sizeParam:"size",sortParam:"sortType"};function A(a){return a.map(e=>typeof e=="string"?e.trim():e&&e.key?(e.direction==="desc"?"-":"")+e.key:"").filter(Boolean).join(",")}function l(a){return new URLSearchParams(a)}function g(a,e,s,t={}){let r={...p,...t},n=l(a);return n.delete(r.pageParam),s==null||s===""?n.delete(e):n.set(e,s),n}function S(a,e,s={}){let t={...p,...s},r=l(a);return r.set(t.pageParam,String(e)),r}function L(a,e,s={}){let t={...p,...s},r=l(a);return r.delete(t.pageParam),r.set(t.sizeParam,String(e)),r}function h(a,e,s={}){let t={...p,...s},r=l(a);r.delete(t.pageParam);let n=A(e);return n?r.set(t.sortParam,n):r.delete(t.sortParam),r}function y(a,e=[]){let s=new URLSearchParams;for(let t of e){let r=a.get(t);r!==null&&s.set(t,r);}return s}function U(a,e){let{schema:s,url:t,with:r}=e;return {read(n){let o=a.fromSearchParams(n,{schema:s,url:t,with:r});return {params:o,clamped:o.clamped===true}},actions(n){return {setParam:(o,c)=>g(n,o,c,t),setPage:o=>S(n,o,t),setSize:o=>L(n,o,t),setSort:o=>h(n,o,t),reset:o=>y(n,o)}}}}function f(a,e){let{schema:s,searchParams:t,setSearchParams:r,url:n,with:o}=e,c=U(a,{schema:s,url:n,with:o}),d=computed(()=>c.read(t())),m=()=>c.actions(t());return {params:computed(()=>d().params),clamped:computed(()=>d().clamped),searchParams:t,setParam:(i,x)=>r(m().setParam(i,x)),setPage:i=>r(m().setPage(i)),setSize:i=>r(m().setSize(i)),setSort:i=>r(m().setSort(i)),reset:i=>r(m().reset(i))}}function T(a){return new URLSearchParams(a.keys.flatMap(e=>a.getAll(e).map(s=>[e,s])))}function w(a,e){let s=inject(ActivatedRoute),t=inject(Router),r=toSignal(s.queryParamMap,{requireSync:true});return f(a,{...e,searchParams:computed(()=>T(r())),setSearchParams:n=>{t.navigate([],{relativeTo:s,queryParams:P(n)});}})}export{w as injectListParams,f as injectListParamsBase};//# sourceMappingURL=index.js.map
|
|
2
|
+
//# sourceMappingURL=index.js.map
|