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 +21 -0
- package/README.md +657 -0
- package/analog/index.d.ts +76 -0
- package/django/index.d.ts +56 -0
- package/express-validator/index.d.ts +60 -0
- package/fesm2022/ngx-api-forms-analog.mjs +195 -0
- package/fesm2022/ngx-api-forms-analog.mjs.map +1 -0
- package/fesm2022/ngx-api-forms-django.mjs +173 -0
- package/fesm2022/ngx-api-forms-django.mjs.map +1 -0
- package/fesm2022/ngx-api-forms-express-validator.mjs +202 -0
- package/fesm2022/ngx-api-forms-express-validator.mjs.map +1 -0
- package/fesm2022/ngx-api-forms-laravel.mjs +167 -0
- package/fesm2022/ngx-api-forms-laravel.mjs.map +1 -0
- package/fesm2022/ngx-api-forms-zod.mjs +226 -0
- package/fesm2022/ngx-api-forms-zod.mjs.map +1 -0
- package/fesm2022/ngx-api-forms.mjs +890 -0
- package/fesm2022/ngx-api-forms.mjs.map +1 -0
- package/index.d.ts +621 -0
- package/laravel/index.d.ts +49 -0
- package/package.json +85 -0
- package/schematics/collection.json +10 -0
- package/schematics/ng-add/index.js +300 -0
- package/schematics/ng-add/index.spec.js +119 -0
- package/schematics/ng-add/schema.json +34 -0
- package/zod/index.d.ts +58 -0
|
@@ -0,0 +1,890 @@
|
|
|
1
|
+
import * as i0 from '@angular/core';
|
|
2
|
+
import { signal, computed, inject, ElementRef, Renderer2, DestroyRef, Input, Directive } from '@angular/core';
|
|
3
|
+
import { FormGroup, FormArray } from '@angular/forms';
|
|
4
|
+
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
|
5
|
+
import { Subject, merge, takeUntil, tap, catchError, throwError } from 'rxjs';
|
|
6
|
+
import { HttpContextToken, HttpContext } from '@angular/common/http';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* ngx-api-forms - Models & Interfaces
|
|
10
|
+
*
|
|
11
|
+
* Core types for bridging API validation errors to Angular Reactive Forms.
|
|
12
|
+
*/
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
// Global (Non-Field) Errors
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
/**
|
|
17
|
+
* Sentinel field name used by presets to tag errors that are not bound to a
|
|
18
|
+
* specific form control (e.g. Django `non_field_errors`, Zod `formErrors`).
|
|
19
|
+
*
|
|
20
|
+
* FormBridge also routes any ApiFieldError whose field does not match a control
|
|
21
|
+
* to `globalErrorsSignal`, so custom presets do not need to use this constant -
|
|
22
|
+
* unmatched fields are captured automatically.
|
|
23
|
+
*/
|
|
24
|
+
const GLOBAL_ERROR_FIELD = '__global__';
|
|
25
|
+
|
|
26
|
+
function flattenErrors(errors, parentPath = '') {
|
|
27
|
+
const result = [];
|
|
28
|
+
for (const error of errors) {
|
|
29
|
+
const path = parentPath ? `${parentPath}.${error.property}` : error.property;
|
|
30
|
+
if (error.constraints) {
|
|
31
|
+
for (const [constraint, message] of Object.entries(error.constraints)) {
|
|
32
|
+
result.push({ field: path, constraint, message });
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
// Recursively handle nested validation (e.g. nested DTOs)
|
|
36
|
+
if (error.children && error.children.length > 0) {
|
|
37
|
+
result.push(...flattenErrors(error.children, path));
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return result;
|
|
41
|
+
}
|
|
42
|
+
function parseStringMessage(message) {
|
|
43
|
+
// Attempt to extract field name and constraint from common class-validator messages
|
|
44
|
+
const patterns = [
|
|
45
|
+
{ regex: /^(\w+) is required$/, constraint: 'required' },
|
|
46
|
+
{ regex: /^(\w+) must be shorter/, constraint: 'maxLength' },
|
|
47
|
+
{ regex: /^(\w+) must be longer/, constraint: 'minLength' },
|
|
48
|
+
{ regex: /^(\w+) must not be less/, constraint: 'min' },
|
|
49
|
+
{ regex: /^(\w+) must not be more/, constraint: 'max' },
|
|
50
|
+
{ regex: /^(\w+) must be an? email/, constraint: 'isEmail' },
|
|
51
|
+
{ regex: /^(\w+) must be an? valid phone/, constraint: 'isPhoneNumber' },
|
|
52
|
+
{ regex: /^(\w+) must be one of the following/, constraint: 'isEnum' },
|
|
53
|
+
{ regex: /^(\w+) must be an? Date/, constraint: 'isDate' },
|
|
54
|
+
{ regex: /^(\w+) must be an? IBAN/, constraint: 'isIBAN' },
|
|
55
|
+
{ regex: /^(\w+) must be an? EAN/, constraint: 'isEAN' },
|
|
56
|
+
{ regex: /^(\w+) must be an? URL/, constraint: 'isUrl' },
|
|
57
|
+
{ regex: /^(\w+) must be an? valid url/i, constraint: 'isUrl' },
|
|
58
|
+
{ regex: /^(\w+) is not valid/i, constraint: 'invalid' },
|
|
59
|
+
{ regex: /^(\w+) is already taken/i, constraint: 'unique' },
|
|
60
|
+
{ regex: /^(\w+) is already used/i, constraint: 'unique' },
|
|
61
|
+
{ regex: /^(\w+) must be a valid decimal/i, constraint: 'isDecimal' },
|
|
62
|
+
];
|
|
63
|
+
for (const { regex, constraint } of patterns) {
|
|
64
|
+
const match = message.match(regex);
|
|
65
|
+
if (match) {
|
|
66
|
+
const field = match[1]
|
|
67
|
+
? match[1].charAt(0).toLowerCase() + match[1].slice(1)
|
|
68
|
+
: 'unknown';
|
|
69
|
+
return [{ field, constraint, message }];
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
// Specific messages without a field name captured by regex
|
|
73
|
+
if (/^file is required/i.test(message)) {
|
|
74
|
+
return [{ field: 'file', constraint: 'required', message }];
|
|
75
|
+
}
|
|
76
|
+
return [];
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Creates a class-validator / NestJS error preset.
|
|
80
|
+
*
|
|
81
|
+
* @param options.noInference - When true, skips constraint guessing for string messages.
|
|
82
|
+
* Structured `constraints` objects (e.g. `{ isEmail: 'message' }`) are always used as-is.
|
|
83
|
+
* Only affects the fallback string message parser.
|
|
84
|
+
*
|
|
85
|
+
* @example
|
|
86
|
+
* ```typescript
|
|
87
|
+
* import { classValidatorPreset } from 'ngx-api-forms';
|
|
88
|
+
*
|
|
89
|
+
* const bridge = createFormBridge(form, { preset: classValidatorPreset() });
|
|
90
|
+
*
|
|
91
|
+
* // No inference on string messages
|
|
92
|
+
* const bridge = createFormBridge(form, { preset: classValidatorPreset({ noInference: true }) });
|
|
93
|
+
* ```
|
|
94
|
+
*/
|
|
95
|
+
function classValidatorPreset(options) {
|
|
96
|
+
const skipInference = options?.noInference ?? false;
|
|
97
|
+
return {
|
|
98
|
+
name: 'class-validator',
|
|
99
|
+
constraintMap: CLASS_VALIDATOR_CONSTRAINT_MAP,
|
|
100
|
+
parse(error) {
|
|
101
|
+
if (!error || typeof error !== 'object')
|
|
102
|
+
return [];
|
|
103
|
+
const err = error;
|
|
104
|
+
// Standard NestJS ValidationPipe format: { message: ClassValidatorError[] }
|
|
105
|
+
if (Array.isArray(err['message'])) {
|
|
106
|
+
const messages = err['message'];
|
|
107
|
+
// Check if it's an array of ClassValidatorError objects
|
|
108
|
+
if (messages.length > 0 && typeof messages[0] === 'object' && 'property' in messages[0]) {
|
|
109
|
+
return flattenErrors(messages);
|
|
110
|
+
}
|
|
111
|
+
// It might be an array of strings (simple messages)
|
|
112
|
+
if (messages.length > 0 && typeof messages[0] === 'string') {
|
|
113
|
+
if (skipInference) {
|
|
114
|
+
return messages.map(msg => ({ field: 'unknown', constraint: 'serverError', message: msg }));
|
|
115
|
+
}
|
|
116
|
+
const results = [];
|
|
117
|
+
for (const msg of messages) {
|
|
118
|
+
results.push(...parseStringMessage(msg));
|
|
119
|
+
}
|
|
120
|
+
return results;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
// Single string message: { message: "email is required" }
|
|
124
|
+
if (typeof err['message'] === 'string') {
|
|
125
|
+
if (skipInference) {
|
|
126
|
+
return [{ field: 'unknown', constraint: 'serverError', message: err['message'] }];
|
|
127
|
+
}
|
|
128
|
+
return parseStringMessage(err['message']);
|
|
129
|
+
}
|
|
130
|
+
// Direct array (no wrapper): [{ property: 'email', constraints: {...} }]
|
|
131
|
+
if (Array.isArray(error)) {
|
|
132
|
+
const items = error;
|
|
133
|
+
if (items.length > 0 && typeof items[0] === 'object' && items[0] !== null && 'property' in items[0]) {
|
|
134
|
+
return flattenErrors(items);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return [];
|
|
138
|
+
},
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Default constraint map for class-validator.
|
|
143
|
+
* Maps class-validator constraint names to Angular form error keys.
|
|
144
|
+
*/
|
|
145
|
+
const CLASS_VALIDATOR_CONSTRAINT_MAP = {
|
|
146
|
+
isNotEmpty: 'required',
|
|
147
|
+
isEmail: 'email',
|
|
148
|
+
minLength: 'minlength',
|
|
149
|
+
maxLength: 'maxlength',
|
|
150
|
+
min: 'min',
|
|
151
|
+
max: 'max',
|
|
152
|
+
isPhoneNumber: 'phone',
|
|
153
|
+
isEnum: 'enum',
|
|
154
|
+
isDate: 'date',
|
|
155
|
+
isDateString: 'date',
|
|
156
|
+
isIBAN: 'iban',
|
|
157
|
+
isEAN: 'ean',
|
|
158
|
+
isUrl: 'url',
|
|
159
|
+
isURL: 'url',
|
|
160
|
+
isDecimal: 'decimal',
|
|
161
|
+
isNumber: 'number',
|
|
162
|
+
isInt: 'integer',
|
|
163
|
+
isBoolean: 'boolean',
|
|
164
|
+
isString: 'string',
|
|
165
|
+
isArray: 'array',
|
|
166
|
+
arrayMinSize: 'minlength',
|
|
167
|
+
arrayMaxSize: 'maxlength',
|
|
168
|
+
matches: 'pattern',
|
|
169
|
+
isStrongPassword: 'password',
|
|
170
|
+
unique: 'unique',
|
|
171
|
+
invalid: 'invalid',
|
|
172
|
+
required: 'required',
|
|
173
|
+
serverError: 'serverError',
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* FormBridge - The core class of ngx-api-forms.
|
|
178
|
+
*
|
|
179
|
+
* Bridges API validation errors to Angular Reactive Forms with:
|
|
180
|
+
* - Automatic error parsing via presets (class-validator, Laravel, Django, Zod)
|
|
181
|
+
* - i18n-friendly error messages
|
|
182
|
+
* - Angular Signals support
|
|
183
|
+
* - SSR-safe operations
|
|
184
|
+
*/
|
|
185
|
+
/**
|
|
186
|
+
* FormBridge wraps an Angular FormGroup and provides a clean API
|
|
187
|
+
* for bridging API validation errors and managing form state.
|
|
188
|
+
*
|
|
189
|
+
* @example
|
|
190
|
+
* ```typescript
|
|
191
|
+
* const bridge = new FormBridge(myForm, {
|
|
192
|
+
* preset: classValidatorPreset(),
|
|
193
|
+
* });
|
|
194
|
+
*
|
|
195
|
+
* // Apply API errors
|
|
196
|
+
* bridge.applyApiErrors(err.error);
|
|
197
|
+
*
|
|
198
|
+
* // Use signals in templates
|
|
199
|
+
* readonly errors = bridge.errorsSignal;
|
|
200
|
+
* readonly firstError = bridge.firstErrorSignal;
|
|
201
|
+
* ```
|
|
202
|
+
*/
|
|
203
|
+
class FormBridge {
|
|
204
|
+
_form;
|
|
205
|
+
_presets;
|
|
206
|
+
_constraintMap;
|
|
207
|
+
_i18n;
|
|
208
|
+
_catchAll;
|
|
209
|
+
_mergeErrors;
|
|
210
|
+
_debug;
|
|
211
|
+
_interceptors = [];
|
|
212
|
+
/** Signal containing the last set of resolved API errors */
|
|
213
|
+
_apiErrors = signal([], ...(ngDevMode ? [{ debugName: "_apiErrors" }] : []));
|
|
214
|
+
/** Signal containing global (non-field) errors */
|
|
215
|
+
_globalErrors = signal([], ...(ngDevMode ? [{ debugName: "_globalErrors" }] : []));
|
|
216
|
+
/** Tracks which error keys were set by the API on each control */
|
|
217
|
+
_apiErrorKeys = new Map();
|
|
218
|
+
// ---- Public Signals ----
|
|
219
|
+
/** Reactive signal of all current API errors applied to the form */
|
|
220
|
+
errorsSignal = this._apiErrors.asReadonly();
|
|
221
|
+
/**
|
|
222
|
+
* Reactive signal of global (non-field) errors.
|
|
223
|
+
*
|
|
224
|
+
* Contains errors from:
|
|
225
|
+
* - Django `non_field_errors` / `detail`
|
|
226
|
+
* - Zod `formErrors`
|
|
227
|
+
* - Any API error whose field does not match a form control
|
|
228
|
+
*/
|
|
229
|
+
globalErrorsSignal = this._globalErrors.asReadonly();
|
|
230
|
+
/** Reactive signal of the first error (or null) */
|
|
231
|
+
firstErrorSignal = computed(() => {
|
|
232
|
+
const errors = this._apiErrors();
|
|
233
|
+
return errors.length > 0 ? errors[0] : null;
|
|
234
|
+
}, ...(ngDevMode ? [{ debugName: "firstErrorSignal" }] : []));
|
|
235
|
+
/** Whether any API errors are currently applied */
|
|
236
|
+
hasErrorsSignal = computed(() => this._apiErrors().length > 0 || this._globalErrors().length > 0, ...(ngDevMode ? [{ debugName: "hasErrorsSignal" }] : []));
|
|
237
|
+
constructor(form, config) {
|
|
238
|
+
this._form = form;
|
|
239
|
+
this._catchAll = config?.catchAll ?? false;
|
|
240
|
+
this._mergeErrors = config?.mergeErrors ?? false;
|
|
241
|
+
this._debug = config?.debug ?? false;
|
|
242
|
+
this._i18n = config?.i18n;
|
|
243
|
+
// Resolve presets
|
|
244
|
+
if (config?.preset) {
|
|
245
|
+
this._presets = Array.isArray(config.preset) ? config.preset : [config.preset];
|
|
246
|
+
}
|
|
247
|
+
else {
|
|
248
|
+
this._presets = [classValidatorPreset()];
|
|
249
|
+
}
|
|
250
|
+
// Build constraint map: preset defaults + custom overrides
|
|
251
|
+
const presetDefaults = this._resolvePresetConstraintMap(this._presets);
|
|
252
|
+
this._constraintMap = {
|
|
253
|
+
...presetDefaults,
|
|
254
|
+
...(config?.constraintMap ?? {}),
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
// ---- Public API - Error Management ----
|
|
258
|
+
/**
|
|
259
|
+
* Parse and apply API validation errors to the form.
|
|
260
|
+
*
|
|
261
|
+
* Tries each configured preset in order until one returns results.
|
|
262
|
+
* Errors are mapped to Angular form control errors.
|
|
263
|
+
*
|
|
264
|
+
* @param apiError - The raw error body from the API (e.g. `err.error`)
|
|
265
|
+
* @returns The array of resolved field errors that were applied
|
|
266
|
+
*/
|
|
267
|
+
applyApiErrors(apiError) {
|
|
268
|
+
let fieldErrors = [];
|
|
269
|
+
// Try each preset until one returns results
|
|
270
|
+
for (const preset of this._presets) {
|
|
271
|
+
fieldErrors = preset.parse(apiError);
|
|
272
|
+
if (fieldErrors.length > 0)
|
|
273
|
+
break;
|
|
274
|
+
}
|
|
275
|
+
if (this._debug && fieldErrors.length === 0) {
|
|
276
|
+
console.warn('[ngx-api-forms] No preset produced results for the given error payload.', 'Presets tried:', this._presets.map(p => p.name).join(', '), 'Payload:', apiError);
|
|
277
|
+
}
|
|
278
|
+
// Run interceptors
|
|
279
|
+
for (const interceptor of this._interceptors) {
|
|
280
|
+
fieldErrors = interceptor(fieldErrors, this._form);
|
|
281
|
+
}
|
|
282
|
+
// Map and apply to form
|
|
283
|
+
const resolved = this._applyErrors(fieldErrors);
|
|
284
|
+
this._apiErrors.set(resolved);
|
|
285
|
+
return resolved;
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* Clear only the errors that were set by `applyApiErrors()`.
|
|
289
|
+
* Client-side validation errors (e.g. `Validators.required`) are preserved.
|
|
290
|
+
*/
|
|
291
|
+
clearApiErrors() {
|
|
292
|
+
for (const [control, keys] of this._apiErrorKeys) {
|
|
293
|
+
if (!control.errors)
|
|
294
|
+
continue;
|
|
295
|
+
const remaining = { ...control.errors };
|
|
296
|
+
for (const key of keys) {
|
|
297
|
+
delete remaining[key];
|
|
298
|
+
}
|
|
299
|
+
control.setErrors(Object.keys(remaining).length > 0 ? remaining : null);
|
|
300
|
+
control.updateValueAndValidity();
|
|
301
|
+
}
|
|
302
|
+
this._apiErrorKeys.clear();
|
|
303
|
+
this._form.updateValueAndValidity({ onlySelf: false, emitEvent: true });
|
|
304
|
+
this._apiErrors.set([]);
|
|
305
|
+
this._globalErrors.set([]);
|
|
306
|
+
}
|
|
307
|
+
/**
|
|
308
|
+
* Get the first error across all form controls.
|
|
309
|
+
*/
|
|
310
|
+
getFirstError() {
|
|
311
|
+
// Check API errors first
|
|
312
|
+
const apiErrors = this._apiErrors();
|
|
313
|
+
if (apiErrors.length > 0) {
|
|
314
|
+
return apiErrors[0];
|
|
315
|
+
}
|
|
316
|
+
// Fall back to client-side validation errors
|
|
317
|
+
for (const key of Object.keys(this._form.controls)) {
|
|
318
|
+
const control = this._form.controls[key];
|
|
319
|
+
if (control?.errors) {
|
|
320
|
+
const errorKeys = Object.keys(control.errors);
|
|
321
|
+
if (errorKeys.length > 0) {
|
|
322
|
+
const errorKey = errorKeys[0];
|
|
323
|
+
const errorValue = control.errors[errorKey];
|
|
324
|
+
return {
|
|
325
|
+
field: key,
|
|
326
|
+
errorKey,
|
|
327
|
+
message: typeof errorValue === 'string' ? errorValue : errorKey,
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
return null;
|
|
333
|
+
}
|
|
334
|
+
/**
|
|
335
|
+
* Get all errors for a specific field.
|
|
336
|
+
*/
|
|
337
|
+
getFieldErrors(fieldName) {
|
|
338
|
+
return this._form.controls[fieldName]?.errors ?? null;
|
|
339
|
+
}
|
|
340
|
+
/**
|
|
341
|
+
* Register an error interceptor that can modify or filter errors
|
|
342
|
+
* before they are applied to the form.
|
|
343
|
+
* Returns a dispose function to remove the interceptor.
|
|
344
|
+
*/
|
|
345
|
+
addInterceptor(interceptor) {
|
|
346
|
+
this._interceptors.push(interceptor);
|
|
347
|
+
return () => {
|
|
348
|
+
this._interceptors = this._interceptors.filter(i => i !== interceptor);
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
// ---- Public API - Utilities ----
|
|
352
|
+
/**
|
|
353
|
+
* Access the underlying FormGroup.
|
|
354
|
+
*/
|
|
355
|
+
get form() {
|
|
356
|
+
return this._form;
|
|
357
|
+
}
|
|
358
|
+
// ---- Private Methods ----
|
|
359
|
+
_resolvePresetConstraintMap(presets) {
|
|
360
|
+
const merged = {};
|
|
361
|
+
for (const preset of presets) {
|
|
362
|
+
if (preset.constraintMap)
|
|
363
|
+
Object.assign(merged, preset.constraintMap);
|
|
364
|
+
}
|
|
365
|
+
// If no preset provided a constraint map, fall back to class-validator
|
|
366
|
+
if (Object.keys(merged).length === 0) {
|
|
367
|
+
Object.assign(merged, CLASS_VALIDATOR_CONSTRAINT_MAP);
|
|
368
|
+
}
|
|
369
|
+
return merged;
|
|
370
|
+
}
|
|
371
|
+
_applyErrors(fieldErrors) {
|
|
372
|
+
const resolved = [];
|
|
373
|
+
const globalErrors = [];
|
|
374
|
+
// Accumulate errors per control to avoid overwrite within a single applyApiErrors call
|
|
375
|
+
const pendingErrors = new Map();
|
|
376
|
+
for (const fieldError of fieldErrors) {
|
|
377
|
+
// Global errors (explicit sentinel or unmatched field)
|
|
378
|
+
if (fieldError.field === GLOBAL_ERROR_FIELD) {
|
|
379
|
+
globalErrors.push({
|
|
380
|
+
message: fieldError.message,
|
|
381
|
+
constraint: fieldError.constraint,
|
|
382
|
+
});
|
|
383
|
+
continue;
|
|
384
|
+
}
|
|
385
|
+
let control = this._form.controls[fieldError.field] ?? null;
|
|
386
|
+
if (!control) {
|
|
387
|
+
// Try nested path (e.g. 'address.city' or 'items.0.name')
|
|
388
|
+
control = this._resolveNestedControl(fieldError.field);
|
|
389
|
+
if (!control) {
|
|
390
|
+
// Route unmatched field errors to global errors
|
|
391
|
+
globalErrors.push({
|
|
392
|
+
message: fieldError.message,
|
|
393
|
+
constraint: fieldError.constraint,
|
|
394
|
+
originalField: fieldError.field,
|
|
395
|
+
});
|
|
396
|
+
if (this._debug) {
|
|
397
|
+
console.warn(`[ngx-api-forms] Field "${fieldError.field}" does not match any form control - routed to globalErrorsSignal.`, 'Available controls:', Object.keys(this._form.controls).join(', '));
|
|
398
|
+
}
|
|
399
|
+
continue;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
const errorKey = this._resolveErrorKey(fieldError.constraint);
|
|
403
|
+
const message = this._resolveMessage(fieldError);
|
|
404
|
+
if (!errorKey && !this._catchAll)
|
|
405
|
+
continue;
|
|
406
|
+
const finalErrorKey = errorKey || 'generic';
|
|
407
|
+
// Deduplicate: if the same key already exists on this control, suffix it
|
|
408
|
+
const existing = pendingErrors.get(control) ?? (this._mergeErrors ? (control.errors ?? {}) : {});
|
|
409
|
+
let uniqueKey = finalErrorKey;
|
|
410
|
+
if (existing[uniqueKey] !== undefined) {
|
|
411
|
+
let idx = 1;
|
|
412
|
+
while (existing[`${finalErrorKey}_${idx}`] !== undefined)
|
|
413
|
+
idx++;
|
|
414
|
+
uniqueKey = `${finalErrorKey}_${idx}`;
|
|
415
|
+
}
|
|
416
|
+
pendingErrors.set(control, { ...existing, [uniqueKey]: message });
|
|
417
|
+
// Track this key as API-set
|
|
418
|
+
if (!this._apiErrorKeys.has(control)) {
|
|
419
|
+
this._apiErrorKeys.set(control, new Set());
|
|
420
|
+
}
|
|
421
|
+
this._apiErrorKeys.get(control).add(uniqueKey);
|
|
422
|
+
resolved.push({ field: fieldError.field, errorKey: uniqueKey, message });
|
|
423
|
+
}
|
|
424
|
+
// Apply accumulated errors once per control
|
|
425
|
+
for (const [control, errors] of pendingErrors) {
|
|
426
|
+
control.markAsTouched();
|
|
427
|
+
control.setErrors(errors);
|
|
428
|
+
}
|
|
429
|
+
// Publish global errors
|
|
430
|
+
this._globalErrors.set(globalErrors);
|
|
431
|
+
return resolved;
|
|
432
|
+
}
|
|
433
|
+
_resolveNestedControl(path) {
|
|
434
|
+
const parts = path.split('.');
|
|
435
|
+
let current = this._form;
|
|
436
|
+
for (const part of parts) {
|
|
437
|
+
if (current instanceof FormGroup) {
|
|
438
|
+
const child = current.controls[part];
|
|
439
|
+
if (!child)
|
|
440
|
+
return null;
|
|
441
|
+
current = child;
|
|
442
|
+
}
|
|
443
|
+
else if (current instanceof FormArray) {
|
|
444
|
+
const index = parseInt(part, 10);
|
|
445
|
+
if (isNaN(index) || index < 0 || index >= current.length)
|
|
446
|
+
return null;
|
|
447
|
+
current = current.at(index);
|
|
448
|
+
}
|
|
449
|
+
else {
|
|
450
|
+
return null;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
return current;
|
|
454
|
+
}
|
|
455
|
+
_resolveErrorKey(constraint) {
|
|
456
|
+
// If constraint is in the map, use the mapped value.
|
|
457
|
+
// Otherwise, use the constraint as-is (pass-through).
|
|
458
|
+
// This returns '' only when constraint is empty.
|
|
459
|
+
if (this._constraintMap[constraint] !== undefined) {
|
|
460
|
+
return this._constraintMap[constraint];
|
|
461
|
+
}
|
|
462
|
+
return constraint;
|
|
463
|
+
}
|
|
464
|
+
_resolveMessage(error) {
|
|
465
|
+
if (this._i18n?.resolver) {
|
|
466
|
+
const resolved = this._i18n.resolver(error.field, error.constraint, error.message);
|
|
467
|
+
if (resolved !== null)
|
|
468
|
+
return resolved;
|
|
469
|
+
}
|
|
470
|
+
if (this._i18n?.prefix) {
|
|
471
|
+
// Return the i18n key (to be resolved by the consumer's i18n system)
|
|
472
|
+
return `${this._i18n.prefix}.${error.field}.${error.constraint}`;
|
|
473
|
+
}
|
|
474
|
+
return error.message;
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
function createFormBridge(form, config) {
|
|
478
|
+
return new FormBridge(form, config);
|
|
479
|
+
}
|
|
480
|
+
function provideFormBridge(form, config) {
|
|
481
|
+
return new FormBridge(form, config);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
* NgxFormError directive.
|
|
486
|
+
*
|
|
487
|
+
* Automatically displays the error message for a form control.
|
|
488
|
+
* Works with both client-side validators and API errors set by FormBridge.
|
|
489
|
+
*
|
|
490
|
+
* @example
|
|
491
|
+
* ```html
|
|
492
|
+
* <input formControlName="email" />
|
|
493
|
+
* <span ngxFormError="email" [form]="myForm"></span>
|
|
494
|
+
* <!-- Displays: "email must be a valid email" -->
|
|
495
|
+
*
|
|
496
|
+
* <!-- With custom error map -->
|
|
497
|
+
* <span ngxFormError="email"
|
|
498
|
+
* [form]="myForm"
|
|
499
|
+
* [errorMessages]="{ required: 'Email is required', email: 'Invalid email' }">
|
|
500
|
+
* </span>
|
|
501
|
+
* ```
|
|
502
|
+
*/
|
|
503
|
+
class NgxFormErrorDirective {
|
|
504
|
+
el = inject(ElementRef);
|
|
505
|
+
renderer = inject(Renderer2);
|
|
506
|
+
destroyRef = inject(DestroyRef);
|
|
507
|
+
resubscribe$ = new Subject();
|
|
508
|
+
/** The form control name to observe */
|
|
509
|
+
controlName;
|
|
510
|
+
/** The parent FormGroup */
|
|
511
|
+
form;
|
|
512
|
+
/**
|
|
513
|
+
* Optional map of error keys to display messages.
|
|
514
|
+
* If not provided, the raw error value is used (which is the API message
|
|
515
|
+
* when set by FormBridge).
|
|
516
|
+
*/
|
|
517
|
+
errorMessages;
|
|
518
|
+
/**
|
|
519
|
+
* CSS class applied to the host element when an error is displayed.
|
|
520
|
+
* Default: 'ngx-form-error-visible'
|
|
521
|
+
*/
|
|
522
|
+
errorClass = 'ngx-form-error-visible';
|
|
523
|
+
/**
|
|
524
|
+
* Whether to show errors only when the control is touched.
|
|
525
|
+
* Default: true
|
|
526
|
+
*/
|
|
527
|
+
showOnTouched = true;
|
|
528
|
+
ngOnInit() {
|
|
529
|
+
this._subscribe();
|
|
530
|
+
}
|
|
531
|
+
ngOnChanges(changes) {
|
|
532
|
+
if ((changes['form'] || changes['controlName']) && !changes['form']?.firstChange) {
|
|
533
|
+
this._subscribe();
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
_subscribe() {
|
|
537
|
+
// Signal existing subscription to complete
|
|
538
|
+
this.resubscribe$.next();
|
|
539
|
+
const control = this.form?.get(this.controlName);
|
|
540
|
+
if (!control) {
|
|
541
|
+
this._hide();
|
|
542
|
+
return;
|
|
543
|
+
}
|
|
544
|
+
merge(control.statusChanges, control.valueChanges).pipe(takeUntil(this.resubscribe$), takeUntilDestroyed(this.destroyRef)).subscribe(() => {
|
|
545
|
+
this._updateDisplay(control);
|
|
546
|
+
});
|
|
547
|
+
this._updateDisplay(control);
|
|
548
|
+
}
|
|
549
|
+
_updateDisplay(control) {
|
|
550
|
+
if (!control.errors || (this.showOnTouched && !control.touched && !control.dirty)) {
|
|
551
|
+
this._hide();
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
554
|
+
const errorKeys = Object.keys(control.errors);
|
|
555
|
+
if (errorKeys.length === 0) {
|
|
556
|
+
this._hide();
|
|
557
|
+
return;
|
|
558
|
+
}
|
|
559
|
+
const firstKey = errorKeys[0];
|
|
560
|
+
const rawValue = control.errors[firstKey];
|
|
561
|
+
let message;
|
|
562
|
+
// Priority: custom errorMessages map > raw string value > error key
|
|
563
|
+
if (this.errorMessages?.[firstKey]) {
|
|
564
|
+
message = this.errorMessages[firstKey];
|
|
565
|
+
}
|
|
566
|
+
else if (typeof rawValue === 'string') {
|
|
567
|
+
message = rawValue;
|
|
568
|
+
}
|
|
569
|
+
else {
|
|
570
|
+
message = firstKey;
|
|
571
|
+
}
|
|
572
|
+
this._show(message);
|
|
573
|
+
}
|
|
574
|
+
_show(message) {
|
|
575
|
+
const el = this.el.nativeElement;
|
|
576
|
+
this.renderer.setProperty(el, 'textContent', message);
|
|
577
|
+
this.renderer.addClass(el, this.errorClass);
|
|
578
|
+
this.renderer.setStyle(el, 'display', '');
|
|
579
|
+
}
|
|
580
|
+
_hide() {
|
|
581
|
+
const el = this.el.nativeElement;
|
|
582
|
+
this.renderer.setProperty(el, 'textContent', '');
|
|
583
|
+
this.renderer.removeClass(el, this.errorClass);
|
|
584
|
+
this.renderer.setStyle(el, 'display', 'none');
|
|
585
|
+
}
|
|
586
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: NgxFormErrorDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
587
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "20.3.16", type: NgxFormErrorDirective, isStandalone: true, selector: "[ngxFormError]", inputs: { controlName: ["ngxFormError", "controlName"], form: "form", errorMessages: "errorMessages", errorClass: "errorClass", showOnTouched: "showOnTouched" }, usesOnChanges: true, ngImport: i0 });
|
|
588
|
+
}
|
|
589
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: NgxFormErrorDirective, decorators: [{
|
|
590
|
+
type: Directive,
|
|
591
|
+
args: [{
|
|
592
|
+
selector: '[ngxFormError]',
|
|
593
|
+
standalone: true,
|
|
594
|
+
}]
|
|
595
|
+
}], propDecorators: { controlName: [{
|
|
596
|
+
type: Input,
|
|
597
|
+
args: ['ngxFormError']
|
|
598
|
+
}], form: [{
|
|
599
|
+
type: Input
|
|
600
|
+
}], errorMessages: [{
|
|
601
|
+
type: Input
|
|
602
|
+
}], errorClass: [{
|
|
603
|
+
type: Input
|
|
604
|
+
}], showOnTouched: [{
|
|
605
|
+
type: Input
|
|
606
|
+
}] } });
|
|
607
|
+
|
|
608
|
+
/**
|
|
609
|
+
* Parse raw API error body into normalized field errors without touching any form.
|
|
610
|
+
* Tries each preset in order until one returns results.
|
|
611
|
+
*
|
|
612
|
+
* Use this when you only need the parsing logic (e.g. in an HttpInterceptor,
|
|
613
|
+
* a store effect, or any context where you don't have a FormBridge).
|
|
614
|
+
*
|
|
615
|
+
* @param apiError - The raw error body from the API (e.g. `err.error`)
|
|
616
|
+
* @param preset - One or more presets to try. Defaults to class-validator.
|
|
617
|
+
* @param options - Optional settings. `debug: true` logs a warning when no preset matches.
|
|
618
|
+
* @returns Normalized array of field errors
|
|
619
|
+
*
|
|
620
|
+
* @example
|
|
621
|
+
* ```typescript
|
|
622
|
+
* import { parseApiErrors } from 'ngx-api-forms';
|
|
623
|
+
* import { laravelPreset } from 'ngx-api-forms/laravel';
|
|
624
|
+
*
|
|
625
|
+
* const errors = parseApiErrors(err.error, laravelPreset());
|
|
626
|
+
* // [{ field: 'email', constraint: 'required', message: 'The email field is required.' }]
|
|
627
|
+
* ```
|
|
628
|
+
*/
|
|
629
|
+
function parseApiErrors(apiError, preset, options) {
|
|
630
|
+
const presets = preset
|
|
631
|
+
? (Array.isArray(preset) ? preset : [preset])
|
|
632
|
+
: [classValidatorPreset()];
|
|
633
|
+
for (const p of presets) {
|
|
634
|
+
const result = p.parse(apiError);
|
|
635
|
+
if (result.length > 0)
|
|
636
|
+
return result;
|
|
637
|
+
}
|
|
638
|
+
if (options?.debug) {
|
|
639
|
+
console.warn('[ngx-api-forms] parseApiErrors: no preset produced results.', 'Presets tried:', presets.map(p => p.name).join(', '), 'Payload:', apiError);
|
|
640
|
+
}
|
|
641
|
+
return [];
|
|
642
|
+
}
|
|
643
|
+
/**
|
|
644
|
+
* Wrap an Observable with form submit lifecycle management.
|
|
645
|
+
*
|
|
646
|
+
* Standalone submit helper. Useful when you want disable/enable lifecycle
|
|
647
|
+
* without a full FormBridge instance.
|
|
648
|
+
*
|
|
649
|
+
* - Disables the form before subscribing
|
|
650
|
+
* - Re-enables the form on success or error
|
|
651
|
+
* - Returns the original Observable (errors are re-thrown)
|
|
652
|
+
*
|
|
653
|
+
* @param form - The FormGroup to manage
|
|
654
|
+
* @param source - The Observable to wrap (e.g. HttpClient call)
|
|
655
|
+
* @param options.onError - Callback invoked with the error before re-throwing
|
|
656
|
+
* @returns The wrapped Observable
|
|
657
|
+
*
|
|
658
|
+
* @example
|
|
659
|
+
* ```typescript
|
|
660
|
+
* import { wrapSubmit } from 'ngx-api-forms';
|
|
661
|
+
*
|
|
662
|
+
* wrapSubmit(this.form, this.http.post('/api', data), {
|
|
663
|
+
* onError: (err) => this.bridge.applyApiErrors(err.error),
|
|
664
|
+
* }).subscribe();
|
|
665
|
+
* ```
|
|
666
|
+
*/
|
|
667
|
+
function wrapSubmit(form, source, options) {
|
|
668
|
+
form.disable();
|
|
669
|
+
return source.pipe(tap(() => form.enable()), catchError((err) => {
|
|
670
|
+
form.enable();
|
|
671
|
+
options?.onError?.(err);
|
|
672
|
+
throw err;
|
|
673
|
+
}));
|
|
674
|
+
}
|
|
675
|
+
/**
|
|
676
|
+
* Convert a plain object to FormData.
|
|
677
|
+
* Handles Files, Blobs, Arrays, Dates, nested objects.
|
|
678
|
+
*/
|
|
679
|
+
function toFormData(data) {
|
|
680
|
+
const formData = new FormData();
|
|
681
|
+
for (const [key, value] of Object.entries(data)) {
|
|
682
|
+
if (value === null || value === undefined)
|
|
683
|
+
continue;
|
|
684
|
+
if (value instanceof File || value instanceof Blob) {
|
|
685
|
+
formData.append(key, value);
|
|
686
|
+
continue;
|
|
687
|
+
}
|
|
688
|
+
if (Array.isArray(value)) {
|
|
689
|
+
for (const item of value) {
|
|
690
|
+
if (item instanceof File || item instanceof Blob) {
|
|
691
|
+
formData.append(key, item);
|
|
692
|
+
}
|
|
693
|
+
else if (typeof item === 'object' && item !== null) {
|
|
694
|
+
formData.append(key, JSON.stringify(item));
|
|
695
|
+
}
|
|
696
|
+
else {
|
|
697
|
+
formData.append(key, String(item));
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
continue;
|
|
701
|
+
}
|
|
702
|
+
if (value instanceof Date) {
|
|
703
|
+
formData.append(key, value.toISOString());
|
|
704
|
+
continue;
|
|
705
|
+
}
|
|
706
|
+
if (typeof value === 'object') {
|
|
707
|
+
formData.append(key, JSON.stringify(value));
|
|
708
|
+
continue;
|
|
709
|
+
}
|
|
710
|
+
formData.append(key, String(value));
|
|
711
|
+
}
|
|
712
|
+
return formData;
|
|
713
|
+
}
|
|
714
|
+
/**
|
|
715
|
+
* Enable all controls in a form, with optional exceptions.
|
|
716
|
+
*/
|
|
717
|
+
function enableForm(form, options) {
|
|
718
|
+
for (const key of Object.keys(form.controls)) {
|
|
719
|
+
if (options?.except?.includes(key))
|
|
720
|
+
continue;
|
|
721
|
+
form.controls[key].enable();
|
|
722
|
+
}
|
|
723
|
+
form.updateValueAndValidity({ onlySelf: false, emitEvent: true });
|
|
724
|
+
}
|
|
725
|
+
/**
|
|
726
|
+
* Disable all controls in a form, with optional exceptions.
|
|
727
|
+
*/
|
|
728
|
+
function disableForm(form, options) {
|
|
729
|
+
for (const key of Object.keys(form.controls)) {
|
|
730
|
+
if (options?.except?.includes(key))
|
|
731
|
+
continue;
|
|
732
|
+
form.controls[key].disable();
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
/**
|
|
736
|
+
* Reset all errors on a form's controls (client-side and API).
|
|
737
|
+
*/
|
|
738
|
+
function clearFormErrors(form) {
|
|
739
|
+
for (const key of Object.keys(form.controls)) {
|
|
740
|
+
form.controls[key].setErrors(null);
|
|
741
|
+
form.controls[key].updateValueAndValidity();
|
|
742
|
+
}
|
|
743
|
+
form.updateValueAndValidity({ onlySelf: false, emitEvent: true });
|
|
744
|
+
}
|
|
745
|
+
/**
|
|
746
|
+
* Get a flat record of only the dirty (changed) fields.
|
|
747
|
+
*/
|
|
748
|
+
function getDirtyValues(form) {
|
|
749
|
+
const result = {};
|
|
750
|
+
for (const key of Object.keys(form.controls)) {
|
|
751
|
+
if (form.controls[key].dirty) {
|
|
752
|
+
result[key] = form.controls[key].value;
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
return result;
|
|
756
|
+
}
|
|
757
|
+
/**
|
|
758
|
+
* Check if any control in the form has a specific error key.
|
|
759
|
+
*/
|
|
760
|
+
function hasError(form, errorKey) {
|
|
761
|
+
for (const key of Object.keys(form.controls)) {
|
|
762
|
+
if (form.controls[key].hasError(errorKey)) {
|
|
763
|
+
return true;
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
return false;
|
|
767
|
+
}
|
|
768
|
+
/**
|
|
769
|
+
* Get the error message for a specific field and error key.
|
|
770
|
+
* Returns the error value if it's a string, otherwise the error key.
|
|
771
|
+
*/
|
|
772
|
+
function getErrorMessage(form, fieldName, errorKey) {
|
|
773
|
+
const control = form.controls[fieldName];
|
|
774
|
+
if (!control?.errors)
|
|
775
|
+
return null;
|
|
776
|
+
if (errorKey) {
|
|
777
|
+
const value = control.errors[errorKey];
|
|
778
|
+
return value ? (typeof value === 'string' ? value : errorKey) : null;
|
|
779
|
+
}
|
|
780
|
+
// Return first error
|
|
781
|
+
const keys = Object.keys(control.errors);
|
|
782
|
+
if (keys.length === 0)
|
|
783
|
+
return null;
|
|
784
|
+
const firstValue = control.errors[keys[0]];
|
|
785
|
+
return typeof firstValue === 'string' ? firstValue : keys[0];
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
/**
|
|
789
|
+
* Angular HttpInterceptorFn that automatically applies API validation errors
|
|
790
|
+
* to a FormBridge instance attached via HttpContext.
|
|
791
|
+
*
|
|
792
|
+
* Two usage patterns:
|
|
793
|
+
* 1. Per-request: tag individual requests with `withFormBridge(bridge)` --
|
|
794
|
+
* the interceptor catches 422/400 and calls `bridge.applyApiErrors()`.
|
|
795
|
+
* 2. Global: provide a custom `onError` callback in the interceptor config
|
|
796
|
+
* to centralize error handling (e.g. dispatch to a store or service).
|
|
797
|
+
*/
|
|
798
|
+
/**
|
|
799
|
+
* HttpContext token that carries a FormBridge reference on a request.
|
|
800
|
+
* Used by `withFormBridge()` to tag requests for automatic error handling.
|
|
801
|
+
*/
|
|
802
|
+
const FORM_BRIDGE = new HttpContextToken(() => null);
|
|
803
|
+
/**
|
|
804
|
+
* Create an Angular functional HTTP interceptor that handles API validation errors.
|
|
805
|
+
*
|
|
806
|
+
* When a response matches one of the configured status codes (default: 422, 400):
|
|
807
|
+
* - If the request was tagged with `withFormBridge(bridge)`, errors are automatically
|
|
808
|
+
* applied to that bridge. Zero code needed in `subscribe()`.
|
|
809
|
+
* - If an `onError` callback is provided, it is called with the parsed errors for
|
|
810
|
+
* global handling (store, toast, logging, etc.).
|
|
811
|
+
* - The error is always re-thrown so downstream `catchError` or `error` callbacks
|
|
812
|
+
* still fire when needed.
|
|
813
|
+
*
|
|
814
|
+
* @example
|
|
815
|
+
* ```typescript
|
|
816
|
+
* // app.config.ts
|
|
817
|
+
* import { provideHttpClient, withInterceptors } from '@angular/common/http';
|
|
818
|
+
* import { apiErrorInterceptor } from 'ngx-api-forms';
|
|
819
|
+
*
|
|
820
|
+
* export const appConfig = {
|
|
821
|
+
* providers: [
|
|
822
|
+
* provideHttpClient(
|
|
823
|
+
* withInterceptors([apiErrorInterceptor()])
|
|
824
|
+
* ),
|
|
825
|
+
* ],
|
|
826
|
+
* };
|
|
827
|
+
*
|
|
828
|
+
* // component.ts -- zero error handling code
|
|
829
|
+
* this.http.post('/api/save', data, withFormBridge(this.bridge)).subscribe({
|
|
830
|
+
* next: () => this.router.navigate(['/done']),
|
|
831
|
+
* });
|
|
832
|
+
* ```
|
|
833
|
+
*/
|
|
834
|
+
function apiErrorInterceptor(config) {
|
|
835
|
+
const statusCodes = config?.statusCodes ?? [422, 400];
|
|
836
|
+
const fallbackPreset = config?.preset ?? classValidatorPreset();
|
|
837
|
+
return (req, next) => {
|
|
838
|
+
return next(req).pipe(catchError((error) => {
|
|
839
|
+
if (statusCodes.includes(error.status)) {
|
|
840
|
+
const bridge = req.context.get(FORM_BRIDGE);
|
|
841
|
+
if (bridge) {
|
|
842
|
+
bridge.applyApiErrors(error.error);
|
|
843
|
+
}
|
|
844
|
+
if (config?.onError) {
|
|
845
|
+
const parsed = bridge
|
|
846
|
+
? bridge.errorsSignal()
|
|
847
|
+
: parseApiErrors(error.error, fallbackPreset);
|
|
848
|
+
config.onError(parsed, error);
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
return throwError(() => error);
|
|
852
|
+
}));
|
|
853
|
+
};
|
|
854
|
+
}
|
|
855
|
+
/**
|
|
856
|
+
* Attach a FormBridge to an HTTP request via HttpContext.
|
|
857
|
+
*
|
|
858
|
+
* When used with `apiErrorInterceptor`, the interceptor automatically calls
|
|
859
|
+
* `bridge.applyApiErrors()` on 422/400 responses. No error handling needed
|
|
860
|
+
* in `subscribe()`.
|
|
861
|
+
*
|
|
862
|
+
* @param bridge - The FormBridge instance to receive API errors
|
|
863
|
+
* @returns An options object to spread into HttpClient methods
|
|
864
|
+
*
|
|
865
|
+
* @example
|
|
866
|
+
* ```typescript
|
|
867
|
+
* // Errors are applied automatically -- no subscribe error handler needed
|
|
868
|
+
* this.http.post('/api/save', data, withFormBridge(this.bridge)).subscribe();
|
|
869
|
+
*
|
|
870
|
+
* // Works with all HttpClient methods
|
|
871
|
+
* this.http.put('/api/users/1', data, withFormBridge(this.bridge)).subscribe();
|
|
872
|
+
* ```
|
|
873
|
+
*/
|
|
874
|
+
function withFormBridge(bridge) {
|
|
875
|
+
return {
|
|
876
|
+
context: new HttpContext().set(FORM_BRIDGE, bridge),
|
|
877
|
+
};
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
/*
|
|
881
|
+
* Public API Surface of ngx-api-forms
|
|
882
|
+
*/
|
|
883
|
+
// Models and types
|
|
884
|
+
|
|
885
|
+
/**
|
|
886
|
+
* Generated bundle index. Do not edit.
|
|
887
|
+
*/
|
|
888
|
+
|
|
889
|
+
export { CLASS_VALIDATOR_CONSTRAINT_MAP, FORM_BRIDGE, FormBridge, GLOBAL_ERROR_FIELD, NgxFormErrorDirective, apiErrorInterceptor, classValidatorPreset, clearFormErrors, createFormBridge, disableForm, enableForm, getDirtyValues, getErrorMessage, hasError, parseApiErrors, provideFormBridge, toFormData, withFormBridge, wrapSubmit };
|
|
890
|
+
//# sourceMappingURL=ngx-api-forms.mjs.map
|