ngx-api-forms 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Mikhaël GERBET
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,657 @@
1
+ # ngx-api-forms
2
+
3
+ **API error parsing library for Angular.** Normalizes validation error responses from any backend into a consistent format your forms can consume. Not a display library -- a parsing library.
4
+
5
+ [![npm version](https://img.shields.io/npm/v/ngx-api-forms?style=flat-square)](https://www.npmjs.com/package/ngx-api-forms)
6
+ [![License: MIT](https://img.shields.io/npm/l/ngx-api-forms?style=flat-square)](LICENSE)
7
+ [![Angular 17+](https://img.shields.io/badge/Angular-17%2B-dd0031?style=flat-square&logo=angular)](https://angular.dev)
8
+ [![CI/CD](https://img.shields.io/github/actions/workflow/status/MikhaelGerbet/ngx-api-forms/ci.yml?style=flat-square&label=CI%2FCD)](https://github.com/MikhaelGerbet/ngx-api-forms/actions)
9
+ [![Zero dependencies](https://img.shields.io/badge/dependencies-0-brightgreen?style=flat-square)](https://www.npmjs.com/package/ngx-api-forms?activeTab=dependencies)
10
+
11
+ **[Live Demo](https://mikhaelgerbet.github.io/ngx-api-forms/)**
12
+
13
+ ## The Problem
14
+
15
+ Libraries like `@ngneat/error-tailor` or `ngx-valdemort` handle the **display** side -- rendering `Validators.required` messages in templates. But when your API returns a 422, those libraries can't help. You're left writing backend-specific parsing logic by hand:
16
+
17
+ ```typescript
18
+ // Brittle, repetitive, backend-specific
19
+ this.http.post('/api/register', data).subscribe({
20
+ error: (err) => {
21
+ const messages = err.error?.message; // NestJS format
22
+ if (Array.isArray(messages)) {
23
+ for (const msg of messages) {
24
+ const ctrl = this.form.get(msg.property);
25
+ if (ctrl) {
26
+ ctrl.setErrors(msg.constraints);
27
+ ctrl.markAsTouched();
28
+ }
29
+ }
30
+ }
31
+ }
32
+ });
33
+ ```
34
+
35
+ Switch from NestJS to Laravel and every error handler must be rewritten. Ten forms means ten copies of the same parsing logic. Most teams flatten everything into `{ serverError: message }`, losing constraint semantics entirely.
36
+
37
+ **ngx-api-forms fills the gap between the API and Reactive Forms.**
38
+
39
+ ## When NOT to Use This
40
+
41
+ This library only helps when your API returns **structured, field-level validation errors** (e.g. `{ email: ["required"] }`). If your backend returns flat messages like `{ message: "Bad request" }` with no per-field breakdown, ngx-api-forms cannot map anything to form controls.
42
+
43
+ In practice, this rules out:
44
+ - APIs that only return a single error string for the whole request
45
+ - Generic 500 errors
46
+ - Errors not tied to user input (infrastructure failures, rate limiting)
47
+
48
+ If you're unsure, call `parseApiErrors(err.error, yourPreset())` and check the output. Empty array means the format is not supported.
49
+
50
+ ## Quick Start
51
+
52
+ ### Minimal: parse errors without a form
53
+
54
+ ```typescript
55
+ import { parseApiErrors } from 'ngx-api-forms';
56
+ import { laravelPreset } from 'ngx-api-forms/laravel';
57
+
58
+ const errors = parseApiErrors(apiResponse, laravelPreset());
59
+ // [{ field: 'email', constraint: 'required', message: 'The email field is required.' }]
60
+ ```
61
+
62
+ One function, one preset, structured output. No form needed. Works in interceptors, NgRx effects, services, tests -- anywhere.
63
+
64
+ ### Full: parse and apply to a form
65
+
66
+ ```typescript
67
+ import { Component, inject } from '@angular/core';
68
+ import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';
69
+ import { HttpClient } from '@angular/common/http';
70
+ import { provideFormBridge, classValidatorPreset, NgxFormErrorDirective } from 'ngx-api-forms';
71
+
72
+ @Component({
73
+ standalone: true,
74
+ imports: [ReactiveFormsModule, NgxFormErrorDirective],
75
+ template: `
76
+ <form [formGroup]="form" (ngSubmit)="onSubmit()">
77
+ <input formControlName="email" />
78
+ <span ngxFormError="email" [form]="form"></span>
79
+
80
+ <input formControlName="name" />
81
+ <span ngxFormError="name" [form]="form"></span>
82
+
83
+ <button type="submit">Save</button>
84
+ </form>
85
+ `
86
+ })
87
+ export class MyComponent {
88
+ private http = inject(HttpClient);
89
+ private fb = inject(FormBuilder);
90
+
91
+ form = this.fb.group({
92
+ email: ['', [Validators.required, Validators.email]],
93
+ name: ['', [Validators.required, Validators.minLength(3)]],
94
+ });
95
+
96
+ bridge = provideFormBridge(this.form, {
97
+ preset: classValidatorPreset(),
98
+ });
99
+
100
+ onSubmit() {
101
+ this.http.post('/api/save', this.form.value).subscribe({
102
+ error: (err) => this.bridge.applyApiErrors(err.error)
103
+ });
104
+ }
105
+ }
106
+ ```
107
+
108
+ ## Installation
109
+
110
+ ```bash
111
+ npm install ngx-api-forms
112
+ ```
113
+
114
+ Import the core library and the preset for your backend:
115
+
116
+ ```typescript
117
+ // Core (FormBridge, interceptor, utilities, classValidatorPreset)
118
+ import { provideFormBridge, classValidatorPreset } from 'ngx-api-forms';
119
+
120
+ // Backend-specific presets (secondary entry points, tree-shakable)
121
+ import { laravelPreset } from 'ngx-api-forms/laravel';
122
+ import { djangoPreset } from 'ngx-api-forms/django';
123
+ import { zodPreset } from 'ngx-api-forms/zod';
124
+ import { expressValidatorPreset } from 'ngx-api-forms/express-validator';
125
+ import { analogPreset } from 'ngx-api-forms/analog';
126
+ ```
127
+
128
+ Each preset is a separate entry point. If you only use `laravelPreset`, the Django, Zod, express-validator, and Analog code is never included in your bundle.
129
+
130
+ `ng add` installs the package and auto-injects `apiErrorInterceptor` into your `app.config.ts`:
131
+
132
+ ```bash
133
+ ng add ngx-api-forms --preset=laravel
134
+ ```
135
+
136
+ Available presets: `laravel`, `django`, `class-validator`, `zod`, `express-validator`, `analog`.
137
+
138
+ ## Supported Backend Formats
139
+
140
+ ### NestJS / class-validator
141
+ ```json
142
+ {
143
+ "statusCode": 400,
144
+ "message": [
145
+ { "property": "email", "constraints": { "isEmail": "email must be a valid email" } }
146
+ ]
147
+ }
148
+ ```
149
+
150
+ ### Laravel
151
+ ```json
152
+ {
153
+ "message": "The given data was invalid.",
154
+ "errors": {
155
+ "email": ["The email field is required."]
156
+ }
157
+ }
158
+ ```
159
+
160
+ ### Django REST Framework
161
+ ```json
162
+ {
163
+ "email": ["This field is required."],
164
+ "name": ["Ensure this field has at least 3 characters."]
165
+ }
166
+ ```
167
+
168
+ ### Zod
169
+ ```json
170
+ {
171
+ "fieldErrors": {
172
+ "email": ["Invalid email"]
173
+ }
174
+ }
175
+ ```
176
+
177
+ ### Express / express-validator
178
+ ```json
179
+ {
180
+ "errors": [
181
+ { "type": "field", "path": "email", "msg": "Invalid value", "location": "body" }
182
+ ]
183
+ }
184
+ ```
185
+
186
+ Also handles the legacy v5/v6 format (`{ param, msg }`) and direct arrays.
187
+
188
+ ### Analog (Nitro/h3)
189
+ ```json
190
+ {
191
+ "statusCode": 422,
192
+ "statusMessage": "Validation failed",
193
+ "data": {
194
+ "email": ["This field is required."],
195
+ "name": ["Must be at least 3 characters."]
196
+ }
197
+ }
198
+ ```
199
+
200
+ Unwraps the Nitro/h3 `createError()` envelope. Also handles direct `{ field: string[] }` format without the envelope.
201
+
202
+ ## Constraint Inference and i18n Limitation
203
+
204
+ The Laravel, Django, Zod, and express-validator presets infer constraint types (e.g. "required", "email") by pattern-matching on the English text of error messages. This works reliably with default backend messages.
205
+
206
+ When a message does not match any pattern, the constraint falls back to `'serverError'` with the original message preserved. Unrecognized messages are never lost.
207
+
208
+ **Important: inference only works with default English messages.** The built-in regex patterns match strings like `"The email field is required."` (Laravel) or `"This field is required."` (Django). If your backend returns messages in another language (e.g. `"Ce champ est obligatoire."`), the pattern will not match and the error will use `constraint: 'serverError'` instead of `constraint: 'required'`.
209
+
210
+ This is by design: parsing free-text in multiple languages reliably is not feasible. If your backend returns non-English messages, you have several options:
211
+
212
+ Known limitations:
213
+
214
+ - **Translated messages**: Non-English messages fall back to `'serverError'`.
215
+ - **Custom messages**: Overridden validation messages may not match the built-in patterns.
216
+ - **NestJS/class-validator does not have this limitation** because it transmits the constraint key directly.
217
+
218
+ When inference is not enough:
219
+
220
+ ```typescript
221
+ // 1. Disable inference entirely and use the raw message
222
+ const bridge = provideFormBridge(form, {
223
+ preset: laravelPreset({ noInference: true }),
224
+ });
225
+ // All errors get constraint: 'serverError' with the original message preserved.
226
+ // Display the message directly in your template.
227
+
228
+ // 2. Custom constraintMap to map specific messages to constraints
229
+ const bridge = provideFormBridge(form, {
230
+ preset: laravelPreset(),
231
+ constraintMap: {
232
+ 'Ce champ est obligatoire.': 'required',
233
+ 'Adresse email invalide.': 'email',
234
+ },
235
+ });
236
+
237
+ // 3. catchAll to apply unmatched errors as { generic: msg }
238
+ const bridge = provideFormBridge(form, {
239
+ preset: laravelPreset(),
240
+ catchAll: true,
241
+ });
242
+
243
+ // 4. constraintPatterns: provide regex patterns for your language
244
+ const bridge = provideFormBridge(form, {
245
+ preset: laravelPreset({
246
+ constraintPatterns: {
247
+ required: /est obligatoire/i,
248
+ email: /courriel.*invalide/i,
249
+ minlength: /au moins \d+ caract/i,
250
+ },
251
+ }),
252
+ });
253
+ // User patterns are checked first; unmatched messages fall through to English inference.
254
+
255
+ // 5. Write a custom preset for full control (see below)
256
+ ```
257
+
258
+ ### Schema-Based Inference
259
+
260
+ When your backend returns structured error codes alongside messages, presets use them directly without text matching. This makes constraint inference fully language-independent.
261
+
262
+ ```json
263
+ // Django DRF with custom exception handler
264
+ { "email": [{ "message": "Ce champ est obligatoire.", "code": "required" }] }
265
+
266
+ // Laravel with rule names
267
+ { "errors": { "email": [{ "message": "Le champ est requis.", "rule": "required" }] } }
268
+
269
+ // express-validator with code field
270
+ { "errors": [{ "type": "field", "path": "email", "msg": "Adresse invalide", "code": "email" }] }
271
+ ```
272
+
273
+ When `code` (Django/Analog), `rule` (Laravel), or `code` (express-validator) is present, the value is used as the constraint directly. No regex matching, no language assumption. Falls back to text inference when the structured field is absent.
274
+
275
+ ## Global Errors
276
+
277
+ Some backends return errors not tied to any specific field -- Django's `non_field_errors`, Zod's `formErrors`, or a field name that does not match any form control. These errors are collected in `globalErrorsSignal` instead of being silently dropped.
278
+
279
+ ```typescript
280
+ bridge.applyApiErrors({
281
+ non_field_errors: ['Unable to log in with provided credentials.'],
282
+ email: ['This field is required.'],
283
+ });
284
+
285
+ // Field errors applied to controls
286
+ console.log(form.controls.email.hasError('required')); // true
287
+
288
+ // Global errors available via signal
289
+ console.log(bridge.globalErrorsSignal());
290
+ // [{ message: 'Unable to log in with provided credentials.', constraint: 'serverError' }]
291
+ ```
292
+
293
+ `clearApiErrors()` clears both field errors and global errors. `hasErrorsSignal` accounts for global errors too.
294
+
295
+ Unmatched fields (errors referencing a field that does not exist in the form) are also routed to `globalErrorsSignal` with the original field name preserved in the `originalField` property.
296
+
297
+ ## Switching Backends
298
+
299
+ Each backend has its own preset. Pass an array if your app talks to multiple APIs -- they are tried in order until one matches.
300
+
301
+ ```typescript
302
+ import { provideFormBridge, classValidatorPreset } from 'ngx-api-forms';
303
+ import { laravelPreset } from 'ngx-api-forms/laravel';
304
+ import { djangoPreset } from 'ngx-api-forms/django';
305
+ import { zodPreset } from 'ngx-api-forms/zod';
306
+ import { expressValidatorPreset } from 'ngx-api-forms/express-validator';
307
+ import { analogPreset } from 'ngx-api-forms/analog';
308
+
309
+ // Laravel
310
+ const bridge = provideFormBridge(form, { preset: laravelPreset() });
311
+
312
+ // Django REST Framework
313
+ const bridge = provideFormBridge(form, { preset: djangoPreset() });
314
+
315
+ // Zod (e.g. with tRPC)
316
+ const bridge = provideFormBridge(form, { preset: zodPreset() });
317
+
318
+ // Express / express-validator
319
+ const bridge = provideFormBridge(form, { preset: expressValidatorPreset() });
320
+
321
+ // Analog (Nitro/h3)
322
+ const bridge = provideFormBridge(form, { preset: analogPreset() });
323
+
324
+ // Multiple presets, tried in order
325
+ const bridge = provideFormBridge(form, {
326
+ preset: [classValidatorPreset(), laravelPreset()]
327
+ });
328
+ ```
329
+
330
+ ## Automatic Error Handling with HttpInterceptor
331
+
332
+ The library ships a ready-to-use `apiErrorInterceptor` that catches 422/400 responses and applies errors to the right FormBridge automatically.
333
+
334
+ ### Setup
335
+
336
+ ```typescript
337
+ // app.config.ts
338
+ import { provideHttpClient, withInterceptors } from '@angular/common/http';
339
+ import { apiErrorInterceptor } from 'ngx-api-forms';
340
+
341
+ export const appConfig = {
342
+ providers: [
343
+ provideHttpClient(
344
+ withInterceptors([apiErrorInterceptor()])
345
+ ),
346
+ ],
347
+ };
348
+ ```
349
+
350
+ ### Per-request: tag with `withFormBridge()`
351
+
352
+ ```typescript
353
+ import { withFormBridge } from 'ngx-api-forms';
354
+
355
+ // Errors are applied automatically -- no error handler needed
356
+ this.http.post('/api/save', data, withFormBridge(this.bridge)).subscribe({
357
+ next: () => this.router.navigate(['/done']),
358
+ });
359
+ ```
360
+
361
+ ### Global: centralize with `onError`
362
+
363
+ ```typescript
364
+ apiErrorInterceptor({
365
+ preset: classValidatorPreset(),
366
+ onError: (errors, response) => {
367
+ errorStore.setFieldErrors(errors);
368
+ },
369
+ })
370
+ ```
371
+
372
+ ### Standalone: `parseApiErrors` in your own interceptor
373
+
374
+ ```typescript
375
+ import { HttpInterceptorFn, HttpErrorResponse } from '@angular/common/http';
376
+ import { catchError, throwError } from 'rxjs';
377
+ import { parseApiErrors, classValidatorPreset } from 'ngx-api-forms';
378
+
379
+ export const myInterceptor: HttpInterceptorFn = (req, next) => {
380
+ return next(req).pipe(
381
+ catchError((err: HttpErrorResponse) => {
382
+ if (err.status === 422) {
383
+ const fieldErrors = parseApiErrors(err.error, classValidatorPreset());
384
+ // route to your store, service, or whatever you need
385
+ }
386
+ return throwError(() => err);
387
+ }),
388
+ );
389
+ };
390
+ ```
391
+
392
+ ## Resource Integration (Angular 19+)
393
+
394
+ When using `resource()` or `rxResource()`, a simple `effect()` is all you need to wire the error signal to a FormBridge:
395
+
396
+ ```typescript
397
+ import { effect } from '@angular/core';
398
+ import { rxResource } from '@angular/core/rxjs-interop';
399
+ import { provideFormBridge } from 'ngx-api-forms';
400
+ import { djangoPreset } from 'ngx-api-forms/django';
401
+
402
+ @Component({ ... })
403
+ export class EditComponent {
404
+ private http = inject(HttpClient);
405
+ form = inject(FormBuilder).group({ name: [''], email: [''] });
406
+ bridge = provideFormBridge(this.form, { preset: djangoPreset() });
407
+
408
+ saveResource = rxResource({
409
+ loader: () => this.http.put('/api/profile', this.form.value),
410
+ });
411
+
412
+ private ref = effect(() => {
413
+ const err = this.saveResource.error();
414
+ err ? this.bridge.applyApiErrors(err) : this.bridge.clearApiErrors();
415
+ });
416
+ }
417
+ ```
418
+
419
+ This pattern works with any `Signal<unknown>` -- not limited to Angular resources. No wrapper API needed: Angular's `effect()` already tracks signal dependencies and re-runs when the error changes.
420
+
421
+ ## Typed Forms
422
+
423
+ `FormBridge` is generic. When you pass a typed `FormGroup`, the `form` getter preserves the type:
424
+
425
+ ```typescript
426
+ interface LoginForm {
427
+ email: FormControl<string>;
428
+ password: FormControl<string>;
429
+ }
430
+
431
+ const form = new FormGroup<LoginForm>({ ... });
432
+ const bridge = provideFormBridge(form);
433
+
434
+ // bridge.form is typed as FormGroup<LoginForm>
435
+ bridge.form.controls.email; // FormControl<string> -- full autocompletion
436
+ ```
437
+
438
+ ## API Reference
439
+
440
+ ### `parseApiErrors(error, preset?, options?)`
441
+
442
+ Parse API errors without a form. Works in interceptors, stores, effects, tests -- anywhere. Returns `ApiFieldError[]`.
443
+
444
+ ```typescript
445
+ import { parseApiErrors } from 'ngx-api-forms';
446
+ import { laravelPreset } from 'ngx-api-forms/laravel';
447
+
448
+ const errors = parseApiErrors(err.error, laravelPreset());
449
+ // [{ field: 'email', constraint: 'required', message: 'The email field is required.' }]
450
+ ```
451
+
452
+ ### HttpInterceptor
453
+
454
+ | Export | Description |
455
+ |--------|-------------|
456
+ | `apiErrorInterceptor(config?)` | Functional interceptor. Catches 422/400 and auto-applies errors to tagged bridges |
457
+ | `withFormBridge(bridge)` | Attach a FormBridge to an HTTP request via HttpContext |
458
+ | `FORM_BRIDGE` | The `HttpContextToken` used internally (advanced) |
459
+
460
+ ### FormBridge (form integration)
461
+
462
+ Create with `provideFormBridge(form, config?)` or `createFormBridge(form, config?)`. Both are equivalent.
463
+
464
+ | Method | Returns | Description |
465
+ |--------|---------|-------------|
466
+ | `applyApiErrors(error)` | `ResolvedFieldError[]` | Parse and apply API errors to form controls |
467
+ | `clearApiErrors()` | `void` | Remove only the API-set errors (client-side validators are preserved) |
468
+ | `getFirstError()` | `FirstError \| null` | First error across all controls |
469
+ | `getFieldErrors(field)` | `ValidationErrors \| null` | Errors for a specific field |
470
+ | `addInterceptor(fn)` | `() => void` | Register an error interceptor. Returns a dispose function |
471
+
472
+ ### Signals
473
+
474
+ | Signal | Type | Description |
475
+ |--------|------|-------------|
476
+ | `errorsSignal` | `Signal<ResolvedFieldError[]>` | All current field-level API errors |
477
+ | `globalErrorsSignal` | `Signal<GlobalError[]>` | Non-field errors (Django `non_field_errors`, Zod `formErrors`, unmatched fields) |
478
+ | `firstErrorSignal` | `Signal<FirstError \| null>` | First error, or null |
479
+ | `hasErrorsSignal` | `Signal<boolean>` | Whether any API errors exist (field or global) |
480
+
481
+ ### Constants
482
+
483
+ | Export | Description |
484
+ |--------|-------------|
485
+ | `GLOBAL_ERROR_FIELD` | Sentinel field name (`'__global__'`) used by presets to mark non-field errors |
486
+
487
+ ### Standalone Utility Functions
488
+
489
+ | Function | Description |
490
+ |----------|-------------|
491
+ | `wrapSubmit(form, source, options?)` | Submit lifecycle (disable/enable) without FormBridge |
492
+ | `toFormData(data)` | Convert a plain object to FormData. Handles Files, Blobs, Arrays, nested objects |
493
+ | `enableForm(form, options?)` | Enable all controls, with optional `except` list |
494
+ | `disableForm(form, options?)` | Disable all controls, with optional `except` list |
495
+ | `clearFormErrors(form)` | Clear all errors from all controls |
496
+ | `getDirtyValues(form)` | Return only the dirty fields and their values |
497
+ | `hasError(form, errorKey)` | Check if any control has a specific error |
498
+ | `getErrorMessage(form, field, key?)` | Get the error message string for a field |
499
+
500
+ ### Preset Options
501
+
502
+ All built-in presets accept a `noInference` option. The Laravel, Django, Zod, and express-validator presets also accept `constraintPatterns` for custom i18n regex matching:
503
+
504
+ ```typescript
505
+ // Skip inference entirely
506
+ laravelPreset({ noInference: true })
507
+ djangoPreset({ noInference: true })
508
+ zodPreset({ noInference: true })
509
+ expressValidatorPreset({ noInference: true })
510
+ analogPreset({ noInference: true })
511
+ classValidatorPreset({ noInference: true }) // only affects string message fallback
512
+
513
+ // Provide regex patterns for non-English messages
514
+ laravelPreset({
515
+ constraintPatterns: {
516
+ required: /est obligatoire/i,
517
+ email: /courriel.*invalide/i,
518
+ },
519
+ })
520
+ ```
521
+
522
+ When `noInference: true`, all errors use `constraint: 'serverError'` with the original message preserved.
523
+
524
+ `constraintPatterns` takes a `Record<string, RegExp>`. Each regex is tested against the raw error message. Matched patterns return the corresponding constraint key. Unmatched messages fall through to the default English inference.
525
+
526
+ ### Configuration
527
+
528
+ ```typescript
529
+ interface FormBridgeConfig {
530
+ preset?: ErrorPreset | ErrorPreset[];
531
+ constraintMap?: Record<string, string>;
532
+ i18n?: {
533
+ prefix?: string;
534
+ resolver?: (field, constraint, message) => string | null;
535
+ };
536
+ catchAll?: boolean; // Apply unmatched errors as { generic: msg }
537
+ mergeErrors?: boolean; // Merge with existing errors instead of replacing
538
+ debug?: boolean; // Log warnings when presets or fields don't match
539
+ }
540
+ ```
541
+
542
+ ## Debug Mode
543
+
544
+ Set `debug: true` to log warnings during development:
545
+
546
+ ```typescript
547
+ const bridge = provideFormBridge(form, {
548
+ preset: laravelPreset(),
549
+ debug: true,
550
+ });
551
+
552
+ // Or standalone:
553
+ const errors = parseApiErrors(err.error, laravelPreset(), { debug: true });
554
+ ```
555
+
556
+ The library warns when:
557
+ - No preset produces results for a given error payload (format might be wrong or unsupported)
558
+ - A parsed error field does not match any form control (possible typo or missing control)
559
+
560
+ ## Submit and Loading State
561
+
562
+ `wrapSubmit` handles the disable/enable lifecycle as a standalone function:
563
+
564
+ ```typescript
565
+ import { wrapSubmit } from 'ngx-api-forms';
566
+
567
+ wrapSubmit(this.form, this.http.post('/api', data), {
568
+ onError: (err) => this.bridge.applyApiErrors(err.error),
569
+ }).subscribe({
570
+ next: () => this.router.navigate(['/done']),
571
+ });
572
+ ```
573
+
574
+ ## i18n
575
+
576
+ Generate translation keys automatically or provide a custom resolver:
577
+
578
+ ```typescript
579
+ // Translation key prefix
580
+ const bridge = provideFormBridge(form, {
581
+ preset: classValidatorPreset(),
582
+ i18n: { prefix: 'validation' }
583
+ });
584
+ // Produces keys like "validation.email.isEmail"
585
+
586
+ // Custom resolver
587
+ const bridge = provideFormBridge(form, {
588
+ i18n: {
589
+ resolver: (field, constraint, originalMessage) => {
590
+ return this.translate.instant(`errors.${field}.${constraint}`);
591
+ }
592
+ }
593
+ });
594
+ ```
595
+
596
+ ## Error Interceptors
597
+
598
+ Interceptors let you filter or transform errors before they reach the form:
599
+
600
+ ```typescript
601
+ const dispose = bridge.addInterceptor((errors, form) => {
602
+ return errors.filter(e => e.field !== 'internalField');
603
+ });
604
+ // Later: dispose() to remove the interceptor
605
+ ```
606
+
607
+ ## NgxFormError Directive
608
+
609
+ ```html
610
+ <!-- Basic usage -->
611
+ <span ngxFormError="email" [form]="myForm"></span>
612
+
613
+ <!-- Custom error messages -->
614
+ <span ngxFormError="email"
615
+ [form]="myForm"
616
+ [errorMessages]="{ required: 'Email requis', email: 'Email invalide' }">
617
+ </span>
618
+
619
+ <!-- Show errors before the field is touched -->
620
+ <span ngxFormError="email" [form]="myForm" [showOnTouched]="false"></span>
621
+ ```
622
+
623
+ ## Custom Preset
624
+
625
+ If your backend uses a different format, write a preset in a few lines:
626
+
627
+ ```typescript
628
+ import { ErrorPreset, ApiFieldError } from 'ngx-api-forms';
629
+
630
+ export function myBackendPreset(): ErrorPreset {
631
+ return {
632
+ name: 'my-backend',
633
+ parse(error: unknown): ApiFieldError[] {
634
+ const err = error as { validationErrors: Array<{ field: string; rule: string; msg: string }> };
635
+ return (err.validationErrors ?? []).map(e => ({
636
+ field: e.field,
637
+ constraint: e.rule,
638
+ message: e.msg,
639
+ }));
640
+ }
641
+ };
642
+ }
643
+ ```
644
+
645
+ ## Angular Compatibility
646
+
647
+ | ngx-api-forms | Angular |
648
+ |:---:|:---:|
649
+ | 1.x | 17.x, 18.x, 19.x, 20.x |
650
+
651
+ ## Contributing
652
+
653
+ See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.
654
+
655
+ ## License
656
+
657
+ MIT - [Mikhael GERBET](https://github.com/MikhaelGerbet)