From c51b962bbbd4d8ddea9f83186960c71f6b5e008b Mon Sep 17 00:00:00 2001 From: Cyrille Tuzi <555867+cyrilletuzi@users.noreply.github.com> Date: Wed, 18 Feb 2026 18:18:56 +0100 Subject: [PATCH 01/53] first implementation --- README.md | 139 +++++++++------- lib/src/lib/rx-submit.spec.ts | 305 ++++++++++++++++++++++++++++++++++ lib/src/lib/rx-submit.ts | 51 +++++- 3 files changed, 435 insertions(+), 60 deletions(-) create mode 100644 lib/src/lib/rx-submit.spec.ts diff --git a/README.md b/README.md index 67a8b67..b639bd3 100644 --- a/README.md +++ b/README.md @@ -1,59 +1,82 @@ -# AngularRxSubmit - -This project was generated using [Angular CLI](https://github.com/angular/angular-cli) version 21.2.0-next.2. - -## Development server - -To start a local development server, run: - -```bash -ng serve -``` - -Once the server is running, open your browser and navigate to `http://localhost:4200/`. The application will automatically reload whenever you modify any of the source files. - -## Code scaffolding - -Angular CLI includes powerful code scaffolding tools. To generate a new component, run: - -```bash -ng generate component component-name -``` - -For a complete list of available schematics (such as `components`, `directives`, or `pipes`), run: - -```bash -ng generate --help -``` - -## Building - -To build the project run: - -```bash -ng build -``` - -This will compile your project and store the build artifacts in the `dist/` directory. By default, the production build optimizes your application for performance and speed. - -## Running unit tests - -To execute unit tests with the [Vitest](https://vitest.dev/) test runner, use the following command: - -```bash -ng test +# angular-rx-submit + +## Examples + +```ts +import { rxSubmit } from 'angular-rx-submit'; + +interface EditModel { + username: string; +} + +interface ApiResponse { + success: boolean; + error?: { message: string }; +} + +export function mapApiResponseToTreeValidationResult(response: ApiResponse): TreeValidationResult { + return response.success + ? null + : { + kind: 'apiError', + message: response.error?.message, + }; +} + +@Injectable({ + providedIn: 'root', +}) +export class Api { + private readonly httpClient = inject(HttpClient); + + save(body: EditModel): Observable { + return this.httpClient.post('/api/save', body); + } +} + +@Component({ + template: ` +
+ + +
+ `, +}) +export class EditPage { + private readonly injector = inject(Injector); + private readonly httpApi = inject(HttpApi); + private readonly router = inject(router); + + private readonly formModel = signal({ + username: '', + }); + protected readonly form = form(formModel); + + protected save(): void { + rxSubmit( + this.form, + (submittedForm) => + this.httpApi.save(submittedForm().value()).pipe(map(mapApiResponseToTreeValidationResult)), + { + injector: this.injector, + }, + ).subscribe({ + next: (success) => { + if (success) { + this.router.navigate(['/some/page']).catch(() => {}); + } + }, + error: (error: unknown) => { + if (error instanceof HttpErrorResponse && error.status === 500) { + console.log(`Display service unavailable`); + } else { + console.log(`Display unexpected error`); + } + }, + }); + } +} ``` - -## Running end-to-end tests - -For end-to-end (e2e) testing, run: - -```bash -ng e2e -``` - -Angular CLI does not come with an end-to-end testing framework by default. You can choose one that suits your needs. - -## Additional Resources - -For more information on using the Angular CLI, including detailed command references, visit the [Angular CLI Overview and Command Reference](https://angular.dev/tools/cli) page. diff --git a/lib/src/lib/rx-submit.spec.ts b/lib/src/lib/rx-submit.spec.ts new file mode 100644 index 0000000..271c1eb --- /dev/null +++ b/lib/src/lib/rx-submit.spec.ts @@ -0,0 +1,305 @@ +/* eslint-disable @typescript-eslint/prefer-promise-reject-errors */ +import { Component, DestroyRef, inject, Injector, signal } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { form, type TreeValidationResult } from '@angular/forms/signals'; +import { asyncScheduler, delay, Observable, of, scheduled, throwError } from 'rxjs'; +import { beforeEach, describe, expect, it } from 'vitest'; +import { rxSubmit } from './rx-submit'; + +describe('rxSubmit ', () => { + describe('with explicit injector', () => { + @Component({ + template: '', + }) + class TestComponent { + readonly injector = inject(Injector); + private readonly formModel = signal({ + test: '', + }); + readonly form = form(this.formModel); + } + + let componentFixture: ComponentFixture; + let componentInstance: TestComponent; + let injector: Injector; + + beforeEach(() => { + componentFixture = TestBed.createComponent(TestComponent); + componentInstance = componentFixture.componentInstance; + injector = componentInstance.injector; + }); + + it('should succeed when returning undefined', () => + new Promise((resolve, reject) => { + let success: boolean; + + const observable: Observable = scheduled( + of(undefined), + asyncScheduler, + ); + + rxSubmit(componentInstance.form, () => observable, { injector }).subscribe({ + next: (result) => { + success = result; + }, + error: () => { + reject(); + }, + complete: () => { + expect(success).toBe(true); + expect(componentInstance.form().invalid()).toBe(false); + + resolve(undefined); + }, + }); + })); + + it('should succeed when returning null', () => + new Promise((resolve, reject) => { + let success: boolean; + + const observable: Observable = scheduled(of(null), asyncScheduler); + + rxSubmit(componentInstance.form, () => observable, { injector }).subscribe({ + next: (result) => { + success = result; + }, + error: () => { + reject(); + }, + complete: () => { + expect(success).toBe(true); + expect(componentInstance.form().invalid()).toBe(false); + + resolve(undefined); + }, + }); + })); + + it('should succeed be invalid when returning one validation error', () => + new Promise((resolve, reject) => { + let success: boolean; + + const treeValidationResult: TreeValidationResult = { + kind: 'apiError', + }; + const observable: Observable = scheduled( + of(treeValidationResult), + asyncScheduler, + ); + + rxSubmit(componentInstance.form, () => observable, { + injector, + }).subscribe({ + next: (result) => { + success = result; + }, + error: () => { + reject(); + }, + complete: () => { + expect(success).toBe(false); + expect(componentInstance.form().invalid()).toBe(true); + expect(componentInstance.form().errors()[0]).toEqual(treeValidationResult); + + resolve(undefined); + }, + }); + })); + + it('should be invalid when returning multiple validation errors', () => + new Promise((resolve, reject) => { + let success: boolean; + + const error1: TreeValidationResult = { + kind: 'apiError1', + }; + const error2: TreeValidationResult = { + kind: 'apiError2', + }; + const treeValidationResult: TreeValidationResult = [error1, error2]; + const observable: Observable = scheduled( + of(treeValidationResult), + asyncScheduler, + ); + + rxSubmit(componentInstance.form, () => observable, { injector }).subscribe({ + next: (result) => { + success = result; + }, + error: () => { + reject(); + }, + complete: () => { + expect(success).toBe(false); + expect(componentInstance.form().invalid()).toBe(true); + expect(componentInstance.form().errors()[0]).toEqual(error1); + expect(componentInstance.form().errors()[1]).toEqual(error2); + + resolve(undefined); + }, + }); + })); + + it('should throw when the observable fails', () => + new Promise((resolve, reject) => { + const errorMessage = 'Obserable error'; + const observable: Observable = scheduled( + throwError(() => new Error(errorMessage)), + asyncScheduler, + ); + + rxSubmit(componentInstance.form, () => observable, { injector }).subscribe({ + next: () => { + reject(); + }, + error: (error: unknown) => { + expect(error).toBeInstanceOf(Error); + // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion + expect((error as Error).message).toBe(errorMessage); + + resolve(undefined); + }, + complete: () => { + reject(); + }, + }); + })); + + it('should complete when cancelled', () => + new Promise((resolve, reject) => { + const observable: Observable = of(undefined).pipe(delay(2000)); + + rxSubmit(componentInstance.form, () => observable, { injector }).subscribe({ + next: () => { + reject(); + }, + error: () => { + reject(); + }, + complete: () => { + const destroyed = injector.get(DestroyRef).destroyed; + expect(destroyed).toBe(true); + + resolve(undefined); + }, + }); + + componentFixture.destroy(); + })); + }); + + describe('inside injection context', () => { + it('should succeed', () => + new Promise((resolve, reject) => { + let success: boolean; + + @Component({ + template: '', + }) + class TestComponent { + private readonly formModel = signal({ + test: '', + }); + readonly form = form(this.formModel); + private readonly submitObservable: Observable; + + constructor() { + const observable: Observable = scheduled( + of(undefined), + asyncScheduler, + ); + this.submitObservable = rxSubmit(this.form, () => observable); + } + + save(): void { + this.submitObservable.subscribe({ + next: (result) => { + success = result; + }, + error: () => { + reject(); + }, + complete: () => { + expect(success).toBe(true); + expect(this.form().invalid()).toBe(false); + + resolve(undefined); + }, + }); + } + } + + const componentFixture = TestBed.createComponent(TestComponent); + componentFixture.componentInstance.save(); + })); + + it('should complete when cancelled', () => + new Promise((resolve, reject) => { + @Component({ + template: '', + }) + class TestComponent { + private readonly destroyRef = inject(DestroyRef); + private readonly formModel = signal({ + test: '', + }); + readonly form = form(this.formModel); + private readonly submitObservable: Observable; + + constructor() { + const observable: Observable = of(undefined).pipe(delay(2000)); + this.submitObservable = rxSubmit(this.form, () => observable); + } + + save(): void { + this.submitObservable.subscribe({ + next: () => { + reject(); + }, + error: () => { + reject(); + }, + complete: () => { + const destroyed = this.destroyRef.destroyed; + expect(destroyed).toBe(true); + + resolve(undefined); + }, + }); + } + } + + const componentFixture = TestBed.createComponent(TestComponent); + componentFixture.componentInstance.save(); + componentFixture.destroy(); + })); + }); + + describe('outside injection context', () => { + it('should throw if outside injection context and no injector is provided', () => { + @Component({ + template: '', + }) + class TestComponent { + private readonly formModel = signal({ + test: '', + }); + private readonly form = form(this.formModel); + + save(): void { + const observable: Observable = scheduled( + of(undefined), + asyncScheduler, + ); + expect(() => { + rxSubmit(this.form, () => observable).subscribe(); + }).toThrowError(/NG0203/); + } + } + + const componentFixture = TestBed.createComponent(TestComponent); + componentFixture.componentInstance.save(); + }); + }); +}); diff --git a/lib/src/lib/rx-submit.ts b/lib/src/lib/rx-submit.ts index 5704160..3098072 100644 --- a/lib/src/lib/rx-submit.ts +++ b/lib/src/lib/rx-submit.ts @@ -1,3 +1,50 @@ -export function rxSubmit(): void { - // TODO +import { assertInInjectionContext, DestroyRef, inject, type Injector } from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { submit, type FieldTree, type TreeValidationResult } from '@angular/forms/signals'; +import { firstValueFrom, from, type Observable } from 'rxjs'; + +interface RxSubmitOptions { + /** + * `Injector` which will provide the `DestroyRef` used to clean up the Observable subscription. + * + * If this is not provided, a `DestroyRef` will be retrieved from the current injection context. + */ + injector?: Injector; +} + +export function rxSubmit( + form: FieldTree, + action: (form: FieldTree) => Observable, + options: RxSubmitOptions = {}, +): Observable { + if (!options.injector) { + assertInInjectionContext(rxSubmit); + } + + const destroyRef: DestroyRef = options.injector?.get(DestroyRef) ?? inject(DestroyRef); + + /* Prepare the Promise-based action callback */ + const actionCallback = async ( + submittedForm: FieldTree, + ): Promise => { + /* Pass the form to the user-provided and Observable-based action callback */ + const actionObservable: Observable = action(submittedForm).pipe( + takeUntilDestroyed(destroyRef), + ); + + /* Transform the action Observable into a Promise */ + const treeValidationResult: TreeValidationResult = await firstValueFrom(actionObservable, { + /* If `takeUntilDestroyed()` happens, returns `undefined` instead of throwing an `EmptyError` */ + defaultValue: undefined, + }); + + return treeValidationResult; + }; + + /* `submit()` the form and transform the Promise return into an Observable */ + const submitObservable: Observable = from(submit(form, actionCallback)).pipe( + takeUntilDestroyed(destroyRef), + ); + + return submitObservable; } From ca481b7838965e2e62f11a1beac6473334c20224 Mon Sep 17 00:00:00 2001 From: Cyrille Tuzi <555867+cyrilletuzi@users.noreply.github.com> Date: Thu, 19 Feb 2026 12:22:06 +0100 Subject: [PATCH 02/53] doc --- README.md | 163 +++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 160 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index b639bd3..18f3fbe 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,163 @@ # angular-rx-submit -## Examples +## What is this library and why? + +This library provides the function `rxSubmit()`, on Observable-based equivalent of the official Angular Promise-based `submit()`. Why? + +- cancellation +- simple function +- consistency + +## Requirements + +- Angular version >= 21.2.0 [^1] +- RxJS version >= 7.4.0 [^2] + +> [!NOTE] +> Angular versions 21.0 and 21.1 are not supported, as this library requires a new `submit()` feature introduced in version 21.2. + +> [!NOTE] +> RxJS version 6 is not supported. + +## Getting started + +- `npm install angular-rx-submit` + +## Injection context + +One advantage of `rxSubmit()` is automatic cancellation (if the user leaves the page). But for that to work, like many other Angular functions (`takeUntilDestroyed()`, `toSignal()`...), it requires an injection context. `rxSubmit()` follows the same pattern as those other similar Angular functions, with 2 options: + +- provide an `Injector` + +```ts +@Component({ + template: `
`, +}) +export class EditPage { + private readonly injector = inject(Injector); // here + + private readonly formModel = signal({ username: '' }); + protected readonly form = form(this.formModel); + + protected save(): void { + rxSubmit(this.form, (submittedForm) => someObservable(submittedForm().value()), { + injector: this.injector, // here + }).subscribe(); + } +} +``` + +- use `rxSubmit()` inside an injection context (field initializer, constructor...) + +```ts +@Component({ + template: `
`, +}) +export class EditPage { + private readonly formModel = signal({ username: '' }); + protected readonly form = form(this.formModel); + + private readonly submitObservable = rxSubmit(this.form, (submittedForm) => + someObservable(submittedForm().value()), + ); + + protected save(): void { + submitObservable.subscribe(); + } +} +``` + +## Subscription + +This is not specific to `rxSubmit()`, but as for any Observable, subscribing is mandatory: + +```ts +// Nothing happens +rxSubmit(this.form, () => (submittedForm) => someObservable(submittedForm().value()), { + injector: this.injector, +}); + +// Triggers submission +rxSubmit(this.form, () => (submittedForm) => someObservable(submittedForm().value()), { + injector: this.injector, +}).subscribe(); +``` + +## Errors + +As for any Observable, handling errors is recommended. If the Observable you provide throws, the error will be propagated by `rxSubmit()`. The most common case is the HTTP request failing. + +```ts +rxSubmit(this.form, () => (submittedForm) => someObservable(submittedForm().value()), { + injector: this.injector, +}).subscribe({ + next: (success) => { + if (success) { + // Manage success + } + }, + error: (error: unknown) => { + // Manage error + if (error instanceof HttpErrorResponse && error.status === 500) { + console.log(`Display service unavailable`); + } else { + console.log(`Display unexpected error`); + } + }, +}); +``` + +## Validation result + +As for the official Angular `submit()`, the Observable you provide to `rxSubmit()` should return an official `TreeValidationResult`. It is similar to Validators in previous reactive forms, meaning returning either: + +- `null`, `undefined` or `void` if there is no validation error +- a `ValidationError.WithOptionalFieldTree` if there is a validation error +- one array of `ValidationError.WithOptionalFieldTree` if there are multiple validations errors + +```ts +interface ApiResponse { + readonly success: boolean; + readonly error?: { message: string }; +} + +export function mapApiResponseToTreeValidationResult(response: ApiResponse): TreeValidationResult { + return response.success + ? null + : { + kind: 'apiError', + message: response.error?.message, + }; +} +``` + +## Actions after validation + +Let us imagine a classic scenario: if the form passes validation, we want to redirect to another page. The question is: where to do the redirection? + +It is not specific to `rxSubmit()`, but the official Angular `submit()` can be confusing because there is 2 places where we can do that redirection: + +- directly inside the Observable / Promise we provide +- after the `rxSubmit()` / `submit()`, in the `next` / `then()` callback + +The `rxSubmit()` / `submit()` purpose is only to manage the form submission progress and validation. So the Observable / Promise we provide should be limited to just that, returning a `TreeValidationResult` as explained above. + +Subsequent actions should be done in the `next` / `then()` callback: + +```ts +rxSubmit(this.form, () => (submittedForm) => someObservable(submittedForm().value()), { + injector: this.injector, +}).subscribe({ + next: (success) => { + if (success) { + this.router.navigate(['/some/other/page']).catch(() => {}); + } + }, + error: (error: unknown) => {}, +}); +``` + +## Full example ```ts import { rxSubmit } from 'angular-rx-submit'; @@ -10,8 +167,8 @@ interface EditModel { } interface ApiResponse { - success: boolean; - error?: { message: string }; + readonly success: boolean; + readonly error?: { message: string }; } export function mapApiResponseToTreeValidationResult(response: ApiResponse): TreeValidationResult { From 80db02d82e31afda38d0a014fb346ec05b1393ce Mon Sep 17 00:00:00 2001 From: Cyrille Tuzi <555867+cyrilletuzi@users.noreply.github.com> Date: Thu, 19 Feb 2026 12:39:19 +0100 Subject: [PATCH 03/53] doc --- README.md | 28 +++++++++++++++++----------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 18f3fbe..65d097f 100644 --- a/README.md +++ b/README.md @@ -14,10 +14,10 @@ This library provides the function `rxSubmit()`, on Observable-based equivalent - RxJS version >= 7.4.0 [^2] > [!NOTE] -> Angular versions 21.0 and 21.1 are not supported, as this library requires a new `submit()` feature introduced in version 21.2. +> Angular versions 21.0 and 21.1 are _not_ supported, as this library requires a new `submit()` feature introduced in version 21.2. > [!NOTE] -> RxJS version 6 is not supported. +> RxJS version 6 is _not_ supported. ## Getting started @@ -25,29 +25,31 @@ This library provides the function `rxSubmit()`, on Observable-based equivalent ## Injection context -One advantage of `rxSubmit()` is automatic cancellation (if the user leaves the page). But for that to work, like many other Angular functions (`takeUntilDestroyed()`, `toSignal()`...), it requires an injection context. `rxSubmit()` follows the same pattern as those other similar Angular functions, with 2 options: +One advantage of `rxSubmit()` is automatic cancellation (if the user leaves the page). -- provide an `Injector` +But for that to work, like many other Angular functions (`takeUntilDestroyed()`, `toSignal()`...), **it requires an injection context**. `rxSubmit()` follows the same pattern as those other similar Angular functions, with 2 options: + +- **provide an `Injector`** ```ts @Component({ template: `
`, }) export class EditPage { - private readonly injector = inject(Injector); // here + private readonly injector = inject(Injector); // ⬅️ private readonly formModel = signal({ username: '' }); protected readonly form = form(this.formModel); protected save(): void { rxSubmit(this.form, (submittedForm) => someObservable(submittedForm().value()), { - injector: this.injector, // here + injector: this.injector, // ⬅️ }).subscribe(); } } ``` -- use `rxSubmit()` inside an injection context (field initializer, constructor...) +- or use `rxSubmit()` inside an [injection context](https://angular.dev/guide/di/dependency-injection-context) (field initializer, constructor...) ```ts @Component({ @@ -67,17 +69,21 @@ export class EditPage { } ``` +**Using `rxSubmit()` outside an injection context and without providing an injector will throw the [`NG0203` error](https://angular.dev/errors/NG0203).** + ## Subscription -This is not specific to `rxSubmit()`, but as for any Observable, subscribing is mandatory: +You do _not_ need to unsubscribe, `rxSubmit()` does it for you via the injection context (see above). + +But **you _DO_ need to subscribe**, even if you do not have something specific to do after submission (because it is how all `Observable`s work). ```ts -// Nothing happens +// ❌ Nothing happens rxSubmit(this.form, () => (submittedForm) => someObservable(submittedForm().value()), { injector: this.injector, }); -// Triggers submission +// ✅ Triggers submission rxSubmit(this.form, () => (submittedForm) => someObservable(submittedForm().value()), { injector: this.injector, }).subscribe(); @@ -113,7 +119,7 @@ As for the official Angular `submit()`, the Observable you provide to `rxSubmit( - `null`, `undefined` or `void` if there is no validation error - a `ValidationError.WithOptionalFieldTree` if there is a validation error -- one array of `ValidationError.WithOptionalFieldTree` if there are multiple validations errors +- an array of `ValidationError.WithOptionalFieldTree` if there are multiple validation errors ```ts interface ApiResponse { From 06c482cd359299f438c404a984bd66280deb4ba8 Mon Sep 17 00:00:00 2001 From: Cyrille Tuzi <555867+cyrilletuzi@users.noreply.github.com> Date: Thu, 19 Feb 2026 14:33:29 +0100 Subject: [PATCH 04/53] doc --- README.md | 105 ++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 103 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 65d097f..85725a8 100644 --- a/README.md +++ b/README.md @@ -5,8 +5,8 @@ This library provides the function `rxSubmit()`, on Observable-based equivalent of the official Angular Promise-based `submit()`. Why? - cancellation -- simple function - consistency +- simple function ## Requirements @@ -141,7 +141,7 @@ export function mapApiResponseToTreeValidationResult(response: ApiResponse): Tre Let us imagine a classic scenario: if the form passes validation, we want to redirect to another page. The question is: where to do the redirection? -It is not specific to `rxSubmit()`, but the official Angular `submit()` can be confusing because there is 2 places where we can do that redirection: +It is not specific to `rxSubmit()`, but the official Angular `submit()` can be confusing because there is 2 places where we could do that redirection: - directly inside the Observable / Promise we provide - after the `rxSubmit()` / `submit()`, in the `next` / `then()` callback @@ -163,6 +163,107 @@ rxSubmit(this.form, () => (submittedForm) => someObservable(submittedForm().valu }); ``` +## Problems solved + +### Cancellation + +Let us take a common and basic example with the Promise-based `submit()`: + +```ts +@Component({ + template: `
`, +}) +export class EditPage { + private readonly router = inject(Router); + + private readonly formModel = signal({ username: '' }); + protected readonly form = form(this.formModel); + + protected save(): void { + submit(this.form, async (submittedForm) => await somePromise(submittedForm().value())) + .then((success) => { + if (success) { + this.router.navigate(['/some/other/page']).catch(() => {}); + } + }) + .catch(() => {}); + } +} +``` + +Where `somePromise()` implies a HTTP request to the server. Let us say the request takes 10 seconds. Now the scenario: + +- the user submits the form +- as it is taking too long, the user leaves the page by going to another one +- the user starts interacting with this other page +- after a few seconds, when the HTTP request succeed and without notice, the user will be redirected to `/some/other/page` + +In addition to a bad user experience (UX), it can also provokes technical issues, like keeping useless things in memory or accessing to component-related things that have been destroyed in the meantime. + +With `rxSubmit()`, all the process will be automatically cancelled if the user leaves the page. + +### Consistency + +Nearly everytime, submitting a form implies a HTTP request. `HttpClient`, the official way to do HTTP requests in Angular, is still Observable-based (and for good reasons, like cancellation explained above). + +A given project should be consistent, and having similar actions sometimes Observable-based, and some other times Promise-based, is not consistent. + +### Simple function + +Even if you are OK to sacrifice consistency, you can transform your Observable to a Promise, but doing so in the `submit()` scenario is not as trivial as it seems: + +```ts +@Component({ + template: `
`, +}) +export class EditPage { + private readonly destroyRef = inject(DestroyRef); + + private readonly formModel = signal({ username: '' }); + protected readonly form = form(this.formModel); + + protected save(): void { + from( + submit( + this.form, + async (submittedForm) => + await firstValueFrom( + someObservable(submittedForm().value()).pipe(takeUntilDestroyed(this.destroyRef)), + { + defaultValue: undefined, + }, + ), + ), + ).pipe(takeUntilDestroyed(this.destroyRef)).subscribe({ + next: (success) => { + if (success) { + this.router.navigate(['/some/other/page']).catch(() => {}); + } + }, + error: (error: unknown) { + console.log('Display an error snack bar/toast'); + }, + }); + } +} +``` + +As you can see: + +- there is not just 1 but 2 Promise to transform +- there is thus 2 cancellation to manage +- if the first `takeUntilDestroyed()` happens, the Observable will be empty, and it makes `firstValueFrom()` throws an error, which would trigger the last error callcack and thus display the snack bar/toast; this is why `defaultValue` must be set + +It complexifies things a lot, and should be repeated in each form. `rxSubmit()` is a simple function which does it for you. + +## Why not in Angular directly? + +I personnally think `rxSubmit()` should be part of `@angular/rxjs-interop`. + +For now, the Angular team has discarded [this request](https://github.com/angular/angular/issues/65199) (from someone else), without even allowing proper discussion about it. + +Feel free to advocate for it if you want. + ## Full example ```ts From d899a2568852a0edd320b09a717a465d6042ccb9 Mon Sep 17 00:00:00 2001 From: Cyrille Tuzi <555867+cyrilletuzi@users.noreply.github.com> Date: Thu, 19 Feb 2026 15:29:54 +0100 Subject: [PATCH 05/53] doc --- README.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/README.md b/README.md index 85725a8..d151125 100644 --- a/README.md +++ b/README.md @@ -163,6 +163,22 @@ rxSubmit(this.form, () => (submittedForm) => someObservable(submittedForm().valu }); ``` +### Multiple submissions + +As the official `submit()`, do not trigger `rxSubmit()` in parallel, to avoid race issues. So be sure to block submission when one is already in progress: + +```ts +@Component({ + template: `
+ +
`, +}) +export class EditPage { + private readonly formModel = signal({ username: '' }); + protected readonly form = form(this.formModel); +} +``` + ## Problems solved ### Cancellation From 4d10ebe4e4f3f82d36e338e1df3cfd1d5a6f71f5 Mon Sep 17 00:00:00 2001 From: Cyrille Tuzi <555867+cyrilletuzi@users.noreply.github.com> Date: Thu, 19 Feb 2026 16:09:43 +0100 Subject: [PATCH 06/53] add other submit options --- README.md | 42 ++++++++------- lib/src/lib/rx-submit.spec.ts | 23 +++++---- lib/src/lib/rx-submit.ts | 97 +++++++++++++++++++++++++++++++---- 3 files changed, 121 insertions(+), 41 deletions(-) diff --git a/README.md b/README.md index d151125..a7ab3ef 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,8 @@ export class EditPage { protected readonly form = form(this.formModel); protected save(): void { - rxSubmit(this.form, (submittedForm) => someObservable(submittedForm().value()), { + rxSubmit(this.form, { + action: (submittedForm) => someObservable(submittedForm().value()), injector: this.injector, // ⬅️ }).subscribe(); } @@ -59,9 +60,9 @@ export class EditPage { private readonly formModel = signal({ username: '' }); protected readonly form = form(this.formModel); - private readonly submitObservable = rxSubmit(this.form, (submittedForm) => - someObservable(submittedForm().value()), - ); + private readonly submitObservable = rxSubmit(this.form, { + action: (submittedForm) => someObservable(submittedForm().value()), + }); protected save(): void { submitObservable.subscribe(); @@ -79,12 +80,14 @@ But **you _DO_ need to subscribe**, even if you do not have something specific t ```ts // ❌ Nothing happens -rxSubmit(this.form, () => (submittedForm) => someObservable(submittedForm().value()), { +rxSubmit(this.form, () => { + action: (submittedForm) => someObservable(submittedForm().value()), injector: this.injector, }); // ✅ Triggers submission -rxSubmit(this.form, () => (submittedForm) => someObservable(submittedForm().value()), { +rxSubmit(this.form, { + action: (submittedForm) => someObservable(submittedForm().value()), injector: this.injector, }).subscribe(); ``` @@ -94,7 +97,8 @@ rxSubmit(this.form, () => (submittedForm) => someObservable(submittedForm().valu As for any Observable, handling errors is recommended. If the Observable you provide throws, the error will be propagated by `rxSubmit()`. The most common case is the HTTP request failing. ```ts -rxSubmit(this.form, () => (submittedForm) => someObservable(submittedForm().value()), { +rxSubmit(this.form, { + action: () => (submittedForm) => someObservable(submittedForm().value()), injector: this.injector, }).subscribe({ next: (success) => { @@ -151,7 +155,8 @@ The `rxSubmit()` / `submit()` purpose is only to manage the form submission prog Subsequent actions should be done in the `next` / `then()` callback: ```ts -rxSubmit(this.form, () => (submittedForm) => someObservable(submittedForm().value()), { +rxSubmit(this.form, { + action: () => (submittedForm) => someObservable(submittedForm().value()), injector: this.injector, }).subscribe({ next: (success) => { @@ -196,7 +201,9 @@ export class EditPage { protected readonly form = form(this.formModel); protected save(): void { - submit(this.form, async (submittedForm) => await somePromise(submittedForm().value())) + submit(this.form, { + action: async (submittedForm) => await somePromise(submittedForm().value()), + }) .then((success) => { if (success) { this.router.navigate(['/some/other/page']).catch(() => {}); @@ -241,15 +248,15 @@ export class EditPage { protected save(): void { from( submit( - this.form, - async (submittedForm) => + this.form, { + action: async (submittedForm) => await firstValueFrom( someObservable(submittedForm().value()).pipe(takeUntilDestroyed(this.destroyRef)), { defaultValue: undefined, }, ), - ), + }), ).pipe(takeUntilDestroyed(this.destroyRef)).subscribe({ next: (success) => { if (success) { @@ -336,14 +343,11 @@ export class EditPage { protected readonly form = form(formModel); protected save(): void { - rxSubmit( - this.form, - (submittedForm) => + rxSubmit(this.form, { + action: (submittedForm) => this.httpApi.save(submittedForm().value()).pipe(map(mapApiResponseToTreeValidationResult)), - { - injector: this.injector, - }, - ).subscribe({ + injector: this.injector, + }).subscribe({ next: (success) => { if (success) { this.router.navigate(['/some/page']).catch(() => {}); diff --git a/lib/src/lib/rx-submit.spec.ts b/lib/src/lib/rx-submit.spec.ts index 271c1eb..cb14e6d 100644 --- a/lib/src/lib/rx-submit.spec.ts +++ b/lib/src/lib/rx-submit.spec.ts @@ -38,7 +38,10 @@ describe('rxSubmit ', () => { asyncScheduler, ); - rxSubmit(componentInstance.form, () => observable, { injector }).subscribe({ + rxSubmit(componentInstance.form, { + action: () => observable, + injector, + }).subscribe({ next: (result) => { success = result; }, @@ -60,7 +63,7 @@ describe('rxSubmit ', () => { const observable: Observable = scheduled(of(null), asyncScheduler); - rxSubmit(componentInstance.form, () => observable, { injector }).subscribe({ + rxSubmit(componentInstance.form, { action: () => observable, injector }).subscribe({ next: (result) => { success = result; }, @@ -88,9 +91,7 @@ describe('rxSubmit ', () => { asyncScheduler, ); - rxSubmit(componentInstance.form, () => observable, { - injector, - }).subscribe({ + rxSubmit(componentInstance.form, { action: () => observable, injector }).subscribe({ next: (result) => { success = result; }, @@ -123,7 +124,7 @@ describe('rxSubmit ', () => { asyncScheduler, ); - rxSubmit(componentInstance.form, () => observable, { injector }).subscribe({ + rxSubmit(componentInstance.form, { action: () => observable, injector }).subscribe({ next: (result) => { success = result; }, @@ -149,7 +150,7 @@ describe('rxSubmit ', () => { asyncScheduler, ); - rxSubmit(componentInstance.form, () => observable, { injector }).subscribe({ + rxSubmit(componentInstance.form, { action: () => observable, injector }).subscribe({ next: () => { reject(); }, @@ -170,7 +171,7 @@ describe('rxSubmit ', () => { new Promise((resolve, reject) => { const observable: Observable = of(undefined).pipe(delay(2000)); - rxSubmit(componentInstance.form, () => observable, { injector }).subscribe({ + rxSubmit(componentInstance.form, { action: () => observable, injector }).subscribe({ next: () => { reject(); }, @@ -209,7 +210,7 @@ describe('rxSubmit ', () => { of(undefined), asyncScheduler, ); - this.submitObservable = rxSubmit(this.form, () => observable); + this.submitObservable = rxSubmit(this.form, { action: () => observable }); } save(): void { @@ -249,7 +250,7 @@ describe('rxSubmit ', () => { constructor() { const observable: Observable = of(undefined).pipe(delay(2000)); - this.submitObservable = rxSubmit(this.form, () => observable); + this.submitObservable = rxSubmit(this.form, { action: () => observable }); } save(): void { @@ -293,7 +294,7 @@ describe('rxSubmit ', () => { asyncScheduler, ); expect(() => { - rxSubmit(this.form, () => observable).subscribe(); + rxSubmit(this.form, { action: () => observable }).subscribe(); }).toThrowError(/NG0203/); } } diff --git a/lib/src/lib/rx-submit.ts b/lib/src/lib/rx-submit.ts index 3098072..94913d2 100644 --- a/lib/src/lib/rx-submit.ts +++ b/lib/src/lib/rx-submit.ts @@ -1,9 +1,35 @@ import { assertInInjectionContext, DestroyRef, inject, type Injector } from '@angular/core'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; -import { submit, type FieldTree, type TreeValidationResult } from '@angular/forms/signals'; +import { + submit, + type FieldTree, + type FormSubmitOptions, + type TreeValidationResult, +} from '@angular/forms/signals'; import { firstValueFrom, from, type Observable } from 'rxjs'; -interface RxSubmitOptions { +/** + * Options that can be specified when submitting a form with `rxSubmit()`. + * + * @experimental 21.2.0 + */ +interface RxSubmitOptions extends Pick< + FormSubmitOptions, + 'onInvalid' | 'ignoreValidators' +> { + /** + * Function to run when submitting the form data (when form is valid). + * + * @param field The submitted field + * @param detail An object containing the root field of the submitted form as well as the submitted field itself + */ + action: ( + field: FieldTree, + detail: { + root: FieldTree; + submitted: FieldTree; + }, + ) => Observable; /** * `Injector` which will provide the `DestroyRef` used to clean up the Observable subscription. * @@ -12,10 +38,49 @@ interface RxSubmitOptions { injector?: Injector; } +/** + * Submits a given `FieldTree` using the given action function and applies any submission errors + * resulting from the action to the field. Submission errors returned by the `action` will be integrated + * into the field as a `ValidationError` on the sub-field indicated by the `fieldTree` property of the + * submission error. + * + * @example + * ```ts + * function registerNewUser(registrationForm: FieldTree<{username: string, password: string}>): Observable { + * return myClient.registerNewUser(registrationForm().value()).pipe( + * map((result) => { + * if (result.errorCode === myClient.ErrorCode.USERNAME_TAKEN) { + * return [{ + * fieldTree: registrationForm.username, + * kind: 'server', + * message: 'Username already taken', + * }]; + * } + * return undefined; + * }), + * ); + * } + * + * const registrationForm = form(signal({ username: 'elmo', password: '' })); + * + * rxSubmit(registrationForm, { + * action: async (f) => registerNewUser(f), + * }).subscribe(); + * + * registrationForm.username().errors(); // [{kind: 'server', message: 'Username already taken'}] + * ``` + * + * @param form The field to submit. + * @param options Options for the submission. + * @returns Whether the submission was successful. + * @template TModel The data type of the field being submitted. + * + * @category submission + * @experimental 21.2.0 + */ export function rxSubmit( form: FieldTree, - action: (form: FieldTree) => Observable, - options: RxSubmitOptions = {}, + options: RxSubmitOptions, ): Observable { if (!options.injector) { assertInInjectionContext(rxSubmit); @@ -25,12 +90,16 @@ export function rxSubmit( /* Prepare the Promise-based action callback */ const actionCallback = async ( - submittedForm: FieldTree, + field: FieldTree, + detail: { + root: FieldTree; + submitted: FieldTree; + }, ): Promise => { /* Pass the form to the user-provided and Observable-based action callback */ - const actionObservable: Observable = action(submittedForm).pipe( - takeUntilDestroyed(destroyRef), - ); + const actionObservable: Observable = options + .action(field, detail) + .pipe(takeUntilDestroyed(destroyRef)); /* Transform the action Observable into a Promise */ const treeValidationResult: TreeValidationResult = await firstValueFrom(actionObservable, { @@ -42,9 +111,15 @@ export function rxSubmit( }; /* `submit()` the form and transform the Promise return into an Observable */ - const submitObservable: Observable = from(submit(form, actionCallback)).pipe( - takeUntilDestroyed(destroyRef), - ); + const submitObservable: Observable = from( + submit(form, { + action: actionCallback, + ...(options.onInvalid ? { onInvalid: options.onInvalid } : {}), + ...(options.ignoreValidators !== undefined + ? { ignoreValidators: options.ignoreValidators } + : {}), + }), + ).pipe(takeUntilDestroyed(destroyRef)); return submitObservable; } From 75a23061538833a12b5621d700a1b7ec526f3e18 Mon Sep 17 00:00:00 2001 From: Cyrille Tuzi <555867+cyrilletuzi@users.noreply.github.com> Date: Fri, 20 Feb 2026 10:45:29 +0100 Subject: [PATCH 07/53] lazy submit --- README.md | 21 ++++++++++----------- lib/src/lib/rx-submit.ts | 8 +++++--- 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index a7ab3ef..18437be 100644 --- a/README.md +++ b/README.md @@ -246,17 +246,16 @@ export class EditPage { protected readonly form = form(this.formModel); protected save(): void { - from( - submit( - this.form, { - action: async (submittedForm) => - await firstValueFrom( - someObservable(submittedForm().value()).pipe(takeUntilDestroyed(this.destroyRef)), - { - defaultValue: undefined, - }, - ), - }), + defer(() => submit( + this.form, { + action: async (submittedForm) => + await firstValueFrom( + someObservable(submittedForm().value()).pipe(takeUntilDestroyed(this.destroyRef)), + { + defaultValue: undefined, + }, + ), + }), ).pipe(takeUntilDestroyed(this.destroyRef)).subscribe({ next: (success) => { if (success) { diff --git a/lib/src/lib/rx-submit.ts b/lib/src/lib/rx-submit.ts index 94913d2..0012f13 100644 --- a/lib/src/lib/rx-submit.ts +++ b/lib/src/lib/rx-submit.ts @@ -6,7 +6,7 @@ import { type FormSubmitOptions, type TreeValidationResult, } from '@angular/forms/signals'; -import { firstValueFrom, from, type Observable } from 'rxjs'; +import { defer, firstValueFrom, type Observable } from 'rxjs'; /** * Options that can be specified when submitting a form with `rxSubmit()`. @@ -110,8 +110,10 @@ export function rxSubmit( return treeValidationResult; }; - /* `submit()` the form and transform the Promise return into an Observable */ - const submitObservable: Observable = from( + /* `submit()` the form and transform the Promise return into a lazy Observable. + * It is important to use `defer()` so that the submission happens only when the Observable is subscribed to; + * otherwise with `from()`, `submit()` would be called immediately. */ + const submitObservable: Observable = defer(() => submit(form, { action: actionCallback, ...(options.onInvalid ? { onInvalid: options.onInvalid } : {}), From 4be79e869c055795e2558a17316402edfa7b40b0 Mon Sep 17 00:00:00 2001 From: Cyrille Tuzi <555867+cyrilletuzi@users.noreply.github.com> Date: Fri, 20 Feb 2026 13:57:52 +0100 Subject: [PATCH 08/53] doc --- README.md | 40 +++++++++++++++++++++++++++++++++++++--- 1 file changed, 37 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 18437be..8719a79 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,36 @@ This library provides the function `rxSubmit()`, on Observable-based equivalent - `npm install angular-rx-submit` +```ts +import { rxSubmit } from 'angular-rx-submit'; + +@Component({ + template: `
`, +}) +export class EditPage { + private readonly injector = inject(Injector); + + private readonly formModel = signal({ userName: '' }); + protected readonly form = form(this.formModel); + + protected save(): void { + rxSubmit(this.form, { + action: (submittedForm) => someObservable(submittedForm().value()), + injector: this.injector, + }).subscribe({ + next: (success) => { + if (success) { + // Manage success here + } + }, + error: (error: unknown) => { + // Manage error here + }, + }); + } +} +``` + ## Injection context One advantage of `rxSubmit()` is automatic cancellation (if the user leaves the page). @@ -300,13 +330,16 @@ interface ApiResponse { readonly error?: { message: string }; } +/** + * Transforms the API response into a `TreeValidationResult`, which is what is expected by the Angular `submit()` + */ export function mapApiResponseToTreeValidationResult(response: ApiResponse): TreeValidationResult { return response.success - ? null + ? null // `null`, `undefined` or `void` if no error : { kind: 'apiError', message: response.error?.message, - }; + }; // a `ValidationError.WithOptionalFieldTree`, or an array of that } @Injectable({ @@ -334,7 +367,7 @@ export class Api { export class EditPage { private readonly injector = inject(Injector); private readonly httpApi = inject(HttpApi); - private readonly router = inject(router); + private readonly router = inject(Router); private readonly formModel = signal({ username: '', @@ -344,6 +377,7 @@ export class EditPage { protected save(): void { rxSubmit(this.form, { action: (submittedForm) => + // Like the `submit()` action Promise, the Observable must return a `TreeValidationResult` this.httpApi.save(submittedForm().value()).pipe(map(mapApiResponseToTreeValidationResult)), injector: this.injector, }).subscribe({ From ae076c1d9ae703d8333f563963a65035a3e4a727 Mon Sep 17 00:00:00 2001 From: Cyrille Tuzi <555867+cyrilletuzi@users.noreply.github.com> Date: Fri, 20 Feb 2026 14:04:42 +0100 Subject: [PATCH 09/53] destroyRef instead of Injector --- README.md | 24 ++++++++++++------------ lib/src/lib/rx-submit.spec.ts | 27 +++++++++++++-------------- lib/src/lib/rx-submit.ts | 13 ++++++------- 3 files changed, 31 insertions(+), 33 deletions(-) diff --git a/README.md b/README.md index 8719a79..00e1993 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ import { rxSubmit } from 'angular-rx-submit'; template: `
`, }) export class EditPage { - private readonly injector = inject(Injector); + private readonly destroyRef = inject(DestroyRef); private readonly formModel = signal({ userName: '' }); protected readonly form = form(this.formModel); @@ -38,7 +38,7 @@ export class EditPage { protected save(): void { rxSubmit(this.form, { action: (submittedForm) => someObservable(submittedForm().value()), - injector: this.injector, + destroyRef: this.destroyRef, }).subscribe({ next: (success) => { if (success) { @@ -59,14 +59,14 @@ One advantage of `rxSubmit()` is automatic cancellation (if the user leaves the But for that to work, like many other Angular functions (`takeUntilDestroyed()`, `toSignal()`...), **it requires an injection context**. `rxSubmit()` follows the same pattern as those other similar Angular functions, with 2 options: -- **provide an `Injector`** +- **provide a `DestroyRef`** ```ts @Component({ template: `
`, }) export class EditPage { - private readonly injector = inject(Injector); // ⬅️ + private readonly destroyRef = inject(DestroyRef); // ⬅️ private readonly formModel = signal({ username: '' }); protected readonly form = form(this.formModel); @@ -74,7 +74,7 @@ export class EditPage { protected save(): void { rxSubmit(this.form, { action: (submittedForm) => someObservable(submittedForm().value()), - injector: this.injector, // ⬅️ + destroyRef: this.destroyRef, // ⬅️ }).subscribe(); } } @@ -100,7 +100,7 @@ export class EditPage { } ``` -**Using `rxSubmit()` outside an injection context and without providing an injector will throw the [`NG0203` error](https://angular.dev/errors/NG0203).** +**Using `rxSubmit()` outside an injection context and without providing a `DestroyRef` will throw the [`NG0203` error](https://angular.dev/errors/NG0203).** ## Subscription @@ -112,13 +112,13 @@ But **you _DO_ need to subscribe**, even if you do not have something specific t // ❌ Nothing happens rxSubmit(this.form, () => { action: (submittedForm) => someObservable(submittedForm().value()), - injector: this.injector, + destroyRef: this.destroyRef, }); // ✅ Triggers submission rxSubmit(this.form, { action: (submittedForm) => someObservable(submittedForm().value()), - injector: this.injector, + destroyRef: this.destroyRef, }).subscribe(); ``` @@ -129,7 +129,7 @@ As for any Observable, handling errors is recommended. If the Observable you pro ```ts rxSubmit(this.form, { action: () => (submittedForm) => someObservable(submittedForm().value()), - injector: this.injector, + destroyRef: this.destroyRef, }).subscribe({ next: (success) => { if (success) { @@ -187,7 +187,7 @@ Subsequent actions should be done in the `next` / `then()` callback: ```ts rxSubmit(this.form, { action: () => (submittedForm) => someObservable(submittedForm().value()), - injector: this.injector, + destroyRef: this.destroyRef, }).subscribe({ next: (success) => { if (success) { @@ -365,7 +365,7 @@ export class Api { `, }) export class EditPage { - private readonly injector = inject(Injector); + private readonly destroyRef = inject(DestroyRef); private readonly httpApi = inject(HttpApi); private readonly router = inject(Router); @@ -379,7 +379,7 @@ export class EditPage { action: (submittedForm) => // Like the `submit()` action Promise, the Observable must return a `TreeValidationResult` this.httpApi.save(submittedForm().value()).pipe(map(mapApiResponseToTreeValidationResult)), - injector: this.injector, + destroyRef: this.destroyRef, }).subscribe({ next: (success) => { if (success) { diff --git a/lib/src/lib/rx-submit.spec.ts b/lib/src/lib/rx-submit.spec.ts index cb14e6d..35e97eb 100644 --- a/lib/src/lib/rx-submit.spec.ts +++ b/lib/src/lib/rx-submit.spec.ts @@ -1,5 +1,5 @@ /* eslint-disable @typescript-eslint/prefer-promise-reject-errors */ -import { Component, DestroyRef, inject, Injector, signal } from '@angular/core'; +import { Component, DestroyRef, inject, signal } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { form, type TreeValidationResult } from '@angular/forms/signals'; import { asyncScheduler, delay, Observable, of, scheduled, throwError } from 'rxjs'; @@ -7,12 +7,12 @@ import { beforeEach, describe, expect, it } from 'vitest'; import { rxSubmit } from './rx-submit'; describe('rxSubmit ', () => { - describe('with explicit injector', () => { + describe('with explicit DestroyRef', () => { @Component({ template: '', }) class TestComponent { - readonly injector = inject(Injector); + readonly destroyRef = inject(DestroyRef); private readonly formModel = signal({ test: '', }); @@ -21,12 +21,12 @@ describe('rxSubmit ', () => { let componentFixture: ComponentFixture; let componentInstance: TestComponent; - let injector: Injector; + let destroyRef: DestroyRef; beforeEach(() => { componentFixture = TestBed.createComponent(TestComponent); componentInstance = componentFixture.componentInstance; - injector = componentInstance.injector; + destroyRef = componentInstance.destroyRef; }); it('should succeed when returning undefined', () => @@ -40,7 +40,7 @@ describe('rxSubmit ', () => { rxSubmit(componentInstance.form, { action: () => observable, - injector, + destroyRef, }).subscribe({ next: (result) => { success = result; @@ -63,7 +63,7 @@ describe('rxSubmit ', () => { const observable: Observable = scheduled(of(null), asyncScheduler); - rxSubmit(componentInstance.form, { action: () => observable, injector }).subscribe({ + rxSubmit(componentInstance.form, { action: () => observable, destroyRef }).subscribe({ next: (result) => { success = result; }, @@ -91,7 +91,7 @@ describe('rxSubmit ', () => { asyncScheduler, ); - rxSubmit(componentInstance.form, { action: () => observable, injector }).subscribe({ + rxSubmit(componentInstance.form, { action: () => observable, destroyRef }).subscribe({ next: (result) => { success = result; }, @@ -124,7 +124,7 @@ describe('rxSubmit ', () => { asyncScheduler, ); - rxSubmit(componentInstance.form, { action: () => observable, injector }).subscribe({ + rxSubmit(componentInstance.form, { action: () => observable, destroyRef }).subscribe({ next: (result) => { success = result; }, @@ -150,7 +150,7 @@ describe('rxSubmit ', () => { asyncScheduler, ); - rxSubmit(componentInstance.form, { action: () => observable, injector }).subscribe({ + rxSubmit(componentInstance.form, { action: () => observable, destroyRef }).subscribe({ next: () => { reject(); }, @@ -171,7 +171,7 @@ describe('rxSubmit ', () => { new Promise((resolve, reject) => { const observable: Observable = of(undefined).pipe(delay(2000)); - rxSubmit(componentInstance.form, { action: () => observable, injector }).subscribe({ + rxSubmit(componentInstance.form, { action: () => observable, destroyRef }).subscribe({ next: () => { reject(); }, @@ -179,8 +179,7 @@ describe('rxSubmit ', () => { reject(); }, complete: () => { - const destroyed = injector.get(DestroyRef).destroyed; - expect(destroyed).toBe(true); + expect(destroyRef.destroyed).toBe(true); resolve(undefined); }, @@ -278,7 +277,7 @@ describe('rxSubmit ', () => { }); describe('outside injection context', () => { - it('should throw if outside injection context and no injector is provided', () => { + it('should throw if outside injection context and no DestroyRef is provided', () => { @Component({ template: '', }) diff --git a/lib/src/lib/rx-submit.ts b/lib/src/lib/rx-submit.ts index 0012f13..f29a7b8 100644 --- a/lib/src/lib/rx-submit.ts +++ b/lib/src/lib/rx-submit.ts @@ -1,4 +1,4 @@ -import { assertInInjectionContext, DestroyRef, inject, type Injector } from '@angular/core'; +import { assertInInjectionContext, DestroyRef, inject } from '@angular/core'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { submit, @@ -31,11 +31,10 @@ interface RxSubmitOptions extends Pick< }, ) => Observable; /** - * `Injector` which will provide the `DestroyRef` used to clean up the Observable subscription. - * - * If this is not provided, a `DestroyRef` will be retrieved from the current injection context. + * The `DestroyRef` representing the current context. This can be passed explicitly to use `rxSubmit()` + * outside of an injection context. Otherwise, the current `DestroyRef` is injected. */ - injector?: Injector; + destroyRef?: DestroyRef; } /** @@ -82,11 +81,11 @@ export function rxSubmit( form: FieldTree, options: RxSubmitOptions, ): Observable { - if (!options.injector) { + if (!options.destroyRef) { assertInInjectionContext(rxSubmit); } - const destroyRef: DestroyRef = options.injector?.get(DestroyRef) ?? inject(DestroyRef); + const destroyRef: DestroyRef = options.destroyRef ?? inject(DestroyRef); /* Prepare the Promise-based action callback */ const actionCallback = async ( From 4e6e8064c1b3820692b9dd7cefd7f6af04a30224 Mon Sep 17 00:00:00 2001 From: Cyrille Tuzi <555867+cyrilletuzi@users.noreply.github.com> Date: Fri, 20 Feb 2026 14:15:48 +0100 Subject: [PATCH 10/53] fix --- lib/src/lib/rx-submit.ts | 38 +++++++++++--------------------------- 1 file changed, 11 insertions(+), 27 deletions(-) diff --git a/lib/src/lib/rx-submit.ts b/lib/src/lib/rx-submit.ts index f29a7b8..b8896c6 100644 --- a/lib/src/lib/rx-submit.ts +++ b/lib/src/lib/rx-submit.ts @@ -84,43 +84,27 @@ export function rxSubmit( if (!options.destroyRef) { assertInInjectionContext(rxSubmit); } - const destroyRef: DestroyRef = options.destroyRef ?? inject(DestroyRef); - /* Prepare the Promise-based action callback */ - const actionCallback = async ( - field: FieldTree, - detail: { - root: FieldTree; - submitted: FieldTree; - }, - ): Promise => { - /* Pass the form to the user-provided and Observable-based action callback */ - const actionObservable: Observable = options - .action(field, detail) - .pipe(takeUntilDestroyed(destroyRef)); - - /* Transform the action Observable into a Promise */ - const treeValidationResult: TreeValidationResult = await firstValueFrom(actionObservable, { - /* If `takeUntilDestroyed()` happens, returns `undefined` instead of throwing an `EmptyError` */ - defaultValue: undefined, - }); - - return treeValidationResult; - }; - /* `submit()` the form and transform the Promise return into a lazy Observable. * It is important to use `defer()` so that the submission happens only when the Observable is subscribed to; * otherwise with `from()`, `submit()` would be called immediately. */ - const submitObservable: Observable = defer(() => + return defer(() => submit(form, { - action: actionCallback, + action: async (submittedForm, detail) => + /* Transform the action Observable into a Promise */ + await firstValueFrom( + /* Pass the form to the user-provided and Observable-based action callback */ + options.action(submittedForm, detail).pipe(takeUntilDestroyed(destroyRef)), + { + /* If `takeUntilDestroyed()` happens, returns `undefined` instead of throwing an `EmptyError` */ + defaultValue: undefined, + }, + ), ...(options.onInvalid ? { onInvalid: options.onInvalid } : {}), ...(options.ignoreValidators !== undefined ? { ignoreValidators: options.ignoreValidators } : {}), }), ).pipe(takeUntilDestroyed(destroyRef)); - - return submitObservable; } From 37a768e98dda408499895ff26d6d942fa370adc1 Mon Sep 17 00:00:00 2001 From: Cyrille Tuzi <555867+cyrilletuzi@users.noreply.github.com> Date: Fri, 20 Feb 2026 22:03:25 +0100 Subject: [PATCH 11/53] add toFormSubmitOptions --- eslint.config.js | 13 ++++++++ lib/src/lib/rx-form-submit-options.ts | 32 ++++++++++++++++++ lib/src/lib/rx-submit.ts | 47 ++++----------------------- lib/src/lib/to-form-submit-options.ts | 45 +++++++++++++++++++++++++ lib/src/public-api.ts | 3 +- 5 files changed, 99 insertions(+), 41 deletions(-) create mode 100644 lib/src/lib/rx-form-submit-options.ts create mode 100644 lib/src/lib/to-form-submit-options.ts diff --git a/eslint.config.js b/eslint.config.js index e4e6c71..eaf3465 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -76,12 +76,25 @@ module.exports = defineConfig([ allow: ['arrowFunctions'], // some callbacks are required (like in promises `.catch()`), but there is not always something to do inside }, ], + '@typescript-eslint/no-empty-object-type': [ + 'error', + { + allowInterfaces: 'with-single-extends', + }, + ], '@typescript-eslint/no-extraneous-class': [ 'error', { allowWithDecorator: true, // some Angular classes can be empty }, ], + // Loosen some annoying and inadequate no unused rules + '@typescript-eslint/no-unused-vars': [ + 'error', + { + ignoreRestSiblings: true, + }, + ], // Enforce Angular good practices '@angular-eslint/consistent-component-styles': 'error', '@angular-eslint/sort-lifecycle-methods': 'error', diff --git a/lib/src/lib/rx-form-submit-options.ts b/lib/src/lib/rx-form-submit-options.ts new file mode 100644 index 0000000..30ad9de --- /dev/null +++ b/lib/src/lib/rx-form-submit-options.ts @@ -0,0 +1,32 @@ +import type { DestroyRef } from '@angular/core'; +import type { FieldTree, FormSubmitOptions, TreeValidationResult } from '@angular/forms/signals'; +import type { Observable } from 'rxjs'; + +/** + * Options that can be specified when submitting a form with `rxSubmit()`. + * + * @experimental 21.2.0 + */ +export interface CommonRxFormSubmitOptions extends Omit< + FormSubmitOptions, + 'action' +> { + /** + * Function to run when submitting the form data (when form is valid). + * + * @param field The submitted field + * @param detail An object containing the root field of the submitted form as well as the submitted field itself + */ + action: ( + field: FieldTree, + detail: { + root: FieldTree; + submitted: FieldTree; + }, + ) => Observable; + /** + * The `DestroyRef` representing the current context. This can be passed explicitly to use `rxSubmit()` + * outside of an injection context. Otherwise, the current `DestroyRef` is injected. + */ + destroyRef?: DestroyRef; +} diff --git a/lib/src/lib/rx-submit.ts b/lib/src/lib/rx-submit.ts index b8896c6..33d8c54 100644 --- a/lib/src/lib/rx-submit.ts +++ b/lib/src/lib/rx-submit.ts @@ -1,41 +1,10 @@ import { assertInInjectionContext, DestroyRef, inject } from '@angular/core'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; -import { - submit, - type FieldTree, - type FormSubmitOptions, - type TreeValidationResult, -} from '@angular/forms/signals'; +import { submit, type FieldTree } from '@angular/forms/signals'; import { defer, firstValueFrom, type Observable } from 'rxjs'; +import type { CommonRxFormSubmitOptions } from './rx-form-submit-options'; -/** - * Options that can be specified when submitting a form with `rxSubmit()`. - * - * @experimental 21.2.0 - */ -interface RxSubmitOptions extends Pick< - FormSubmitOptions, - 'onInvalid' | 'ignoreValidators' -> { - /** - * Function to run when submitting the form data (when form is valid). - * - * @param field The submitted field - * @param detail An object containing the root field of the submitted form as well as the submitted field itself - */ - action: ( - field: FieldTree, - detail: { - root: FieldTree; - submitted: FieldTree; - }, - ) => Observable; - /** - * The `DestroyRef` representing the current context. This can be passed explicitly to use `rxSubmit()` - * outside of an injection context. Otherwise, the current `DestroyRef` is injected. - */ - destroyRef?: DestroyRef; -} +export interface RxSubmitOptions extends CommonRxFormSubmitOptions {} /** * Submits a given `FieldTree` using the given action function and applies any submission errors @@ -84,7 +53,8 @@ export function rxSubmit( if (!options.destroyRef) { assertInInjectionContext(rxSubmit); } - const destroyRef: DestroyRef = options.destroyRef ?? inject(DestroyRef); + + const { action, destroyRef = inject(DestroyRef), ...otherOptions } = options; /* `submit()` the form and transform the Promise return into a lazy Observable. * It is important to use `defer()` so that the submission happens only when the Observable is subscribed to; @@ -95,16 +65,13 @@ export function rxSubmit( /* Transform the action Observable into a Promise */ await firstValueFrom( /* Pass the form to the user-provided and Observable-based action callback */ - options.action(submittedForm, detail).pipe(takeUntilDestroyed(destroyRef)), + action(submittedForm, detail).pipe(takeUntilDestroyed(destroyRef)), { /* If `takeUntilDestroyed()` happens, returns `undefined` instead of throwing an `EmptyError` */ defaultValue: undefined, }, ), - ...(options.onInvalid ? { onInvalid: options.onInvalid } : {}), - ...(options.ignoreValidators !== undefined - ? { ignoreValidators: options.ignoreValidators } - : {}), + ...otherOptions, }), ).pipe(takeUntilDestroyed(destroyRef)); } diff --git a/lib/src/lib/to-form-submit-options.ts b/lib/src/lib/to-form-submit-options.ts new file mode 100644 index 0000000..a72b230 --- /dev/null +++ b/lib/src/lib/to-form-submit-options.ts @@ -0,0 +1,45 @@ +import { assertInInjectionContext } from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import type { FormSubmitOptions } from '@angular/forms/signals'; +import { catchError, firstValueFrom, of, tap } from 'rxjs'; +import type { CommonRxFormSubmitOptions } from './rx-form-submit-options'; + +export interface RxFormSubmitOptions extends CommonRxFormSubmitOptions { + onSuccess?: () => void; + onError?: (error: unknown) => void; +} + +export function toFormSubmitOptions( + options: RxFormSubmitOptions, +): FormSubmitOptions { + if (!options.destroyRef) { + assertInInjectionContext(toFormSubmitOptions); + } + + const { action, destroyRef, onSuccess, onError, ...otherOptions } = options; + + return { + action: (form, detail) => + firstValueFrom( + action(form, detail).pipe( + takeUntilDestroyed(destroyRef), + tap(() => { + if (onSuccess && form().valid()) { + onSuccess(); + } + }), + catchError((error: unknown) => { + if (onError) { + onError(error); + return of(undefined); + } + throw error; + }), + ), + { + defaultValue: undefined, + }, + ), + ...otherOptions, + }; +} diff --git a/lib/src/public-api.ts b/lib/src/public-api.ts index 3b1c04f..6fd8c7d 100644 --- a/lib/src/public-api.ts +++ b/lib/src/public-api.ts @@ -2,4 +2,5 @@ * Public API Surface of lib */ -export { rxSubmit } from './lib/rx-submit'; +export { rxSubmit, type RxSubmitOptions } from './lib/rx-submit'; +export { toFormSubmitOptions, type RxFormSubmitOptions } from './lib/to-form-submit-options'; From d0eda76c2878f788d0ce251a00071cbb62d600c9 Mon Sep 17 00:00:00 2001 From: Cyrille Tuzi <555867+cyrilletuzi@users.noreply.github.com> Date: Sat, 21 Feb 2026 10:10:33 +0100 Subject: [PATCH 12/53] renaming --- eslint.config.js | 7 -- lib/src/lib/rx-common-form-submit-options.ts | 27 ++++++++ lib/src/lib/rx-form-submit-options.ts | 73 ++++++++++++-------- lib/src/lib/rx-submit.ts | 9 ++- lib/src/lib/to-form-submit-options.ts | 45 ------------ lib/src/public-api.ts | 2 +- 6 files changed, 78 insertions(+), 85 deletions(-) create mode 100644 lib/src/lib/rx-common-form-submit-options.ts delete mode 100644 lib/src/lib/to-form-submit-options.ts diff --git a/eslint.config.js b/eslint.config.js index eaf3465..4da3734 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -88,13 +88,6 @@ module.exports = defineConfig([ allowWithDecorator: true, // some Angular classes can be empty }, ], - // Loosen some annoying and inadequate no unused rules - '@typescript-eslint/no-unused-vars': [ - 'error', - { - ignoreRestSiblings: true, - }, - ], // Enforce Angular good practices '@angular-eslint/consistent-component-styles': 'error', '@angular-eslint/sort-lifecycle-methods': 'error', diff --git a/lib/src/lib/rx-common-form-submit-options.ts b/lib/src/lib/rx-common-form-submit-options.ts new file mode 100644 index 0000000..a32716d --- /dev/null +++ b/lib/src/lib/rx-common-form-submit-options.ts @@ -0,0 +1,27 @@ +import type { DestroyRef } from '@angular/core'; +import type { FieldTree, FormSubmitOptions, TreeValidationResult } from '@angular/forms/signals'; +import type { Observable } from 'rxjs'; + +export interface RxCommonFormSubmitOptions extends Omit< + FormSubmitOptions, + 'action' +> { + /** + * Function to run when submitting the form data (when form is valid). + * + * @param field The submitted field + * @param detail An object containing the root field of the submitted form as well as the submitted field itself + */ + action: ( + field: FieldTree, + detail: { + root: FieldTree; + submitted: FieldTree; + }, + ) => Observable; + /** + * The `DestroyRef` representing the current context. This can be passed explicitly to use `rxSubmit()` + * outside of an injection context. Otherwise, the current `DestroyRef` is injected. + */ + destroyRef?: DestroyRef; +} diff --git a/lib/src/lib/rx-form-submit-options.ts b/lib/src/lib/rx-form-submit-options.ts index 30ad9de..a0a1af2 100644 --- a/lib/src/lib/rx-form-submit-options.ts +++ b/lib/src/lib/rx-form-submit-options.ts @@ -1,32 +1,45 @@ -import type { DestroyRef } from '@angular/core'; -import type { FieldTree, FormSubmitOptions, TreeValidationResult } from '@angular/forms/signals'; -import type { Observable } from 'rxjs'; +import { assertInInjectionContext } from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import type { FormSubmitOptions } from '@angular/forms/signals'; +import { catchError, firstValueFrom, of, tap } from 'rxjs'; +import type { RxCommonFormSubmitOptions } from './rx-common-form-submit-options'; -/** - * Options that can be specified when submitting a form with `rxSubmit()`. - * - * @experimental 21.2.0 - */ -export interface CommonRxFormSubmitOptions extends Omit< - FormSubmitOptions, - 'action' -> { - /** - * Function to run when submitting the form data (when form is valid). - * - * @param field The submitted field - * @param detail An object containing the root field of the submitted form as well as the submitted field itself - */ - action: ( - field: FieldTree, - detail: { - root: FieldTree; - submitted: FieldTree; - }, - ) => Observable; - /** - * The `DestroyRef` representing the current context. This can be passed explicitly to use `rxSubmit()` - * outside of an injection context. Otherwise, the current `DestroyRef` is injected. - */ - destroyRef?: DestroyRef; +export interface RxFormSubmitOptions extends RxCommonFormSubmitOptions { + onSuccess?: () => void; + onError?: (error: unknown) => void; +} + +export function rxFormSubmitOptions( + options: RxFormSubmitOptions, +): FormSubmitOptions { + if (!options.destroyRef) { + assertInInjectionContext(rxFormSubmitOptions); + } + + const { action, destroyRef, onSuccess, onError, ...otherOptions } = options; + + return { + action: (form, detail) => + firstValueFrom( + action(form, detail).pipe( + takeUntilDestroyed(destroyRef), + tap(() => { + if (onSuccess && form().valid()) { + onSuccess(); + } + }), + catchError((error: unknown) => { + if (onError) { + onError(error); + return of(undefined); + } + throw error; + }), + ), + { + defaultValue: undefined, + }, + ), + ...otherOptions, + }; } diff --git a/lib/src/lib/rx-submit.ts b/lib/src/lib/rx-submit.ts index 33d8c54..8d84f6a 100644 --- a/lib/src/lib/rx-submit.ts +++ b/lib/src/lib/rx-submit.ts @@ -2,9 +2,14 @@ import { assertInInjectionContext, DestroyRef, inject } from '@angular/core'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { submit, type FieldTree } from '@angular/forms/signals'; import { defer, firstValueFrom, type Observable } from 'rxjs'; -import type { CommonRxFormSubmitOptions } from './rx-form-submit-options'; +import type { RxCommonFormSubmitOptions } from './rx-common-form-submit-options'; -export interface RxSubmitOptions extends CommonRxFormSubmitOptions {} +/** + * Options that can be specified when submitting a form with `rxSubmit()`. + * + * @experimental 21.2.0 + */ +export interface RxSubmitOptions extends RxCommonFormSubmitOptions {} /** * Submits a given `FieldTree` using the given action function and applies any submission errors diff --git a/lib/src/lib/to-form-submit-options.ts b/lib/src/lib/to-form-submit-options.ts deleted file mode 100644 index a72b230..0000000 --- a/lib/src/lib/to-form-submit-options.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { assertInInjectionContext } from '@angular/core'; -import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; -import type { FormSubmitOptions } from '@angular/forms/signals'; -import { catchError, firstValueFrom, of, tap } from 'rxjs'; -import type { CommonRxFormSubmitOptions } from './rx-form-submit-options'; - -export interface RxFormSubmitOptions extends CommonRxFormSubmitOptions { - onSuccess?: () => void; - onError?: (error: unknown) => void; -} - -export function toFormSubmitOptions( - options: RxFormSubmitOptions, -): FormSubmitOptions { - if (!options.destroyRef) { - assertInInjectionContext(toFormSubmitOptions); - } - - const { action, destroyRef, onSuccess, onError, ...otherOptions } = options; - - return { - action: (form, detail) => - firstValueFrom( - action(form, detail).pipe( - takeUntilDestroyed(destroyRef), - tap(() => { - if (onSuccess && form().valid()) { - onSuccess(); - } - }), - catchError((error: unknown) => { - if (onError) { - onError(error); - return of(undefined); - } - throw error; - }), - ), - { - defaultValue: undefined, - }, - ), - ...otherOptions, - }; -} diff --git a/lib/src/public-api.ts b/lib/src/public-api.ts index 6fd8c7d..880dc57 100644 --- a/lib/src/public-api.ts +++ b/lib/src/public-api.ts @@ -2,5 +2,5 @@ * Public API Surface of lib */ +export { rxFormSubmitOptions, type RxFormSubmitOptions } from './lib/rx-form-submit-options'; export { rxSubmit, type RxSubmitOptions } from './lib/rx-submit'; -export { toFormSubmitOptions, type RxFormSubmitOptions } from './lib/to-form-submit-options'; From 9adc7b9cdf179a1de38562b24a8edad0069047f1 Mon Sep 17 00:00:00 2001 From: Cyrille Tuzi <555867+cyrilletuzi@users.noreply.github.com> Date: Sat, 21 Feb 2026 10:16:10 +0100 Subject: [PATCH 13/53] fix --- lib/src/lib/rx-form-submit-options.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/src/lib/rx-form-submit-options.ts b/lib/src/lib/rx-form-submit-options.ts index a0a1af2..ecf1d2c 100644 --- a/lib/src/lib/rx-form-submit-options.ts +++ b/lib/src/lib/rx-form-submit-options.ts @@ -23,8 +23,8 @@ export function rxFormSubmitOptions( firstValueFrom( action(form, detail).pipe( takeUntilDestroyed(destroyRef), - tap(() => { - if (onSuccess && form().valid()) { + tap((treeValidationResult) => { + if (onSuccess && !treeValidationResult) { onSuccess(); } }), From 20be5b19ba36c814f9944b5a7144a1b43528ba78 Mon Sep 17 00:00:00 2001 From: Cyrille Tuzi <555867+cyrilletuzi@users.noreply.github.com> Date: Fri, 27 Feb 2026 11:12:38 +0100 Subject: [PATCH 14/53] fix --- lib/src/lib/rx-form-submit-options.ts | 4 ++-- lib/src/lib/rx-submit.spec.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/src/lib/rx-form-submit-options.ts b/lib/src/lib/rx-form-submit-options.ts index ecf1d2c..5a053c2 100644 --- a/lib/src/lib/rx-form-submit-options.ts +++ b/lib/src/lib/rx-form-submit-options.ts @@ -1,4 +1,4 @@ -import { assertInInjectionContext } from '@angular/core'; +import { assertInInjectionContext, DestroyRef, inject } from '@angular/core'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import type { FormSubmitOptions } from '@angular/forms/signals'; import { catchError, firstValueFrom, of, tap } from 'rxjs'; @@ -16,7 +16,7 @@ export function rxFormSubmitOptions( assertInInjectionContext(rxFormSubmitOptions); } - const { action, destroyRef, onSuccess, onError, ...otherOptions } = options; + const { action, destroyRef = inject(DestroyRef), onSuccess, onError, ...otherOptions } = options; return { action: (form, detail) => diff --git a/lib/src/lib/rx-submit.spec.ts b/lib/src/lib/rx-submit.spec.ts index 35e97eb..769297b 100644 --- a/lib/src/lib/rx-submit.spec.ts +++ b/lib/src/lib/rx-submit.spec.ts @@ -79,7 +79,7 @@ describe('rxSubmit ', () => { }); })); - it('should succeed be invalid when returning one validation error', () => + it('should be invalid when returning one validation error', () => new Promise((resolve, reject) => { let success: boolean; From 4138e61cb0609072341eebedf0a64ffd4f635233 Mon Sep 17 00:00:00 2001 From: Cyrille Tuzi <555867+cyrilletuzi@users.noreply.github.com> Date: Fri, 27 Feb 2026 14:21:28 +0100 Subject: [PATCH 15/53] simpler options --- ...ons.ts => rx-form-submit-options-model.ts} | 2 +- lib/src/lib/rx-form-submit-options.ts | 41 ++++++------------- lib/src/lib/rx-submit.ts | 11 +---- lib/src/public-api.ts | 5 ++- 4 files changed, 18 insertions(+), 41 deletions(-) rename lib/src/lib/{rx-common-form-submit-options.ts => rx-form-submit-options-model.ts} (91%) diff --git a/lib/src/lib/rx-common-form-submit-options.ts b/lib/src/lib/rx-form-submit-options-model.ts similarity index 91% rename from lib/src/lib/rx-common-form-submit-options.ts rename to lib/src/lib/rx-form-submit-options-model.ts index a32716d..dffcac2 100644 --- a/lib/src/lib/rx-common-form-submit-options.ts +++ b/lib/src/lib/rx-form-submit-options-model.ts @@ -2,7 +2,7 @@ import type { DestroyRef } from '@angular/core'; import type { FieldTree, FormSubmitOptions, TreeValidationResult } from '@angular/forms/signals'; import type { Observable } from 'rxjs'; -export interface RxCommonFormSubmitOptions extends Omit< +export interface RxFormSubmitOptions extends Omit< FormSubmitOptions, 'action' > { diff --git a/lib/src/lib/rx-form-submit-options.ts b/lib/src/lib/rx-form-submit-options.ts index 5a053c2..cdf468b 100644 --- a/lib/src/lib/rx-form-submit-options.ts +++ b/lib/src/lib/rx-form-submit-options.ts @@ -1,45 +1,28 @@ import { assertInInjectionContext, DestroyRef, inject } from '@angular/core'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import type { FormSubmitOptions } from '@angular/forms/signals'; -import { catchError, firstValueFrom, of, tap } from 'rxjs'; -import type { RxCommonFormSubmitOptions } from './rx-common-form-submit-options'; - -export interface RxFormSubmitOptions extends RxCommonFormSubmitOptions { - onSuccess?: () => void; - onError?: (error: unknown) => void; -} +import { firstValueFrom } from 'rxjs'; +import type { RxFormSubmitOptions } from './rx-form-submit-options-model'; +/** + * Options that can be specified when submitting a form with `rxSubmit()`. + * + * @experimental 21.2.0 + */ export function rxFormSubmitOptions( - options: RxFormSubmitOptions, + options: RxFormSubmitOptions, ): FormSubmitOptions { if (!options.destroyRef) { assertInInjectionContext(rxFormSubmitOptions); } - const { action, destroyRef = inject(DestroyRef), onSuccess, onError, ...otherOptions } = options; + const { action, destroyRef = inject(DestroyRef), ...otherOptions } = options; return { action: (form, detail) => - firstValueFrom( - action(form, detail).pipe( - takeUntilDestroyed(destroyRef), - tap((treeValidationResult) => { - if (onSuccess && !treeValidationResult) { - onSuccess(); - } - }), - catchError((error: unknown) => { - if (onError) { - onError(error); - return of(undefined); - } - throw error; - }), - ), - { - defaultValue: undefined, - }, - ), + firstValueFrom(action(form, detail).pipe(takeUntilDestroyed(destroyRef)), { + defaultValue: undefined, + }), ...otherOptions, }; } diff --git a/lib/src/lib/rx-submit.ts b/lib/src/lib/rx-submit.ts index 8d84f6a..2de14e8 100644 --- a/lib/src/lib/rx-submit.ts +++ b/lib/src/lib/rx-submit.ts @@ -2,14 +2,7 @@ import { assertInInjectionContext, DestroyRef, inject } from '@angular/core'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { submit, type FieldTree } from '@angular/forms/signals'; import { defer, firstValueFrom, type Observable } from 'rxjs'; -import type { RxCommonFormSubmitOptions } from './rx-common-form-submit-options'; - -/** - * Options that can be specified when submitting a form with `rxSubmit()`. - * - * @experimental 21.2.0 - */ -export interface RxSubmitOptions extends RxCommonFormSubmitOptions {} +import type { RxFormSubmitOptions } from './rx-form-submit-options-model'; /** * Submits a given `FieldTree` using the given action function and applies any submission errors @@ -53,7 +46,7 @@ export interface RxSubmitOptions extends RxCommonFormSubmitOptions( form: FieldTree, - options: RxSubmitOptions, + options: RxFormSubmitOptions, ): Observable { if (!options.destroyRef) { assertInInjectionContext(rxSubmit); diff --git a/lib/src/public-api.ts b/lib/src/public-api.ts index 880dc57..8cc8c59 100644 --- a/lib/src/public-api.ts +++ b/lib/src/public-api.ts @@ -2,5 +2,6 @@ * Public API Surface of lib */ -export { rxFormSubmitOptions, type RxFormSubmitOptions } from './lib/rx-form-submit-options'; -export { rxSubmit, type RxSubmitOptions } from './lib/rx-submit'; +export { rxFormSubmitOptions } from './lib/rx-form-submit-options'; +export { type RxFormSubmitOptions } from './lib/rx-form-submit-options-model'; +export { rxSubmit } from './lib/rx-submit'; From 59e2f5e1f53cdef91716d0e024fe50f9890ca513 Mon Sep 17 00:00:00 2001 From: Cyrille Tuzi <555867+cyrilletuzi@users.noreply.github.com> Date: Fri, 27 Feb 2026 14:21:41 +0100 Subject: [PATCH 16/53] options spec --- lib/src/lib/rx-form-submit-options.spec.ts | 72 ++++++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 lib/src/lib/rx-form-submit-options.spec.ts diff --git a/lib/src/lib/rx-form-submit-options.spec.ts b/lib/src/lib/rx-form-submit-options.spec.ts new file mode 100644 index 0000000..bd2e973 --- /dev/null +++ b/lib/src/lib/rx-form-submit-options.spec.ts @@ -0,0 +1,72 @@ +import { Component, signal } from '@angular/core'; +import { TestBed } from '@angular/core/testing'; +import { form, submit, type TreeValidationResult } from '@angular/forms/signals'; +import { asyncScheduler, Observable, of, scheduled } from 'rxjs'; +import { beforeEach, describe, expect, it } from 'vitest'; +import { rxFormSubmitOptions } from './rx-form-submit-options'; + +describe('rxFormSubmitOptions', () => { + @Component({ + template: '', + }) + class TestComponent { + observable!: Observable; + form = form( + signal({ + test: '', + }), + { + submission: rxFormSubmitOptions({ + action: () => this.observable, + }), + }, + ); + } + let componentInstance: TestComponent; + + beforeEach(() => { + const componentFixture = TestBed.createComponent(TestComponent); + componentInstance = componentFixture.componentInstance; + }); + + it('should succeed when returning undefined', async () => { + componentInstance.observable = scheduled(of(undefined), asyncScheduler); + await submit(componentInstance.form); + + expect(componentInstance.form().valid()).toBe(true); + }); + + it('should succeed when returning null', async () => { + componentInstance.observable = scheduled(of(null), asyncScheduler); + await submit(componentInstance.form); + + expect(componentInstance.form().valid()).toBe(true); + }); + + it('should be invalid when returning one validation error', async () => { + const treeValidationResult: TreeValidationResult = { + kind: 'apiError', + }; + componentInstance.observable = scheduled(of(treeValidationResult), asyncScheduler); + await submit(componentInstance.form); + + expect(componentInstance.form().valid()).toBe(false); + expect(componentInstance.form().errors()[0]).toEqual(treeValidationResult); + }); + + it('should be invalid when returning multiple validation errors', async () => { + const error1: TreeValidationResult = { + kind: 'apiError1', + }; + const error2: TreeValidationResult = { + kind: 'apiError2', + }; + const treeValidationResult: TreeValidationResult = [error1, error2]; + componentInstance.observable = scheduled(of(treeValidationResult), asyncScheduler); + await submit(componentInstance.form); + + expect(componentInstance.form().valid()).toBe(false); + expect(componentInstance.form().errors()[0]).toEqual(error1); + expect(componentInstance.form().errors()[1]).toEqual(error2); + }); +}); From 78e3a3d782edb0a779ed257cfc67ff49e1275595 Mon Sep 17 00:00:00 2001 From: Cyrille Tuzi <555867+cyrilletuzi@users.noreply.github.com> Date: Fri, 27 Feb 2026 14:25:41 +0100 Subject: [PATCH 17/53] ng 21.2 stable --- lib/package.json | 4 +- package-lock.json | 1263 ++++++++++++++++++++------------------------- package.json | 22 +- 3 files changed, 565 insertions(+), 724 deletions(-) diff --git a/lib/package.json b/lib/package.json index 3628bd9..87300f4 100644 --- a/lib/package.json +++ b/lib/package.json @@ -2,8 +2,8 @@ "name": "angular-rx-submit", "version": "0.0.1", "peerDependencies": { - "@angular/core": "^21.2.0-next.0", - "@angular/forms": "^21.2.0-next.0", + "@angular/core": "^21.2.0", + "@angular/forms": "^21.2.0", "rxjs": "^7.4.0" }, "dependencies": { diff --git a/package-lock.json b/package-lock.json index b8ce0db..57a32c0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,26 +8,26 @@ "name": "angular-rx-submit", "version": "0.0.0", "dependencies": { - "@angular/common": "^21.2.0-next.0", - "@angular/compiler": "^21.2.0-next.0", - "@angular/core": "^21.2.0-next.0", - "@angular/forms": "^21.2.0-next.0", - "@angular/platform-browser": "^21.2.0-next.0", - "@angular/router": "^21.2.0-next.0", + "@angular/common": "^21.2.0", + "@angular/compiler": "^21.2.0", + "@angular/core": "^21.2.0", + "@angular/forms": "^21.2.0", + "@angular/platform-browser": "^21.2.0", + "@angular/router": "^21.2.0", "rxjs": "~7.8.0", "tslib": "^2.8.1" }, "devDependencies": { - "@angular/build": "^21.2.0-next.2", - "@angular/cli": "^21.2.0-next.2", - "@angular/compiler-cli": "^21.2.0-next.0", + "@angular/build": "^21.2.0", + "@angular/cli": "^21.2.0", + "@angular/compiler-cli": "^21.2.0", "angular-eslint": "^21.2.0", "eslint": "^9.39.3", "jsdom": "^28.1.0", - "ng-packagr": "^21.2.0-next.0", + "ng-packagr": "^21.2.0", "prettier": "^3.8.1", "typescript": "~5.9.2", - "typescript-eslint": "^8.56.0", + "typescript-eslint": "^8.56.1", "vitest": "^4.0.18" } }, @@ -262,13 +262,13 @@ } }, "node_modules/@angular-devkit/architect": { - "version": "0.2102.0-rc.0", - "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.2102.0-rc.0.tgz", - "integrity": "sha512-WutplD9wHcm6B1IraZULr5BU4jcLXDu17GzAU3oWnX4FkVly7jUcsfovHWNpndw0JDb1hK7wW74Xp7wP8PRJtw==", + "version": "0.2102.0", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.2102.0.tgz", + "integrity": "sha512-kYFwTNzToG2SJMxj2f41w3QRtdqlrFuF+bpZrtIaHOP078Ktld8EPIp9KqB0Y46Vvs69ifby5Q1/wPD9wA3iaw==", "dev": true, "license": "MIT", "dependencies": { - "@angular-devkit/core": "21.2.0-rc.0", + "@angular-devkit/core": "21.2.0", "rxjs": "7.8.2" }, "bin": { @@ -281,9 +281,9 @@ } }, "node_modules/@angular-devkit/core": { - "version": "21.2.0-rc.0", - "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-21.2.0-rc.0.tgz", - "integrity": "sha512-HnSUf2pkjSDdJMdkcb/eMgwI9TQG/TqZlqtTGjreraD0tr5WI2FWsdOQu2JGys8VgJK1Y+XYpvfL7EF1eSHeWw==", + "version": "21.2.0", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-21.2.0.tgz", + "integrity": "sha512-HZdTn46Ca6qbb9Zef8R/+TWsk6mNKRm4rJyL3PxHP6HnVCwSPNZ0LNN9BjVREBs+UlRdXqBGFBZh5D1nBgu5GQ==", "dev": true, "license": "MIT", "dependencies": { @@ -309,13 +309,13 @@ } }, "node_modules/@angular-devkit/schematics": { - "version": "21.2.0-rc.0", - "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-21.2.0-rc.0.tgz", - "integrity": "sha512-G8idL0k1thHcy6cHnSLbBNKG1MSsTpqoEe4aaQIOtoAe8KEhIZ3jQfeFYWCHPFOPhnrExZc0l32UeDdEzQ4akw==", + "version": "21.2.0", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-21.2.0.tgz", + "integrity": "sha512-3kn3FI5v7BQ7Zct6raek+WgvyDwOJ8wElbyC903GxMQCDBRGGcevhHvTAIHhknihEsrgplzPhTlWeMbk1JfdFg==", "dev": true, "license": "MIT", "dependencies": { - "@angular-devkit/core": "21.2.0-rc.0", + "@angular-devkit/core": "21.2.0", "jsonc-parser": "3.3.1", "magic-string": "0.30.21", "ora": "9.3.0", @@ -327,6 +327,22 @@ "yarn": ">= 1.13.0" } }, + "node_modules/@angular-eslint/builder": { + "version": "21.2.0", + "resolved": "https://registry.npmjs.org/@angular-eslint/builder/-/builder-21.2.0.tgz", + "integrity": "sha512-wcp3J9cbrDwSeI/o1D/DSvMQa8zpKjc5WhRGTx33omhWijCfiVNEAiBLWiEx5Sb/dWcoX8yFNWY5jSgFVy9Sjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/architect": ">= 0.2100.0 < 0.2200.0", + "@angular-devkit/core": ">= 21.0.0 < 22.0.0" + }, + "peerDependencies": { + "@angular/cli": ">= 21.0.0 < 22.0.0", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": "*" + } + }, "node_modules/@angular-eslint/bundled-angular-compiler": { "version": "21.2.0", "resolved": "https://registry.npmjs.org/@angular-eslint/bundled-angular-compiler/-/bundled-angular-compiler-21.2.0.tgz", @@ -371,6 +387,38 @@ "typescript": "*" } }, + "node_modules/@angular-eslint/schematics": { + "version": "21.2.0", + "resolved": "https://registry.npmjs.org/@angular-eslint/schematics/-/schematics-21.2.0.tgz", + "integrity": "sha512-WtT4fPKIUQ/hswy+l2GF/rKOdD+42L3fUzzcwRzNutQbe2tU9SimoSOAsay/ylWEuhIOQTs7ysPB8fUgFQoLpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": ">= 21.0.0 < 22.0.0", + "@angular-devkit/schematics": ">= 21.0.0 < 22.0.0", + "@angular-eslint/eslint-plugin": "21.2.0", + "@angular-eslint/eslint-plugin-template": "21.2.0", + "ignore": "7.0.5", + "semver": "7.7.3", + "strip-json-comments": "3.1.1" + }, + "peerDependencies": { + "@angular/cli": ">= 21.0.0 < 22.0.0" + } + }, + "node_modules/@angular-eslint/schematics/node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/@angular-eslint/template-parser": { "version": "21.2.0", "resolved": "https://registry.npmjs.org/@angular-eslint/template-parser/-/template-parser-21.2.0.tgz", @@ -402,14 +450,14 @@ } }, "node_modules/@angular/build": { - "version": "21.2.0-rc.0", - "resolved": "https://registry.npmjs.org/@angular/build/-/build-21.2.0-rc.0.tgz", - "integrity": "sha512-8EORjR9+2KtU9Jc0PFjK8J5hrK1QJdkI9aTZZI7bTZ9Fie2Ymll5p3bEGL5mO5DZaDxGY+iA9vyfl10SsLKBpA==", + "version": "21.2.0", + "resolved": "https://registry.npmjs.org/@angular/build/-/build-21.2.0.tgz", + "integrity": "sha512-K0EqiHz2y7TSyD4adWD0+C/P9khKlrsSWavXWxGRvoSJC/H3I3SK5Z6BWwftBibXR1Fis7njwvl5IGAlQrDchA==", "dev": true, "license": "MIT", "dependencies": { "@ampproject/remapping": "2.3.0", - "@angular-devkit/architect": "0.2102.0-rc.0", + "@angular-devkit/architect": "0.2102.0", "@babel/core": "7.29.0", "@babel/helper-annotate-as-pure": "7.27.3", "@babel/helper-split-export-declaration": "7.24.7", @@ -445,17 +493,17 @@ "lmdb": "3.5.1" }, "peerDependencies": { - "@angular/compiler": "^21.0.0 || ^21.2.0-next.0", - "@angular/compiler-cli": "^21.0.0 || ^21.2.0-next.0", - "@angular/core": "^21.0.0 || ^21.2.0-next.0", - "@angular/localize": "^21.0.0 || ^21.2.0-next.0", - "@angular/platform-browser": "^21.0.0 || ^21.2.0-next.0", - "@angular/platform-server": "^21.0.0 || ^21.2.0-next.0", - "@angular/service-worker": "^21.0.0 || ^21.2.0-next.0", - "@angular/ssr": "^21.2.0-rc.0", + "@angular/compiler": "^21.0.0", + "@angular/compiler-cli": "^21.0.0", + "@angular/core": "^21.0.0", + "@angular/localize": "^21.0.0", + "@angular/platform-browser": "^21.0.0", + "@angular/platform-server": "^21.0.0", + "@angular/service-worker": "^21.0.0", + "@angular/ssr": "^21.2.0", "karma": "^6.4.0", "less": "^4.2.0", - "ng-packagr": "^21.0.0 || ^21.2.0-next.0", + "ng-packagr": "^21.0.0", "postcss": "^8.4.0", "tailwindcss": "^2.0.0 || ^3.0.0 || ^4.0.0", "tslib": "^2.3.0", @@ -502,19 +550,19 @@ } }, "node_modules/@angular/cli": { - "version": "21.2.0-rc.0", - "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-21.2.0-rc.0.tgz", - "integrity": "sha512-Manaq4uJ+NBAfvtnsHPED88MiUYeD4a8Uy2aBREqypDU2iZOBzi3MdYtVso4TQkA68JtEb5WyazfB4tVZwl/Yw==", + "version": "21.2.0", + "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-21.2.0.tgz", + "integrity": "sha512-yaGEpckqgOemcHkoWeH92i9eNrcbr9iE/dnxL+Du6s9spTAXJ2jjtYfszhmowuQZkCK5rjecMb8ctNtHlaGCjg==", "dev": true, "license": "MIT", "dependencies": { - "@angular-devkit/architect": "0.2102.0-rc.0", - "@angular-devkit/core": "21.2.0-rc.0", - "@angular-devkit/schematics": "21.2.0-rc.0", + "@angular-devkit/architect": "0.2102.0", + "@angular-devkit/core": "21.2.0", + "@angular-devkit/schematics": "21.2.0", "@inquirer/prompts": "7.10.1", "@listr2/prompt-adapter-inquirer": "3.0.5", "@modelcontextprotocol/sdk": "1.26.0", - "@schematics/angular": "21.2.0-rc.0", + "@schematics/angular": "21.2.0", "@yarnpkg/lockfile": "1.1.0", "algoliasearch": "5.48.1", "ini": "6.0.0", @@ -537,9 +585,9 @@ } }, "node_modules/@angular/common": { - "version": "21.2.0-rc.0", - "resolved": "https://registry.npmjs.org/@angular/common/-/common-21.2.0-rc.0.tgz", - "integrity": "sha512-2O3cbBIM8ls97XwkYVdgJwEg3UOy/bVZ0uBoY14cLskhETj6wbrFENiy8KpikZ+xsTxE9gWohairxCRiEoFuKg==", + "version": "21.2.0", + "resolved": "https://registry.npmjs.org/@angular/common/-/common-21.2.0.tgz", + "integrity": "sha512-6zJMPi0i/XDniEgv3/t2BjuDHiOG44lgIR5PYyxqGpgJ0kqB5hku/0TuentNEi1VnBYgthnfhjek7c+lakXmhw==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -548,14 +596,14 @@ "node": "^20.19.0 || ^22.12.0 || >=24.0.0" }, "peerDependencies": { - "@angular/core": "21.2.0-rc.0", + "@angular/core": "21.2.0", "rxjs": "^6.5.3 || ^7.4.0" } }, "node_modules/@angular/compiler": { - "version": "21.2.0-rc.0", - "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-21.2.0-rc.0.tgz", - "integrity": "sha512-gl2dQgMFKkH/iM/YxqWO0oIbYRpBZpOZuxj9W5TjG50EpMDioD1CNu2DfXpODVo3/rOfZJvOa+Jw0Cxpj5ClMQ==", + "version": "21.2.0", + "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-21.2.0.tgz", + "integrity": "sha512-0RPkma8UVNpse/VJcXT9w6SKzTMz4J/uMGj0l9enM1frg9xrx1fwi/lLmaVV9Nr9LfqPjQdxNFFlvaBB7g/2zg==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -565,9 +613,9 @@ } }, "node_modules/@angular/compiler-cli": { - "version": "21.2.0-rc.0", - "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-21.2.0-rc.0.tgz", - "integrity": "sha512-lHPA5BzQq9ttHXCAnAC/zW5vW0lFiawBIvRpNRbsI8Vw1noMs614OqZ2R6UBReVPE4kpyGwh7HCQIZyX5Cth6g==", + "version": "21.2.0", + "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-21.2.0.tgz", + "integrity": "sha512-gZd58p0/JjgdxMX3v+LjCB6e3dBIfNVr/YzXoh55TfffdBCUQY94hl1+DFQkJ72K5EX+1zbaz03dIm30kw1bGw==", "dev": true, "license": "MIT", "dependencies": { @@ -588,7 +636,7 @@ "node": "^20.19.0 || ^22.12.0 || >=24.0.0" }, "peerDependencies": { - "@angular/compiler": "21.2.0-rc.0", + "@angular/compiler": "21.2.0", "typescript": ">=5.9 <6.1" }, "peerDependenciesMeta": { @@ -598,9 +646,9 @@ } }, "node_modules/@angular/core": { - "version": "21.2.0-rc.0", - "resolved": "https://registry.npmjs.org/@angular/core/-/core-21.2.0-rc.0.tgz", - "integrity": "sha512-Txdi6ocSNC8GbCYMGh2AUm1lx+J11LNRjNrfGcgxQhQAiEPVO8jGojfObM+rdjv1h8t0pCms5vzEmFiJ9z06hg==", + "version": "21.2.0", + "resolved": "https://registry.npmjs.org/@angular/core/-/core-21.2.0.tgz", + "integrity": "sha512-VnTbmZq3g3Q+s3nCZ8VUDMLjMezOg/bqUxAJ/DrRWCrEcTP5JO3mrNPs3FHj+qlB0T+BQP7uQv6QTzPVKybwoA==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -609,7 +657,7 @@ "node": "^20.19.0 || ^22.12.0 || >=24.0.0" }, "peerDependencies": { - "@angular/compiler": "21.2.0-rc.0", + "@angular/compiler": "21.2.0", "rxjs": "^6.5.3 || ^7.4.0", "zone.js": "~0.15.0 || ~0.16.0" }, @@ -623,9 +671,9 @@ } }, "node_modules/@angular/forms": { - "version": "21.2.0-rc.0", - "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-21.2.0-rc.0.tgz", - "integrity": "sha512-GsX4c+GYUxcKla8yO/hpEzpliCsMslP+yIIk9l6831PkSf3uCyKHUHriJtOEca4uN5fNo23ix5NXo5VPkmldLQ==", + "version": "21.2.0", + "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-21.2.0.tgz", + "integrity": "sha512-NduUtPWLauH/FLayEDkLyaKAGqKzXbcfO7468LOWCXN3crhNVQyIWRQPOUcdpoJwDAGLpN85m3DhJhXNnA9c5w==", "license": "MIT", "dependencies": { "@standard-schema/spec": "^1.0.0", @@ -635,16 +683,16 @@ "node": "^20.19.0 || ^22.12.0 || >=24.0.0" }, "peerDependencies": { - "@angular/common": "21.2.0-rc.0", - "@angular/core": "21.2.0-rc.0", - "@angular/platform-browser": "21.2.0-rc.0", + "@angular/common": "21.2.0", + "@angular/core": "21.2.0", + "@angular/platform-browser": "21.2.0", "rxjs": "^6.5.3 || ^7.4.0" } }, "node_modules/@angular/platform-browser": { - "version": "21.2.0-rc.0", - "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-21.2.0-rc.0.tgz", - "integrity": "sha512-FCNRhzkf35l6B18gzaT6GDdTJ3v4AiLOSXc5RdiQ2PMn2dLOkm70SH0caXQGzvtXAzHrHlR+Evj/ZqaFYvjcYg==", + "version": "21.2.0", + "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-21.2.0.tgz", + "integrity": "sha512-IUGukpvvT2B5Dl76qzk6rY7UIHUT9u4BhT2AwVz+5JqcX9KwQtYD17Gt7wj6bvIgCXKWG+CfN8Zd9DECOCYWjg==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -653,9 +701,9 @@ "node": "^20.19.0 || ^22.12.0 || >=24.0.0" }, "peerDependencies": { - "@angular/animations": "21.2.0-rc.0", - "@angular/common": "21.2.0-rc.0", - "@angular/core": "21.2.0-rc.0" + "@angular/animations": "21.2.0", + "@angular/common": "21.2.0", + "@angular/core": "21.2.0" }, "peerDependenciesMeta": { "@angular/animations": { @@ -664,9 +712,9 @@ } }, "node_modules/@angular/router": { - "version": "21.2.0-rc.0", - "resolved": "https://registry.npmjs.org/@angular/router/-/router-21.2.0-rc.0.tgz", - "integrity": "sha512-KZmZCdm4H+w3UC1vorQ/Nj4FEHY4sgRkwp+zf4E4WFV+sflyFzqFxzVIKGJ1xTzjaDebCRmipJnc3DZ09fv1fg==", + "version": "21.2.0", + "resolved": "https://registry.npmjs.org/@angular/router/-/router-21.2.0.tgz", + "integrity": "sha512-siliJ+jJRUCRZ0cdkqc7zww9Didz56Z0Z2YPIuR2n5TZLiuJY+jAf6xotXKp/v6v8XoGJwLiRNipGgNDRIAlWA==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -675,24 +723,27 @@ "node": "^20.19.0 || ^22.12.0 || >=24.0.0" }, "peerDependencies": { - "@angular/common": "21.2.0-rc.0", - "@angular/core": "21.2.0-rc.0", - "@angular/platform-browser": "21.2.0-rc.0", + "@angular/common": "21.2.0", + "@angular/core": "21.2.0", + "@angular/platform-browser": "21.2.0", "rxjs": "^6.5.3 || ^7.4.0" } }, "node_modules/@asamuzakjp/css-color": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-4.1.2.tgz", - "integrity": "sha512-NfBUvBaYgKIuq6E/RBLY1m0IohzNHAYyaJGuTK79Z23uNwmz2jl1mPsC5ZxCCxylinKhT1Amn5oNTlx1wN8cQg==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-5.0.1.tgz", + "integrity": "sha512-2SZFvqMyvboVV1d15lMf7XiI3m7SDqXUuKaTymJYLN6dSGadqp+fVojqJlVoMlbZnlTmu3S0TLwLTJpvBMO1Aw==", "dev": true, "license": "MIT", "dependencies": { - "@csstools/css-calc": "^3.0.0", - "@csstools/css-color-parser": "^4.0.1", + "@csstools/css-calc": "^3.1.1", + "@csstools/css-color-parser": "^4.0.2", "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0", - "lru-cache": "^11.2.5" + "lru-cache": "^11.2.6" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" } }, "node_modules/@asamuzakjp/css-color/node_modules/lru-cache": { @@ -1043,9 +1094,9 @@ } }, "node_modules/@csstools/color-helpers": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.0.1.tgz", - "integrity": "sha512-NmXRccUJMk2AWA5A7e5a//3bCIMyOu2hAtdRYrhPPHjDxINuCwX1w6rnIZ4xjLcp0ayv6h8Pc3X0eJUGiAAXHQ==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.0.2.tgz", + "integrity": "sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q==", "dev": true, "funding": [ { @@ -1087,9 +1138,9 @@ } }, "node_modules/@csstools/css-color-parser": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.0.1.tgz", - "integrity": "sha512-vYwO15eRBEkeF6xjAno/KQ61HacNhfQuuU/eGwH67DplL0zD5ZixUa563phQvUelA07yDczIXdtmYojCphKJcw==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.0.2.tgz", + "integrity": "sha512-0GEfbBLmTFf0dJlpsNU7zwxRIH0/BGEMuXLTCvFYxuL1tNhqzTbtnFICyJLTNK4a+RechKP75e7w42ClXSnJQw==", "dev": true, "funding": [ { @@ -1103,8 +1154,8 @@ ], "license": "MIT", "dependencies": { - "@csstools/color-helpers": "^6.0.1", - "@csstools/css-calc": "^3.0.0" + "@csstools/color-helpers": "^6.0.2", + "@csstools/css-calc": "^3.1.1" }, "engines": { "node": ">=20.19.0" @@ -1138,9 +1189,9 @@ } }, "node_modules/@csstools/css-syntax-patches-for-csstree": { - "version": "1.0.27", - "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.0.27.tgz", - "integrity": "sha512-sxP33Jwg1bviSUXAV43cVYdmjt2TLnLXNqCWl9xmxHawWVjGz/kEbdkr7F9pxJNBN2Mh+dq0crgItbW6tQvyow==", + "version": "1.0.28", + "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.0.28.tgz", + "integrity": "sha512-1NRf1CUBjnr3K7hu8BLxjQrKCxEe8FP/xmPTenAxCRZWVLbmGotkFvG9mfNpjA6k7Bw1bw4BilZq9cu19RA5pg==", "dev": true, "funding": [ { @@ -1694,6 +1745,13 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@eslint/config-array/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, "node_modules/@eslint/config-array/node_modules/brace-expansion": { "version": "1.1.12", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", @@ -1706,9 +1764,9 @@ } }, "node_modules/@eslint/config-array/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "dependencies": { @@ -1745,20 +1803,20 @@ } }, "node_modules/@eslint/eslintrc": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.3.tgz", - "integrity": "sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==", + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.4.tgz", + "integrity": "sha512-4h4MVF8pmBsncB60r0wSJiIeUKTSD4m7FmTFThG8RHlsg9ajqckLm9OraguFGZE4vVdpiI1Q4+hFnisopmG6gQ==", "dev": true, "license": "MIT", "dependencies": { - "ajv": "^6.12.4", + "ajv": "^6.14.0", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.1", - "minimatch": "^3.1.2", + "minimatch": "^3.1.3", "strip-json-comments": "^3.1.1" }, "engines": { @@ -1785,6 +1843,13 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/@eslint/eslintrc/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { "version": "1.1.12", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", @@ -1796,6 +1861,16 @@ "concat-map": "0.0.1" } }, + "node_modules/@eslint/eslintrc/node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, "node_modules/@eslint/eslintrc/node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -1804,9 +1879,9 @@ "license": "MIT" }, "node_modules/@eslint/eslintrc/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "dependencies": { @@ -1871,6 +1946,19 @@ } } }, + "node_modules/@gar/promise-retry": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@gar/promise-retry/-/promise-retry-1.0.2.tgz", + "integrity": "sha512-Lm/ZLhDZcBECta3TmCQSngiQykFdfw+QtI1/GYMsZd4l3nG+P8WLB16XuS7WaBGLQ+9E+cOcWQsth9cayuGt8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "retry": "^0.13.1" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, "node_modules/@harperfast/extended-iterable": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/@harperfast/extended-iterable/-/extended-iterable-1.0.3.tgz", @@ -2751,6 +2839,9 @@ "arm64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -2768,6 +2859,9 @@ "arm64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -2785,6 +2879,9 @@ "ppc64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -2802,6 +2899,9 @@ "riscv64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -2819,6 +2919,9 @@ "s390x" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -2836,6 +2939,9 @@ "x64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -2853,6 +2959,9 @@ "x64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -2988,18 +3097,18 @@ } }, "node_modules/@npmcli/git": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/@npmcli/git/-/git-7.0.1.tgz", - "integrity": "sha512-+XTFxK2jJF/EJJ5SoAzXk3qwIDfvFc5/g+bD274LZ7uY7LE8sTfG6Z8rOanPl2ZEvZWqNvmEdtXC25cE54VcoA==", + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@npmcli/git/-/git-7.0.2.tgz", + "integrity": "sha512-oeolHDjExNAJAnlYP2qzNjMX/Xi9bmu78C9dIGr4xjobrSKbuMYCph8lTzn4vnW3NjIqVmw/f8BCfouqyJXlRg==", "dev": true, "license": "ISC", "dependencies": { + "@gar/promise-retry": "^1.0.0", "@npmcli/promise-spawn": "^9.0.0", "ini": "^6.0.0", "lru-cache": "^11.2.1", "npm-pick-manifest": "^11.0.1", "proc-log": "^6.0.0", - "promise-retry": "^2.0.1", "semver": "^7.3.5", "which": "^6.0.0" }, @@ -3139,9 +3248,9 @@ } }, "node_modules/@npmcli/run-script": { - "version": "10.0.3", - "resolved": "https://registry.npmjs.org/@npmcli/run-script/-/run-script-10.0.3.tgz", - "integrity": "sha512-ER2N6itRkzWbbtVmZ9WKaWxVlKlOeBFF1/7xx+KA5J1xKa4JjUwBdb6tDpk0v1qA+d+VDwHI9qmLcXSWcmi+Rw==", + "version": "10.0.4", + "resolved": "https://registry.npmjs.org/@npmcli/run-script/-/run-script-10.0.4.tgz", + "integrity": "sha512-mGUWr1uMnf0le2TwfOZY4SFxZGXGfm4Jtay/nwAa2FLNAKXUoUwaGwBMNH36UHPtinWfTSJ3nqFQr0091CxVGg==", "dev": true, "license": "ISC", "dependencies": { @@ -3149,34 +3258,7 @@ "@npmcli/package-json": "^7.0.0", "@npmcli/promise-spawn": "^9.0.0", "node-gyp": "^12.1.0", - "proc-log": "^6.0.0", - "which": "^6.0.0" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/@npmcli/run-script/node_modules/isexe": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-4.0.0.tgz", - "integrity": "sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=20" - } - }, - "node_modules/@npmcli/run-script/node_modules/which": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/which/-/which-6.0.1.tgz", - "integrity": "sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg==", - "dev": true, - "license": "ISC", - "dependencies": { - "isexe": "^4.0.0" - }, - "bin": { - "node-which": "bin/which.js" + "proc-log": "^6.0.0" }, "engines": { "node": "^20.17.0 || >=22.9.0" @@ -3321,6 +3403,9 @@ "arm" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -3342,6 +3427,9 @@ "arm" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -3363,6 +3451,9 @@ "arm64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -3384,6 +3475,9 @@ "arm64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -3405,6 +3499,9 @@ "x64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -3426,6 +3523,9 @@ "x64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -3603,6 +3703,9 @@ "arm64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -3620,6 +3723,9 @@ "arm64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -3637,6 +3743,9 @@ "x64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -3654,6 +3763,9 @@ "x64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -3783,9 +3895,9 @@ } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.58.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.58.0.tgz", - "integrity": "sha512-mr0tmS/4FoVk1cnaeN244A/wjvGDNItZKR8hRhnmCzygyRXYtKF5jVDSIILR1U97CTzAYmbgIj/Dukg62ggG5w==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz", + "integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==", "cpu": [ "arm" ], @@ -3797,9 +3909,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.58.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.58.0.tgz", - "integrity": "sha512-+s++dbp+/RTte62mQD9wLSbiMTV+xr/PeRJEc/sFZFSBRlHPNPVaf5FXlzAL77Mr8FtSfQqCN+I598M8U41ccQ==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz", + "integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==", "cpu": [ "arm64" ], @@ -3811,9 +3923,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.58.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.58.0.tgz", - "integrity": "sha512-MFWBwTcYs0jZbINQBXHfSrpSQJq3IUOakcKPzfeSznONop14Pxuqa0Kg19GD0rNBMPQI2tFtu3UzapZpH0Uc1Q==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz", + "integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==", "cpu": [ "arm64" ], @@ -3825,9 +3937,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.58.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.58.0.tgz", - "integrity": "sha512-yiKJY7pj9c9JwzuKYLFaDZw5gma3fI9bkPEIyofvVfsPqjCWPglSHdpdwXpKGvDeYDms3Qal8qGMEHZ1M/4Udg==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz", + "integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==", "cpu": [ "x64" ], @@ -3839,9 +3951,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.58.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.58.0.tgz", - "integrity": "sha512-x97kCoBh5MOevpn/CNK9W1x8BEzO238541BGWBc315uOlN0AD/ifZ1msg+ZQB05Ux+VF6EcYqpiagfLJ8U3LvQ==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz", + "integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==", "cpu": [ "arm64" ], @@ -3853,9 +3965,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.58.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.58.0.tgz", - "integrity": "sha512-Aa8jPoZ6IQAG2eIrcXPpjRcMjROMFxCt1UYPZZtCxRV68WkuSigYtQ/7Zwrcr2IvtNJo7T2JfDXyMLxq5L4Jlg==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz", + "integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==", "cpu": [ "x64" ], @@ -3867,13 +3979,16 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.58.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.58.0.tgz", - "integrity": "sha512-Ob8YgT5kD/lSIYW2Rcngs5kNB/44Q2RzBSPz9brf2WEtcGR7/f/E9HeHn1wYaAwKBni+bdXEwgHvUd0x12lQSA==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz", + "integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==", "cpu": [ "arm" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -3881,13 +3996,16 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.58.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.58.0.tgz", - "integrity": "sha512-K+RI5oP1ceqoadvNt1FecL17Qtw/n9BgRSzxif3rTL2QlIu88ccvY+Y9nnHe/cmT5zbH9+bpiJuG1mGHRVwF4Q==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz", + "integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==", "cpu": [ "arm" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -3895,13 +4013,16 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.58.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.58.0.tgz", - "integrity": "sha512-T+17JAsCKUjmbopcKepJjHWHXSjeW7O5PL7lEFaeQmiVyw4kkc5/lyYKzrv6ElWRX/MrEWfPiJWqbTvfIvjM1Q==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz", + "integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==", "cpu": [ "arm64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -3909,13 +4030,16 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.58.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.58.0.tgz", - "integrity": "sha512-cCePktb9+6R9itIJdeCFF9txPU7pQeEHB5AbHu/MKsfH/k70ZtOeq1k4YAtBv9Z7mmKI5/wOLYjQ+B9QdxR6LA==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz", + "integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==", "cpu": [ "arm64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -3923,13 +4047,16 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.58.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.58.0.tgz", - "integrity": "sha512-iekUaLkfliAsDl4/xSdoCJ1gnnIXvoNz85C8U8+ZxknM5pBStfZjeXgB8lXobDQvvPRCN8FPmmuTtH+z95HTmg==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz", + "integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==", "cpu": [ "loong64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -3937,13 +4064,16 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-musl": { - "version": "4.58.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.58.0.tgz", - "integrity": "sha512-68ofRgJNl/jYJbxFjCKE7IwhbfxOl1muPN4KbIqAIe32lm22KmU7E8OPvyy68HTNkI2iV/c8y2kSPSm2mW/Q9Q==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz", + "integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==", "cpu": [ "loong64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -3951,13 +4081,16 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.58.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.58.0.tgz", - "integrity": "sha512-dpz8vT0i+JqUKuSNPCP5SYyIV2Lh0sNL1+FhM7eLC457d5B9/BC3kDPp5BBftMmTNsBarcPcoz5UGSsnCiw4XQ==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz", + "integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==", "cpu": [ "ppc64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -3965,13 +4098,16 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-musl": { - "version": "4.58.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.58.0.tgz", - "integrity": "sha512-4gdkkf9UJ7tafnweBCR/mk4jf3Jfl0cKX9Np80t5i78kjIH0ZdezUv/JDI2VtruE5lunfACqftJ8dIMGN4oHew==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz", + "integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==", "cpu": [ "ppc64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -3979,13 +4115,16 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.58.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.58.0.tgz", - "integrity": "sha512-YFS4vPnOkDTD/JriUeeZurFYoJhPf9GQQEF/v4lltp3mVcBmnsAdjEWhr2cjUCZzZNzxCG0HZOvJU44UGHSdzw==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz", + "integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==", "cpu": [ "riscv64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -3993,13 +4132,16 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.58.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.58.0.tgz", - "integrity": "sha512-x2xgZlFne+QVNKV8b4wwaCS8pwq3y14zedZ5DqLzjdRITvreBk//4Knbcvm7+lWmms9V9qFp60MtUd0/t/PXPw==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz", + "integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==", "cpu": [ "riscv64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -4007,13 +4149,16 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.58.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.58.0.tgz", - "integrity": "sha512-jIhrujyn4UnWF8S+DHSkAkDEO3hLX0cjzxJZPLF80xFyzyUIYgSMRcYQ3+uqEoyDD2beGq7Dj7edi8OnJcS/hg==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz", + "integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==", "cpu": [ "s390x" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -4021,13 +4166,16 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.58.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.58.0.tgz", - "integrity": "sha512-+410Srdoh78MKSJxTQ+hZ/Mx+ajd6RjjPwBPNd0R3J9FtL6ZA0GqiiyNjCO9In0IzZkCNrpGymSfn+kgyPQocg==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz", + "integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==", "cpu": [ "x64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -4035,13 +4183,16 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.58.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.58.0.tgz", - "integrity": "sha512-ZjMyby5SICi227y1MTR3VYBpFTdZs823Rs/hpakufleBoufoOIB6jtm9FEoxn/cgO7l6PM2rCEl5Kre5vX0QrQ==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz", + "integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==", "cpu": [ "x64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -4049,9 +4200,9 @@ ] }, "node_modules/@rollup/rollup-openbsd-x64": { - "version": "4.58.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.58.0.tgz", - "integrity": "sha512-ds4iwfYkSQ0k1nb8LTcyXw//ToHOnNTJtceySpL3fa7tc/AsE+UpUFphW126A6fKBGJD5dhRvg8zw1rvoGFxmw==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz", + "integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==", "cpu": [ "x64" ], @@ -4063,9 +4214,9 @@ ] }, "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.58.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.58.0.tgz", - "integrity": "sha512-fd/zpJniln4ICdPkjWFhZYeY/bpnaN9pGa6ko+5WD38I0tTqk9lXMgXZg09MNdhpARngmxiCg0B0XUamNw/5BQ==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz", + "integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==", "cpu": [ "arm64" ], @@ -4077,9 +4228,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.58.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.58.0.tgz", - "integrity": "sha512-YpG8dUOip7DCz3nr/JUfPbIUo+2d/dy++5bFzgi4ugOGBIox+qMbbqt/JoORwvI/C9Kn2tz6+Bieoqd5+B1CjA==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz", + "integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==", "cpu": [ "arm64" ], @@ -4091,9 +4242,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.58.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.58.0.tgz", - "integrity": "sha512-b9DI8jpFQVh4hIXFr0/+N/TzLdpBIoPzjt0Rt4xJbW3mzguV3mduR9cNgiuFcuL/TeORejJhCWiAXe3E/6PxWA==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz", + "integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==", "cpu": [ "ia32" ], @@ -4105,9 +4256,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.58.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.58.0.tgz", - "integrity": "sha512-CSrVpmoRJFN06LL9xhkitkwUcTZtIotYAF5p6XOR2zW0Zz5mzb3IPpcoPhB02frzMHFNo1reQ9xSF5fFm3hUsQ==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz", + "integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==", "cpu": [ "x64" ], @@ -4119,9 +4270,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.58.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.58.0.tgz", - "integrity": "sha512-QFsBgQNTnh5K0t/sBsjJLq24YVqEIVkGpfN2VHsnN90soZyhaiA9UUHufcctVNL4ypJY0wrwad0wslx2KJQ1/w==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz", + "integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==", "cpu": [ "x64" ], @@ -4133,9 +4284,9 @@ ] }, "node_modules/@rollup/wasm-node": { - "version": "4.58.0", - "resolved": "https://registry.npmjs.org/@rollup/wasm-node/-/wasm-node-4.58.0.tgz", - "integrity": "sha512-G4YrvWabOgRPfRSYEM95ZZGESbzVGUS+uvO/gVPNs3P93z8lBLTDpRwoo6zeEg6AqK9AdIshWbvDGOehZOikMw==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/wasm-node/-/wasm-node-4.59.0.tgz", + "integrity": "sha512-cKB/Pe05aJWQYw3UFS79Id+KVXdExBxWful0+CSl24z3ukwOgBSy6l39XZNwfm3vCh/fpUrAAs+T7PsJ6dC8NA==", "dev": true, "license": "MIT", "dependencies": { @@ -4153,14 +4304,14 @@ } }, "node_modules/@schematics/angular": { - "version": "21.2.0-rc.0", - "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-21.2.0-rc.0.tgz", - "integrity": "sha512-IcQJVAkaEbFe1n7Ar/ktJRzpaxvJVo7pyjq00CK2QWdfwWzPm4KbLh6Dr6JhKomXSE6+OEKEcMmHE0ffr7ZQ1Q==", + "version": "21.2.0", + "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-21.2.0.tgz", + "integrity": "sha512-GQUIeGzZwCT9/W5MAkKnkwETROPbA1eRmy3JF56jLmvr95tJnypGOG8jGYy0d+tcEVujIouh48r4J3bJQg5mrw==", "dev": true, "license": "MIT", "dependencies": { - "@angular-devkit/core": "21.2.0-rc.0", - "@angular-devkit/schematics": "21.2.0-rc.0", + "@angular-devkit/core": "21.2.0", + "@angular-devkit/schematics": "21.2.0", "jsonc-parser": "3.3.1" }, "engines": { @@ -4279,45 +4430,6 @@ "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/@tufjs/models/node_modules/balanced-match": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.3.tgz", - "integrity": "sha512-1pHv8LX9CpKut1Zp4EXey7Z8OfH11ONNH6Dhi2WDUt31VVZFXZzKwXcysBgqSumFCmR+0dqjMK5v5JiFHzi0+g==", - "dev": true, - "license": "MIT", - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/@tufjs/models/node_modules/brace-expansion": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.2.tgz", - "integrity": "sha512-Pdk8c9poy+YhOgVWw1JNN22/HcivgKWwpxKq04M/jTmHyCZn12WPJebZxdjSa5TmBqISrUSgNYU3eRORljfCCw==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^4.0.2" - }, - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/@tufjs/models/node_modules/minimatch": { - "version": "10.2.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.2.tgz", - "integrity": "sha512-+G4CpNBxa5MprY+04MbgOw1v7So6n5JY166pFi9KfYwT78fxScCeSNQSNzp6dpPSW2rONOps6Ocam1wFhCgoVw==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "brace-expansion": "^5.0.2" - }, - "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/@tybys/wasm-util": { "version": "0.10.1", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", @@ -4369,17 +4481,17 @@ "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.56.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.56.0.tgz", - "integrity": "sha512-lRyPDLzNCuae71A3t9NEINBiTn7swyOhvUj3MyUOxb8x6g6vPEFoOU+ZRmGMusNC3X3YMhqMIX7i8ShqhT74Pw==", + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.56.1.tgz", + "integrity": "sha512-Jz9ZztpB37dNC+HU2HI28Bs9QXpzCz+y/twHOwhyrIRdbuVDxSytJNDl6z/aAKlaRIwC7y8wJdkBv7FxYGgi0A==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.12.2", - "@typescript-eslint/scope-manager": "8.56.0", - "@typescript-eslint/type-utils": "8.56.0", - "@typescript-eslint/utils": "8.56.0", - "@typescript-eslint/visitor-keys": "8.56.0", + "@typescript-eslint/scope-manager": "8.56.1", + "@typescript-eslint/type-utils": "8.56.1", + "@typescript-eslint/utils": "8.56.1", + "@typescript-eslint/visitor-keys": "8.56.1", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.4.0" @@ -4392,32 +4504,22 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.56.0", + "@typescript-eslint/parser": "^8.56.1", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { - "version": "7.0.5", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", - "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, "node_modules/@typescript-eslint/parser": { - "version": "8.56.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.56.0.tgz", - "integrity": "sha512-IgSWvLobTDOjnaxAfDTIHaECbkNlAlKv2j5SjpB2v7QHKv1FIfjwMy8FsDbVfDX/KjmCmYICcw7uGaXLhtsLNg==", + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.56.1.tgz", + "integrity": "sha512-klQbnPAAiGYFyI02+znpBRLyjL4/BrBd0nyWkdC0s/6xFLkXYQ8OoRrSkqacS1ddVxf/LDyODIKbQ5TgKAf/Fg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.56.0", - "@typescript-eslint/types": "8.56.0", - "@typescript-eslint/typescript-estree": "8.56.0", - "@typescript-eslint/visitor-keys": "8.56.0", + "@typescript-eslint/scope-manager": "8.56.1", + "@typescript-eslint/types": "8.56.1", + "@typescript-eslint/typescript-estree": "8.56.1", + "@typescript-eslint/visitor-keys": "8.56.1", "debug": "^4.4.3" }, "engines": { @@ -4433,14 +4535,14 @@ } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.56.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.56.0.tgz", - "integrity": "sha512-M3rnyL1vIQOMeWxTWIW096/TtVP+8W3p/XnaFflhmcFp+U4zlxUxWj4XwNs6HbDeTtN4yun0GNTTDBw/SvufKg==", + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.56.1.tgz", + "integrity": "sha512-TAdqQTzHNNvlVFfR+hu2PDJrURiwKsUvxFn1M0h95BB8ah5jejas08jUWG4dBA68jDMI988IvtfdAI53JzEHOQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.56.0", - "@typescript-eslint/types": "^8.56.0", + "@typescript-eslint/tsconfig-utils": "^8.56.1", + "@typescript-eslint/types": "^8.56.1", "debug": "^4.4.3" }, "engines": { @@ -4455,14 +4557,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.56.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.56.0.tgz", - "integrity": "sha512-7UiO/XwMHquH+ZzfVCfUNkIXlp/yQjjnlYUyYz7pfvlK3/EyyN6BK+emDmGNyQLBtLGaYrTAI6KOw8tFucWL2w==", + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.56.1.tgz", + "integrity": "sha512-YAi4VDKcIZp0O4tz/haYKhmIDZFEUPOreKbfdAN3SzUDMcPhJ8QI99xQXqX+HoUVq8cs85eRKnD+rne2UAnj2w==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.56.0", - "@typescript-eslint/visitor-keys": "8.56.0" + "@typescript-eslint/types": "8.56.1", + "@typescript-eslint/visitor-keys": "8.56.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -4473,9 +4575,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.56.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.56.0.tgz", - "integrity": "sha512-bSJoIIt4o3lKXD3xmDh9chZcjCz5Lk8xS7Rxn+6l5/pKrDpkCwtQNQQwZ2qRPk7TkUYhrq3WPIHXOXlbXP0itg==", + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.56.1.tgz", + "integrity": "sha512-qOtCYzKEeyr3aR9f28mPJqBty7+DBqsdd63eO0yyDwc6vgThj2UjWfJIcsFeSucYydqcuudMOprZ+x1SpF3ZuQ==", "dev": true, "license": "MIT", "engines": { @@ -4490,15 +4592,15 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.56.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.56.0.tgz", - "integrity": "sha512-qX2L3HWOU2nuDs6GzglBeuFXviDODreS58tLY/BALPC7iu3Fa+J7EOTwnX9PdNBxUI7Uh0ntP0YWGnxCkXzmfA==", + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.56.1.tgz", + "integrity": "sha512-yB/7dxi7MgTtGhZdaHCemf7PuwrHMenHjmzgUW1aJpO+bBU43OycnM3Wn+DdvDO/8zzA9HlhaJ0AUGuvri4oGg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.56.0", - "@typescript-eslint/typescript-estree": "8.56.0", - "@typescript-eslint/utils": "8.56.0", + "@typescript-eslint/types": "8.56.1", + "@typescript-eslint/typescript-estree": "8.56.1", + "@typescript-eslint/utils": "8.56.1", "debug": "^4.4.3", "ts-api-utils": "^2.4.0" }, @@ -4515,9 +4617,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.56.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.56.0.tgz", - "integrity": "sha512-DBsLPs3GsWhX5HylbP9HNG15U0bnwut55Lx12bHB9MpXxQ+R5GC8MwQe+N1UFXxAeQDvEsEDY6ZYwX03K7Z6HQ==", + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.56.1.tgz", + "integrity": "sha512-dbMkdIUkIkchgGDIv7KLUpa0Mda4IYjo4IAMJUZ+3xNoUXxMsk9YtKpTHSChRS85o+H9ftm51gsK1dZReY9CVw==", "dev": true, "license": "MIT", "engines": { @@ -4529,18 +4631,18 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.56.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.56.0.tgz", - "integrity": "sha512-ex1nTUMWrseMltXUHmR2GAQ4d+WjkZCT4f+4bVsps8QEdh0vlBsaCokKTPlnqBFqqGaxilDNJG7b8dolW2m43Q==", + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.56.1.tgz", + "integrity": "sha512-qzUL1qgalIvKWAf9C1HpvBjif+Vm6rcT5wZd4VoMb9+Km3iS3Cv9DY6dMRMDtPnwRAFyAi7YXJpTIEXLvdfPxg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.56.0", - "@typescript-eslint/tsconfig-utils": "8.56.0", - "@typescript-eslint/types": "8.56.0", - "@typescript-eslint/visitor-keys": "8.56.0", + "@typescript-eslint/project-service": "8.56.1", + "@typescript-eslint/tsconfig-utils": "8.56.1", + "@typescript-eslint/types": "8.56.1", + "@typescript-eslint/visitor-keys": "8.56.1", "debug": "^4.4.3", - "minimatch": "^9.0.5", + "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.4.0" @@ -4557,16 +4659,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.56.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.56.0.tgz", - "integrity": "sha512-RZ3Qsmi2nFGsS+n+kjLAYDPVlrzf7UhTffrDIKr+h2yzAlYP/y5ZulU0yeDEPItos2Ph46JAL5P/On3pe7kDIQ==", + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.56.1.tgz", + "integrity": "sha512-HPAVNIME3tABJ61siYlHzSWCGtOoeP2RTIaHXFMPqjrQKCGB9OgUVdiNgH7TJS2JNIQ5qQ4RsAUDuGaGme/KOA==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", - "@typescript-eslint/scope-manager": "8.56.0", - "@typescript-eslint/types": "8.56.0", - "@typescript-eslint/typescript-estree": "8.56.0" + "@typescript-eslint/scope-manager": "8.56.1", + "@typescript-eslint/types": "8.56.1", + "@typescript-eslint/typescript-estree": "8.56.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -4581,13 +4683,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.56.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.56.0.tgz", - "integrity": "sha512-q+SL+b+05Ud6LbEE35qe4A99P+htKTKVbyiNEe45eCbJFyh/HVK9QXwlrbz+Q4L8SOW4roxSVwXYj4DMBT7Ieg==", + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.56.1.tgz", + "integrity": "sha512-KiROIzYdEV85YygXw6BI/Dx4fnBlFQu6Mq4QE4MOH9fFnhohw6wX/OAvDY2/C+ut0I3RSPKenvZJIVYqJNkhEw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.56.0", + "@typescript-eslint/types": "8.56.1", "eslint-visitor-keys": "^5.0.0" }, "engines": { @@ -4894,197 +4996,6 @@ "typescript-eslint": "^8.0.0" } }, - "node_modules/angular-eslint/node_modules/@angular-devkit/architect": { - "version": "0.2101.4", - "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.2101.4.tgz", - "integrity": "sha512-3yyebORk+ovtO+LfDjIGbGCZhCMDAsyn9vkCljARj3sSshS4blOQBar0g+V3kYAweLT5Gf+rTKbN5jneOkBAFQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@angular-devkit/core": "21.1.4", - "rxjs": "7.8.2" - }, - "bin": { - "architect": "bin/cli.js" - }, - "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0", - "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", - "yarn": ">= 1.13.0" - } - }, - "node_modules/angular-eslint/node_modules/@angular-devkit/core": { - "version": "21.1.4", - "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-21.1.4.tgz", - "integrity": "sha512-ObPTI5gYCB1jGxTRhcqZ6oQVUBFVJ8GH4LksVuAiz0nFX7xxpzARWvlhq943EtnlovVlUd9I8fM3RQqjfGVVAQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ajv": "8.17.1", - "ajv-formats": "3.0.1", - "jsonc-parser": "3.3.1", - "picomatch": "4.0.3", - "rxjs": "7.8.2", - "source-map": "0.7.6" - }, - "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0", - "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", - "yarn": ">= 1.13.0" - }, - "peerDependencies": { - "chokidar": "^5.0.0" - }, - "peerDependenciesMeta": { - "chokidar": { - "optional": true - } - } - }, - "node_modules/angular-eslint/node_modules/@angular-devkit/schematics": { - "version": "21.1.4", - "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-21.1.4.tgz", - "integrity": "sha512-Nqq0ioCUxrbEX+L4KOarETcZZJNnJ1mAJ0ubO4VM91qnn8RBBM9SnQ91590TfC34Szk/wh+3+Uj6KUvTJNuegQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@angular-devkit/core": "21.1.4", - "jsonc-parser": "3.3.1", - "magic-string": "0.30.21", - "ora": "9.0.0", - "rxjs": "7.8.2" - }, - "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0", - "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", - "yarn": ">= 1.13.0" - } - }, - "node_modules/angular-eslint/node_modules/@angular-eslint/builder": { - "version": "21.2.0", - "resolved": "https://registry.npmjs.org/@angular-eslint/builder/-/builder-21.2.0.tgz", - "integrity": "sha512-wcp3J9cbrDwSeI/o1D/DSvMQa8zpKjc5WhRGTx33omhWijCfiVNEAiBLWiEx5Sb/dWcoX8yFNWY5jSgFVy9Sjw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@angular-devkit/architect": ">= 0.2100.0 < 0.2200.0", - "@angular-devkit/core": ">= 21.0.0 < 22.0.0" - }, - "peerDependencies": { - "@angular/cli": ">= 21.0.0 < 22.0.0", - "eslint": "^8.57.0 || ^9.0.0", - "typescript": "*" - } - }, - "node_modules/angular-eslint/node_modules/@angular-eslint/schematics": { - "version": "21.2.0", - "resolved": "https://registry.npmjs.org/@angular-eslint/schematics/-/schematics-21.2.0.tgz", - "integrity": "sha512-WtT4fPKIUQ/hswy+l2GF/rKOdD+42L3fUzzcwRzNutQbe2tU9SimoSOAsay/ylWEuhIOQTs7ysPB8fUgFQoLpA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@angular-devkit/core": ">= 21.0.0 < 22.0.0", - "@angular-devkit/schematics": ">= 21.0.0 < 22.0.0", - "@angular-eslint/eslint-plugin": "21.2.0", - "@angular-eslint/eslint-plugin-template": "21.2.0", - "ignore": "7.0.5", - "semver": "7.7.3", - "strip-json-comments": "3.1.1" - }, - "peerDependencies": { - "@angular/cli": ">= 21.0.0 < 22.0.0" - } - }, - "node_modules/angular-eslint/node_modules/ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/angular-eslint/node_modules/chalk": { - "version": "5.6.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", - "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.17.0 || ^14.13 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/angular-eslint/node_modules/ignore": { - "version": "7.0.5", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", - "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/angular-eslint/node_modules/ora": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/ora/-/ora-9.0.0.tgz", - "integrity": "sha512-m0pg2zscbYgWbqRR6ABga5c3sZdEon7bSgjnlXC64kxtxLOyjRcbbUkLj7HFyy/FTD+P2xdBWu8snGhYI0jc4A==", - "dev": true, - "license": "MIT", - "dependencies": { - "chalk": "^5.6.2", - "cli-cursor": "^5.0.0", - "cli-spinners": "^3.2.0", - "is-interactive": "^2.0.0", - "is-unicode-supported": "^2.1.0", - "log-symbols": "^7.0.1", - "stdin-discarder": "^0.2.2", - "string-width": "^8.1.0", - "strip-ansi": "^7.1.2" - }, - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/angular-eslint/node_modules/semver": { - "version": "7.7.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", - "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/angular-eslint/node_modules/stdin-discarder": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.2.2.tgz", - "integrity": "sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/ansi-colors": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", @@ -5178,11 +5089,14 @@ } }, "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", "dev": true, - "license": "MIT" + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } }, "node_modules/baseline-browser-mapping": { "version": "2.10.0", @@ -5261,13 +5175,16 @@ "license": "ISC" }, "node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", + "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", "dev": true, "license": "MIT", "dependencies": { - "balanced-match": "^1.0.0" + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" } }, "node_modules/browserslist": { @@ -5396,9 +5313,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001770", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001770.tgz", - "integrity": "sha512-x/2CLQ1jHENRbHg5PSId2sXq1CIO1CISvwWAj027ltMVG2UNgW+w9oH2+HzgEIRFembL8bUlXtfbBHR1fCg2xw==", + "version": "1.0.30001774", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001774.tgz", + "integrity": "sha512-DDdwPGz99nmIEv216hKSgLD+D4ikHQHjBC/seF98N9CPqRX4M5mSxT9eTV6oyisnJcuzxtZy4n17yKKQYmYQOA==", "dev": true, "funding": [ { @@ -5789,16 +5706,16 @@ } }, "node_modules/cssstyle": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-6.0.1.tgz", - "integrity": "sha512-IoJs7La+oFp/AB033wBStxNOJt4+9hHMxsXUPANcoXL2b3W4DZKghlJ2cI/eyeRZIQ9ysvYEorVhjrcYctWbog==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-6.1.0.tgz", + "integrity": "sha512-Ml4fP2UT2K3CUBQnVlbdV/8aFDdlY69E+YnwJM+3VUWl08S3J8c8aRuJqCkD9Py8DHZ7zNNvsfKl8psocHZEFg==", "dev": true, "license": "MIT", "dependencies": { - "@asamuzakjp/css-color": "^4.1.2", - "@csstools/css-syntax-patches-for-csstree": "^1.0.26", + "@asamuzakjp/css-color": "^5.0.0", + "@csstools/css-syntax-patches-for-csstree": "^1.0.28", "css-tree": "^3.1.0", - "lru-cache": "^11.2.5" + "lru-cache": "^11.2.6" }, "engines": { "node": ">=20" @@ -5996,31 +5913,6 @@ "node": ">= 0.8" } }, - "node_modules/encoding": { - "version": "0.1.13", - "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", - "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "iconv-lite": "^0.6.2" - } - }, - "node_modules/encoding/node_modules/iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/entities": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", @@ -6299,6 +6191,13 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/eslint/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, "node_modules/eslint/node_modules/brace-expansion": { "version": "1.1.12", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", @@ -6340,6 +6239,16 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/eslint/node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, "node_modules/eslint/node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -6348,9 +6257,9 @@ "license": "MIT" }, "node_modules/eslint/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "dependencies": { @@ -6891,45 +6800,6 @@ "dev": true, "license": "BSD-2-Clause" }, - "node_modules/glob/node_modules/balanced-match": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.3.tgz", - "integrity": "sha512-1pHv8LX9CpKut1Zp4EXey7Z8OfH11ONNH6Dhi2WDUt31VVZFXZzKwXcysBgqSumFCmR+0dqjMK5v5JiFHzi0+g==", - "dev": true, - "license": "MIT", - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/glob/node_modules/brace-expansion": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.2.tgz", - "integrity": "sha512-Pdk8c9poy+YhOgVWw1JNN22/HcivgKWwpxKq04M/jTmHyCZn12WPJebZxdjSa5TmBqISrUSgNYU3eRORljfCCw==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^4.0.2" - }, - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/glob/node_modules/minimatch": { - "version": "10.2.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.2.tgz", - "integrity": "sha512-+G4CpNBxa5MprY+04MbgOw1v7So6n5JY166pFi9KfYwT78fxScCeSNQSNzp6dpPSW2rONOps6Ocam1wFhCgoVw==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "brace-expansion": "^5.0.2" - }, - "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/globals": { "version": "14.0.0", "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", @@ -7000,9 +6870,9 @@ } }, "node_modules/hono": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.0.tgz", - "integrity": "sha512-NekXntS5M94pUfiVZ8oXXK/kkri+5WpX2/Ik+LVsl+uvw+soj4roXIsPqO+XsWrAw20mOzaXOZf3Q7PfB9A/IA==", + "version": "4.12.3", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.3.tgz", + "integrity": "sha512-SFsVSjp8sj5UumXOOFlkZOG6XS9SJDKw0TbwFeV+AJ8xlST8kxK5Z/5EYa111UY8732lK2S/xB653ceuaoGwpg==", "dev": true, "license": "MIT", "engines": { @@ -7152,9 +7022,9 @@ } }, "node_modules/ignore": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", - "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", "dev": true, "license": "MIT", "engines": { @@ -7174,45 +7044,6 @@ "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/ignore-walk/node_modules/balanced-match": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.3.tgz", - "integrity": "sha512-1pHv8LX9CpKut1Zp4EXey7Z8OfH11ONNH6Dhi2WDUt31VVZFXZzKwXcysBgqSumFCmR+0dqjMK5v5JiFHzi0+g==", - "dev": true, - "license": "MIT", - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/ignore-walk/node_modules/brace-expansion": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.2.tgz", - "integrity": "sha512-Pdk8c9poy+YhOgVWw1JNN22/HcivgKWwpxKq04M/jTmHyCZn12WPJebZxdjSa5TmBqISrUSgNYU3eRORljfCCw==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^4.0.2" - }, - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/ignore-walk/node_modules/minimatch": { - "version": "10.2.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.2.tgz", - "integrity": "sha512-+G4CpNBxa5MprY+04MbgOw1v7So6n5JY166pFi9KfYwT78fxScCeSNQSNzp6dpPSW2rONOps6Ocam1wFhCgoVw==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "brace-expansion": "^5.0.2" - }, - "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/image-size": { "version": "0.5.5", "resolved": "https://registry.npmjs.org/image-size/-/image-size-0.5.5.tgz", @@ -7895,12 +7726,13 @@ } }, "node_modules/make-fetch-happen": { - "version": "15.0.3", - "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-15.0.3.tgz", - "integrity": "sha512-iyyEpDty1mwW3dGlYXAJqC/azFn5PPvgKVwXayOGBSmKLxhKZ9fg4qIan2ePpp1vJIwfFiO34LAPZgq9SZW9Aw==", + "version": "15.0.4", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-15.0.4.tgz", + "integrity": "sha512-vM2sG+wbVeVGYcCm16mM3d5fuem9oC28n436HjsGO3LcxoTI8LNVa4rwZDn3f76+cWyT4GGJDxjTYU1I2nr6zw==", "dev": true, "license": "ISC", "dependencies": { + "@gar/promise-retry": "^1.0.0", "@npmcli/agent": "^4.0.0", "cacache": "^20.0.1", "http-cache-semantics": "^4.1.1", @@ -7910,7 +7742,6 @@ "minipass-pipeline": "^1.2.4", "negotiator": "^1.0.0", "proc-log": "^6.0.0", - "promise-retry": "^2.0.1", "ssri": "^13.0.0" }, "engines": { @@ -8012,16 +7843,16 @@ } }, "node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "version": "10.2.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", + "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", "dev": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "dependencies": { - "brace-expansion": "^2.0.1" + "brace-expansion": "^5.0.2" }, "engines": { - "node": ">=16 || 14 >=14.17" + "node": "18 || 20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -8051,9 +7882,9 @@ } }, "node_modules/minipass-fetch": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-5.0.1.tgz", - "integrity": "sha512-yHK8pb0iCGat0lDrs/D6RZmCdaBT64tULXjdxjSMAqoDi18Q3qKEUTHypHQZQd9+FYpIS+lkvpq6C/R6SbUeRw==", + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-5.0.2.tgz", + "integrity": "sha512-2d0q2a8eCi2IRg/IGubCNRJoYbA1+YPXAzQVRFmB45gdGZafyivnZ5YSEfo3JikbjGxOdntGFvBQGqaSMXlAFQ==", "dev": true, "license": "MIT", "dependencies": { @@ -8065,7 +7896,7 @@ "node": "^20.17.0 || >=22.9.0" }, "optionalDependencies": { - "encoding": "^0.1.13" + "iconv-lite": "^0.7.2" } }, "node_modules/minipass-flush": { @@ -8290,9 +8121,9 @@ } }, "node_modules/ng-packagr": { - "version": "21.2.0-next.0", - "resolved": "https://registry.npmjs.org/ng-packagr/-/ng-packagr-21.2.0-next.0.tgz", - "integrity": "sha512-BkRAqx1ZljIYpBbjDi/+3y8AMo9S19vm8zx3YWpqMAaIpDb7cvsT+Une9b4oyEK/7p+XvWw+LaPVleTAQtQEMQ==", + "version": "21.2.0", + "resolved": "https://registry.npmjs.org/ng-packagr/-/ng-packagr-21.2.0.tgz", + "integrity": "sha512-ASlXEboqt+ZgKzNPx3YCr924xqQRFA5qgm77GHf0Fm13hx7gVFYVm6WCdYZyeX/p9NJjFWAL+mIMfhsx2SHKoA==", "dev": true, "license": "MIT", "dependencies": { @@ -8328,7 +8159,7 @@ "rollup": "^4.24.0" }, "peerDependencies": { - "@angular/compiler-cli": "^21.0.0 || ^21.1.0-next || ^21.2.0-next", + "@angular/compiler-cli": "^21.0.0 || ^21.2.0-next", "tailwindcss": "^2.0.0 || ^3.0.0 || ^4.0.0", "tslib": "^2.3.0", "typescript": ">=5.9 <6.0" @@ -8490,9 +8321,9 @@ } }, "node_modules/npm-packlist": { - "version": "10.0.3", - "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-10.0.3.tgz", - "integrity": "sha512-zPukTwJMOu5X5uvm0fztwS5Zxyvmk38H/LfidkOMt3gbZVCyro2cD/ETzwzVPcWZA3JOyPznfUN/nkyFiyUbxg==", + "version": "10.0.4", + "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-10.0.4.tgz", + "integrity": "sha512-uMW73iajD8hiH4ZBxEV3HC+eTnppIqwakjOYuvgddnalIw2lJguKviK1pcUJDlIWm1wSJkchpDZDSVVsZEYRng==", "dev": true, "license": "ISC", "dependencies": { @@ -9112,6 +8943,16 @@ "node": ">=10" } }, + "node_modules/promise-retry/node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -9245,9 +9086,9 @@ } }, "node_modules/retry": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", - "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", "dev": true, "license": "MIT", "engines": { @@ -9294,9 +9135,9 @@ } }, "node_modules/rollup": { - "version": "4.58.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.58.0.tgz", - "integrity": "sha512-wbT0mBmWbIvvq8NeEYWWvevvxnOyhKChir47S66WCxw1SXqhw7ssIYejnQEVt7XYQpsj2y8F9PM+Cr3SNEa0gw==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz", + "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==", "dev": true, "license": "MIT", "dependencies": { @@ -9310,31 +9151,31 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.58.0", - "@rollup/rollup-android-arm64": "4.58.0", - "@rollup/rollup-darwin-arm64": "4.58.0", - "@rollup/rollup-darwin-x64": "4.58.0", - "@rollup/rollup-freebsd-arm64": "4.58.0", - "@rollup/rollup-freebsd-x64": "4.58.0", - "@rollup/rollup-linux-arm-gnueabihf": "4.58.0", - "@rollup/rollup-linux-arm-musleabihf": "4.58.0", - "@rollup/rollup-linux-arm64-gnu": "4.58.0", - "@rollup/rollup-linux-arm64-musl": "4.58.0", - "@rollup/rollup-linux-loong64-gnu": "4.58.0", - "@rollup/rollup-linux-loong64-musl": "4.58.0", - "@rollup/rollup-linux-ppc64-gnu": "4.58.0", - "@rollup/rollup-linux-ppc64-musl": "4.58.0", - "@rollup/rollup-linux-riscv64-gnu": "4.58.0", - "@rollup/rollup-linux-riscv64-musl": "4.58.0", - "@rollup/rollup-linux-s390x-gnu": "4.58.0", - "@rollup/rollup-linux-x64-gnu": "4.58.0", - "@rollup/rollup-linux-x64-musl": "4.58.0", - "@rollup/rollup-openbsd-x64": "4.58.0", - "@rollup/rollup-openharmony-arm64": "4.58.0", - "@rollup/rollup-win32-arm64-msvc": "4.58.0", - "@rollup/rollup-win32-ia32-msvc": "4.58.0", - "@rollup/rollup-win32-x64-gnu": "4.58.0", - "@rollup/rollup-win32-x64-msvc": "4.58.0", + "@rollup/rollup-android-arm-eabi": "4.59.0", + "@rollup/rollup-android-arm64": "4.59.0", + "@rollup/rollup-darwin-arm64": "4.59.0", + "@rollup/rollup-darwin-x64": "4.59.0", + "@rollup/rollup-freebsd-arm64": "4.59.0", + "@rollup/rollup-freebsd-x64": "4.59.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", + "@rollup/rollup-linux-arm-musleabihf": "4.59.0", + "@rollup/rollup-linux-arm64-gnu": "4.59.0", + "@rollup/rollup-linux-arm64-musl": "4.59.0", + "@rollup/rollup-linux-loong64-gnu": "4.59.0", + "@rollup/rollup-linux-loong64-musl": "4.59.0", + "@rollup/rollup-linux-ppc64-gnu": "4.59.0", + "@rollup/rollup-linux-ppc64-musl": "4.59.0", + "@rollup/rollup-linux-riscv64-gnu": "4.59.0", + "@rollup/rollup-linux-riscv64-musl": "4.59.0", + "@rollup/rollup-linux-s390x-gnu": "4.59.0", + "@rollup/rollup-linux-x64-gnu": "4.59.0", + "@rollup/rollup-linux-x64-musl": "4.59.0", + "@rollup/rollup-openbsd-x64": "4.59.0", + "@rollup/rollup-openharmony-arm64": "4.59.0", + "@rollup/rollup-win32-arm64-msvc": "4.59.0", + "@rollup/rollup-win32-ia32-msvc": "4.59.0", + "@rollup/rollup-win32-x64-gnu": "4.59.0", + "@rollup/rollup-win32-x64-msvc": "4.59.0", "fsevents": "~2.3.2" } }, @@ -9804,9 +9645,9 @@ } }, "node_modules/spdx-license-ids": { - "version": "3.0.22", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.22.tgz", - "integrity": "sha512-4PRT4nh1EImPbt2jASOKHX7PB7I+e4IWNLvkKFDxNhJlfjbYlleYQh285Z/3mPTHSAK/AvdMmw5BNNuYH8ShgQ==", + "version": "3.0.23", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.23.tgz", + "integrity": "sha512-CWLcCCH7VLu13TgOH+r8p1O/Znwhqv/dbb6lqWy67G+pT1kHmeD/+V36AVb/vq8QMIQwVShJ6Ssl5FPh0fuSdw==", "dev": true, "license": "CC0-1.0" }, @@ -9878,13 +9719,13 @@ } }, "node_modules/strip-ansi": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", - "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", "dev": true, "license": "MIT", "dependencies": { - "ansi-regex": "^6.0.1" + "ansi-regex": "^6.2.2" }, "engines": { "node": ">=12" @@ -10130,16 +9971,16 @@ } }, "node_modules/typescript-eslint": { - "version": "8.56.0", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.56.0.tgz", - "integrity": "sha512-c7toRLrotJ9oixgdW7liukZpsnq5CZ7PuKztubGYlNppuTqhIoWfhgHo/7EU0v06gS2l/x0i2NEFK1qMIf0rIg==", + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.56.1.tgz", + "integrity": "sha512-U4lM6pjmBX7J5wk4szltF7I1cGBHXZopnAXCMXb3+fZ3B/0Z3hq3wS/CCUB2NZBNAExK92mCU2tEohWuwVMsDQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.56.0", - "@typescript-eslint/parser": "8.56.0", - "@typescript-eslint/typescript-estree": "8.56.0", - "@typescript-eslint/utils": "8.56.0" + "@typescript-eslint/eslint-plugin": "8.56.1", + "@typescript-eslint/parser": "8.56.1", + "@typescript-eslint/typescript-estree": "8.56.1", + "@typescript-eslint/utils": "8.56.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" diff --git a/package.json b/package.json index 7118a65..e822a63 100644 --- a/package.json +++ b/package.json @@ -12,26 +12,26 @@ "private": true, "packageManager": "npm@11.10.0", "dependencies": { - "@angular/common": "^21.2.0-next.0", - "@angular/compiler": "^21.2.0-next.0", - "@angular/core": "^21.2.0-next.0", - "@angular/forms": "^21.2.0-next.0", - "@angular/platform-browser": "^21.2.0-next.0", - "@angular/router": "^21.2.0-next.0", + "@angular/common": "^21.2.0", + "@angular/compiler": "^21.2.0", + "@angular/core": "^21.2.0", + "@angular/forms": "^21.2.0", + "@angular/platform-browser": "^21.2.0", + "@angular/router": "^21.2.0", "rxjs": "~7.8.0", "tslib": "^2.8.1" }, "devDependencies": { - "@angular/build": "^21.2.0-next.2", - "@angular/cli": "^21.2.0-next.2", - "@angular/compiler-cli": "^21.2.0-next.0", + "@angular/build": "^21.2.0", + "@angular/cli": "^21.2.0", + "@angular/compiler-cli": "^21.2.0", "angular-eslint": "^21.2.0", "eslint": "^9.39.3", "jsdom": "^28.1.0", - "ng-packagr": "^21.2.0-next.0", + "ng-packagr": "^21.2.0", "prettier": "^3.8.1", "typescript": "~5.9.2", - "typescript-eslint": "^8.56.0", + "typescript-eslint": "^8.56.1", "vitest": "^4.0.18" } } From ee3ceff050627fecc801d32d5ded82c6e8c5706e Mon Sep 17 00:00:00 2001 From: Cyrille Tuzi <555867+cyrilletuzi@users.noreply.github.com> Date: Fri, 27 Feb 2026 14:28:43 +0100 Subject: [PATCH 18/53] rename --- ....ts => map-rx-form-submit-options.spec.ts} | 4 +- lib/src/lib/map-rx-form-submit-options.ts | 23 ++++++++++ lib/src/lib/rx-form-submit-options-model.ts | 27 ----------- lib/src/lib/rx-form-submit-options.ts | 46 ++++++++++--------- lib/src/lib/rx-submit.ts | 2 +- lib/src/public-api.ts | 4 +- 6 files changed, 53 insertions(+), 53 deletions(-) rename lib/src/lib/{rx-form-submit-options.spec.ts => map-rx-form-submit-options.spec.ts} (95%) create mode 100644 lib/src/lib/map-rx-form-submit-options.ts delete mode 100644 lib/src/lib/rx-form-submit-options-model.ts diff --git a/lib/src/lib/rx-form-submit-options.spec.ts b/lib/src/lib/map-rx-form-submit-options.spec.ts similarity index 95% rename from lib/src/lib/rx-form-submit-options.spec.ts rename to lib/src/lib/map-rx-form-submit-options.spec.ts index bd2e973..a7bd1bb 100644 --- a/lib/src/lib/rx-form-submit-options.spec.ts +++ b/lib/src/lib/map-rx-form-submit-options.spec.ts @@ -3,7 +3,7 @@ import { TestBed } from '@angular/core/testing'; import { form, submit, type TreeValidationResult } from '@angular/forms/signals'; import { asyncScheduler, Observable, of, scheduled } from 'rxjs'; import { beforeEach, describe, expect, it } from 'vitest'; -import { rxFormSubmitOptions } from './rx-form-submit-options'; +import { mapRxFormSubmitOptions } from './map-rx-form-submit-options'; describe('rxFormSubmitOptions', () => { @Component({ @@ -16,7 +16,7 @@ describe('rxFormSubmitOptions', () => { test: '', }), { - submission: rxFormSubmitOptions({ + submission: mapRxFormSubmitOptions({ action: () => this.observable, }), }, diff --git a/lib/src/lib/map-rx-form-submit-options.ts b/lib/src/lib/map-rx-form-submit-options.ts new file mode 100644 index 0000000..e1cd4ed --- /dev/null +++ b/lib/src/lib/map-rx-form-submit-options.ts @@ -0,0 +1,23 @@ +import { assertInInjectionContext, DestroyRef, inject } from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import type { FormSubmitOptions } from '@angular/forms/signals'; +import { firstValueFrom } from 'rxjs'; +import type { RxFormSubmitOptions } from './rx-form-submit-options'; + +export function mapRxFormSubmitOptions( + options: RxFormSubmitOptions, +): FormSubmitOptions { + if (!options.destroyRef) { + assertInInjectionContext(mapRxFormSubmitOptions); + } + + const { action, destroyRef = inject(DestroyRef), ...otherOptions } = options; + + return { + action: (form, detail) => + firstValueFrom(action(form, detail).pipe(takeUntilDestroyed(destroyRef)), { + defaultValue: undefined, + }), + ...otherOptions, + }; +} diff --git a/lib/src/lib/rx-form-submit-options-model.ts b/lib/src/lib/rx-form-submit-options-model.ts deleted file mode 100644 index dffcac2..0000000 --- a/lib/src/lib/rx-form-submit-options-model.ts +++ /dev/null @@ -1,27 +0,0 @@ -import type { DestroyRef } from '@angular/core'; -import type { FieldTree, FormSubmitOptions, TreeValidationResult } from '@angular/forms/signals'; -import type { Observable } from 'rxjs'; - -export interface RxFormSubmitOptions extends Omit< - FormSubmitOptions, - 'action' -> { - /** - * Function to run when submitting the form data (when form is valid). - * - * @param field The submitted field - * @param detail An object containing the root field of the submitted form as well as the submitted field itself - */ - action: ( - field: FieldTree, - detail: { - root: FieldTree; - submitted: FieldTree; - }, - ) => Observable; - /** - * The `DestroyRef` representing the current context. This can be passed explicitly to use `rxSubmit()` - * outside of an injection context. Otherwise, the current `DestroyRef` is injected. - */ - destroyRef?: DestroyRef; -} diff --git a/lib/src/lib/rx-form-submit-options.ts b/lib/src/lib/rx-form-submit-options.ts index cdf468b..d1e6b31 100644 --- a/lib/src/lib/rx-form-submit-options.ts +++ b/lib/src/lib/rx-form-submit-options.ts @@ -1,28 +1,32 @@ -import { assertInInjectionContext, DestroyRef, inject } from '@angular/core'; -import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; -import type { FormSubmitOptions } from '@angular/forms/signals'; -import { firstValueFrom } from 'rxjs'; -import type { RxFormSubmitOptions } from './rx-form-submit-options-model'; +import type { DestroyRef } from '@angular/core'; +import type { FieldTree, FormSubmitOptions, TreeValidationResult } from '@angular/forms/signals'; +import type { Observable } from 'rxjs'; /** * Options that can be specified when submitting a form with `rxSubmit()`. * * @experimental 21.2.0 */ -export function rxFormSubmitOptions( - options: RxFormSubmitOptions, -): FormSubmitOptions { - if (!options.destroyRef) { - assertInInjectionContext(rxFormSubmitOptions); - } - - const { action, destroyRef = inject(DestroyRef), ...otherOptions } = options; - - return { - action: (form, detail) => - firstValueFrom(action(form, detail).pipe(takeUntilDestroyed(destroyRef)), { - defaultValue: undefined, - }), - ...otherOptions, - }; +export interface RxFormSubmitOptions extends Omit< + FormSubmitOptions, + 'action' +> { + /** + * Function to run when submitting the form data (when form is valid). + * + * @param field The submitted field + * @param detail An object containing the root field of the submitted form as well as the submitted field itself + */ + action: ( + field: FieldTree, + detail: { + root: FieldTree; + submitted: FieldTree; + }, + ) => Observable; + /** + * The `DestroyRef` representing the current context. This can be passed explicitly to use `rxSubmit()` + * outside of an injection context. Otherwise, the current `DestroyRef` is injected. + */ + destroyRef?: DestroyRef; } diff --git a/lib/src/lib/rx-submit.ts b/lib/src/lib/rx-submit.ts index 2de14e8..1ec8182 100644 --- a/lib/src/lib/rx-submit.ts +++ b/lib/src/lib/rx-submit.ts @@ -2,7 +2,7 @@ import { assertInInjectionContext, DestroyRef, inject } from '@angular/core'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { submit, type FieldTree } from '@angular/forms/signals'; import { defer, firstValueFrom, type Observable } from 'rxjs'; -import type { RxFormSubmitOptions } from './rx-form-submit-options-model'; +import type { RxFormSubmitOptions } from './rx-form-submit-options'; /** * Submits a given `FieldTree` using the given action function and applies any submission errors diff --git a/lib/src/public-api.ts b/lib/src/public-api.ts index 8cc8c59..fdd7975 100644 --- a/lib/src/public-api.ts +++ b/lib/src/public-api.ts @@ -2,6 +2,6 @@ * Public API Surface of lib */ -export { rxFormSubmitOptions } from './lib/rx-form-submit-options'; -export { type RxFormSubmitOptions } from './lib/rx-form-submit-options-model'; +export { mapRxFormSubmitOptions } from './lib/map-rx-form-submit-options'; +export { type RxFormSubmitOptions } from './lib/rx-form-submit-options'; export { rxSubmit } from './lib/rx-submit'; From e9d52f879cfdad74c574a7393e8d677498cb5a31 Mon Sep 17 00:00:00 2001 From: Cyrille Tuzi <555867+cyrilletuzi@users.noreply.github.com> Date: Fri, 27 Feb 2026 15:55:16 +0100 Subject: [PATCH 19/53] doc --- README.md | 192 ++++++++++------------ lib/src/lib/map-rx-form-submit-options.ts | 38 ++++- lib/src/lib/rx-form-submit-options.ts | 28 +++- lib/src/lib/rx-submit.ts | 65 ++++---- 4 files changed, 179 insertions(+), 144 deletions(-) diff --git a/README.md b/README.md index 00e1993..b906d43 100644 --- a/README.md +++ b/README.md @@ -1,48 +1,57 @@ # angular-rx-submit -## What is this library and why? - -This library provides the function `rxSubmit()`, on Observable-based equivalent of the official Angular Promise-based `submit()`. Why? +This library provides the `rxSubmit()` function, on Observable-based equivalent of the Promise-based `submit()` of Angular signal forms. Why? - cancellation - consistency - simple function -## Requirements +More details about the advantages of `rxSubmit()` are available in the "Problems solved" section below. + +## Getting started + +### Status + +> [!CAUTION] +> Angular signal forms are still marked as expertimental, which means breaking changes can happen at any time; which could break this library too. So this library is marked as experimental too for now. -- Angular version >= 21.2.0 [^1] -- RxJS version >= 7.4.0 [^2] +### Requirements + +- Angular version >= 21.2.0 +- RxJS version >= 7.4.0 > [!NOTE] > Angular versions 21.0 and 21.1 are _not_ supported, as this library requires a new `submit()` feature introduced in version 21.2. > [!NOTE] -> RxJS version 6 is _not_ supported. +> While Angular still allows RxJS version 6, it is _not_ supported by this library. -## Getting started +### Installation - `npm install angular-rx-submit` +### Usage + ```ts import { rxSubmit } from 'angular-rx-submit'; @Component({ - template: `
`, + imports: [FormRoot], + template: `
`, }) export class EditPage { private readonly destroyRef = inject(DestroyRef); - private readonly formModel = signal({ userName: '' }); protected readonly form = form(this.formModel); protected save(): void { rxSubmit(this.form, { - action: (submittedForm) => someObservable(submittedForm().value()), + action: (submittedForm) => someObservableOfTreeValidationResult(submittedForm().value()), destroyRef: this.destroyRef, }).subscribe({ next: (success) => { if (success) { - // Manage success here + // Manage success here (like redirecting to another page) } }, error: (error: unknown) => { @@ -53,7 +62,11 @@ export class EditPage { } ``` -## Injection context +A more complete example is available in the "Full example" section below. + +## Common issues + +### Injection context One advantage of `rxSubmit()` is automatic cancellation (if the user leaves the page). @@ -63,17 +76,17 @@ But for that to work, like many other Angular functions (`takeUntilDestroyed()`, ```ts @Component({ - template: `
`, + imports: [FormRoot], + template: `
`, }) export class EditPage { private readonly destroyRef = inject(DestroyRef); // ⬅️ - private readonly formModel = signal({ username: '' }); protected readonly form = form(this.formModel); protected save(): void { rxSubmit(this.form, { - action: (submittedForm) => someObservable(submittedForm().value()), + action: (submittedForm) => someObservableOfTreeValidationResult(submittedForm().value()), destroyRef: this.destroyRef, // ⬅️ }).subscribe(); } @@ -84,14 +97,15 @@ export class EditPage { ```ts @Component({ - template: `
`, + imports: [FormRoot], + template: `
`, }) export class EditPage { private readonly formModel = signal({ username: '' }); protected readonly form = form(this.formModel); private readonly submitObservable = rxSubmit(this.form, { - action: (submittedForm) => someObservable(submittedForm().value()), + action: (submittedForm) => someObservableOfTreeValidationResult(submittedForm().value()), }); protected save(): void { @@ -102,33 +116,33 @@ export class EditPage { **Using `rxSubmit()` outside an injection context and without providing a `DestroyRef` will throw the [`NG0203` error](https://angular.dev/errors/NG0203).** -## Subscription +### Subscription -You do _not_ need to unsubscribe, `rxSubmit()` does it for you via the injection context (see above). +Unsubscribing is _not_ needed, `rxSubmit()` already does a `takeUntilDestroyed()` internally via the injection context (see above). -But **you _DO_ need to subscribe**, even if you do not have something specific to do after submission (because it is how all `Observable`s work). +But **subscribing is required**, even if there is nothing something specific to do after submission (because it is how `Observable`s work). ```ts // ❌ Nothing happens rxSubmit(this.form, () => { - action: (submittedForm) => someObservable(submittedForm().value()), + action: (submittedForm) => someObservableOfTreeValidationResult(submittedForm().value()), destroyRef: this.destroyRef, }); // ✅ Triggers submission rxSubmit(this.form, { - action: (submittedForm) => someObservable(submittedForm().value()), + action: (submittedForm) => someObservableOfTreeValidationResult(submittedForm().value()), destroyRef: this.destroyRef, }).subscribe(); ``` -## Errors +### Errors -As for any Observable, handling errors is recommended. If the Observable you provide throws, the error will be propagated by `rxSubmit()`. The most common case is the HTTP request failing. +As for any Observable, handling errors is recommended. If the provided Observable throws, the error will be propagated by `rxSubmit()`. The most common case is the HTTP request failing. ```ts rxSubmit(this.form, { - action: () => (submittedForm) => someObservable(submittedForm().value()), + action: () => (submittedForm) => someObservableOfTreeValidationResult(submittedForm().value()), destroyRef: this.destroyRef, }).subscribe({ next: (success) => { @@ -147,9 +161,9 @@ rxSubmit(this.form, { }); ``` -## Validation result +### Validation result -As for the official Angular `submit()`, the Observable you provide to `rxSubmit()` should return an official `TreeValidationResult`. It is similar to Validators in previous reactive forms, meaning returning either: +As for the official Angular `submit()`, the Observable provided to `rxSubmit()` should return an official `TreeValidationResult`. It is similar to Validators in previous reactive forms, meaning returning either: - `null`, `undefined` or `void` if there is no validation error - a `ValidationError.WithOptionalFieldTree` if there is a validation error @@ -171,40 +185,14 @@ export function mapApiResponseToTreeValidationResult(response: ApiResponse): Tre } ``` -## Actions after validation - -Let us imagine a classic scenario: if the form passes validation, we want to redirect to another page. The question is: where to do the redirection? - -It is not specific to `rxSubmit()`, but the official Angular `submit()` can be confusing because there is 2 places where we could do that redirection: - -- directly inside the Observable / Promise we provide -- after the `rxSubmit()` / `submit()`, in the `next` / `then()` callback - -The `rxSubmit()` / `submit()` purpose is only to manage the form submission progress and validation. So the Observable / Promise we provide should be limited to just that, returning a `TreeValidationResult` as explained above. - -Subsequent actions should be done in the `next` / `then()` callback: - -```ts -rxSubmit(this.form, { - action: () => (submittedForm) => someObservable(submittedForm().value()), - destroyRef: this.destroyRef, -}).subscribe({ - next: (success) => { - if (success) { - this.router.navigate(['/some/other/page']).catch(() => {}); - } - }, - error: (error: unknown) => {}, -}); -``` - ### Multiple submissions -As the official `submit()`, do not trigger `rxSubmit()` in parallel, to avoid race issues. So be sure to block submission when one is already in progress: +As with the official `submit()`, do _not_ trigger `rxSubmit()` multiple times in parallel, to avoid race issues. So be sure to block submission when one is already in progress: ```ts @Component({ - template: `
+ imports: [FormRoot], + template: `
`, }) @@ -222,7 +210,8 @@ Let us take a common and basic example with the Promise-based `submit()`: ```ts @Component({ - template: `
`, + imports: [FormRoot], + template: `
`, }) export class EditPage { private readonly router = inject(Router); @@ -232,7 +221,7 @@ export class EditPage { protected save(): void { submit(this.form, { - action: async (submittedForm) => await somePromise(submittedForm().value()), + action: async (submittedForm) => somePromise(submittedForm().value()), }) .then((success) => { if (success) { @@ -244,7 +233,7 @@ export class EditPage { } ``` -Where `somePromise()` implies a HTTP request to the server. Let us say the request takes 10 seconds. Now the scenario: +where `somePromise()` implies a HTTP request to the server. Let us say the request takes 10 seconds. Now the scenario: - the user submits the form - as it is taking too long, the user leaves the page by going to another one @@ -263,50 +252,13 @@ A given project should be consistent, and having similar actions sometimes Obser ### Simple function -Even if you are OK to sacrifice consistency, you can transform your Observable to a Promise, but doing so in the `submit()` scenario is not as trivial as it seems: - -```ts -@Component({ - template: `
`, -}) -export class EditPage { - private readonly destroyRef = inject(DestroyRef); +One could transform an Observable to a Promise, but doing so in the `submit()` scenario is not as trivial as it seems, as it can be seen in the [source code](lib/src/lib/rx-submit.ts), which shows multiple pitfalls: - private readonly formModel = signal({ username: '' }); - protected readonly form = form(this.formModel); - - protected save(): void { - defer(() => submit( - this.form, { - action: async (submittedForm) => - await firstValueFrom( - someObservable(submittedForm().value()).pipe(takeUntilDestroyed(this.destroyRef)), - { - defaultValue: undefined, - }, - ), - }), - ).pipe(takeUntilDestroyed(this.destroyRef)).subscribe({ - next: (success) => { - if (success) { - this.router.navigate(['/some/other/page']).catch(() => {}); - } - }, - error: (error: unknown) { - console.log('Display an error snack bar/toast'); - }, - }); - } -} -``` - -As you can see: - -- there is not just 1 but 2 Promise to transform +- there is not just 1 but 2 Observable <=> Promise transformations - there is thus 2 cancellation to manage -- if the first `takeUntilDestroyed()` happens, the Observable will be empty, and it makes `firstValueFrom()` throws an error, which would trigger the last error callcack and thus display the snack bar/toast; this is why `defaultValue` must be set +- if the first `takeUntilDestroyed()` happens, the Observable will be empty, and it makes `firstValueFrom()` throws an error, which would trigger the last error callcack (where things like displaying a snack bar / toast could happen); this is managed by the `defaultValue` -It complexifies things a lot, and should be repeated in each form. `rxSubmit()` is a simple function which does it for you. +It complexifies things a lot, and should be repeated in each form. `rxSubmit()` is a simple function ready to use. ## Why not in Angular directly? @@ -314,7 +266,7 @@ I personnally think `rxSubmit()` should be part of `@angular/rxjs-interop`. For now, the Angular team has discarded [this request](https://github.com/angular/angular/issues/65199) (from someone else), without even allowing proper discussion about it. -Feel free to advocate for it if you want. +One can feel free to advocate for it if one want. ## Full example @@ -354,8 +306,9 @@ export class Api { } @Component({ + imports: [FormRoot], template: ` -
+