@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 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":";;;;;;;MA0Ca,mBAAmB,CAAA;AAE9B,IAAA,OAAO,GAAG,KAAK,CAAC,QAAQ,EAAmB;AAC3C,IAAA,mBAAmB,GAAG,KAAK,CAAmC,SAAS,CAAC;uGAH7D,mBAAmB,EAAA,IAAA,EAAA,EAAA,EAAA,MAAA,EAAA,EAAA,CAAA,eAAA,CAAA,SAAA,EAAA,CAAA;AAAnB,IAAA,OAAA,IAAA,GAAA,EAAA,CAAA,oBAAA,CAAA,EAAA,UAAA,EAAA,QAAA,EAAA,OAAA,EAAA,QAAA,EAAA,IAAA,EAAA,mBAAmB,EAlCpB,YAAA,EAAA,IAAA,EAAA,QAAA,EAAA,gBAAA,EAAA,MAAA,EAAA,EAAA,OAAA,EAAA,EAAA,iBAAA,EAAA,SAAA,EAAA,UAAA,EAAA,SAAA,EAAA,QAAA,EAAA,IAAA,EAAA,UAAA,EAAA,IAAA,EAAA,iBAAA,EAAA,IAAA,EAAA,EAAA,mBAAA,EAAA,EAAA,iBAAA,EAAA,qBAAA,EAAA,UAAA,EAAA,qBAAA,EAAA,QAAA,EAAA,IAAA,EAAA,UAAA,EAAA,KAAA,EAAA,iBAAA,EAAA,IAAA,EAAA,EAAA,EAAA,QAAA,EAAA,EAAA,EAAA,QAAA,EAAA,CAAA;;;;;;;;;;;;;;AAcT,EAAA,CAAA,EAAA,QAAA,EAAA,IAAA,EAAA,MAAA,EAAA,CAAA,8RAAA,CAAA,EAAA,YAAA,EAAA,CAAA,EAAA,IAAA,EAAA,WAAA,EAAA,IAAA,EAfS,gBAAgB,EAAA,QAAA,EAAA,oBAAA,EAAA,MAAA,EAAA,CAAA,yBAAA,EAAA,kBAAA,EAAA,0BAAA,CAAA,EAAA,CAAA,EAAA,CAAA;;2FAmCf,mBAAmB,EAAA,UAAA,EAAA,CAAA;kBAtC/B,SAAS;AACE,YAAA,IAAA,EAAA,CAAA,EAAA,QAAA,EAAA,gBAAgB,cACd,IAAI,EAAA,OAAA,EACP,CAAC,gBAAgB,CAAC,EACjB,QAAA,EAAA,CAAA;;;;;;;;;;;;;;AAcT,EAAA,CAAA,EAAA,MAAA,EAAA,CAAA,8RAAA,CAAA,EAAA;;;ACZH,MAAM,eAAe,GAA0B,IAAI,GAAG,CAA+B;IACjF,CAAC,UAAU,EAAE,CAAC,SAAS,KAAK,CAAA,EAAG,SAAS,CAAA,aAAA,CAAe,CAAC;AACxD,IAAA,CAAC,OAAO,EAAE,MAAM,qCAAqC,CAAC;IACtD,CAAC,WAAW,EAAE,CAAC,SAAS,EAAE,UAAU,KAAK,CAAC,UAAU,GAAG,oBAAoB,GAAG,CAAA,EAAG,SAAS,CAAA,kBAAA,EAAqB,UAAU,EAAE,cAAc,CAAA,YAAA,CAAc,CAAC;IACxJ,CAAC,WAAW,EAAE,CAAC,SAAS,EAAE,UAAU,KAAK,CAAC,UAAU,GAAG,mBAAmB,GAAG,CAAA,EAAG,SAAS,CAAA,sBAAA,EAAyB,UAAU,EAAE,cAAc,CAAA,YAAA,CAAc,CAAC;IAC3J,CAAC,SAAS,EAAE,CAAC,SAAS,KAAK,CAAA,EAAG,SAAS,CAAA,mBAAA,CAAqB,CAAC;IAC7D,CAAC,KAAK,EAAE,CAAC,SAAS,EAAE,UAAU,KAAK,CAAC,UAAU,GAAG,oBAAoB,GAAG,CAAA,EAAG,SAAS,CAAA,kBAAA,EAAqB,UAAU,EAAE,GAAG,CAAA,CAAA,CAAG,CAAC;IAC5H,CAAC,KAAK,EAAE,CAAC,SAAS,EAAE,UAAU,KAAK,CAAC,UAAU,GAAG,oBAAoB,GAAG,CAAA,EAAG,SAAS,CAAA,sBAAA,EAAyB,UAAU,EAAE,GAAG,CAAA,CAAA,CAAG,CAAC;AAChI,IAAA,CAAC,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AACrD,IAAA,CAAC,WAAW,EAAE,MAAM,sBAAsB,CAAC;IAC3C,CAAC,YAAY,EAAE,CAAC,SAAS,KAAK,CAAA,EAAG,SAAS,CAAA,gCAAA,CAAkC,CAAC;IAC7E,CAAC,gBAAgB,EAAE,CAAC,SAAS,EAAE,UAAU,KAAK,CAAC,UAAU,GAAG,2BAA2B,GAAG,CAAA,EAAG,SAAS,CAAA,YAAA,EAAe,UAAU,EAAE,KAAK,CAAA,EAAA,CAAI,CAAC;IAC3I,CAAC,iBAAiB,EAAE,CAAC,SAAS,KAAK,CAAA,EAAG,SAAS,CAAA,yBAAA,CAA2B,CAAC;AAC3E,IAAA,CAAC,aAAa,EAAE,MAAM,4BAA4B,CAAC;AACnD,IAAA,CAAC,YAAY,EAAE,MAAM,6BAA6B,CAAC;AACnD,IAAA,CAAC,UAAU,EAAE,MAAM,2BAA2B,CAAC;AAC/C,IAAA,CAAC,gBAAgB,EAAE,MAAM,mHAAmH,CAAC;IAC7I,CAAC,aAAa,EAAE,CAAC,SAAS,EAAE,UAAU,KAAK,CAAC,UAAU,CAAC,OAAO,GAAG,oCAAoC,GAAG,UAAU,CAAC,OAAO,CAAC;AAC3H,IAAA,CAAC,KAAK,EAAE,MAAM,2BAA2B,CAAC;AAC1C,IAAA,CAAC,QAAQ,EAAE,CAAC,SAAS,KAAK,CAAA,KAAA,EAAQ,SAAS,CAAC,WAAW,EAAE,oBAAoB,CAAC;IAC9E,CAAC,UAAU,EAAE,CAAC,SAAS,EAAE,UAAU,KAAK,CAAC,UAAU,GAAG,mBAAmB,GAAG,+BAA+B,UAAU,EAAE,OAAO,CAAA,CAAA,CAAG,CAAC;AAClI,IAAA,CAAC,UAAU,EAAE,CAAC,SAAS,EAAE,UAAU,KAAK,CAAC,UAAU,GAAG,mBAAmB,GAAG,CAAQ,KAAA,EAAA,UAAU,EAAE,YAAY,EAAE,IAAI,CAAC,IAAI,CAAC,CAAA,mBAAA,CAAqB;AAChJ,CAAA,CAAC;AAGF;MAGa,UAAU,CAAA;AAGnB,IAAA,OAAO,cAAc,CACjB,IAAe,EACf,mBAA2C,EAAA;AAE3C,QAAA,MAAM,QAAQ,GAAG,IAAI,CAAC,QAAQ;AAC9B,QAAA,KAAK,MAAM,IAAI,IAAI,QAAQ,EAAE;AACzB,YAAA,MAAM,OAAO,GAAG,QAAQ,CAAC,IAAI,CAAC;YAC9B,IAAI,OAAO,CAAC,OAAO;gBACf,IAAI,CAAC,oBAAoB,CAAC,IAAI,EAAE,OAAO,EAAE,mBAAmB,CAAC;;;;AAQzE,IAAA,OAAO,oBAAoB,CACvB,IAAY,EACZ,OAAwB,EACxB,mBAA2C,EAAA;AAE3C,QAAA,MAAM,aAAa,GAAG,OAAO,CAAC,MAAM;AACpC,QAAA,MAAM,iBAAiB,GAAG,UAAU,CAAC,oBAAoB,CAAC,IAAI,EAAE,OAAO,EAAE,mBAAmB,CAAC;AAC7F,QAAA,IAAI,iBAAiB;AACjB,YAAA,OAAO,CAAC,SAAS,CACb,EAAE,GAAG,aAAa,EAAE,UAAU,EAAE,iBAAiB,EAAE,EACnD,EAAE,SAAS,EAAE,KAAK,EAAE;aACvB;;;AAST,IAAA,OAAO,oBAAoB,CACvB,IAAY,EACZ,OAAwB,EACxB,mBAA2C,EAAA;QAE3C,MAAM,QAAQ,GAAG,IAAI,CAAC,aAAa,CAAC,OAAO,CAAC;AAC5C,QAAA,IAAI,CAAC,QAAQ;AACT,YAAA,OAAO,IAAI;QAGf,MAAM,UAAU,GAAG,OAAO,CAAC,MAAM,GAAG,QAAQ,CAAC;QAC7C,MAAM,SAAS,GAAG,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC;;QAGxC,IAAI,OAAO,UAAU,KAAK,QAAQ;AAC9B,YAAA,OAAO,UAAU;AAGrB,QAAA,MAAM,cAAc,GAAG,mBAAmB,EAAE,GAAG,CAAC,QAAQ,CAAC,IAAI,eAAe,CAAC,GAAG,CAAC,QAAQ,CAAC;;AAI1F,QAAA,IAAI,cAAc;AACd,YAAA,OAAO,cAAc,CAAC,SAAS,EAAE,UAAU,CAAC;;QAGhD,OAAO,CAAA,kBAAA,EAAqB,SAAS,CAAA,CAAA,CAAG;;;AAMpC,IAAA,OAAO,aAAa,GAAG,CAAC,OAAwB,KACpD,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,MAAM,IAAI,EAAE,CAAC,CAAC,MAAM,GAAG,CAAC,GAAG,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,MAAM,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC,GAAG,IAAI;;IAKtF,OAAO,WAAW,CAAC,CAAS,EAAA;QAChC,MAAM,MAAM,GAAG,CAAC,CAAC,OAAO,CAAC,UAAU,EAAE,KAAK,CAAC;AAC3C,QAAA,OAAO,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;;;;AC5G/D;MAEa,WAAW,CAAA;IAEf,OAAO,uBAAuB,CAAC,IAAe,EAAA;AAEnD,QAAA,MAAM,OAAO,GAAG,IAAI,GAAG,EAAU;AACjC,QAAA,MAAM,QAAQ,GAAG,IAAI,CAAC,QAAQ;AAE9B,QAAA,KAAK,MAAM,IAAI,IAAI,QAAQ,EAAE;AAC3B,YAAA,IAAI,QAAQ,CAAC,IAAI,CAAC,CAAC,OAAO;AACxB,gBAAA,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC;;AAGrB,QAAA,OAAO,OAAO;;;IAKT,OAAO,sBAAsB,CAAC,IAAe,EAAA;AAElD,QAAA,MAAM,OAAO,GAAG,IAAI,GAAG,EAAU;AACjC,QAAA,MAAM,QAAQ,GAAG,IAAI,CAAC,QAAQ;AAE9B,QAAA,KAAK,MAAM,IAAI,IAAI,QAAQ,EAAE;AAC3B,YAAA,IAAI,QAAQ,CAAC,IAAI,CAAC,CAAC,OAAO;AACxB,gBAAA,OAAO,CAAC,GAAG,CAAC,GAAG,IAAI,CAAA,EAAA,EAAK,IAAI,CAAC,aAAa,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,IAAI,SAAS,CAAA,CAAE,CAAC;;AAG9E,QAAA,OAAO,OAAO;;;IAKT,OAAO,mBAAmB,CAAC,IAAe,EAAA;QAE/C,MAAM,OAAO,GAAG,EAAE;AAClB,QAAA,MAAM,QAAQ,GAAG,IAAI,CAAC,QAAQ;AAE9B,QAAA,KAAK,MAAM,IAAI,IAAI,QAAQ,EAAE;AAC3B,YAAA,IAAI,QAAQ,CAAC,IAAI,CAAC,CAAC,OAAO;gBACxB,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;;AAGhC,QAAA,OAAO,OAAO;;;IAKT,OAAO,uBAAuB,CAAC,IAAe,EAAA;QAEnD,MAAM,OAAO,GAAiD,EAAE;AAChE,QAAA,MAAM,QAAQ,GAAG,IAAI,CAAC,QAAQ;AAE9B,QAAA,KAAK,MAAM,IAAI,IAAI,QAAQ,EAAE;AAC3B,YAAA,MAAM,OAAO,GAAG,QAAQ,CAAC,IAAI,CAAC;YAC9B,IAAI,OAAO,CAAC,OAAO;gBACjB,OAAO,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC;;AAGnC,QAAA,OAAO,OAAO;;;IAMhB,OAAO,wBAAwB,CAAC,GAAwB,EAAA;AAEtD,QAAA,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE;YAC9C,IAAI,KAAK,KAAK,IAAI;AAChB,gBAAA,GAAG,CAAC,GAAG,CAAC,GAAG,SAAS;;AAExB,QAAA,OAAO,GAAG;;;IAKZ,OAAO,iBAAiB,CAAC,IAAe,EAAA;QACtC,IAAI,IAAI,CAAC,KAAK;AACZ,YAAA,OAAO,IAAI;;AAGb,QAAA,IAAI,IAAI,CAAC,MAAM,EAAE;AACf,YAAA,MAAM,QAAQ,GAAG,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;AAC5C,YAAA,OAAO,EAAE,CAAC,QAAQ,GAAG,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,EAAE;;;AAI9C,QAAA,KAAK,MAAM,GAAG,IAAI,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE;YAC5C,MAAM,OAAO,GAAG,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC;AAC7B,YAAA,IAAI,OAAO,EAAE,MAAM,EAAE;AACnB,gBAAA,MAAM,QAAQ,GAAG,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;AAC/C,gBAAA,OAAO,EAAE,CAAC,QAAQ,GAAG,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,EAAE;;;AAInD,QAAA,OAAO,IAAI;;;AAKb,IAAA,OAAO,aAAa,GAAG,CAAC,OAAwB,KAC9C,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,MAAM,IAAI,EAAE,CAAC,CAAC,MAAM,GAAG,CAAC,GAAG,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,MAAM,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC,GAAG,IAAI;;;ACtG9F;;;;;;;;;;;;;;AAcG;MAKU,mBAAmB,CAAA;AAEtB,IAAA,WAAW,GAAG,MAAM,CAAC,WAAW,CAAC;AACjC,IAAA,SAAS,GAAG,MAAM,CAAC,SAAS,CAAC;AAC7B,IAAA,KAAK,GAA4B,MAAM,CAAC,UAAU,CAAC;IAG3D,IAA+B,uBAAuB,CAAC,IAAe,EAAA;AACpE,QAAA,IAAI,CAAC,KAAK,GAAG,IAAI;AACjB,QAAA,IAAI,CAAC,mBAAmB,CAAC,IAAI,CAAC,KAAK,CAAC;;AAGtC;;;AAGG;AACM,IAAA,mBAAmB;AAE5B;;;;AAIG;IACM,aAAa,GAAY,KAAK;;AAI/B,IAAA,KAAK;AACL,IAAA,MAAM;AACN,IAAA,aAAa,GAAG,IAAI,GAAG,EAAsB;;IAIrD,WAAW,GAAA;AACT,QAAA,IAAI,CAAC,MAAM,EAAE,WAAW,EAAE;QAC1B,IAAI,CAAC,sBAAsB,EAAE;;;IAKvB,eAAe,CAAC,WAAmB,EAAE,OAAwB,EAAA;;AAGnE,QAAA,MAAM,KAAK,GAAuB,IAAI,CAAC,KAAK,CAAC,aAAa,CAAC,aAAa,CAAC,CAAA,kBAAA,EAAqB,WAAW,CAAA,EAAA,CAAI,CAAC;AAE9G,QAAA,IAAI,CAAC,KAAK;YACR;;AAGF,QAAA,MAAM,QAAQ,GAAG,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,KAAK,EAAE,UAAU,EAAE,MAAK;YAC7D,IAAI,CAAC,OAAO,CAAC,MAAM,GAAG,YAAY,CAAC,EAAE;gBACnC,UAAU,CAAC,oBAAoB,CAC7B,WAAW,EACX,OAAO,EACP,IAAI,CAAC,mBAAmB,CACzB;;;AAGH,YAAA,QAAQ,EAAE;AACV,YAAA,IAAI,CAAC,aAAa,CAAC,MAAM,CAAC,WAAW,CAAC;AACxC,SAAC,CAAC;QAEF,IAAI,CAAC,aAAa,CAAC,GAAG,CAAC,WAAW,EAAE,QAAQ,CAAC;;;IAKvC,sBAAsB,GAAA;QAE5B,KAAK,MAAM,QAAQ,IAAI,IAAI,CAAC,aAAa,CAAC,MAAM,EAAE;AAChD,YAAA,QAAQ,EAAE;AAEZ,QAAA,IAAI,CAAC,aAAa,CAAC,KAAK,EAAE;;;AAMpB,IAAA,mBAAmB,CAAC,IAAe,EAAA;AAEzC,QAAA,IAAI,CAAC,iBAAiB,CAAC,IAAI,CAAC,WAAW,CAAC;YACtC;AAEF,QAAA,IAAI,CAAC,MAAM,EAAE,WAAW,EAAE;AAC1B,QAAA,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC;AAChB,aAAA,IAAI,CACH,SAAS,CAAC,SAAS,CAAC;QACpB,MAAM,CAAC,MAAM,IAAI,CAAC,MAAM,KAAK,SAAS,CAAC,EACvC,GAAG,CAAC,MAAM,WAAW,CAAC,uBAAuB,CAAC,IAAI,CAAC,CAAC;AAErD,aAAA,SAAS,CAAC,CAAC,kBAAkB,KAAI;AAEhC,YAAA,KAAK,MAAM,WAAW,IAAI,kBAAkB,EAAE;AAC5C,gBAAA,MAAM,OAAO,GAAG,WAAW,CAAC,OAAO;AACnC,gBAAA,MAAM,IAAI,GAAG,WAAW,CAAC,IAAI;;AAG7B,gBAAA,IAAI,OAAO,CAAC,MAAM,GAAG,YAAY,CAAC;oBAChC;gBAGF,IAAI,IAAI,CAAC,aAAa,IAAI,OAAO,CAAC,OAAO,EAAE;oBACzC,UAAU,CAAC,oBAAoB,CAAC,IAAI,EAAE,OAAO,EAAE,IAAI,CAAC,mBAAmB,CAAC;;AACnE,qBAAA,IAAI,CAAC,OAAO,CAAC,OAAO,EAAE;;oBAE3B,IAAI,CAAC,IAAI,CAAC,aAAa,CAAC,GAAG,CAAC,IAAI,CAAC;AAC/B,wBAAA,IAAI,CAAC,eAAe,CAAC,IAAI,EAAE,OAAO,CAAC;;;AAG3C,SAAC,CAAC;;uGA7GK,mBAAmB,EAAA,IAAA,EAAA,EAAA,EAAA,MAAA,EAAA,EAAA,CAAA,eAAA,CAAA,SAAA,EAAA,CAAA;2FAAnB,mBAAmB,EAAA,YAAA,EAAA,IAAA,EAAA,QAAA,EAAA,2BAAA,EAAA,MAAA,EAAA,EAAA,uBAAA,EAAA,yBAAA,EAAA,mBAAA,EAAA,qBAAA,EAAA,aAAA,EAAA,eAAA,EAAA,EAAA,QAAA,EAAA,EAAA,EAAA,CAAA;;2FAAnB,mBAAmB,EAAA,UAAA,EAAA,CAAA;kBAJ/B,SAAS;AAAC,YAAA,IAAA,EAAA,CAAA;AACT,oBAAA,QAAQ,EAAE,2BAA2B;AACrC,oBAAA,UAAU,EAAE;AACb,iBAAA;8BAQgC,uBAAuB,EAAA,CAAA;sBAArD,KAAK;uBAAC,EAAE,QAAQ,EAAE,IAAI,EAAE;gBAShB,mBAAmB,EAAA,CAAA;sBAA3B;gBAOQ,aAAa,EAAA,CAAA;sBAArB;;;MC9CU,kBAAkB,CAAA;AAE7B,IAAA,MAAM,GAAG,CAAC,GAAM,KAAQ,WAAW,CAAC,GAAG,EAAE,IAAI,CAAC;uGAFnC,kBAAkB,EAAA,IAAA,EAAA,EAAA,EAAA,MAAA,EAAA,EAAA,CAAA,eAAA,CAAA,UAAA,EAAA,CAAA;AAAlB,IAAA,OAAA,KAAA,GAAA,EAAA,CAAA,qBAAA,CAAA,EAAA,UAAA,EAAA,QAAA,EAAA,OAAA,EAAA,QAAA,EAAA,QAAA,EAAA,EAAA,EAAA,IAAA,EAAA,kBAAkB,cAFjB,MAAM,EAAA,CAAA;;2FAEP,kBAAkB,EAAA,UAAA,EAAA,CAAA;kBAH9B,UAAU;AAAC,YAAA,IAAA,EAAA,CAAA;AACV,oBAAA,UAAU,EAAE,MAAM;AACnB,iBAAA;;AAQD;SAGgB,WAAW,CAAI,GAAM,EAAE,OAAO,GAAG,IAAI,EAAA;AAEnD,IAAA,IAAI,GAAG,KAAK,IAAI,IAAI,GAAG,KAAK,SAAS;AACnC,QAAA,OAAO,GAAG;;AAGZ,IAAA,IAAI,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE;AACtB,QAAA,OAAO;AACJ,aAAA,MAAM,CAAC,IAAI,IAAI,IAAI,KAAK,IAAI,IAAI,IAAI,KAAK,SAAS,CAAC;aACnD,GAAG,CAAC,IAAI,IAAI,OAAO,IAAI,OAAO,IAAI,KAAK,QAAQ,GAAG,WAAW,CAAC,IAAI,EAAE,OAAO,CAAC,GAAG,IAAI,CAAM;;;IAI9F,IAAI,OAAO,GAAG,KAAK,QAAQ;AACzB,QAAA,OAAO,GAAG;AAEZ,IAAA,MAAM,OAAO,GAAG,eAAe,CAAC,GAAG,CAAC;AAEpC,IAAA,KAAK,MAAM,GAAG,IAAI,OAAO,EAAE;AAEzB,QAAA,IAAI,OAAO,CAAC,GAAG,CAAC,KAAK,IAAI,IAAI,OAAO,CAAC,GAAG,CAAC,KAAK,SAAS;AACrD,YAAA,OAAO,OAAO,CAAC,GAAG,CAAC;AAErB,QAAA,IAAI,OAAO,CAAC,GAAG,CAAC,YAAY,MAAM,IAAI,OAAO;AAC3C,YAAA,OAAO,CAAC,GAAG,CAAC,GAAG,WAAW,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,OAAO,CAAC;;AAGrD,IAAA,OAAO,OAAO;AAEhB;;AC5CA;;AAEG;;;;"}
package/index.d.ts ADDED
@@ -0,0 +1,5 @@
1
+ export * from './lib/first-error.component';
2
+ export * from './lib/first-error.directive';
3
+ export * from './lib/form-errors';
4
+ export * from './lib/form-utility';
5
+ export * from './lib/remove-nulls.service';
@@ -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,3 @@
1
+ # @spider-baby/utils-forms/validators
2
+
3
+ Secondary entry point of `@spider-baby/utils-forms`. It can be used by importing from `@spider-baby/utils-forms/validators`.
@@ -0,0 +1,4 @@
1
+ export * from './lib/numeric-validators';
2
+ export * from './lib/password-validators';
3
+ export * from './lib/pwd-regexes';
4
+ export * from './lib/phone-validators';
@@ -0,0 +1,4 @@
1
+ import { ValidatorFn } from '@angular/forms';
2
+ export declare class NumericValidation {
3
+ static lessThanValidator(minControlName: string, maxControlName: string, strictlyLessThan?: boolean, errorMessage?: string): ValidatorFn;
4
+ }
@@ -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,5 @@
1
+ import { ValidatorFn } from '@angular/forms';
2
+ import { CountryCode } from 'libphonenumber-js';
3
+ export declare class PhoneValidation {
4
+ static validator(defaultCountry?: CountryCode): ValidatorFn;
5
+ }
@@ -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;