@spider-baby/utils-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 +377 -0
- package/fesm2022/spider-baby-utils-forms-validators.mjs +141 -0
- package/fesm2022/spider-baby-utils-forms-validators.mjs.map +1 -0
- package/fesm2022/spider-baby-utils-forms.mjs +336 -0
- package/fesm2022/spider-baby-utils-forms.mjs.map +1 -0
- package/index.d.ts +5 -0
- package/lib/first-error.component.d.ts +9 -0
- package/lib/first-error.directive.d.ts +45 -0
- package/lib/form-errors.d.ts +10 -0
- package/lib/form-utility.d.ts +14 -0
- package/lib/remove-nulls.service.d.ts +7 -0
- package/package.json +31 -0
- package/validators/README.md +3 -0
- package/validators/index.d.ts +4 -0
- package/validators/lib/numeric-validators.d.ts +4 -0
- package/validators/lib/password-validators.d.ts +10 -0
- package/validators/lib/phone-validators.d.ts +5 -0
- package/validators/lib/pwd-regexes.d.ts +6 -0
package/README.md
ADDED
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
# Spider Baby Form Utils
|
|
2
|
+
|
|
3
|
+
A comprehensive Angular forms utility library that provides streamlined form validation and error handling with automatic error display and user-friendly messaging.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
This library simplifies Angular form validation by providing:
|
|
8
|
+
- **Automatic error detection** and display
|
|
9
|
+
- **First error only** display strategy to avoid overwhelming users
|
|
10
|
+
- **Accessible error messages** with ARIA attributes
|
|
11
|
+
- **Smooth animations** for error state transitions
|
|
12
|
+
- **Customizable error messaging** system
|
|
13
|
+
- **Touch-based validation** (errors only show after user interaction)
|
|
14
|
+
|
|
15
|
+
## Core Components
|
|
16
|
+
|
|
17
|
+
### 🎯 FirstErrorDirective
|
|
18
|
+
|
|
19
|
+
A powerful directive that automatically manages form validation state and displays the first error for each form control.
|
|
20
|
+
|
|
21
|
+
**Selector:** `[sbFormControlFirstError]`
|
|
22
|
+
|
|
23
|
+
#### Features
|
|
24
|
+
- Automatically monitors form status changes
|
|
25
|
+
- Shows only the first validation error per control
|
|
26
|
+
- Respects touched/untouched state (errors only show after user interaction)
|
|
27
|
+
- Supports custom error messages
|
|
28
|
+
- Memory leak prevention with automatic subscription cleanup
|
|
29
|
+
|
|
30
|
+
#### Basic Usage
|
|
31
|
+
|
|
32
|
+
```typescript
|
|
33
|
+
// Component
|
|
34
|
+
export class LoginFormComponent {
|
|
35
|
+
protected _form: FormGroup<LoginForm> = this.fb.nonNullable.group({
|
|
36
|
+
email: ['', [Validators.required, Validators.email]],
|
|
37
|
+
password: ['', [Validators.required, Validators.minLength(3)]],
|
|
38
|
+
rememberMe: [false, []]
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
```html
|
|
44
|
+
<!-- Template - Apply directive to form element -->
|
|
45
|
+
<form [formGroup]="_form"
|
|
46
|
+
[sbFormControlFirstError]="_form"
|
|
47
|
+
(ngSubmit)="submit()">
|
|
48
|
+
|
|
49
|
+
<div class="form-group">
|
|
50
|
+
<input type="email"
|
|
51
|
+
formControlName="email"
|
|
52
|
+
placeholder="Enter your email"/>
|
|
53
|
+
<sb-first-error [control]="_form.controls.email"/>
|
|
54
|
+
</div>
|
|
55
|
+
|
|
56
|
+
<div class="form-group">
|
|
57
|
+
<input type="password"
|
|
58
|
+
formControlName="password"
|
|
59
|
+
placeholder="Enter your password"/>
|
|
60
|
+
<sb-first-error [control]="_form.controls.password"/>
|
|
61
|
+
</div>
|
|
62
|
+
</form>
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
#### Advanced Configuration
|
|
66
|
+
|
|
67
|
+
```html
|
|
68
|
+
<!-- Custom error messages -->
|
|
69
|
+
<form [formGroup]="myForm"
|
|
70
|
+
[sbFormControlFirstError]="myForm"
|
|
71
|
+
[customErrorMessages]="customMessages">
|
|
72
|
+
<!-- form controls -->
|
|
73
|
+
</form>
|
|
74
|
+
|
|
75
|
+
<!-- Show errors immediately (bypass touch requirement) -->
|
|
76
|
+
<form [formGroup]="myForm"
|
|
77
|
+
[sbFormControlFirstError]="myForm"
|
|
78
|
+
[showUntouched]="true">
|
|
79
|
+
<!-- form controls -->
|
|
80
|
+
</form>
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
#### Inputs
|
|
84
|
+
|
|
85
|
+
| Property | Type | Default | Description |
|
|
86
|
+
| ------------------------- | ----------------------- | ------------ | ----------------------------------- |
|
|
87
|
+
| `sbFormControlFirstError` | `FormGroup` | **required** | The reactive form to monitor |
|
|
88
|
+
| `customErrorMessages` | `CustomErrorMessageMap` | `undefined` | Custom error message mappings |
|
|
89
|
+
| `showUntouched` | `boolean` | `false` | Show errors before user interaction |
|
|
90
|
+
|
|
91
|
+
### 🎨 FirstErrorComponent
|
|
92
|
+
|
|
93
|
+
A standalone component that displays validation errors with smooth animations and accessibility features.
|
|
94
|
+
|
|
95
|
+
**Selector:** `sb-first-error`
|
|
96
|
+
|
|
97
|
+
#### Features
|
|
98
|
+
- Displays only the first validation error
|
|
99
|
+
- Smooth fade-in animation for error appearance
|
|
100
|
+
- ARIA live regions for screen reader compatibility
|
|
101
|
+
|
|
102
|
+
#### Usage
|
|
103
|
+
|
|
104
|
+
```html
|
|
105
|
+
<!-- Basic usage -->
|
|
106
|
+
<sb-first-error [control]="myForm.controls.fieldName"/>
|
|
107
|
+
|
|
108
|
+
<!-- Real example from login form -->
|
|
109
|
+
<input type="email"
|
|
110
|
+
formControlName="email"
|
|
111
|
+
placeholder="Enter your email"/>
|
|
112
|
+
<sb-first-error [control]="_form.controls.email"/>
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
#### Styling
|
|
116
|
+
|
|
117
|
+
The component uses CSS custom properties for easy theming:
|
|
118
|
+
|
|
119
|
+
```css
|
|
120
|
+
sb-first-error {
|
|
121
|
+
--error-color: #custom-error-color;
|
|
122
|
+
}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
**Default styling:**
|
|
126
|
+
- Color: Material Design error color with fallback (`var(--mat-sys-error, #d9534f);`)
|
|
127
|
+
- Font size: 0.875rem
|
|
128
|
+
- Smooth fade-in animation
|
|
129
|
+
- Accessible spacing and layout
|
|
130
|
+
|
|
131
|
+
#### Inputs
|
|
132
|
+
|
|
133
|
+
| Property | Type | Description |
|
|
134
|
+
| --------- | ----------------- | ---------------------------------------------------- |
|
|
135
|
+
| `control` | `AbstractControl` | **Required.** The form control to display errors for |
|
|
136
|
+
|
|
137
|
+
## Installation & Setup
|
|
138
|
+
|
|
139
|
+
### 1. Import the Library
|
|
140
|
+
|
|
141
|
+
```typescript
|
|
142
|
+
import {
|
|
143
|
+
FirstErrorDirective,
|
|
144
|
+
FirstErrorComponent
|
|
145
|
+
} from '@spider-baby/utils-forms';
|
|
146
|
+
|
|
147
|
+
@Component({
|
|
148
|
+
standalone: true,
|
|
149
|
+
imports: [
|
|
150
|
+
ReactiveFormsModule,
|
|
151
|
+
FirstErrorDirective,
|
|
152
|
+
FirstErrorComponent,
|
|
153
|
+
// ...other imports
|
|
154
|
+
],
|
|
155
|
+
// ...component definition
|
|
156
|
+
})
|
|
157
|
+
export class MyFormComponent { }
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
### 2. Apply to Your Form
|
|
161
|
+
|
|
162
|
+
```html
|
|
163
|
+
<form [formGroup]="myForm" [sbFormControlFirstError]="myForm">
|
|
164
|
+
<!-- Your form controls with error display -->
|
|
165
|
+
<input formControlName="email" />
|
|
166
|
+
<sb-first-error [control]="myForm.controls.email"/>
|
|
167
|
+
</form>
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
## Real-World Example
|
|
171
|
+
|
|
172
|
+
Here's a complete login form implementation using the library:
|
|
173
|
+
|
|
174
|
+
```typescript
|
|
175
|
+
// login.component.ts
|
|
176
|
+
@Component({
|
|
177
|
+
selector: 'sb-login-form',
|
|
178
|
+
standalone: true,
|
|
179
|
+
imports: [
|
|
180
|
+
ReactiveFormsModule,
|
|
181
|
+
FirstErrorDirective,
|
|
182
|
+
FirstErrorComponent,
|
|
183
|
+
// ...other imports
|
|
184
|
+
],
|
|
185
|
+
templateUrl: './login.component.html',
|
|
186
|
+
styleUrl: './login.component.scss',
|
|
187
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
188
|
+
})
|
|
189
|
+
export class LoginFormComponent {
|
|
190
|
+
private fb = inject(FormBuilder)
|
|
191
|
+
|
|
192
|
+
protected _form: FormGroup<LoginForm> = this.fb.nonNullable.group({
|
|
193
|
+
email: ['', [Validators.required, Validators.email]],
|
|
194
|
+
password: ['', [Validators.required, Validators.minLength(3)]],
|
|
195
|
+
rememberMe: [false, []]
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
submit() {
|
|
199
|
+
if (!this._form.valid) return;
|
|
200
|
+
|
|
201
|
+
const dto: LoginFormDto = {
|
|
202
|
+
email: this._form.controls.email.value,
|
|
203
|
+
password: this._form.controls.password.value,
|
|
204
|
+
rememberMe: this._form.controls.rememberMe.value,
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
this.login.emit(dto);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
```html
|
|
213
|
+
<!-- login.component.html -->
|
|
214
|
+
<form [formGroup]="_form"
|
|
215
|
+
[sbFormControlFirstError]="_form"
|
|
216
|
+
(ngSubmit)="submit()"
|
|
217
|
+
class="login-form">
|
|
218
|
+
|
|
219
|
+
<div class="form-group">
|
|
220
|
+
<label for="email">Email</label>
|
|
221
|
+
<input id="email"
|
|
222
|
+
type="email"
|
|
223
|
+
formControlName="email"
|
|
224
|
+
placeholder="Enter your email"/>
|
|
225
|
+
<sb-first-error [control]="_form.controls.email"/>
|
|
226
|
+
</div>
|
|
227
|
+
|
|
228
|
+
<div class="form-group">
|
|
229
|
+
<label for="password">Password</label>
|
|
230
|
+
<input id="password"
|
|
231
|
+
type="password"
|
|
232
|
+
formControlName="password"
|
|
233
|
+
placeholder="Enter your password"/>
|
|
234
|
+
<sb-first-error [control]="_form.controls.password"/>
|
|
235
|
+
</div>
|
|
236
|
+
|
|
237
|
+
<button type="submit" [disabled]="_form.invalid">
|
|
238
|
+
Login
|
|
239
|
+
</button>
|
|
240
|
+
</form>
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
## Error Message Customization
|
|
244
|
+
|
|
245
|
+
### Default Error Messages
|
|
246
|
+
|
|
247
|
+
The library provides sensible defaults for common validation errors:
|
|
248
|
+
|
|
249
|
+
| Error Key | Example Message |
|
|
250
|
+
| ---------------- | ------------------------------------------------------------------------------------------------------------------- |
|
|
251
|
+
| required | `Email is required.` |
|
|
252
|
+
| email | `Please enter a valid email address.` |
|
|
253
|
+
| minlength | `Password must be at least 8 characters.` |
|
|
254
|
+
| maxlength | `Field must be no more than 20 characters.` |
|
|
255
|
+
| pattern | `Field format is invalid.` |
|
|
256
|
+
| min | `Value must be at least 1.` |
|
|
257
|
+
| max | `Value must be no more than 100.` |
|
|
258
|
+
| passwordMismatch | `Passwords do not match.` |
|
|
259
|
+
| mustMatch | `Fields do not match.` |
|
|
260
|
+
| whitespace | `Field cannot contain only whitespace.` |
|
|
261
|
+
| forbiddenValue | `Field cannot be "forbidden".` |
|
|
262
|
+
| asyncValidation | `Field validation is pending...` |
|
|
263
|
+
| invalidDate | `Please enter a valid date.` |
|
|
264
|
+
| futureDate | `Date must be in the future.` |
|
|
265
|
+
| pastDate | `Date must be in the past.` |
|
|
266
|
+
| strongPassword | `Password must contain at least one uppercase letter, one lowercase letter, one number, and one special character.` |
|
|
267
|
+
| phoneNumber | `Please enter a valid phone number.` |
|
|
268
|
+
| url | `Please enter a valid URL.` |
|
|
269
|
+
| unique | `This field is already taken.` |
|
|
270
|
+
| fileSize | `File size must be less than 2MB.` |
|
|
271
|
+
| fileType | `Only PDF, DOCX files are allowed.` |
|
|
272
|
+
|
|
273
|
+
> **Note:** Some messages are parameterized and will display the actual field name or value as appropriate.
|
|
274
|
+
|
|
275
|
+
### Custom Error Messages
|
|
276
|
+
|
|
277
|
+
```typescript
|
|
278
|
+
import { CustomErrorMessageMap } from '@spider-baby/utils-forms';
|
|
279
|
+
|
|
280
|
+
const customMessages: CustomErrorMessageMap = new Map([
|
|
281
|
+
['email', (field, error) => {
|
|
282
|
+
if (error.required) return 'Email address is mandatory';
|
|
283
|
+
if (error.email) return 'Please provide a valid email format';
|
|
284
|
+
return '';
|
|
285
|
+
}],
|
|
286
|
+
['password', (field, error) => {
|
|
287
|
+
if (error.required) return 'Password cannot be empty';
|
|
288
|
+
if (error.minlength) return 'Password must be at least 8 characters long';
|
|
289
|
+
return '';
|
|
290
|
+
}],
|
|
291
|
+
]);
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
```html
|
|
295
|
+
<form [formGroup]="myForm"
|
|
296
|
+
[sbFormControlFirstError]="myForm"
|
|
297
|
+
[customErrorMessages]="customMessages">
|
|
298
|
+
<!-- form controls -->
|
|
299
|
+
</form>
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
#### 🌎 Example: Custom Error Messages in Spanish
|
|
304
|
+
|
|
305
|
+
```typescript
|
|
306
|
+
import { CustomErrorMessageMap } from '@spider-baby/utils-forms';
|
|
307
|
+
|
|
308
|
+
const customMessagesEs: CustomErrorMessageMap = new Map([
|
|
309
|
+
['email', (field, error) => {
|
|
310
|
+
if (error.required) return 'El correo electrónico es obligatorio';
|
|
311
|
+
if (error.email) return 'Por favor, introduce un correo electrónico válido';
|
|
312
|
+
return '';
|
|
313
|
+
}],
|
|
314
|
+
['password', (field, error) => {
|
|
315
|
+
if (error.required) return 'La contraseña no puede estar vacía';
|
|
316
|
+
if (error.minlength) return 'La contraseña debe tener al menos 8 caracteres';
|
|
317
|
+
return '';
|
|
318
|
+
}],
|
|
319
|
+
]);
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
```html
|
|
323
|
+
<form [formGroup]="myForm"
|
|
324
|
+
[sbFormControlFirstError]="myForm"
|
|
325
|
+
[customErrorMessages]="customMessagesEs">
|
|
326
|
+
<!-- form controls -->
|
|
327
|
+
</form>
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
#### Custom Error Template
|
|
331
|
+
|
|
332
|
+
You can provide a custom error message template to `sb-first-error` using the `customErrorTemplate` input. This allows you to fully control the rendering and styling of error messages.
|
|
333
|
+
|
|
334
|
+
**Usage:**
|
|
335
|
+
|
|
336
|
+
```html
|
|
337
|
+
<sb-first-error [control]="form.controls.email" [customErrorTemplate]="customErrorMessageTemplate"/>
|
|
338
|
+
|
|
339
|
+
<ng-template #customErrorMessageTemplate let-errorMessage="errorMessage">
|
|
340
|
+
<h3 class="custom-error">****{{errorMessage}}****</h3>
|
|
341
|
+
</ng-template>
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
**Inputs Table:**
|
|
345
|
+
|
|
346
|
+
| Property | Type | Description |
|
|
347
|
+
|-----------------------|-----------------------------|--------------------------------------------------|
|
|
348
|
+
| `control` | `AbstractControl` | **Required.** The form control to display errors for |
|
|
349
|
+
| `customErrorTemplate` | `TemplateRef<unknown>` | Optional. Custom template for error message display |
|
|
350
|
+
|
|
351
|
+
If `customErrorTemplate` is provided, it will be used to render the error message. The template receives an `errorMessage` context variable containing the error text.
|
|
352
|
+
|
|
353
|
+
> **Limitation:**
|
|
354
|
+
> Dynamic form changes (adding or removing controls at runtime) are **not automatically handled** in this version.
|
|
355
|
+
> If you add or remove controls after initialization, you must manually re-run the error setup logic (e.g., by calling the directive's setup method again).
|
|
356
|
+
> Support for automatic handling of dynamic form changes is planned for a future release.
|
|
357
|
+
|
|
358
|
+
## Running unit tests
|
|
359
|
+
|
|
360
|
+
Run `nx test spider-baby-utils-forms` to execute the unit tests.
|
|
361
|
+
|
|
362
|
+
## Contributing
|
|
363
|
+
|
|
364
|
+
This library is part of the Spider Baby ecosystem. For contributions:
|
|
365
|
+
|
|
366
|
+
1. Follow the established coding standards
|
|
367
|
+
2. Add comprehensive tests for new features
|
|
368
|
+
3. Update documentation for API changes
|
|
369
|
+
4. Ensure accessibility compliance
|
|
370
|
+
|
|
371
|
+
## License
|
|
372
|
+
|
|
373
|
+
Part of the Spider Baby utilities collection.
|
|
374
|
+
|
|
375
|
+
---
|
|
376
|
+
|
|
377
|
+
**Built with ❤️ for better Angular forms**
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { Validators } from '@angular/forms';
|
|
2
|
+
import { validatePhoneNumberLength, ParseError } from 'libphonenumber-js';
|
|
3
|
+
|
|
4
|
+
//##########################//
|
|
5
|
+
class NumericValidation {
|
|
6
|
+
// Custom validator for password confirmation
|
|
7
|
+
static lessThanValidator(minControlName, maxControlName, strictlyLessThan = true, errorMessage) {
|
|
8
|
+
return (control) => {
|
|
9
|
+
const minControl = control.get(minControlName);
|
|
10
|
+
const maxControl = control.get(maxControlName);
|
|
11
|
+
if (!minControl || !maxControl)
|
|
12
|
+
return null;
|
|
13
|
+
const minValue = Number(minControl.value);
|
|
14
|
+
const maxValue = Number(maxControl.value);
|
|
15
|
+
if (isNaN(minValue) || isNaN(maxValue))
|
|
16
|
+
return null;
|
|
17
|
+
if (minValue < maxValue)
|
|
18
|
+
return null;
|
|
19
|
+
if (!strictlyLessThan && minValue == maxValue)
|
|
20
|
+
return null;
|
|
21
|
+
return {
|
|
22
|
+
minMaxError: {
|
|
23
|
+
message: errorMessage ?? `${minControlName} must be less ${!strictlyLessThan ? 'or equal to' : ''} than ${maxControlName}`,
|
|
24
|
+
code: 'minMaxError',
|
|
25
|
+
minControlName,
|
|
26
|
+
maxControlName
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
} //Cls
|
|
32
|
+
|
|
33
|
+
//##########################//
|
|
34
|
+
class PasswordValidation {
|
|
35
|
+
static validationArray = (minLength = 6) => [
|
|
36
|
+
Validators.required,
|
|
37
|
+
Validators.minLength(minLength),
|
|
38
|
+
PasswordValidation.hasLowercaseValidator(),
|
|
39
|
+
PasswordValidation.hasUppercaseValidator(),
|
|
40
|
+
PasswordValidation.hasNumberValidator(),
|
|
41
|
+
PasswordValidation.hasNonAlphaNumericValidator(),
|
|
42
|
+
];
|
|
43
|
+
//----------------------------//
|
|
44
|
+
// Custom validator for password confirmation
|
|
45
|
+
static matchValidator(passwordControlName = 'password', confirmPasswordControlName = 'confirmPassword', errorMessage = "Passwords don't match") {
|
|
46
|
+
return (control) => {
|
|
47
|
+
const password = control.get(passwordControlName);
|
|
48
|
+
const confirmPassword = control.get(confirmPasswordControlName);
|
|
49
|
+
if (!password || !confirmPassword)
|
|
50
|
+
return null;
|
|
51
|
+
return password.value === confirmPassword.value
|
|
52
|
+
? null
|
|
53
|
+
: { passwordMismatch: errorMessage ?? "Passwords don't match" };
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
//----------------------------//
|
|
57
|
+
static patternValidator(regex, error) {
|
|
58
|
+
return (control) => {
|
|
59
|
+
// if control is empty return no error
|
|
60
|
+
if (!control.value)
|
|
61
|
+
return null;
|
|
62
|
+
// test the value of the control against the regexp supplied
|
|
63
|
+
const valid = regex.test(control.value);
|
|
64
|
+
// if true, return no error (no error), else return error passed in the second parameter
|
|
65
|
+
return valid ? null : error;
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
//----------------------------//
|
|
69
|
+
static hasUppercaseValidator() {
|
|
70
|
+
return PasswordValidation.patternValidator(/[A-Z]/, {
|
|
71
|
+
hasUpper: 'Password must contain at least 1 uppercase letter',
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
//----------------------------//
|
|
75
|
+
static hasLowercaseValidator() {
|
|
76
|
+
return PasswordValidation.patternValidator(/[a-z]/, {
|
|
77
|
+
hasLower: 'Password must contain at least 1 lowercase letter',
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
//----------------------------//
|
|
81
|
+
static hasNumberValidator() {
|
|
82
|
+
return PasswordValidation.patternValidator(/\d/, {
|
|
83
|
+
hasNumber: 'Password must contain at least 1 number',
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
//----------------------------//
|
|
87
|
+
static hasNonAlphaNumericValidator() {
|
|
88
|
+
return PasswordValidation.patternValidator(/\W/, {
|
|
89
|
+
hasNonAlpahaNumeric: 'Password must contain at least 1 non-alphanumeric character',
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
} //Cls
|
|
93
|
+
|
|
94
|
+
const StrongPassword6Regx = /^(?=[^A-Z]*[A-Z])(?=[^a-z]*[a-z])(?=\D*\d).{6,}$/;
|
|
95
|
+
const StrongPassword6WithSpecialRegx = /^(?=[^A-Z]*[A-Z])(?=[^a-z]*[a-z])(?=\D*\d)(?=.*[!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?]).{6,}$/;
|
|
96
|
+
const StrongPassword8Regx = /^(?=[^A-Z]*[A-Z])(?=[^a-z]*[a-z])(?=\D*\d).{8,}$/;
|
|
97
|
+
const StrongPassword8WithSpecialRegx = /^(?=[^A-Z]*[A-Z])(?=[^a-z]*[a-z])(?=\D*\d)(?=.*[!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?]).{8,}$/;
|
|
98
|
+
const StrongPassword10Regx = /^(?=[^A-Z]*[A-Z])(?=[^a-z]*[a-z])(?=\D*\d).{10,}$/;
|
|
99
|
+
const StrongPassword10WithSpecialRegx = /^(?=[^A-Z]*[A-Z])(?=[^a-z]*[a-z])(?=\D*\d)(?=.*[!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?]).{10,}$/;
|
|
100
|
+
|
|
101
|
+
//##########################//
|
|
102
|
+
class PhoneValidation {
|
|
103
|
+
// Custom validator for password confirmation
|
|
104
|
+
static validator(defaultCountry = 'IE') {
|
|
105
|
+
return (control) => {
|
|
106
|
+
const value = control.value;
|
|
107
|
+
if (!value)
|
|
108
|
+
return null;
|
|
109
|
+
try {
|
|
110
|
+
const result = validatePhoneNumberLength(value, defaultCountry);
|
|
111
|
+
if (!result)
|
|
112
|
+
return null; // valid length
|
|
113
|
+
let reason = '';
|
|
114
|
+
reason = ` : ${result}`;
|
|
115
|
+
return {
|
|
116
|
+
phoneNumber: {
|
|
117
|
+
message: `Invalid phone number. Reason: ${reason}`,
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
// return null; // valid
|
|
121
|
+
}
|
|
122
|
+
catch (error) {
|
|
123
|
+
let reason = '';
|
|
124
|
+
if (error instanceof ParseError)
|
|
125
|
+
reason = ` : ${error.message}`;
|
|
126
|
+
return {
|
|
127
|
+
phoneNumber: {
|
|
128
|
+
message: `Invalid phone number. Reason: ${reason}`,
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
} //Cls
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Generated bundle index. Do not edit.
|
|
138
|
+
*/
|
|
139
|
+
|
|
140
|
+
export { NumericValidation, PasswordValidation, PhoneValidation, StrongPassword10Regx, StrongPassword10WithSpecialRegx, StrongPassword6Regx, StrongPassword6WithSpecialRegx, StrongPassword8Regx, StrongPassword8WithSpecialRegx };
|
|
141
|
+
//# sourceMappingURL=spider-baby-utils-forms-validators.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"spider-baby-utils-forms-validators.mjs","sources":["../../../../../libs/utils/forms/validators/src/lib/numeric-validators.ts","../../../../../libs/utils/forms/validators/src/lib/password-validators.ts","../../../../../libs/utils/forms/validators/src/lib/pwd-regexes.ts","../../../../../libs/utils/forms/validators/src/lib/phone-validators.ts","../../../../../libs/utils/forms/validators/src/spider-baby-utils-forms-validators.ts"],"sourcesContent":["import {\r\n AbstractControl,\r\n ValidatorFn\r\n} from '@angular/forms';\r\n\r\n\r\n//##########################//\r\n\r\n\r\ninterface MinMaxValidationResult {\r\n minMaxError?: {\r\n message: string;\r\n code?: string;\r\n minControlName?: string;\r\n maxControlName?: string;\r\n };\r\n}\r\n\r\n\r\n//##########################//\r\n\r\nexport class NumericValidation {\r\n\r\n // Custom validator for password confirmation\r\n static lessThanValidator(\r\n minControlName: string,\r\n maxControlName: string,\r\n strictlyLessThan: boolean = true,\r\n errorMessage?: string ): ValidatorFn {\r\n\r\n return (control: AbstractControl): MinMaxValidationResult | null => {\r\n const minControl = control.get(minControlName);\r\n const maxControl = control.get(maxControlName);\r\n\r\n if (!minControl || !maxControl)\r\n return null;\r\n\r\n const minValue = Number(minControl.value);\r\n const maxValue = Number(maxControl.value);\r\n \r\n\r\n if (isNaN(minValue) || isNaN(maxValue))\r\n return null;\r\n\r\n if (minValue < maxValue)\r\n return null;\r\n\r\n if (!strictlyLessThan && minValue == maxValue)\r\n return null;\r\n return {\r\n minMaxError: {\r\n message: errorMessage ?? `${minControlName} must be less ${!strictlyLessThan ? 'or equal to': ''} than ${maxControlName}`,\r\n code: 'minMaxError',\r\n minControlName,\r\n maxControlName\r\n }\r\n }\r\n }\r\n\r\n }\r\n\r\n} //Cls\r\n","import {\r\n ValidatorFn,\r\n AbstractControl,\r\n ValidationErrors,\r\n Validators,\r\n} from '@angular/forms';\r\n\r\n\r\n//##########################//\r\n\r\n\r\ninterface PwdMatchValidationResult {\r\n passwordMismatch?: string;\r\n}\r\n\r\n\r\n//##########################//\r\n\r\nexport class PasswordValidation {\r\n\r\n static validationArray = (minLength = 6) => [\r\n Validators.required,\r\n Validators.minLength(minLength),\r\n PasswordValidation.hasLowercaseValidator(),\r\n PasswordValidation.hasUppercaseValidator(),\r\n PasswordValidation.hasNumberValidator(),\r\n PasswordValidation.hasNonAlphaNumericValidator(),\r\n ]\r\n\r\n\r\n //----------------------------//\r\n\r\n\r\n // Custom validator for password confirmation\r\n static matchValidator(\r\n passwordControlName: string = 'password',\r\n confirmPasswordControlName: string = 'confirmPassword',\r\n errorMessage: string = \"Passwords don't match\"): ValidatorFn {\r\n\r\n return (control: AbstractControl): PwdMatchValidationResult | null => {\r\n const password = control.get(passwordControlName);\r\n const confirmPassword = control.get(confirmPasswordControlName);\r\n\r\n if (!password || !confirmPassword)\r\n return null;\r\n\r\n return password.value === confirmPassword.value\r\n ? null\r\n : { passwordMismatch: errorMessage ?? \"Passwords don't match\" };\r\n }\r\n\r\n }\r\n\r\n //----------------------------//\r\n\r\n static patternValidator(regex: RegExp, error: ValidationErrors): ValidatorFn {\r\n return (control: AbstractControl): { [key: string]: any } | null => {\r\n // if control is empty return no error\r\n if (!control.value) return null;\r\n\r\n // test the value of the control against the regexp supplied\r\n const valid = regex.test(control.value);\r\n\r\n // if true, return no error (no error), else return error passed in the second parameter\r\n return valid ? null : error;\r\n }; \r\n }\r\n\r\n //----------------------------//\r\n\r\n static hasUppercaseValidator(): ValidatorFn {\r\n return PasswordValidation.patternValidator(/[A-Z]/, {\r\n hasUpper: 'Password must contain at least 1 uppercase letter',\r\n });\r\n }\r\n\r\n //----------------------------//\r\n\r\n static hasLowercaseValidator(): ValidatorFn {\r\n return PasswordValidation.patternValidator(/[a-z]/, {\r\n hasLower: 'Password must contain at least 1 lowercase letter',\r\n });\r\n }\r\n\r\n //----------------------------//\r\n\r\n static hasNumberValidator(): ValidatorFn {\r\n return PasswordValidation.patternValidator(/\\d/, {\r\n hasNumber: 'Password must contain at least 1 number',\r\n });\r\n }\r\n\r\n //----------------------------//\r\n\r\n static hasNonAlphaNumericValidator(): ValidatorFn {\r\n return PasswordValidation.patternValidator(/\\W/, {\r\n hasNonAlpahaNumeric:\r\n 'Password must contain at least 1 non-alphanumeric character',\r\n });\r\n }\r\n\r\n\r\n} //Cls\r\n","export const StrongPassword6Regx: RegExp =\r\n /^(?=[^A-Z]*[A-Z])(?=[^a-z]*[a-z])(?=\\D*\\d).{6,}$/\r\n\r\nexport const StrongPassword6WithSpecialRegx: RegExp =\r\n /^(?=[^A-Z]*[A-Z])(?=[^a-z]*[a-z])(?=\\D*\\d)(?=.*[!@#$%^&*()_+\\-=[\\]{};':\"\\\\|,.<>/?]).{6,}$/\r\n\r\nexport const StrongPassword8Regx: RegExp =\r\n /^(?=[^A-Z]*[A-Z])(?=[^a-z]*[a-z])(?=\\D*\\d).{8,}$/\r\n\r\nexport const StrongPassword8WithSpecialRegx: RegExp =\r\n /^(?=[^A-Z]*[A-Z])(?=[^a-z]*[a-z])(?=\\D*\\d)(?=.*[!@#$%^&*()_+\\-=[\\]{};':\"\\\\|,.<>/?]).{8,}$/\r\n\r\nexport const StrongPassword10Regx: RegExp =\r\n /^(?=[^A-Z]*[A-Z])(?=[^a-z]*[a-z])(?=\\D*\\d).{10,}$/\r\n\r\nexport const StrongPassword10WithSpecialRegx: RegExp =\r\n /^(?=[^A-Z]*[A-Z])(?=[^a-z]*[a-z])(?=\\D*\\d)(?=.*[!@#$%^&*()_+\\-=[\\]{};':\"\\\\|,.<>/?]).{10,}$/\r\n\r\n","import { AbstractControl, ValidatorFn } from '@angular/forms';\r\nimport { devConsole } from '@spider-baby/dev-console';\r\nimport { CountryCode, ParseError, validatePhoneNumberLength } from 'libphonenumber-js';\r\n\r\n//##########################//\r\n\r\n\r\ninterface PhoneValidationResult {\r\n phoneNumber?: {\r\n message: string;\r\n }\r\n}\r\n\r\n\r\n//##########################//\r\n\r\nexport class PhoneValidation {\r\n\r\n // Custom validator for password confirmation\r\n static validator(defaultCountry: CountryCode = 'IE'): ValidatorFn {\r\n return (control: AbstractControl): PhoneValidationResult | null => {\r\n const value = control.value;\r\n if (!value) \r\n return null;\r\n try {\r\n const result = validatePhoneNumberLength(value, defaultCountry);\r\n \r\n if (!result)\r\n return null; // valid length\r\n let reason = '';\r\n reason = ` : ${result}`;\r\n return {\r\n phoneNumber: {\r\n message: `Invalid phone number. Reason: ${reason}`,\r\n }\r\n };\r\n\r\n // return null; // valid\r\n } catch (error: unknown) {\r\n let reason = '';\r\n if (error instanceof ParseError)\r\n reason = ` : ${error.message}`;\r\n return {\r\n phoneNumber: {\r\n message: `Invalid phone number. Reason: ${reason}`,\r\n }\r\n };\r\n }\r\n };\r\n }\r\n\r\n} //Cls\r\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './index';\n"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
import { NgTemplateOutlet, isPlatformBrowser } from '@angular/common';
|
|
2
|
+
import * as i0 from '@angular/core';
|
|
3
|
+
import { input, Component, inject, PLATFORM_ID, Renderer2, ElementRef, Input, Directive, Injectable } from '@angular/core';
|
|
4
|
+
import '@angular/forms';
|
|
5
|
+
import { filter } from 'rxjs';
|
|
6
|
+
import { startWith, map } from 'rxjs/operators';
|
|
7
|
+
|
|
8
|
+
class FirstErrorComponent {
|
|
9
|
+
control = input.required();
|
|
10
|
+
customErrorTemplate = input(undefined);
|
|
11
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.9", ngImport: i0, type: FirstErrorComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
12
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.9", type: FirstErrorComponent, isStandalone: true, selector: "sb-first-error", inputs: { control: { classPropertyName: "control", publicName: "control", isSignal: true, isRequired: true, transformFunction: null }, customErrorTemplate: { classPropertyName: "customErrorTemplate", publicName: "customErrorTemplate", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: `
|
|
13
|
+
@if(this.control().errors?.['firstError']; as err) {
|
|
14
|
+
@if(customErrorTemplate(); as template){
|
|
15
|
+
<ng-container
|
|
16
|
+
[ngTemplateOutlet]="template"
|
|
17
|
+
[ngTemplateOutletContext]="{errorMessage: err}"/>
|
|
18
|
+
}@else {
|
|
19
|
+
<span class="error"
|
|
20
|
+
[attr.aria-live]="'polite'"
|
|
21
|
+
[attr.role]="'alert'">
|
|
22
|
+
{{err}}
|
|
23
|
+
</span>
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
`, isInline: true, styles: [":host{display:block;--error-color: var(--mat-sys-error, #d9534f)}.error{color:var(--error-color, #d9534f );font-size:.875rem;margin-top:.25rem;display:block;animation:fadeIn .3s ease-in}@keyframes fadeIn{0%{opacity:0;transform:translateY(-2px)}to{opacity:1;transform:translateY(0)}}\n"], dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }] });
|
|
27
|
+
}
|
|
28
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.9", ngImport: i0, type: FirstErrorComponent, decorators: [{
|
|
29
|
+
type: Component,
|
|
30
|
+
args: [{ selector: 'sb-first-error', standalone: true, imports: [NgTemplateOutlet], template: `
|
|
31
|
+
@if(this.control().errors?.['firstError']; as err) {
|
|
32
|
+
@if(customErrorTemplate(); as template){
|
|
33
|
+
<ng-container
|
|
34
|
+
[ngTemplateOutlet]="template"
|
|
35
|
+
[ngTemplateOutletContext]="{errorMessage: err}"/>
|
|
36
|
+
}@else {
|
|
37
|
+
<span class="error"
|
|
38
|
+
[attr.aria-live]="'polite'"
|
|
39
|
+
[attr.role]="'alert'">
|
|
40
|
+
{{err}}
|
|
41
|
+
</span>
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
`, styles: [":host{display:block;--error-color: var(--mat-sys-error, #d9534f)}.error{color:var(--error-color, #d9534f );font-size:.875rem;margin-top:.25rem;display:block;animation:fadeIn .3s ease-in}@keyframes fadeIn{0%{opacity:0;transform:translateY(-2px)}to{opacity:1;transform:translateY(0)}}\n"] }]
|
|
45
|
+
}] });
|
|
46
|
+
|
|
47
|
+
const errorMessageMap = new Map([
|
|
48
|
+
['required', (fieldName) => `${fieldName} is required.`],
|
|
49
|
+
['email', () => 'Please enter a valid email address.'],
|
|
50
|
+
['minlength', (fieldName, errorValue) => !errorValue ? 'Value is too short' : `${fieldName} must be at least ${errorValue?.requiredLength} characters.`],
|
|
51
|
+
['maxlength', (fieldName, errorValue) => !errorValue ? 'Value is too long' : `${fieldName} must be no more than ${errorValue?.requiredLength} characters.`],
|
|
52
|
+
['pattern', (fieldName) => `${fieldName} format is invalid.`],
|
|
53
|
+
['min', (fieldName, errorValue) => !errorValue ? 'Value is too small' : `${fieldName} must be at least ${errorValue?.min}.`],
|
|
54
|
+
['max', (fieldName, errorValue) => !errorValue ? 'Value is too large' : `${fieldName} must be no more than ${errorValue?.max}.`],
|
|
55
|
+
['passwordMismatch', () => 'Passwords do not match.'],
|
|
56
|
+
['mustMatch', () => 'Fields do not match.'],
|
|
57
|
+
['whitespace', (fieldName) => `${fieldName} cannot contain only whitespace.`],
|
|
58
|
+
['forbiddenValue', (fieldName, errorValue) => !errorValue ? 'This value is not allowed' : `${fieldName} cannot be "${errorValue?.value}".`],
|
|
59
|
+
['asyncValidation', (fieldName) => `${fieldName} validation is pending...`],
|
|
60
|
+
['invalidDate', () => 'Please enter a valid date.'],
|
|
61
|
+
['futureDate', () => 'Date must be in the future.'],
|
|
62
|
+
['pastDate', () => 'Date must be in the past.'],
|
|
63
|
+
['strongPassword', () => 'Password must contain at least one uppercase letter, one lowercase letter, one number, and one special character.'],
|
|
64
|
+
['phoneNumber', (fieldName, errorValue) => !errorValue.message ? 'Please enter a valid phone number.' : errorValue.message],
|
|
65
|
+
['url', () => 'Please enter a valid URL.'],
|
|
66
|
+
['unique', (fieldName) => `This ${fieldName.toLowerCase()} is already taken.`],
|
|
67
|
+
['fileSize', (fieldName, errorValue) => !errorValue ? 'File is too large' : `File size must be less than ${errorValue?.maxSize}.`],
|
|
68
|
+
['fileType', (fieldName, errorValue) => !errorValue ? 'Invalid file type' : `Only ${errorValue?.allowedTypes?.join(', ')} files are allowed.`]
|
|
69
|
+
]);
|
|
70
|
+
//##########################//
|
|
71
|
+
class FormErrors {
|
|
72
|
+
static setFirstErrors(form, customErrorMessages) {
|
|
73
|
+
const controls = form.controls;
|
|
74
|
+
for (const name in controls) {
|
|
75
|
+
const control = controls[name];
|
|
76
|
+
if (control.invalid)
|
|
77
|
+
this.setFirstErrorMessage(name, control, customErrorMessages);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
//----------------------------//
|
|
81
|
+
static setFirstErrorMessage(name, control, customErrorMessages) {
|
|
82
|
+
const currentErrors = control.errors;
|
|
83
|
+
const firstErrorMessage = FormErrors.getFirstErrorMessage(name, control, customErrorMessages);
|
|
84
|
+
if (firstErrorMessage)
|
|
85
|
+
control.setErrors({ ...currentErrors, firstError: firstErrorMessage }, { emitEvent: false } // This prevents statusChanges emission
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
//----------------------------//
|
|
89
|
+
static getFirstErrorMessage(name, control, customErrorMessages) {
|
|
90
|
+
const errorKey = this.firstErrorKey(control);
|
|
91
|
+
if (!errorKey)
|
|
92
|
+
return null;
|
|
93
|
+
const errorValue = control.errors?.[errorKey];
|
|
94
|
+
const fieldName = this.toTitleCase(name);
|
|
95
|
+
// Handle string error values (custom error messages)
|
|
96
|
+
if (typeof errorValue === 'string')
|
|
97
|
+
return errorValue;
|
|
98
|
+
const errorMessageFn = customErrorMessages?.get(errorKey) ?? errorMessageMap.get(errorKey);
|
|
99
|
+
// Get error message function and call it
|
|
100
|
+
if (errorMessageFn)
|
|
101
|
+
return errorMessageFn(fieldName, errorValue);
|
|
102
|
+
// Fallback for unknown error types
|
|
103
|
+
return `Invalid value for ${fieldName}.`;
|
|
104
|
+
}
|
|
105
|
+
//----------------------------//
|
|
106
|
+
static firstErrorKey = (control) => Object.keys(control.errors || {}).length > 0 ? Object.keys(control.errors || {})[0] : null;
|
|
107
|
+
//----------------------------//
|
|
108
|
+
static toTitleCase(s) {
|
|
109
|
+
const result = s.replace(/([A-Z])/g, ' $1');
|
|
110
|
+
return result.charAt(0).toUpperCase() + result.slice(1);
|
|
111
|
+
}
|
|
112
|
+
} //Cls
|
|
113
|
+
|
|
114
|
+
//##########################//
|
|
115
|
+
class FormUtility {
|
|
116
|
+
static findInvalidControlNames(form) {
|
|
117
|
+
const invalid = new Set();
|
|
118
|
+
const controls = form.controls;
|
|
119
|
+
for (const name in controls) {
|
|
120
|
+
if (controls[name].invalid)
|
|
121
|
+
invalid.add(name);
|
|
122
|
+
}
|
|
123
|
+
return invalid;
|
|
124
|
+
}
|
|
125
|
+
//----------------------------//
|
|
126
|
+
static findInvalidControlInfo(form) {
|
|
127
|
+
const invalid = new Set();
|
|
128
|
+
const controls = form.controls;
|
|
129
|
+
for (const name in controls) {
|
|
130
|
+
if (controls[name].invalid)
|
|
131
|
+
invalid.add(`${name}: ${this.firstErrorKey(controls[name]) || 'Invalid'}`);
|
|
132
|
+
}
|
|
133
|
+
return invalid;
|
|
134
|
+
}
|
|
135
|
+
//----------------------------//
|
|
136
|
+
static findInvalidControls(form) {
|
|
137
|
+
const invalid = [];
|
|
138
|
+
const controls = form.controls;
|
|
139
|
+
for (const name in controls) {
|
|
140
|
+
if (controls[name].invalid)
|
|
141
|
+
invalid.push(controls[name]);
|
|
142
|
+
}
|
|
143
|
+
return invalid;
|
|
144
|
+
}
|
|
145
|
+
//----------------------------//
|
|
146
|
+
static findInvalidControlsData(form) {
|
|
147
|
+
const invalid = [];
|
|
148
|
+
const controls = form.controls;
|
|
149
|
+
for (const name in controls) {
|
|
150
|
+
const control = controls[name];
|
|
151
|
+
if (control.invalid)
|
|
152
|
+
invalid.push({ name, control });
|
|
153
|
+
}
|
|
154
|
+
return invalid;
|
|
155
|
+
}
|
|
156
|
+
//----------------------------//
|
|
157
|
+
static replaceNullWithUndefined(obj) {
|
|
158
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
159
|
+
if (value === null)
|
|
160
|
+
obj[key] = undefined;
|
|
161
|
+
}
|
|
162
|
+
return obj;
|
|
163
|
+
}
|
|
164
|
+
//----------------------------//
|
|
165
|
+
static getFirstFormError(form) {
|
|
166
|
+
if (form.valid)
|
|
167
|
+
return null;
|
|
168
|
+
// Check form-level errors first
|
|
169
|
+
if (form.errors) {
|
|
170
|
+
const firstKey = Object.keys(form.errors)[0];
|
|
171
|
+
return { [firstKey]: form.errors[firstKey] };
|
|
172
|
+
}
|
|
173
|
+
// Check control-specific errors
|
|
174
|
+
for (const key of Object.keys(form.controls)) {
|
|
175
|
+
const control = form.get(key);
|
|
176
|
+
if (control?.errors) {
|
|
177
|
+
const errorKey = Object.keys(control.errors)[0];
|
|
178
|
+
return { [errorKey]: control.errors[errorKey] };
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
//----------------------------//
|
|
184
|
+
static firstErrorKey = (control) => Object.keys(control.errors || {}).length > 0 ? Object.keys(control.errors || {})[0] : null;
|
|
185
|
+
} //Cls
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Directive: sbFormControlFirstError
|
|
189
|
+
*
|
|
190
|
+
* Automatically manages and displays the first validation error for each control in a FormGroup.
|
|
191
|
+
* - Shows errors only after controls are touched (unless showUntouched is true)
|
|
192
|
+
* - Dynamically adds blur/focusout listeners for untouched invalid controls
|
|
193
|
+
* - Supports custom error messages via CustomErrorMessageMap
|
|
194
|
+
* - Cleans up all listeners and subscriptions on destroy
|
|
195
|
+
* - SSR-safe: all DOM access is guarded by isPlatformBrowser
|
|
196
|
+
*
|
|
197
|
+
* Limitations:
|
|
198
|
+
* - Dynamic form changes (adding/removing controls at runtime) are NOT automatically handled in this version.
|
|
199
|
+
* If you add or remove controls after initialization, you must manually re-run error setup logic.
|
|
200
|
+
* (This feature is planned for a future release.)
|
|
201
|
+
*/
|
|
202
|
+
class FirstErrorDirective {
|
|
203
|
+
_platformId = inject(PLATFORM_ID);
|
|
204
|
+
_renderer = inject(Renderer2);
|
|
205
|
+
_host = inject(ElementRef);
|
|
206
|
+
set sbFormControlFirstError(form) {
|
|
207
|
+
this._form = form;
|
|
208
|
+
this.observeValueChanges(this._form);
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Custom error messages map to override default error messages.
|
|
212
|
+
* If map returns undefined for a specific error, the default message map will be used.
|
|
213
|
+
*/
|
|
214
|
+
customErrorMessages;
|
|
215
|
+
/**
|
|
216
|
+
* If true, errors will be shown immediately for untouched controls.
|
|
217
|
+
* If false, errors will only be shown after the control is touched.
|
|
218
|
+
* Default is false.
|
|
219
|
+
*/
|
|
220
|
+
showUntouched = false;
|
|
221
|
+
//- - - - - - - - - - - - - - //
|
|
222
|
+
_form;
|
|
223
|
+
_vcSub;
|
|
224
|
+
blurListeners = new Map();
|
|
225
|
+
//----------------------------//
|
|
226
|
+
ngOnDestroy() {
|
|
227
|
+
this._vcSub?.unsubscribe();
|
|
228
|
+
this.removeAllBlurListeners();
|
|
229
|
+
}
|
|
230
|
+
//----------------------------//
|
|
231
|
+
addBlurListener(controlName, control) {
|
|
232
|
+
// Find the input element by formControlName
|
|
233
|
+
const input = this._host.nativeElement.querySelector(`[formControlName="${controlName}"]`);
|
|
234
|
+
if (!input)
|
|
235
|
+
return;
|
|
236
|
+
// Use Renderer2 to listen for 'focusout'
|
|
237
|
+
const unlisten = this._renderer.listen(input, 'focusout', () => {
|
|
238
|
+
if (!control.errors?.['firstError']) {
|
|
239
|
+
FormErrors.setFirstErrorMessage(controlName, control, this.customErrorMessages);
|
|
240
|
+
}
|
|
241
|
+
// Remove the event listener after setting the error
|
|
242
|
+
unlisten();
|
|
243
|
+
this.blurListeners.delete(controlName);
|
|
244
|
+
});
|
|
245
|
+
this.blurListeners.set(controlName, unlisten);
|
|
246
|
+
}
|
|
247
|
+
//- - - - - - - - - - - - - - //
|
|
248
|
+
removeAllBlurListeners() {
|
|
249
|
+
for (const unlisten of this.blurListeners.values())
|
|
250
|
+
unlisten();
|
|
251
|
+
this.blurListeners.clear();
|
|
252
|
+
}
|
|
253
|
+
//----------------------------//
|
|
254
|
+
observeValueChanges(form) {
|
|
255
|
+
if (!isPlatformBrowser(this._platformId))
|
|
256
|
+
return;
|
|
257
|
+
this._vcSub?.unsubscribe();
|
|
258
|
+
this._vcSub = form.statusChanges
|
|
259
|
+
.pipe(startWith('PENDING'), // Start with non-Invalid so the first error will be set on blur if the user clicks input without entering any data
|
|
260
|
+
filter(() => form.status === 'INVALID'), map(() => FormUtility.findInvalidControlsData(form)))
|
|
261
|
+
.subscribe((invalidControlData) => {
|
|
262
|
+
for (const controlData of invalidControlData) {
|
|
263
|
+
const control = controlData.control;
|
|
264
|
+
const name = controlData.name;
|
|
265
|
+
// Skip if firstError is already set
|
|
266
|
+
if (control.errors?.['firstError'])
|
|
267
|
+
continue;
|
|
268
|
+
if (this.showUntouched || control.touched) {
|
|
269
|
+
FormErrors.setFirstErrorMessage(name, control, this.customErrorMessages);
|
|
270
|
+
}
|
|
271
|
+
else if (!control.touched) {
|
|
272
|
+
// Add blur listener if not already present
|
|
273
|
+
if (!this.blurListeners.has(name))
|
|
274
|
+
this.addBlurListener(name, control);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.9", ngImport: i0, type: FirstErrorDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
280
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.2.9", type: FirstErrorDirective, isStandalone: true, selector: "[sbFormControlFirstError]", inputs: { sbFormControlFirstError: "sbFormControlFirstError", customErrorMessages: "customErrorMessages", showUntouched: "showUntouched" }, ngImport: i0 });
|
|
281
|
+
} //Cls
|
|
282
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.9", ngImport: i0, type: FirstErrorDirective, decorators: [{
|
|
283
|
+
type: Directive,
|
|
284
|
+
args: [{
|
|
285
|
+
selector: '[sbFormControlFirstError]',
|
|
286
|
+
standalone: true
|
|
287
|
+
}]
|
|
288
|
+
}], propDecorators: { sbFormControlFirstError: [{
|
|
289
|
+
type: Input,
|
|
290
|
+
args: [{ required: true }]
|
|
291
|
+
}], customErrorMessages: [{
|
|
292
|
+
type: Input
|
|
293
|
+
}], showUntouched: [{
|
|
294
|
+
type: Input
|
|
295
|
+
}] } });
|
|
296
|
+
|
|
297
|
+
class RemoveNullsService {
|
|
298
|
+
remove = (obj) => RemoveNulls(obj, true);
|
|
299
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.9", ngImport: i0, type: RemoveNullsService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
300
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.9", ngImport: i0, type: RemoveNullsService, providedIn: 'root' });
|
|
301
|
+
}
|
|
302
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.9", ngImport: i0, type: RemoveNullsService, decorators: [{
|
|
303
|
+
type: Injectable,
|
|
304
|
+
args: [{
|
|
305
|
+
providedIn: 'root',
|
|
306
|
+
}]
|
|
307
|
+
}] });
|
|
308
|
+
//==============================//
|
|
309
|
+
function RemoveNulls(obj, iterate = true) {
|
|
310
|
+
if (obj === null || obj === undefined)
|
|
311
|
+
return obj;
|
|
312
|
+
// Handle arrays
|
|
313
|
+
if (Array.isArray(obj)) {
|
|
314
|
+
return obj
|
|
315
|
+
.filter(item => item !== null && item !== undefined) // Remove null/undefined items
|
|
316
|
+
.map(item => iterate && typeof item === 'object' ? RemoveNulls(item, iterate) : item);
|
|
317
|
+
}
|
|
318
|
+
// Handle non-objects (primitives)
|
|
319
|
+
if (typeof obj !== 'object')
|
|
320
|
+
return obj;
|
|
321
|
+
const cleaned = structuredClone(obj);
|
|
322
|
+
for (const key in cleaned) {
|
|
323
|
+
if (cleaned[key] === null || cleaned[key] === undefined)
|
|
324
|
+
delete cleaned[key];
|
|
325
|
+
if (cleaned[key] instanceof Object && iterate)
|
|
326
|
+
cleaned[key] = RemoveNulls(cleaned[key], iterate);
|
|
327
|
+
}
|
|
328
|
+
return cleaned;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Generated bundle index. Do not edit.
|
|
333
|
+
*/
|
|
334
|
+
|
|
335
|
+
export { FirstErrorComponent, FirstErrorDirective, FormErrors, FormUtility, RemoveNulls, RemoveNullsService };
|
|
336
|
+
//# sourceMappingURL=spider-baby-utils-forms.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"spider-baby-utils-forms.mjs","sources":["../../../../../libs/utils/forms/src/lib/first-error.component.ts","../../../../../libs/utils/forms/src/lib/form-errors.ts","../../../../../libs/utils/forms/src/lib/form-utility.ts","../../../../../libs/utils/forms/src/lib/first-error.directive.ts","../../../../../libs/utils/forms/src/lib/remove-nulls.service.ts","../../../../../libs/utils/forms/src/spider-baby-utils-forms.ts"],"sourcesContent":["import { NgTemplateOutlet } from '@angular/common';\r\nimport { Component, input, TemplateRef } from \"@angular/core\";\r\nimport { AbstractControl } from \"@angular/forms\";\r\n\r\n@Component({\r\n selector: 'sb-first-error',\r\n standalone: true,\r\n imports: [NgTemplateOutlet],\r\n template: `\r\n @if(this.control().errors?.['firstError']; as err) {\r\n @if(customErrorTemplate(); as template){\r\n <ng-container \r\n [ngTemplateOutlet]=\"template\" \r\n [ngTemplateOutletContext]=\"{errorMessage: err}\"/>\r\n }@else {\r\n <span class=\"error\" \r\n [attr.aria-live]=\"'polite'\"\r\n [attr.role]=\"'alert'\">\r\n {{err}}\r\n </span>\r\n }\r\n }\r\n `,\r\n styles: [`\r\n :host {\r\n display: block;\r\n --error-color: var(--mat-sys-error, #d9534f);\r\n }\r\n .error {\r\n color: var(--error-color, #d9534f );\r\n font-size: 0.875rem;\r\n margin-top: 0.25rem;\r\n display: block;\r\n animation: fadeIn 0.3s ease-in;\r\n }\r\n \r\n @keyframes fadeIn {\r\n from { opacity: 0; transform: translateY(-2px); }\r\n to { opacity: 1; transform: translateY(0); }\r\n }\r\n `]\r\n})\r\nexport class FirstErrorComponent {\r\n\r\n control = input.required<AbstractControl>();\r\n customErrorTemplate = input<TemplateRef<unknown> | undefined>(undefined)\r\n\r\n}","/* eslint-disable @typescript-eslint/no-explicit-any */\r\nimport { AbstractControl, FormGroup } from \"@angular/forms\";\r\n\r\n\r\n//##########################//\r\n\r\nexport type ErrorMessageFunction = (fieldName: string, errorValue: any) => string;\r\nexport type CustomErrorMessageMap = Map<string, ErrorMessageFunction>;\r\n\r\n\r\nconst errorMessageMap: CustomErrorMessageMap = new Map<string, ErrorMessageFunction>([\r\n ['required', (fieldName) => `${fieldName} is required.`],\r\n ['email', () => 'Please enter a valid email address.'],\r\n ['minlength', (fieldName, errorValue) => !errorValue ? 'Value is too short' : `${fieldName} must be at least ${errorValue?.requiredLength} characters.`],\r\n ['maxlength', (fieldName, errorValue) => !errorValue ? 'Value is too long' : `${fieldName} must be no more than ${errorValue?.requiredLength} characters.`],\r\n ['pattern', (fieldName) => `${fieldName} format is invalid.`],\r\n ['min', (fieldName, errorValue) => !errorValue ? 'Value is too small' : `${fieldName} must be at least ${errorValue?.min}.`],\r\n ['max', (fieldName, errorValue) => !errorValue ? 'Value is too large' : `${fieldName} must be no more than ${errorValue?.max}.`],\r\n ['passwordMismatch', () => 'Passwords do not match.'],\r\n ['mustMatch', () => 'Fields do not match.'],\r\n ['whitespace', (fieldName) => `${fieldName} cannot contain only whitespace.`],\r\n ['forbiddenValue', (fieldName, errorValue) => !errorValue ? 'This value is not allowed' : `${fieldName} cannot be \"${errorValue?.value}\".`],\r\n ['asyncValidation', (fieldName) => `${fieldName} validation is pending...`],\r\n ['invalidDate', () => 'Please enter a valid date.'],\r\n ['futureDate', () => 'Date must be in the future.'],\r\n ['pastDate', () => 'Date must be in the past.'],\r\n ['strongPassword', () => 'Password must contain at least one uppercase letter, one lowercase letter, one number, and one special character.'],\r\n ['phoneNumber', (fieldName, errorValue) => !errorValue.message ? 'Please enter a valid phone number.' : errorValue.message],\r\n ['url', () => 'Please enter a valid URL.'],\r\n ['unique', (fieldName) => `This ${fieldName.toLowerCase()} is already taken.`],\r\n ['fileSize', (fieldName, errorValue) => !errorValue ? 'File is too large' : `File size must be less than ${errorValue?.maxSize}.`],\r\n ['fileType', (fieldName, errorValue) => !errorValue ? 'Invalid file type' : `Only ${errorValue?.allowedTypes?.join(', ')} files are allowed.`]\r\n]);\r\n\r\n\r\n//##########################//\r\n\r\n\r\nexport class FormErrors {\r\n\r\n\r\n static setFirstErrors(\r\n form: FormGroup,\r\n customErrorMessages?: CustomErrorMessageMap): void {\r\n\r\n const controls = form.controls\r\n for (const name in controls) {\r\n const control = controls[name]\r\n if (control.invalid)\r\n this.setFirstErrorMessage(name, control, customErrorMessages)\r\n }\r\n }\r\n\r\n\r\n //----------------------------// \r\n\r\n\r\n static setFirstErrorMessage(\r\n name: string,\r\n control: AbstractControl,\r\n customErrorMessages?: CustomErrorMessageMap): void {\r\n\r\n const currentErrors = control.errors\r\n const firstErrorMessage = FormErrors.getFirstErrorMessage(name, control, customErrorMessages)\r\n if (firstErrorMessage)\r\n control.setErrors(\r\n { ...currentErrors, firstError: firstErrorMessage },\r\n { emitEvent: false } // This prevents statusChanges emission\r\n )\r\n\r\n }\r\n\r\n\r\n\r\n //----------------------------// \r\n\r\n\r\n static getFirstErrorMessage(\r\n name: string,\r\n control: AbstractControl,\r\n customErrorMessages?: CustomErrorMessageMap): string | null {\r\n\r\n const errorKey = this.firstErrorKey(control)\r\n if (!errorKey)\r\n return null\r\n\r\n\r\n const errorValue = control.errors?.[errorKey]\r\n const fieldName = this.toTitleCase(name) \r\n\r\n // Handle string error values (custom error messages)\r\n if (typeof errorValue === 'string')\r\n return errorValue\r\n\r\n\r\n const errorMessageFn = customErrorMessages?.get(errorKey) ?? errorMessageMap.get(errorKey);\r\n\r\n\r\n // Get error message function and call it\r\n if (errorMessageFn)\r\n return errorMessageFn(fieldName, errorValue)\r\n\r\n // Fallback for unknown error types\r\n return `Invalid value for ${fieldName}.`\r\n }\r\n\r\n //----------------------------//\r\n\r\n\r\n private static firstErrorKey = (control: AbstractControl): string | null =>\r\n Object.keys(control.errors || {}).length > 0 ? Object.keys(control.errors || {})[0] : null;\r\n\r\n\r\n //----------------------------//\r\n\r\n private static toTitleCase(s: string): string {\r\n const result = s.replace(/([A-Z])/g, ' $1');\r\n return result.charAt(0).toUpperCase() + result.slice(1);\r\n }\r\n\r\n}//Cls","import { AbstractControl, FormGroup } from \"@angular/forms\";\r\n\r\n//##########################//\r\n\r\nexport interface ControlData{\r\n name: string;\r\n control: AbstractControl;\r\n}\r\n\r\n//##########################//\r\n\r\nexport class FormUtility {\r\n\r\n public static findInvalidControlNames(form: FormGroup): Set<string> {\r\n\r\n const invalid = new Set<string>()\r\n const controls = form.controls\r\n\r\n for (const name in controls) {\r\n if (controls[name].invalid)\r\n invalid.add(name)\r\n }\r\n\r\n return invalid\r\n }\r\n\r\n //----------------------------//\r\n\r\n public static findInvalidControlInfo(form: FormGroup): Set<string> {\r\n\r\n const invalid = new Set<string>()\r\n const controls = form.controls\r\n\r\n for (const name in controls) {\r\n if (controls[name].invalid)\r\n invalid.add(`${name}: ${this.firstErrorKey(controls[name]) || 'Invalid'}`)\r\n }\r\n\r\n return invalid\r\n }\r\n\r\n //----------------------------//\r\n\r\n public static findInvalidControls(form: FormGroup): AbstractControl[] {\r\n\r\n const invalid = []\r\n const controls = form.controls\r\n\r\n for (const name in controls) {\r\n if (controls[name].invalid)\r\n invalid.push(controls[name])\r\n }\r\n\r\n return invalid\r\n }\r\n\r\n //----------------------------//\r\n\r\n public static findInvalidControlsData(form: FormGroup): ControlData[] {\r\n\r\n const invalid: { name: string, control: AbstractControl }[] = [];\r\n const controls = form.controls\r\n\r\n for (const name in controls) {\r\n const control = controls[name]\r\n if (control.invalid)\r\n invalid.push({ name, control })\r\n }\r\n\r\n return invalid\r\n }\r\n\r\n //----------------------------//\r\n\r\n\r\n static replaceNullWithUndefined(obj: Record<string, any>): Record<string, any> {\r\n\r\n for (const [key, value] of Object.entries(obj)) {\r\n if (value === null) \r\n obj[key] = undefined;\r\n }\r\n return obj;\r\n }\r\n\r\n //----------------------------//\r\n\r\n static getFirstFormError(form: FormGroup): Record<string, any> | null {\r\n if (form.valid)\r\n return null\r\n\r\n // Check form-level errors first\r\n if (form.errors) {\r\n const firstKey = Object.keys(form.errors)[0]\r\n return { [firstKey]: form.errors[firstKey] }\r\n }\r\n\r\n // Check control-specific errors\r\n for (const key of Object.keys(form.controls)) {\r\n const control = form.get(key)\r\n if (control?.errors) {\r\n const errorKey = Object.keys(control.errors)[0]\r\n return { [errorKey]: control.errors[errorKey] }\r\n }\r\n }\r\n\r\n return null\r\n }\r\n\r\n //----------------------------//\r\n\r\n static firstErrorKey = (control: AbstractControl): string | null =>\r\n Object.keys(control.errors || {}).length > 0 ? Object.keys(control.errors || {})[0] : null;\r\n\r\n\r\n}//Cls","import { isPlatformBrowser } from '@angular/common';\r\nimport { Directive, ElementRef, inject, Input, OnDestroy, PLATFORM_ID, Renderer2 } from '@angular/core';\r\nimport { AbstractControl, FormGroup } from '@angular/forms';\r\nimport { filter, Subscription } from 'rxjs';\r\nimport { map, startWith } from 'rxjs/operators';\r\nimport { CustomErrorMessageMap, FormErrors } from './form-errors';\r\nimport { FormUtility } from './form-utility';\r\n\r\n\r\n/**\r\n * Directive: sbFormControlFirstError\r\n *\r\n * Automatically manages and displays the first validation error for each control in a FormGroup.\r\n * - Shows errors only after controls are touched (unless showUntouched is true)\r\n * - Dynamically adds blur/focusout listeners for untouched invalid controls\r\n * - Supports custom error messages via CustomErrorMessageMap\r\n * - Cleans up all listeners and subscriptions on destroy\r\n * - SSR-safe: all DOM access is guarded by isPlatformBrowser\r\n *\r\n * Limitations:\r\n * - Dynamic form changes (adding/removing controls at runtime) are NOT automatically handled in this version.\r\n * If you add or remove controls after initialization, you must manually re-run error setup logic.\r\n * (This feature is planned for a future release.)\r\n */\r\n@Directive({\r\n selector: '[sbFormControlFirstError]',\r\n standalone: true\r\n})\r\nexport class FirstErrorDirective implements OnDestroy {\r\n\r\n private _platformId = inject(PLATFORM_ID);\r\n private _renderer = inject(Renderer2);\r\n private _host: ElementRef<HTMLElement> = inject(ElementRef);\r\n\r\n\r\n @Input({ required: true }) set sbFormControlFirstError(form: FormGroup) {\r\n this._form = form\r\n this.observeValueChanges(this._form)\r\n }\r\n\r\n /** \r\n * Custom error messages map to override default error messages.\r\n * If map returns undefined for a specific error, the default message map will be used.\r\n */\r\n @Input() customErrorMessages?: CustomErrorMessageMap;\r\n\r\n /**\r\n * If true, errors will be shown immediately for untouched controls.\r\n * If false, errors will only be shown after the control is touched.\r\n * Default is false.\r\n */\r\n @Input() showUntouched: boolean = false; \r\n\r\n //- - - - - - - - - - - - - - //\r\n\r\n private _form?: FormGroup\r\n private _vcSub?: Subscription\r\n private blurListeners = new Map<string, () => void>()\r\n\r\n //----------------------------//\r\n\r\n ngOnDestroy(): void {\r\n this._vcSub?.unsubscribe()\r\n this.removeAllBlurListeners();\r\n }\r\n\r\n //----------------------------//\r\n\r\n private addBlurListener(controlName: string, control: AbstractControl): void {\r\n\r\n // Find the input element by formControlName\r\n const input: HTMLElement | null = this._host.nativeElement.querySelector(`[formControlName=\"${controlName}\"]`);\r\n\r\n if (!input)\r\n return;\r\n\r\n // Use Renderer2 to listen for 'focusout'\r\n const unlisten = this._renderer.listen(input, 'focusout', () => {\r\n if (!control.errors?.['firstError']) {\r\n FormErrors.setFirstErrorMessage(\r\n controlName,\r\n control,\r\n this.customErrorMessages\r\n );\r\n }\r\n // Remove the event listener after setting the error\r\n unlisten();\r\n this.blurListeners.delete(controlName);\r\n });\r\n\r\n this.blurListeners.set(controlName, unlisten);\r\n }\r\n\r\n //- - - - - - - - - - - - - - //\r\n\r\n private removeAllBlurListeners() {\r\n\r\n for (const unlisten of this.blurListeners.values()) \r\n unlisten()\r\n \r\n this.blurListeners.clear();\r\n }\r\n\r\n //----------------------------//\r\n\r\n\r\n private observeValueChanges(form: FormGroup) {\r\n\r\n if (!isPlatformBrowser(this._platformId))\r\n return;\r\n\r\n this._vcSub?.unsubscribe()\r\n this._vcSub = form.statusChanges\r\n .pipe(\r\n startWith('PENDING'), // Start with non-Invalid so the first error will be set on blur if the user clicks input without entering any data\r\n filter(() => form.status === 'INVALID'),\r\n map(() => FormUtility.findInvalidControlsData(form))\r\n )\r\n .subscribe((invalidControlData) => {\r\n\r\n for (const controlData of invalidControlData) {\r\n const control = controlData.control;\r\n const name = controlData.name;\r\n\r\n // Skip if firstError is already set\r\n if (control.errors?.['firstError'])\r\n continue;\r\n\r\n\r\n if (this.showUntouched || control.touched) {\r\n FormErrors.setFirstErrorMessage(name, control, this.customErrorMessages);\r\n } else if (!control.touched) {\r\n // Add blur listener if not already present\r\n if (!this.blurListeners.has(name)) \r\n this.addBlurListener(name, control); \r\n }\r\n }\r\n })\r\n }\r\n\r\n}//Cls\r\n","import { Injectable } from '@angular/core';\r\n\r\n@Injectable({\r\n providedIn: 'root',\r\n})\r\nexport class RemoveNullsService<T> {\r\n\r\n remove = (obj: T): T => RemoveNulls(obj, true)\r\n\r\n} \r\n\r\n\r\n//==============================//\r\n\r\n\r\nexport function RemoveNulls<T>(obj: T, iterate = true): T {\r\n\r\n if (obj === null || obj === undefined)\r\n return obj;\r\n\r\n // Handle arrays\r\n if (Array.isArray(obj)) {\r\n return obj\r\n .filter(item => item !== null && item !== undefined) // Remove null/undefined items\r\n .map(item => iterate && typeof item === 'object' ? RemoveNulls(item, iterate) : item) as T;\r\n }\r\n\r\n // Handle non-objects (primitives)\r\n if (typeof obj !== 'object')\r\n return obj;\r\n\r\n const cleaned = structuredClone(obj);\r\n\r\n for (const key in cleaned) {\r\n\r\n if (cleaned[key] === null || cleaned[key] === undefined)\r\n delete cleaned[key];\r\n\r\n if (cleaned[key] instanceof Object && iterate)\r\n cleaned[key] = RemoveNulls(cleaned[key], iterate);\r\n }\r\n\r\n return cleaned\r\n\r\n}\r\n\r\n//==============================//","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './index';\n"],"names":[],"mappings":"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"}
|
package/index.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { TemplateRef } from "@angular/core";
|
|
2
|
+
import { AbstractControl } from "@angular/forms";
|
|
3
|
+
import * as i0 from "@angular/core";
|
|
4
|
+
export declare class FirstErrorComponent {
|
|
5
|
+
control: import("@angular/core").InputSignal<AbstractControl<any, any>>;
|
|
6
|
+
customErrorTemplate: import("@angular/core").InputSignal<TemplateRef<unknown> | undefined>;
|
|
7
|
+
static ɵfac: i0.ɵɵFactoryDeclaration<FirstErrorComponent, never>;
|
|
8
|
+
static ɵcmp: i0.ɵɵComponentDeclaration<FirstErrorComponent, "sb-first-error", never, { "control": { "alias": "control"; "required": true; "isSignal": true; }; "customErrorTemplate": { "alias": "customErrorTemplate"; "required": false; "isSignal": true; }; }, {}, never, never, true, never>;
|
|
9
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { OnDestroy } from '@angular/core';
|
|
2
|
+
import { FormGroup } from '@angular/forms';
|
|
3
|
+
import { CustomErrorMessageMap } from './form-errors';
|
|
4
|
+
import * as i0 from "@angular/core";
|
|
5
|
+
/**
|
|
6
|
+
* Directive: sbFormControlFirstError
|
|
7
|
+
*
|
|
8
|
+
* Automatically manages and displays the first validation error for each control in a FormGroup.
|
|
9
|
+
* - Shows errors only after controls are touched (unless showUntouched is true)
|
|
10
|
+
* - Dynamically adds blur/focusout listeners for untouched invalid controls
|
|
11
|
+
* - Supports custom error messages via CustomErrorMessageMap
|
|
12
|
+
* - Cleans up all listeners and subscriptions on destroy
|
|
13
|
+
* - SSR-safe: all DOM access is guarded by isPlatformBrowser
|
|
14
|
+
*
|
|
15
|
+
* Limitations:
|
|
16
|
+
* - Dynamic form changes (adding/removing controls at runtime) are NOT automatically handled in this version.
|
|
17
|
+
* If you add or remove controls after initialization, you must manually re-run error setup logic.
|
|
18
|
+
* (This feature is planned for a future release.)
|
|
19
|
+
*/
|
|
20
|
+
export declare class FirstErrorDirective implements OnDestroy {
|
|
21
|
+
private _platformId;
|
|
22
|
+
private _renderer;
|
|
23
|
+
private _host;
|
|
24
|
+
set sbFormControlFirstError(form: FormGroup);
|
|
25
|
+
/**
|
|
26
|
+
* Custom error messages map to override default error messages.
|
|
27
|
+
* If map returns undefined for a specific error, the default message map will be used.
|
|
28
|
+
*/
|
|
29
|
+
customErrorMessages?: CustomErrorMessageMap;
|
|
30
|
+
/**
|
|
31
|
+
* If true, errors will be shown immediately for untouched controls.
|
|
32
|
+
* If false, errors will only be shown after the control is touched.
|
|
33
|
+
* Default is false.
|
|
34
|
+
*/
|
|
35
|
+
showUntouched: boolean;
|
|
36
|
+
private _form?;
|
|
37
|
+
private _vcSub?;
|
|
38
|
+
private blurListeners;
|
|
39
|
+
ngOnDestroy(): void;
|
|
40
|
+
private addBlurListener;
|
|
41
|
+
private removeAllBlurListeners;
|
|
42
|
+
private observeValueChanges;
|
|
43
|
+
static ɵfac: i0.ɵɵFactoryDeclaration<FirstErrorDirective, never>;
|
|
44
|
+
static ɵdir: i0.ɵɵDirectiveDeclaration<FirstErrorDirective, "[sbFormControlFirstError]", never, { "sbFormControlFirstError": { "alias": "sbFormControlFirstError"; "required": true; }; "customErrorMessages": { "alias": "customErrorMessages"; "required": false; }; "showUntouched": { "alias": "showUntouched"; "required": false; }; }, {}, never, never, true, never>;
|
|
45
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { AbstractControl, FormGroup } from "@angular/forms";
|
|
2
|
+
export type ErrorMessageFunction = (fieldName: string, errorValue: any) => string;
|
|
3
|
+
export type CustomErrorMessageMap = Map<string, ErrorMessageFunction>;
|
|
4
|
+
export declare class FormErrors {
|
|
5
|
+
static setFirstErrors(form: FormGroup, customErrorMessages?: CustomErrorMessageMap): void;
|
|
6
|
+
static setFirstErrorMessage(name: string, control: AbstractControl, customErrorMessages?: CustomErrorMessageMap): void;
|
|
7
|
+
static getFirstErrorMessage(name: string, control: AbstractControl, customErrorMessages?: CustomErrorMessageMap): string | null;
|
|
8
|
+
private static firstErrorKey;
|
|
9
|
+
private static toTitleCase;
|
|
10
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { AbstractControl, FormGroup } from "@angular/forms";
|
|
2
|
+
export interface ControlData {
|
|
3
|
+
name: string;
|
|
4
|
+
control: AbstractControl;
|
|
5
|
+
}
|
|
6
|
+
export declare class FormUtility {
|
|
7
|
+
static findInvalidControlNames(form: FormGroup): Set<string>;
|
|
8
|
+
static findInvalidControlInfo(form: FormGroup): Set<string>;
|
|
9
|
+
static findInvalidControls(form: FormGroup): AbstractControl[];
|
|
10
|
+
static findInvalidControlsData(form: FormGroup): ControlData[];
|
|
11
|
+
static replaceNullWithUndefined(obj: Record<string, any>): Record<string, any>;
|
|
12
|
+
static getFirstFormError(form: FormGroup): Record<string, any> | null;
|
|
13
|
+
static firstErrorKey: (control: AbstractControl) => string | null;
|
|
14
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import * as i0 from "@angular/core";
|
|
2
|
+
export declare class RemoveNullsService<T> {
|
|
3
|
+
remove: (obj: T) => T;
|
|
4
|
+
static ɵfac: i0.ɵɵFactoryDeclaration<RemoveNullsService<any>, never>;
|
|
5
|
+
static ɵprov: i0.ɵɵInjectableDeclaration<RemoveNullsService<any>>;
|
|
6
|
+
}
|
|
7
|
+
export declare function RemoveNulls<T>(obj: T, iterate?: boolean): T;
|
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@spider-baby/utils-forms",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"peerDependencies": {
|
|
5
|
+
"@angular/core": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
|
6
|
+
"@angular/forms": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
|
7
|
+
"@angular/common": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
|
8
|
+
"rxjs": "~7.8.0",
|
|
9
|
+
"@spider-baby/dev-console": "^1.1.2",
|
|
10
|
+
"libphonenumber-js": "^1.12.9"
|
|
11
|
+
},
|
|
12
|
+
"sideEffects": false,
|
|
13
|
+
"module": "fesm2022/spider-baby-utils-forms.mjs",
|
|
14
|
+
"typings": "index.d.ts",
|
|
15
|
+
"exports": {
|
|
16
|
+
"./package.json": {
|
|
17
|
+
"default": "./package.json"
|
|
18
|
+
},
|
|
19
|
+
".": {
|
|
20
|
+
"types": "./index.d.ts",
|
|
21
|
+
"default": "./fesm2022/spider-baby-utils-forms.mjs"
|
|
22
|
+
},
|
|
23
|
+
"./validators": {
|
|
24
|
+
"types": "./validators/index.d.ts",
|
|
25
|
+
"default": "./fesm2022/spider-baby-utils-forms-validators.mjs"
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"tslib": "^2.3.0"
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { ValidatorFn, ValidationErrors } from '@angular/forms';
|
|
2
|
+
export declare class PasswordValidation {
|
|
3
|
+
static validationArray: (minLength?: number) => ValidatorFn[];
|
|
4
|
+
static matchValidator(passwordControlName?: string, confirmPasswordControlName?: string, errorMessage?: string): ValidatorFn;
|
|
5
|
+
static patternValidator(regex: RegExp, error: ValidationErrors): ValidatorFn;
|
|
6
|
+
static hasUppercaseValidator(): ValidatorFn;
|
|
7
|
+
static hasLowercaseValidator(): ValidatorFn;
|
|
8
|
+
static hasNumberValidator(): ValidatorFn;
|
|
9
|
+
static hasNonAlphaNumericValidator(): ValidatorFn;
|
|
10
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export declare const StrongPassword6Regx: RegExp;
|
|
2
|
+
export declare const StrongPassword6WithSpecialRegx: RegExp;
|
|
3
|
+
export declare const StrongPassword8Regx: RegExp;
|
|
4
|
+
export declare const StrongPassword8WithSpecialRegx: RegExp;
|
|
5
|
+
export declare const StrongPassword10Regx: RegExp;
|
|
6
|
+
export declare const StrongPassword10WithSpecialRegx: RegExp;
|