ng-entity-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/README.md +497 -0
- package/fesm2022/ng-entity-forms.mjs +157 -0
- package/fesm2022/ng-entity-forms.mjs.map +1 -0
- package/package.json +49 -0
- package/types/ng-entity-forms.d.ts +63 -0
package/README.md
ADDED
|
@@ -0,0 +1,497 @@
|
|
|
1
|
+
# ng-entity-forms
|
|
2
|
+
|
|
3
|
+
Strongly-typed reactive forms for Angular. Define your entity interface — the library maps it to a fully-typed `FormGroup` with autocompletion, validation, and error messages out of the box.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Requirements
|
|
8
|
+
|
|
9
|
+
- Angular 17+
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm install ng-entity-forms
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Quick Start
|
|
22
|
+
|
|
23
|
+
```typescript
|
|
24
|
+
import { entity, entityForm } from 'ng-entity-forms';
|
|
25
|
+
|
|
26
|
+
export interface ProductForm {
|
|
27
|
+
name: string;
|
|
28
|
+
description: string | null;
|
|
29
|
+
price: number;
|
|
30
|
+
active: boolean;
|
|
31
|
+
thumbnail: File | null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
protected form = entityForm<ProductForm>({
|
|
35
|
+
name: entity.required(''),
|
|
36
|
+
description: entity.optional<string>(null),
|
|
37
|
+
price: entity.required(0),
|
|
38
|
+
active: entity.required(false),
|
|
39
|
+
thumbnail: entity.file(),
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
// TypeScript knows the exact type of every control
|
|
43
|
+
// form.controls.name → FormControl<string>
|
|
44
|
+
// form.controls.description → FormControl<string | null>
|
|
45
|
+
// form.controls.thumbnail → FormControl<File | null>
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
## API
|
|
51
|
+
|
|
52
|
+
### `entity.required(initialValue, config?)`
|
|
53
|
+
|
|
54
|
+
Creates a non-nullable `FormControl` with `Validators.required` applied automatically.
|
|
55
|
+
|
|
56
|
+
```typescript
|
|
57
|
+
entity.required('');
|
|
58
|
+
entity.required(0); // 0 is a valid initial value
|
|
59
|
+
entity.required(false); // false is a valid initial value
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
The second argument is flexible — pick whatever fits:
|
|
63
|
+
|
|
64
|
+
```typescript
|
|
65
|
+
entity.required('', Validators.minLength(3)) // single validator
|
|
66
|
+
entity.required('', [Validators.minLength(3), myValidator]) // array
|
|
67
|
+
entity.required('', { validators: [...], disabled: true, updateOn: 'blur' }) // full options
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
### `entity.optional<T>(initialValue, config?)`
|
|
73
|
+
|
|
74
|
+
Creates a nullable `FormControl<T | null>`.
|
|
75
|
+
|
|
76
|
+
```typescript
|
|
77
|
+
entity.optional<string>(null)
|
|
78
|
+
entity.optional<number>(null, Validators.max(100))
|
|
79
|
+
entity.optional<string>(null, [Validators.maxLength(500), myValidator])
|
|
80
|
+
entity.optional<string>(null, { validators: [...], disabled: true })
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
### `entity.file(config?)`
|
|
86
|
+
|
|
87
|
+
Creates a `FormControl<File | null>`. The value is the native `File` object — no wrappers, no library types leaking into your entity.
|
|
88
|
+
|
|
89
|
+
```typescript
|
|
90
|
+
entity.file();
|
|
91
|
+
entity.file({ validators: [mimeTypeValidator, maxFileSizeValidator] });
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
### `entityForm<T>(controls, options?)`
|
|
97
|
+
|
|
98
|
+
Creates a fully-typed `FormGroup` from your entity. Supports single or multiple cross-field validators at the form level.
|
|
99
|
+
|
|
100
|
+
```typescript
|
|
101
|
+
// Basic
|
|
102
|
+
const form = entityForm<ProductForm>({ ... });
|
|
103
|
+
|
|
104
|
+
// With cross-field validator
|
|
105
|
+
const form = entityForm<ProductForm>(
|
|
106
|
+
{ ... },
|
|
107
|
+
{ validators: passwordMatchValidator },
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
// Multiple cross-field validators
|
|
111
|
+
const form = entityForm<ProductForm>(
|
|
112
|
+
{ ... },
|
|
113
|
+
{ validators: [passwordMatchValidator, priceRangeValidator] },
|
|
114
|
+
);
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
---
|
|
118
|
+
|
|
119
|
+
## File Handling
|
|
120
|
+
|
|
121
|
+
Declare the field as `File | null` in your entity — no library types needed.
|
|
122
|
+
|
|
123
|
+
```typescript
|
|
124
|
+
export interface ProductForm {
|
|
125
|
+
thumbnail: File | null;
|
|
126
|
+
}
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Use `patchFileControl` and `clearFileControl` to connect the native input:
|
|
130
|
+
|
|
131
|
+
```typescript
|
|
132
|
+
import { patchFileControl, clearFileControl } from 'ng-entity-forms';
|
|
133
|
+
|
|
134
|
+
@Component({
|
|
135
|
+
template: `
|
|
136
|
+
<input #fileInput type="file" (change)="onFileChange($event)" />
|
|
137
|
+
<button type="button" (click)="removeFile()">Remove</button>
|
|
138
|
+
@if (form.controls.thumbnail.value; as file) {
|
|
139
|
+
<span>{{ file.name }} — {{ (file.size / 1024).toFixed(1) }}KB</span>
|
|
140
|
+
}
|
|
141
|
+
`,
|
|
142
|
+
})
|
|
143
|
+
export class MyComponent {
|
|
144
|
+
@ViewChild('fileInput') fileInput!: ElementRef<HTMLInputElement>;
|
|
145
|
+
|
|
146
|
+
protected form = entityForm<ProductForm>({
|
|
147
|
+
thumbnail: entity.file({ validators: [mimeTypeValidator] }),
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
onFileChange(event: Event): void {
|
|
151
|
+
patchFileControl(event, this.form.controls.thumbnail);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
removeFile(): void {
|
|
155
|
+
// Resets the native input so the browser forgets the previous selection
|
|
156
|
+
clearFileControl(this.form.controls.thumbnail, this.fileInput.nativeElement);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
onSubmit(): void {
|
|
160
|
+
const { thumbnail } = this.form.getRawValue();
|
|
161
|
+
const formData = new FormData();
|
|
162
|
+
if (thumbnail) formData.append('thumbnail', thumbnail); // native File, ready to upload
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
---
|
|
168
|
+
|
|
169
|
+
## Global Validators
|
|
170
|
+
|
|
171
|
+
Register validators once in `app.config.ts`. They are applied automatically to every control — no need to repeat them per field.
|
|
172
|
+
|
|
173
|
+
```typescript
|
|
174
|
+
// app.config.ts
|
|
175
|
+
import { provideDefaultValidators } from 'ng-entity-forms';
|
|
176
|
+
|
|
177
|
+
provideDefaultValidators({
|
|
178
|
+
all: [Validators.maxLength(255)], // every control
|
|
179
|
+
required: [trimValidator], // required controls only
|
|
180
|
+
optional: [], // optional controls only
|
|
181
|
+
});
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
Per-control validators are always **additive** — they stack on top of the global ones.
|
|
185
|
+
|
|
186
|
+
---
|
|
187
|
+
|
|
188
|
+
## Error Messages
|
|
189
|
+
|
|
190
|
+
Built-in Angular validators (`required`, `minlength`, `maxlength`, `min`, `max`, `email`, `pattern`) are resolved automatically. **English is the default locale.**
|
|
191
|
+
|
|
192
|
+
### `provideErrorMessages` — custom messages
|
|
193
|
+
|
|
194
|
+
Merges your custom messages on top of the English built-ins. Only define what you need.
|
|
195
|
+
|
|
196
|
+
```typescript
|
|
197
|
+
// app.config.ts
|
|
198
|
+
import { provideErrorMessages } from 'ng-entity-forms';
|
|
199
|
+
|
|
200
|
+
provideErrorMessages({
|
|
201
|
+
// Custom validator messages
|
|
202
|
+
whitespace: 'Cannot contain only whitespace',
|
|
203
|
+
passwordMismatch: 'Passwords do not match',
|
|
204
|
+
slugTaken: (err) => `The slug "${err.value}" is already taken`,
|
|
205
|
+
mimeType: (err) => `Invalid format. Allowed: ${err.allowed.join(', ')}`,
|
|
206
|
+
maxFileSize: (err) => `File too large. Max size: ${err.maxMb}MB`,
|
|
207
|
+
|
|
208
|
+
// Override a built-in if needed
|
|
209
|
+
required: 'This field cannot be empty',
|
|
210
|
+
});
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
Message values can be a plain `string` or a function that receives the Angular error object:
|
|
214
|
+
|
|
215
|
+
```typescript
|
|
216
|
+
// Plain string
|
|
217
|
+
whitespace: 'Cannot contain only whitespace';
|
|
218
|
+
|
|
219
|
+
// Function with error data
|
|
220
|
+
minPrice: (err) => `Min price is ${err.min}`;
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
### `provideErrorMessagesLocale` — switch locale
|
|
224
|
+
|
|
225
|
+
Switches all built-in messages to a supported locale (`'en'` | `'es'`). Optionally extend with your custom messages on top.
|
|
226
|
+
|
|
227
|
+
```typescript
|
|
228
|
+
import { provideErrorMessagesLocale } from 'ng-entity-forms';
|
|
229
|
+
|
|
230
|
+
// Spanish built-ins only
|
|
231
|
+
provideErrorMessagesLocale('es');
|
|
232
|
+
|
|
233
|
+
// Spanish + custom messages
|
|
234
|
+
provideErrorMessagesLocale('es', {
|
|
235
|
+
whitespace: 'No puede contener solo espacios',
|
|
236
|
+
passwordMismatch: 'Las contraseñas no coinciden',
|
|
237
|
+
slugTaken: (err) => `El slug "${err.value}" ya está en uso`,
|
|
238
|
+
|
|
239
|
+
// Override a Spanish built-in
|
|
240
|
+
required: 'Campo requerido',
|
|
241
|
+
});
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
> Use either `provideErrorMessages` or `provideErrorMessagesLocale` — not both. If you need a locale other than English with custom messages, always use `provideErrorMessagesLocale`.
|
|
245
|
+
|
|
246
|
+
### `fieldErrors` pipe
|
|
247
|
+
|
|
248
|
+
Returns `FieldError[]` — only when the control is `touched` or `dirty`. Each item has a stable `key` and a resolved `message`.
|
|
249
|
+
|
|
250
|
+
```typescript
|
|
251
|
+
imports: [FieldErrorsPipe];
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
```html
|
|
255
|
+
<!-- Global messages -->
|
|
256
|
+
@for (error of form.controls.name | fieldErrors; track error.key) {
|
|
257
|
+
<small class="error">{{ error.message }}</small>
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
<!-- Local override — takes priority over global messages for this field only -->
|
|
261
|
+
@for ( error of form.controls.name | fieldErrors: { required: 'Product name is required' }; track
|
|
262
|
+
error.key ) {
|
|
263
|
+
<small class="error">{{ error.message }}</small>
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
<!-- Cross-field errors on the FormGroup -->
|
|
267
|
+
@for (error of form | fieldErrors; track error.key) {
|
|
268
|
+
<p class="error">{{ error.message }}</p>
|
|
269
|
+
}
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
Always use `track error.key` — the error key is stable and avoids Angular's `NG0956` warning.
|
|
273
|
+
|
|
274
|
+
---
|
|
275
|
+
|
|
276
|
+
## Async Validators
|
|
277
|
+
|
|
278
|
+
Pass them through the options object along with `updateOn: 'blur'` to avoid hammering the server on every keystroke.
|
|
279
|
+
|
|
280
|
+
```typescript
|
|
281
|
+
entity.required('', {
|
|
282
|
+
validators: [Validators.minLength(3), Validators.pattern(/^[a-z0-9-]+$/)],
|
|
283
|
+
asyncValidators: [slugAvailableValidator],
|
|
284
|
+
updateOn: 'blur',
|
|
285
|
+
});
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
Always check `form.pending` before submitting — async validators may still be running:
|
|
289
|
+
|
|
290
|
+
```typescript
|
|
291
|
+
onSubmit(): void {
|
|
292
|
+
this.form.markAllAsTouched();
|
|
293
|
+
if (this.form.pending) return; // async validators still running
|
|
294
|
+
if (this.form.invalid) return;
|
|
295
|
+
|
|
296
|
+
const value = this.form.getRawValue();
|
|
297
|
+
}
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
---
|
|
301
|
+
|
|
302
|
+
## Dynamic Fields
|
|
303
|
+
|
|
304
|
+
Start a field as disabled and toggle it based on business logic:
|
|
305
|
+
|
|
306
|
+
```typescript
|
|
307
|
+
protected form = entityForm<MyEntity>({
|
|
308
|
+
featured: entity.optional<boolean>(null),
|
|
309
|
+
discountCode: entity.optional<string>(null, { disabled: true }), // starts disabled
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
onFeaturedChange(): void {
|
|
313
|
+
if (this.form.controls.featured.value) {
|
|
314
|
+
this.form.controls.discountCode.enable();
|
|
315
|
+
} else {
|
|
316
|
+
this.form.controls.discountCode.disable();
|
|
317
|
+
this.form.controls.discountCode.setValue(null);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
`getRawValue()` includes disabled fields. `value` does not.
|
|
323
|
+
|
|
324
|
+
---
|
|
325
|
+
|
|
326
|
+
## Considerations
|
|
327
|
+
|
|
328
|
+
### Optional fields require `string | null` — not `name?`
|
|
329
|
+
|
|
330
|
+
Angular's `FormControl` does not support `undefined`. Use explicit null unions instead of optional properties.
|
|
331
|
+
|
|
332
|
+
```typescript
|
|
333
|
+
// will not work
|
|
334
|
+
export interface ProductForm {
|
|
335
|
+
description?: string;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// correct
|
|
339
|
+
export interface ProductForm {
|
|
340
|
+
description: string | null;
|
|
341
|
+
}
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
If your domain entity uses `?`, create a dedicated form interface:
|
|
345
|
+
|
|
346
|
+
```typescript
|
|
347
|
+
// Domain entity — keep as is
|
|
348
|
+
export interface Product {
|
|
349
|
+
name: string;
|
|
350
|
+
description?: string;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// Form interface — explicit nulls
|
|
354
|
+
export interface ProductForm {
|
|
355
|
+
name: string;
|
|
356
|
+
description: string | null;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
protected form = entityForm<ProductForm>({ ... });
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
### `0` and `false` are valid initial values
|
|
363
|
+
|
|
364
|
+
```typescript
|
|
365
|
+
entity.required(0); // stock starting at zero
|
|
366
|
+
entity.required(false); // checkbox starting unchecked
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
### Always use `getRawValue()` on submit
|
|
370
|
+
|
|
371
|
+
`form.value` omits disabled fields. `getRawValue()` includes them.
|
|
372
|
+
|
|
373
|
+
```typescript
|
|
374
|
+
const value = this.form.getRawValue(); // includes disabled fields
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
### The `fieldErrors` pipe is `pure: false`
|
|
378
|
+
|
|
379
|
+
It re-evaluates on every change detection cycle to react to control state changes (`touched`, `dirty`, `errors`). This is intentional — a pure pipe would miss mutations on the same `FormControl` reference. For large forms, pair it with `OnPush` change detection on your component.
|
|
380
|
+
|
|
381
|
+
---
|
|
382
|
+
|
|
383
|
+
## Full Example
|
|
384
|
+
|
|
385
|
+
```typescript
|
|
386
|
+
import { Component, ElementRef, ViewChild } from '@angular/core';
|
|
387
|
+
import { ReactiveFormsModule, Validators, AbstractControl, ValidationErrors } from '@angular/forms';
|
|
388
|
+
import {
|
|
389
|
+
entity,
|
|
390
|
+
entityForm,
|
|
391
|
+
patchFileControl,
|
|
392
|
+
clearFileControl,
|
|
393
|
+
FieldErrorsPipe,
|
|
394
|
+
} from 'ng-entity-forms';
|
|
395
|
+
|
|
396
|
+
export interface ProductForm {
|
|
397
|
+
name: string;
|
|
398
|
+
description: string | null;
|
|
399
|
+
price: number;
|
|
400
|
+
active: boolean;
|
|
401
|
+
thumbnail: File | null;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function noNegativePrice(control: AbstractControl): ValidationErrors | null {
|
|
405
|
+
return (control.value as number) < 0 ? { negativePrice: true } : null;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
@Component({
|
|
409
|
+
standalone: true,
|
|
410
|
+
imports: [ReactiveFormsModule, FieldErrorsPipe],
|
|
411
|
+
template: `
|
|
412
|
+
<form [formGroup]="form" (ngSubmit)="onSubmit()">
|
|
413
|
+
<input formControlName="name" placeholder="Product name" />
|
|
414
|
+
@for (e of form.controls.name | fieldErrors; track e.key) {
|
|
415
|
+
<small>{{ e.message }}</small>
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
<input type="number" formControlName="price" />
|
|
419
|
+
@for (e of form.controls.price | fieldErrors; track e.key) {
|
|
420
|
+
<small>{{ e.message }}</small>
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
<input #fileInput type="file" (change)="onFileChange($event)" />
|
|
424
|
+
@if (form.controls.thumbnail.value; as file) {
|
|
425
|
+
<span>{{ file.name }}</span>
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
<button type="submit">Save</button>
|
|
429
|
+
</form>
|
|
430
|
+
`,
|
|
431
|
+
})
|
|
432
|
+
export class ProductFormComponent {
|
|
433
|
+
protected form = entityForm<ProductForm>({
|
|
434
|
+
name: entity.required('', Validators.minLength(3)),
|
|
435
|
+
description: entity.optional<string>(null),
|
|
436
|
+
price: entity.required(0, [Validators.min(0), noNegativePrice]),
|
|
437
|
+
active: entity.required(false),
|
|
438
|
+
thumbnail: entity.file(),
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
onFileChange(event: Event): void {
|
|
442
|
+
patchFileControl(event, this.form.controls.thumbnail);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
onSubmit(): void {
|
|
446
|
+
this.form.markAllAsTouched();
|
|
447
|
+
if (this.form.pending || this.form.invalid) return;
|
|
448
|
+
|
|
449
|
+
const value = this.form.getRawValue();
|
|
450
|
+
// value.name → string
|
|
451
|
+
// value.price → number
|
|
452
|
+
// value.thumbnail → File | null
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
```
|
|
456
|
+
|
|
457
|
+
---
|
|
458
|
+
|
|
459
|
+
## Public API
|
|
460
|
+
|
|
461
|
+
| Export | Description |
|
|
462
|
+
| ---------------------------- | ------------------------------------------------------------------------------------ |
|
|
463
|
+
| `entity` | Builder object — `entity.required`, `entity.optional`, `entity.file` |
|
|
464
|
+
| `entityForm` | Creates a typed `FormGroup<EntityFields<T>>` |
|
|
465
|
+
| `patchFileControl` | Updates a file control from a native input `change` event |
|
|
466
|
+
| `clearFileControl` | Clears a file control and resets the native input element |
|
|
467
|
+
| `FieldErrorsPipe` | Pipe that resolves control errors to `FieldError[]` |
|
|
468
|
+
| `provideDefaultValidators` | Registers global validators in `app.config` |
|
|
469
|
+
| `provideErrorMessages` | Registers custom error messages in `app.config` (English base) |
|
|
470
|
+
| `provideErrorMessagesLocale` | Switches built-in messages to a locale (`'en'` \| `'es'`) + optional custom messages |
|
|
471
|
+
| `EntityForm<T>` | Type alias for `FormGroup<EntityFields<T>>` |
|
|
472
|
+
| `EntityFields<T>` | Maps entity fields to typed `FormControl` |
|
|
473
|
+
| `ControlConfig` | Second argument type for `entity.required` / `entity.optional` |
|
|
474
|
+
| `FieldError` | `{ key: string; message: string }` — returned by `fieldErrors` pipe |
|
|
475
|
+
| `ErrorMessages` | Error messages map type |
|
|
476
|
+
| `SupportedLocale` | `'en' \| 'es'` |
|
|
477
|
+
| `DefaultValidatorsConfig` | Config type for `provideDefaultValidators` |
|
|
478
|
+
|
|
479
|
+
---
|
|
480
|
+
|
|
481
|
+
## Philosophy
|
|
482
|
+
|
|
483
|
+
Angular's reactive forms are powerful but verbose. Typed forms (introduced in Angular 14) improved the situation, but the boilerplate of creating controls, wiring validators, and displaying errors still adds up fast across a real project.
|
|
484
|
+
|
|
485
|
+
`ng-entity-forms` takes the position that your form should follow your entity — not the other way around. You define the shape of your data once, and the library derives the form structure from it. TypeScript does the rest.
|
|
486
|
+
|
|
487
|
+
- **Entity-first** — your interface is the source of truth, the form follows it
|
|
488
|
+
- **Zero guessing** — full autocompletion on `form.controls.X` with the correct type
|
|
489
|
+
- **Flat API** — one builder object, three methods, one function to create the group
|
|
490
|
+
- **Additive** — global validators and error messages layer on top without touching your controls
|
|
491
|
+
- **No lock-in** — built entirely on Angular's own `FormControl` and `FormGroup`, no custom abstractions underneath
|
|
492
|
+
|
|
493
|
+
---
|
|
494
|
+
|
|
495
|
+
## License
|
|
496
|
+
|
|
497
|
+
MIT
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import * as i0 from '@angular/core';
|
|
2
|
+
import { InjectionToken, inject, Pipe } from '@angular/core';
|
|
3
|
+
import { FormControl, Validators, FormGroup } from '@angular/forms';
|
|
4
|
+
|
|
5
|
+
const DEFAULT_VALIDATORS_TOKEN = new InjectionToken('NG_ENTITY_FORMS_DEFAULT_VALIDATORS', {
|
|
6
|
+
providedIn: 'root',
|
|
7
|
+
factory: () => ({}),
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
function normalizeConfig(config) {
|
|
11
|
+
if (typeof config === 'function') {
|
|
12
|
+
return { validators: [config] };
|
|
13
|
+
}
|
|
14
|
+
if (Array.isArray(config)) {
|
|
15
|
+
return { validators: config };
|
|
16
|
+
}
|
|
17
|
+
return config;
|
|
18
|
+
}
|
|
19
|
+
function resolveGlobalValidators(type) {
|
|
20
|
+
const config = inject(DEFAULT_VALIDATORS_TOKEN);
|
|
21
|
+
return [...(config.all ?? []), ...(config[type] ?? [])];
|
|
22
|
+
}
|
|
23
|
+
function requiredControl(initialValue, config = []) {
|
|
24
|
+
const { validators = [], asyncValidators = [], disabled = false, updateOn, } = normalizeConfig(config);
|
|
25
|
+
const globalValidators = resolveGlobalValidators('required');
|
|
26
|
+
return new FormControl({ value: initialValue, disabled }, {
|
|
27
|
+
nonNullable: true,
|
|
28
|
+
validators: [Validators.required, ...globalValidators, ...validators],
|
|
29
|
+
asyncValidators,
|
|
30
|
+
updateOn,
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
function optionalControl(initialValue, config = []) {
|
|
34
|
+
const { validators = [], asyncValidators = [], disabled = false, updateOn, } = normalizeConfig(config);
|
|
35
|
+
const globalValidators = resolveGlobalValidators('optional');
|
|
36
|
+
return new FormControl({ value: initialValue, disabled }, {
|
|
37
|
+
validators: [...globalValidators, ...validators],
|
|
38
|
+
asyncValidators,
|
|
39
|
+
updateOn,
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const entity = {
|
|
44
|
+
required(initialValue, config = []) {
|
|
45
|
+
return requiredControl(initialValue, config);
|
|
46
|
+
},
|
|
47
|
+
optional(initialValue, config = []) {
|
|
48
|
+
return optionalControl(initialValue, config);
|
|
49
|
+
},
|
|
50
|
+
file(config = []) {
|
|
51
|
+
return optionalControl(null, config);
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
function entityForm(controls, options) {
|
|
56
|
+
return new FormGroup(controls, options);
|
|
57
|
+
}
|
|
58
|
+
function patchFileControl(event, control) {
|
|
59
|
+
if (!(event.target instanceof HTMLInputElement)) {
|
|
60
|
+
throw new Error('patchFileControl expects an HTMLInputElement');
|
|
61
|
+
}
|
|
62
|
+
const file = event.target.files?.[0] ?? null;
|
|
63
|
+
control.setValue(file);
|
|
64
|
+
control.markAsTouched();
|
|
65
|
+
}
|
|
66
|
+
function clearFileControl(control, inputElement) {
|
|
67
|
+
control.setValue(null);
|
|
68
|
+
control.markAsTouched();
|
|
69
|
+
if (inputElement) {
|
|
70
|
+
inputElement.value = '';
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function provideDefaultValidators(config) {
|
|
75
|
+
return {
|
|
76
|
+
provide: DEFAULT_VALIDATORS_TOKEN,
|
|
77
|
+
useValue: config,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const BUILT_IN_ERROR_MESSAGES_EN = {
|
|
82
|
+
required: 'This field is required',
|
|
83
|
+
email: 'Invalid email format',
|
|
84
|
+
pattern: 'Invalid format',
|
|
85
|
+
minlength: (err) => `Minimum ${err.requiredLength} characters`,
|
|
86
|
+
maxlength: (err) => `Maximum ${err.requiredLength} characters`,
|
|
87
|
+
min: (err) => `Minimum value is ${err.min}`,
|
|
88
|
+
max: (err) => `Maximum value is ${err.max}`,
|
|
89
|
+
};
|
|
90
|
+
const BUILT_IN_ERROR_MESSAGES_ES = {
|
|
91
|
+
required: 'Este campo es obligatorio',
|
|
92
|
+
email: 'El formato del email no es válido',
|
|
93
|
+
pattern: 'El formato no es válido',
|
|
94
|
+
minlength: (err) => `Mínimo ${err.requiredLength} caracteres`,
|
|
95
|
+
maxlength: (err) => `Máximo ${err.requiredLength} caracteres`,
|
|
96
|
+
min: (err) => `El valor mínimo es ${err.min}`,
|
|
97
|
+
max: (err) => `El valor máximo es ${err.max}`,
|
|
98
|
+
};
|
|
99
|
+
const BUILT_IN_ERROR_MESSAGES = {
|
|
100
|
+
en: BUILT_IN_ERROR_MESSAGES_EN,
|
|
101
|
+
es: BUILT_IN_ERROR_MESSAGES_ES,
|
|
102
|
+
};
|
|
103
|
+
const ERROR_MESSAGES_TOKEN = new InjectionToken('NG_ENTITY_FORMS_ERROR_MESSAGES', {
|
|
104
|
+
providedIn: 'root',
|
|
105
|
+
factory: () => ({ ...BUILT_IN_ERROR_MESSAGES_EN }),
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
function provideErrorMessages(messages) {
|
|
109
|
+
return {
|
|
110
|
+
provide: ERROR_MESSAGES_TOKEN,
|
|
111
|
+
useValue: { ...BUILT_IN_ERROR_MESSAGES['en'], ...messages },
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
function provideErrorMessagesLocale(locale, messages = {}) {
|
|
115
|
+
return {
|
|
116
|
+
provide: ERROR_MESSAGES_TOKEN,
|
|
117
|
+
useValue: { ...BUILT_IN_ERROR_MESSAGES[locale], ...messages },
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function resolveMessage(messageValue, errorData) {
|
|
122
|
+
return typeof messageValue === 'function' ? messageValue(errorData) : messageValue;
|
|
123
|
+
}
|
|
124
|
+
class FieldErrorsPipe {
|
|
125
|
+
globalMessages = inject(ERROR_MESSAGES_TOKEN);
|
|
126
|
+
transform(control, overrides) {
|
|
127
|
+
if (!control || !control.errors)
|
|
128
|
+
return [];
|
|
129
|
+
if (!control.touched && !control.dirty)
|
|
130
|
+
return [];
|
|
131
|
+
const messages = overrides
|
|
132
|
+
? { ...this.globalMessages, ...overrides }
|
|
133
|
+
: this.globalMessages;
|
|
134
|
+
return Object.entries(control.errors).map(([key, errorData]) => {
|
|
135
|
+
const messageValue = messages[key];
|
|
136
|
+
const message = messageValue ? resolveMessage(messageValue, errorData) : `[${key}]`;
|
|
137
|
+
return { key, message };
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: FieldErrorsPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe });
|
|
141
|
+
static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.1.5", ngImport: i0, type: FieldErrorsPipe, isStandalone: true, name: "fieldErrors", pure: false });
|
|
142
|
+
}
|
|
143
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: FieldErrorsPipe, decorators: [{
|
|
144
|
+
type: Pipe,
|
|
145
|
+
args: [{
|
|
146
|
+
name: 'fieldErrors',
|
|
147
|
+
standalone: true,
|
|
148
|
+
pure: false,
|
|
149
|
+
}]
|
|
150
|
+
}] });
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Generated bundle index. Do not edit.
|
|
154
|
+
*/
|
|
155
|
+
|
|
156
|
+
export { BUILT_IN_ERROR_MESSAGES, DEFAULT_VALIDATORS_TOKEN, ERROR_MESSAGES_TOKEN, FieldErrorsPipe, clearFileControl, entity, entityForm, patchFileControl, provideDefaultValidators, provideErrorMessages, provideErrorMessagesLocale };
|
|
157
|
+
//# sourceMappingURL=ng-entity-forms.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ng-entity-forms.mjs","sources":["../../../projects/ng-entity-forms/src/lib/validation/validation.token.ts","../../../projects/ng-entity-forms/src/lib/core/controls.ts","../../../projects/ng-entity-forms/src/lib/core/entity.ts","../../../projects/ng-entity-forms/src/lib/core/entity-form.ts","../../../projects/ng-entity-forms/src/lib/validation/validation.provider.ts","../../../projects/ng-entity-forms/src/lib/errors/error-messages.token.ts","../../../projects/ng-entity-forms/src/lib/errors/error-messages.provider.ts","../../../projects/ng-entity-forms/src/lib/errors/field-errors.pipe.ts","../../../projects/ng-entity-forms/src/ng-entity-forms.ts"],"sourcesContent":["import { InjectionToken } from '@angular/core';\nimport { ValidatorFn } from '@angular/forms';\n\nexport interface DefaultValidatorsConfig {\n all?: ValidatorFn[];\n required?: ValidatorFn[];\n optional?: ValidatorFn[];\n}\n\nexport const DEFAULT_VALIDATORS_TOKEN = new InjectionToken<DefaultValidatorsConfig>(\n 'NG_ENTITY_FORMS_DEFAULT_VALIDATORS',\n {\n providedIn: 'root',\n factory: () => ({}),\n },\n);\n","import { inject } from '@angular/core';\nimport { FormControl, ValidatorFn, Validators } from '@angular/forms';\n\nimport { ControlOptions, ControlConfig } from './types';\nimport { DEFAULT_VALIDATORS_TOKEN } from '../validation/validation.token';\n\nexport function normalizeConfig(config: ControlConfig): ControlOptions {\n if (typeof config === 'function') {\n return { validators: [config as ValidatorFn] };\n }\n\n if (Array.isArray(config)) {\n return { validators: config as ValidatorFn[] };\n }\n\n return config as ControlOptions;\n}\n\nexport function resolveGlobalValidators(type: 'required' | 'optional'): ValidatorFn[] {\n const config = inject(DEFAULT_VALIDATORS_TOKEN);\n return [...(config.all ?? []), ...(config[type] ?? [])];\n}\n\nexport function requiredControl<T>(\n initialValue: NonNullable<T>,\n config: ControlConfig = [],\n): FormControl<NonNullable<T>> {\n const {\n validators = [],\n asyncValidators = [],\n disabled = false,\n updateOn,\n } = normalizeConfig(config);\n\n const globalValidators = resolveGlobalValidators('required');\n\n return new FormControl<NonNullable<T>>(\n { value: initialValue, disabled },\n {\n nonNullable: true,\n validators: [Validators.required, ...globalValidators, ...validators],\n asyncValidators,\n updateOn,\n },\n ) as FormControl<NonNullable<T>>;\n}\n\nexport function optionalControl<T>(\n initialValue: T | null,\n config: ControlConfig = [],\n): FormControl<T | null> {\n const {\n validators = [],\n asyncValidators = [],\n disabled = false,\n updateOn,\n } = normalizeConfig(config);\n\n const globalValidators = resolveGlobalValidators('optional');\n\n return new FormControl<T | null>(\n { value: initialValue, disabled },\n {\n validators: [...globalValidators, ...validators],\n asyncValidators,\n updateOn,\n },\n );\n}\n","import { FormControl } from '@angular/forms';\n\nimport { ControlConfig } from './types';\nimport { optionalControl, requiredControl } from './controls';\n\nexport const entity = {\n required<T>(\n initialValue: NonNullable<T>,\n config: ControlConfig = [],\n ): FormControl<NonNullable<T>> {\n return requiredControl<T>(initialValue, config);\n },\n\n optional<T>(initialValue: T | null, config: ControlConfig = []): FormControl<T | null> {\n return optionalControl<T>(initialValue, config);\n },\n\n file(config: ControlConfig = []): FormControl<File | null> {\n return optionalControl<File>(null, config);\n },\n};\n","import { FormControl, FormGroup } from '@angular/forms';\n\nimport { EntityFields, EntityForm, GroupOptions } from './types';\n\nexport function entityForm<T>(controls: EntityFields<T>, options?: GroupOptions): EntityForm<T> {\n return new FormGroup<EntityFields<T>>(controls, options);\n}\n\nexport function patchFileControl(event: Event, control: FormControl<File | null>): void {\n if (!(event.target instanceof HTMLInputElement)) {\n throw new Error('patchFileControl expects an HTMLInputElement');\n }\n\n const file = event.target.files?.[0] ?? null;\n\n control.setValue(file);\n control.markAsTouched();\n}\n\nexport function clearFileControl(\n control: FormControl<File | null>,\n inputElement?: HTMLInputElement,\n): void {\n control.setValue(null);\n control.markAsTouched();\n\n if (inputElement) {\n inputElement.value = '';\n }\n}\n","import { Provider } from '@angular/core';\n\nimport { DEFAULT_VALIDATORS_TOKEN, DefaultValidatorsConfig } from './validation.token';\n\nexport function provideDefaultValidators(config: DefaultValidatorsConfig): Provider {\n return {\n provide: DEFAULT_VALIDATORS_TOKEN,\n useValue: config,\n };\n}\n","import { InjectionToken } from '@angular/core';\n\nexport type ErrorMessageFn = (err: unknown) => string;\nexport type ErrorMessageValue = string | ErrorMessageFn;\nexport type ErrorMessages = Record<string, ErrorMessageValue>;\n\nexport type SupportedLocale = 'en' | 'es';\n\nconst BUILT_IN_ERROR_MESSAGES_EN: ErrorMessages = {\n required: 'This field is required',\n email: 'Invalid email format',\n pattern: 'Invalid format',\n minlength: (err: unknown) =>\n `Minimum ${(err as { requiredLength: number }).requiredLength} characters`,\n maxlength: (err: unknown) =>\n `Maximum ${(err as { requiredLength: number }).requiredLength} characters`,\n min: (err: unknown) => `Minimum value is ${(err as { min: number }).min}`,\n max: (err: unknown) => `Maximum value is ${(err as { max: number }).max}`,\n};\n\nconst BUILT_IN_ERROR_MESSAGES_ES: ErrorMessages = {\n required: 'Este campo es obligatorio',\n email: 'El formato del email no es válido',\n pattern: 'El formato no es válido',\n minlength: (err: unknown) =>\n `Mínimo ${(err as { requiredLength: number }).requiredLength} caracteres`,\n maxlength: (err: unknown) =>\n `Máximo ${(err as { requiredLength: number }).requiredLength} caracteres`,\n min: (err: unknown) => `El valor mínimo es ${(err as { min: number }).min}`,\n max: (err: unknown) => `El valor máximo es ${(err as { max: number }).max}`,\n};\n\nexport const BUILT_IN_ERROR_MESSAGES: Record<SupportedLocale, ErrorMessages> = {\n en: BUILT_IN_ERROR_MESSAGES_EN,\n es: BUILT_IN_ERROR_MESSAGES_ES,\n};\n\nexport const ERROR_MESSAGES_TOKEN = new InjectionToken<ErrorMessages>(\n 'NG_ENTITY_FORMS_ERROR_MESSAGES',\n {\n providedIn: 'root',\n factory: () => ({ ...BUILT_IN_ERROR_MESSAGES_EN }),\n },\n);\n","import { Provider } from '@angular/core';\n\nimport {\n BUILT_IN_ERROR_MESSAGES,\n ERROR_MESSAGES_TOKEN,\n ErrorMessages,\n SupportedLocale,\n} from './error-messages.token';\n\nexport function provideErrorMessages(messages: ErrorMessages): Provider {\n return {\n provide: ERROR_MESSAGES_TOKEN,\n useValue: { ...BUILT_IN_ERROR_MESSAGES['en'], ...messages },\n };\n}\n\nexport function provideErrorMessagesLocale(\n locale: SupportedLocale,\n messages: ErrorMessages = {},\n): Provider {\n return {\n provide: ERROR_MESSAGES_TOKEN,\n useValue: { ...BUILT_IN_ERROR_MESSAGES[locale], ...messages },\n };\n}\n","import { inject, Pipe, PipeTransform } from '@angular/core';\nimport { AbstractControl } from '@angular/forms';\n\nimport { ErrorMessageValue, ERROR_MESSAGES_TOKEN, ErrorMessages } from './error-messages.token';\n\nfunction resolveMessage(messageValue: ErrorMessageValue, errorData: unknown): string {\n return typeof messageValue === 'function' ? messageValue(errorData) : messageValue;\n}\n\nexport interface FieldError {\n key: string;\n message: string;\n}\n\n@Pipe({\n name: 'fieldErrors',\n standalone: true,\n pure: false,\n})\nexport class FieldErrorsPipe implements PipeTransform {\n private readonly globalMessages = inject(ERROR_MESSAGES_TOKEN);\n transform(control: AbstractControl | null | undefined, overrides?: ErrorMessages): FieldError[] {\n if (!control || !control.errors) return [];\n\n if (!control.touched && !control.dirty) return [];\n\n const messages: ErrorMessages = overrides\n ? { ...this.globalMessages, ...overrides }\n : this.globalMessages;\n\n return Object.entries(control.errors).map(([key, errorData]) => {\n const messageValue = messages[key];\n const message = messageValue ? resolveMessage(messageValue, errorData) : `[${key}]`;\n return { key, message };\n });\n }\n}\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './public-api';\n"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "ng-entity-forms",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"description": "Typed reactive forms for Angular. Build strongly-typed forms from your entity interfaces with minimal boilerplate.",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"angular",
|
|
8
|
+
"forms",
|
|
9
|
+
"reactive-forms",
|
|
10
|
+
"typed-forms",
|
|
11
|
+
"entity-forms",
|
|
12
|
+
"form-builder",
|
|
13
|
+
"typescript",
|
|
14
|
+
"angular-library",
|
|
15
|
+
"form-validation",
|
|
16
|
+
"angular-forms"
|
|
17
|
+
],
|
|
18
|
+
"peerDependencies": {
|
|
19
|
+
"@angular/common": ">=17.0.0",
|
|
20
|
+
"@angular/core": ">=17.0.0",
|
|
21
|
+
"@angular/forms": ">=17.0.0"
|
|
22
|
+
},
|
|
23
|
+
"peerDependenciesMeta": {
|
|
24
|
+
"@angular/common": {
|
|
25
|
+
"optional": false
|
|
26
|
+
},
|
|
27
|
+
"@angular/core": {
|
|
28
|
+
"optional": false
|
|
29
|
+
},
|
|
30
|
+
"@angular/forms": {
|
|
31
|
+
"optional": false
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
"dependencies": {
|
|
35
|
+
"tslib": "^2.3.0"
|
|
36
|
+
},
|
|
37
|
+
"sideEffects": false,
|
|
38
|
+
"module": "fesm2022/ng-entity-forms.mjs",
|
|
39
|
+
"typings": "types/ng-entity-forms.d.ts",
|
|
40
|
+
"exports": {
|
|
41
|
+
"./package.json": {
|
|
42
|
+
"default": "./package.json"
|
|
43
|
+
},
|
|
44
|
+
".": {
|
|
45
|
+
"types": "./types/ng-entity-forms.d.ts",
|
|
46
|
+
"default": "./fesm2022/ng-entity-forms.mjs"
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { ValidatorFn, AsyncValidatorFn, FormControl, FormGroup, AbstractControl } from '@angular/forms';
|
|
2
|
+
import * as i0 from '@angular/core';
|
|
3
|
+
import { InjectionToken, Provider, PipeTransform } from '@angular/core';
|
|
4
|
+
|
|
5
|
+
type EntityFields<T> = {
|
|
6
|
+
[K in keyof T]: FormControl<T[K]>;
|
|
7
|
+
};
|
|
8
|
+
type EntityForm<T> = FormGroup<EntityFields<T>>;
|
|
9
|
+
interface ControlOptions {
|
|
10
|
+
validators?: ValidatorFn[];
|
|
11
|
+
asyncValidators?: AsyncValidatorFn[];
|
|
12
|
+
disabled?: boolean;
|
|
13
|
+
updateOn?: 'change' | 'blur' | 'submit';
|
|
14
|
+
}
|
|
15
|
+
type ControlConfig = ValidatorFn | ValidatorFn[] | ControlOptions;
|
|
16
|
+
interface GroupOptions {
|
|
17
|
+
validators?: ValidatorFn | ValidatorFn[];
|
|
18
|
+
asyncValidators?: AsyncValidatorFn | AsyncValidatorFn[];
|
|
19
|
+
updateOn?: 'change' | 'blur' | 'submit';
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
declare const entity: {
|
|
23
|
+
required<T>(initialValue: NonNullable<T>, config?: ControlConfig): FormControl<NonNullable<T>>;
|
|
24
|
+
optional<T>(initialValue: T | null, config?: ControlConfig): FormControl<T | null>;
|
|
25
|
+
file(config?: ControlConfig): FormControl<File | null>;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
declare function entityForm<T>(controls: EntityFields<T>, options?: GroupOptions): EntityForm<T>;
|
|
29
|
+
declare function patchFileControl(event: Event, control: FormControl<File | null>): void;
|
|
30
|
+
declare function clearFileControl(control: FormControl<File | null>, inputElement?: HTMLInputElement): void;
|
|
31
|
+
|
|
32
|
+
interface DefaultValidatorsConfig {
|
|
33
|
+
all?: ValidatorFn[];
|
|
34
|
+
required?: ValidatorFn[];
|
|
35
|
+
optional?: ValidatorFn[];
|
|
36
|
+
}
|
|
37
|
+
declare const DEFAULT_VALIDATORS_TOKEN: InjectionToken<DefaultValidatorsConfig>;
|
|
38
|
+
|
|
39
|
+
declare function provideDefaultValidators(config: DefaultValidatorsConfig): Provider;
|
|
40
|
+
|
|
41
|
+
type ErrorMessageFn = (err: unknown) => string;
|
|
42
|
+
type ErrorMessageValue = string | ErrorMessageFn;
|
|
43
|
+
type ErrorMessages = Record<string, ErrorMessageValue>;
|
|
44
|
+
type SupportedLocale = 'en' | 'es';
|
|
45
|
+
declare const BUILT_IN_ERROR_MESSAGES: Record<SupportedLocale, ErrorMessages>;
|
|
46
|
+
declare const ERROR_MESSAGES_TOKEN: InjectionToken<ErrorMessages>;
|
|
47
|
+
|
|
48
|
+
declare function provideErrorMessages(messages: ErrorMessages): Provider;
|
|
49
|
+
declare function provideErrorMessagesLocale(locale: SupportedLocale, messages?: ErrorMessages): Provider;
|
|
50
|
+
|
|
51
|
+
interface FieldError {
|
|
52
|
+
key: string;
|
|
53
|
+
message: string;
|
|
54
|
+
}
|
|
55
|
+
declare class FieldErrorsPipe implements PipeTransform {
|
|
56
|
+
private readonly globalMessages;
|
|
57
|
+
transform(control: AbstractControl | null | undefined, overrides?: ErrorMessages): FieldError[];
|
|
58
|
+
static ɵfac: i0.ɵɵFactoryDeclaration<FieldErrorsPipe, never>;
|
|
59
|
+
static ɵpipe: i0.ɵɵPipeDeclaration<FieldErrorsPipe, "fieldErrors", true>;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export { BUILT_IN_ERROR_MESSAGES, DEFAULT_VALIDATORS_TOKEN, ERROR_MESSAGES_TOKEN, FieldErrorsPipe, clearFileControl, entity, entityForm, patchFileControl, provideDefaultValidators, provideErrorMessages, provideErrorMessagesLocale };
|
|
63
|
+
export type { ControlConfig, ControlOptions, DefaultValidatorsConfig, EntityFields, EntityForm, ErrorMessageFn, ErrorMessageValue, ErrorMessages, FieldError, GroupOptions, SupportedLocale };
|