Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 11 additions & 2 deletions apps/validation-messages-example-app/src/app/app.component.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,26 @@
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { FormBuilder, FormControl, Validators } from '@angular/forms';
// eslint-disable-next-line @nrwl/nx/enforce-module-boundaries
import {
ApiErrorMessage,
VALIDATION_MESSAGES_CONFIG,
ValidationMessagesService,
} from '../../../../projects/validation-messages/src';
import { BehaviorSubject, of } from 'rxjs';
import { BehaviorSubject } from 'rxjs';

@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
providers: [
// {
// provide: VALIDATION_MESSAGES_CONFIG,
// useValue: {
// minlength: 'Too short {{actualLength}}/{{requiredLength}}',
// },
// },
],
})
export class AppComponent {
apiErrors: Array<ApiErrorMessage | string> = [
Expand Down
43 changes: 21 additions & 22 deletions apps/validation-messages-example-app/src/app/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,28 @@ import { AppComponent } from './app.component';
import { appRoutes } from './app.routes';
// eslint-disable-next-line @nrwl/nx/enforce-module-boundaries
import {
VALIDATION_MESSAGES_CONFIG,
ValidationMessagesConfig,
ValidationMessagesModule,
ValidationMessagesService,
} from '../../../../projects/validation-messages/src';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { ReactiveFormsModule } from '@angular/forms';
import { MatInputModule } from '@angular/material/input';
import { MatButtonModule } from '@angular/material/button';

const validationMessagesConfig: ValidationMessagesConfig = {
required: 'Field is required',
email: 'Invalid email',
min: 'Value is too small: {{actual}}/{{min}}',
max: 'Value is too large {{actual}}/{{max}}',
minlength: 'Too short {{actualLength}}/{{requiredLength}}',
maxlength: 'Too long {{actualLength}}/{{requiredLength}}',
lettersPattern: {
message: 'Must contains only letters {{requiredPattern}}',
pattern: '^[a-zA-Z]*$',
},
};

@NgModule({
declarations: [AppComponent],
imports: [
Expand All @@ -25,26 +38,12 @@ import { MatButtonModule } from '@angular/material/button';
MatInputModule,
MatButtonModule,
],
providers: [],
providers: [
{
provide: VALIDATION_MESSAGES_CONFIG,
useValue: validationMessagesConfig,
},
],
bootstrap: [AppComponent],
})
export class AppModule {
validationMessagesConfig: ValidationMessagesConfig = {
required: 'Field is required',
email: 'Invalid email',
min: 'Value is too small: {{actual}}/{{min}}',
max: 'Value is too large {{actual}}/{{max}}',
minlength: 'Too short {{actualLength}}/{{requiredLength}}',
maxlength: 'Too long {{actualLength}}/{{requiredLength}}',
lettersPattern: {
message: 'Must contains only letters {{requiredPattern}}',
pattern: '^[a-zA-Z]*$',
},
};

constructor(private validationMessagesService: ValidationMessagesService) {
this.validationMessagesService.setValidationMessages(
this.validationMessagesConfig
);
}
}
export class AppModule {}
55 changes: 11 additions & 44 deletions projects/validation-messages/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,36 +21,7 @@ export class AppModule {}

### 2. Configuration

To configure the validation messages, you need to perform the following steps:

**Step 2.1: Inject ValidationMessagesService**

Inject the ValidationMessagesService into the constructor of the root module of your application or create a service that is provided at the root level and inject the ValidationMessagesService into it.

```ts
@NgModule({
imports: [CommonModule, ValidationMessagesModule],
})
export class AppModule {
constructor(private validationMessagesService: ValidationMessagesService) {}
}
```

or

```ts
@Injectable({
providedIn: 'root',
})
export class ValidationMessagesConfigService {
constructor(private validationMessagesService: ValidationMessagesService) {}
}
```

**Step 2.2: Define Validation Messages**

Define the global messages for validators by creating a configuration object of type ValidationMessagesConfig. There are two ways to provide error message definitions:

- Using key-value pairs, where the key is the validator key in the `FormControl`'s "errors" object and the value is the error message to be displayed. For example, see the **required** and **email** messages in the example configuration below.
- Providing an object of type ValidationMessage under a key. For example, see the **lettersPattern** message in the example configuration below.

Expand All @@ -72,22 +43,19 @@ const validationMessagesConfig: ValidationMessagesConfig = {
};
```

Once the configuration object is created, pass it as a parameter to the `setValidationMessages` method of the ValidationMessagesService.
Once the configuration object is created, provide it as a value of VALIDATION_MESSAGES_CONFIG injection token in app module.

```ts
const validationMessagesConfig: ValidationMessagesConfig = {
required: 'Field is required',
...
};

@NgModule({
imports: [...]
imports: [CommonModule,ValidationMessagesModule,],
providers: [
{
provide: VALIDATION_MESSAGES_CONFIG,
useValue: validationMessagesConfig,
},
],
})
export class AppModule {
constructor(private validationMessagesService: ValidationMessagesService) {
this.validationMessagesService.setValidationMessages(validationMessagesConfig);
}
}
export class AppModule {}
```

# 3. Usage in templates
Expand All @@ -107,7 +75,8 @@ Now, in your component's template, you can use the `ValidationMessagesComponent`
Alternatively, you can manually pass the control to the ValidationMessagesComponent by specifying either the `controlName` or `control` input:

```html
<ng-validation-messages [control]="name"></ng-validation-messages> <ng-validation-messages controlName="formControlName"></ng-validation-messages>
<ng-validation-messages [control]="name"></ng-validation-messages>
<ng-validation-messages controlName="formControlName"></ng-validation-messages>
```

### Local validator messages
Expand All @@ -132,8 +101,6 @@ You can specify parameters in the error messages using `{{parameterName}}`. Thes

##### Methods:

`setValidationMessages(config: ValidationMessagesConfig)`: Sets validation messages configuration

`setTemplateMatcher(matcher: RegExp)`: Sets specifies which part of the message string will be replaced with default interpolated value (the default matcher is `/{{(.*)}}+/g`)

`useMaterialErrorMatcher()`: If ValidationMessagesComponent is used together with custom errorStateMatcher for Angular Material's matInput and this method is called, the errors will be shown instantly and not on lost focus (errorStateMatcher needs to reflect that).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { MatFormField } from '@angular/material/form-field';
import { createHostFactory } from '@ngneat/spectator/jest';
// eslint-disable-next-line @nrwl/nx/enforce-module-boundaries
import {
ApiErrorMessage,
ValidationMessagesConfig,
ValidationMessagesService,
} from 'validation-messages';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
computed,
DestroyRef,
Host,
Inject,
Injector,
Input,
OnInit,
Expand All @@ -32,16 +33,25 @@ import {
} from '@angular/material/form-field';
import { takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop';
import { fromEvent, map } from 'rxjs';
import { VALIDATION_MESSAGES_CONFIG } from '../../resources/providers/validation-messages-config.provider';

@Component({
selector: 'ng-validation-messages',
templateUrl: './validation-messages.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ValidationMessagesComponent implements OnInit, AfterContentInit {
@Input() errorsMessages: ValidationMessagesConfig = {};
@Input()
set errorsMessages(config: ValidationMessagesConfig) {
this._errorsMessages = this._errorsMessages
? Object.assign(this._errorsMessages, config)
: config;
}

private _errorsMessages: ValidationMessagesConfig = {};
@Input() control?: FormControl;
@Input() controlName?: string;

@Input()
set multiple(multiple: boolean) {
this._multiple.set(multiple);
Expand All @@ -58,14 +68,22 @@ export class ValidationMessagesComponent implements OnInit, AfterContentInit {

private _controlSig!: Signal<undefined | FormControl>;
private _multiple = signal(false);

constructor(
@Optional()
@Inject(VALIDATION_MESSAGES_CONFIG)
errorMessagesDiConfig: ValidationMessagesConfig,
@Host() @Optional() protected host: MatFormField,
private _validationMessagesService: ValidationMessagesService,
private _controlContainer: ControlContainer,
private _destroyRef: DestroyRef,
private _injector: Injector,
private _cdr: ChangeDetectorRef
) {}
) {
if (errorMessagesDiConfig) {
this.errorsMessages = errorMessagesDiConfig;
}
}

get _serverErrors() {
return this._validationMessagesService.serverErrors();
Expand Down Expand Up @@ -147,7 +165,7 @@ export class ValidationMessagesComponent implements OnInit, AfterContentInit {
this._validationMessagesService.getValidatorErrorMessage(
propertyName,
value,
this.errorsMessages
this._errorsMessages
);
errorMessages.push(errorMessage);
} else {
Expand Down
1 change: 1 addition & 0 deletions projects/validation-messages/src/lib/resources/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './interfaces';
export * from './const';
export * from './enums';
export * from './providers';
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './validation-messages-config.provider';
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { InjectionToken } from '@angular/core';
import { ValidationMessagesConfig } from '../interfaces';

export const VALIDATION_MESSAGES_CONFIG =
new InjectionToken<ValidationMessagesConfig>('validation-messages-config');

export const defaultValidationMessagesConfigProvider = {
provide: VALIDATION_MESSAGES_CONFIG,
useValue: {},
};
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,20 @@ import { DestroyRef, Injectable, signal } from '@angular/core';
import {
ApiErrorMessages,
Parser,
ValidationMessage,
ValidationMessagesConfig,
} from '../resources';
import { KeyValue } from '@angular/common';
import { getInterpolableParams, getPropByPath } from '../utils';
import { mergeValidationMessagesConfigs } from '../utils/merge-validation-messages-configs.util';
import { distinctUntilChanged, Observable, isObservable } from 'rxjs';
import { distinctUntilChanged, isObservable, Observable } from 'rxjs';
import { AbstractControl, ValidationErrors } from '@angular/forms';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { adaptValidationMessagesConfigs } from '../utils/adapt-validation-messages-configs.util';

@Injectable({
providedIn: 'root',
})
export class ValidationMessagesService {
private parser!: Parser;
private validationMessagesFinalConfig: ValidationMessagesConfig<
ValidationMessage | any
> = {}; // types
private templateMatcher = /{{(.*)}}+/g;

_serverErrors = signal<ApiErrorMessages>(null);
Expand Down Expand Up @@ -56,15 +52,11 @@ export class ValidationMessagesService {
getValidatorErrorMessage(
validatorName: string,
validatorValue: any = {},
localValidationMessagesConfig: ValidationMessagesConfig = {}
errorMessages: ValidationMessagesConfig = {}
): string {
// types
const validationMessages = mergeValidationMessagesConfigs(
this.validationMessagesFinalConfig,
localValidationMessagesConfig
) as { [p: string]: any };

const validationMessages = adaptValidationMessagesConfigs(errorMessages);
const validatorMessage = validationMessages[validatorName];

if (!validatorMessage) {
return this.validatorNotSpecified(validatorName);
}
Expand Down Expand Up @@ -95,14 +87,6 @@ export class ValidationMessagesService {
: message;
}

setValidationMessages(
validationMessagesConfig: ValidationMessagesConfig
): void {
// Set validation errorMessages
const config = mergeValidationMessagesConfigs({}, validationMessagesConfig);
this.validationMessagesFinalConfig = { ...config };
}

setServerMessagesParser(serverMessageParser: Parser): void {
this.parser = serverMessageParser;
}
Expand Down Expand Up @@ -157,7 +141,7 @@ export class ValidationMessagesService {
private validatorNotSpecified(validatorName: string): string {
console.warn(
`Validation message for ${validatorName} validator is not specified in this.`,
`Did you called 'this.setValidationMessages()'?`
`Did you specify it in providers?`
);

return '';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,14 @@ const getValidatorValue = (key: string): string => {
return (angularValidatorsWithValueMap as any)[key] || key; // types
};

export const mergeValidationMessagesConfigs = (
config: ValidationMessagesConfig,
toBeMerged: ValidationMessagesConfig
) => {
config = { ...config };
toBeMerged = { ...toBeMerged };
export const adaptValidationMessagesConfigs = (
toBeAdapted: ValidationMessagesConfig
): Record<string, any> => {
const config: Record<string, any> = {};
toBeAdapted = { ...toBeAdapted };

for (const key in toBeMerged) {
const value = toBeMerged[key];
for (const key in toBeAdapted) {
const value = toBeAdapted[key];

if (typeof value === 'string') {
config[key] = {
Expand Down
2 changes: 1 addition & 1 deletion projects/validation-messages/src/lib/utils/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
export * from './get-prop-by-path.util';
export * from './get-interpolable-params.util';
export * from './merge-validation-messages-configs.util';
export * from './adapt-validation-messages-configs.util';
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { ValidationMessagesComponent } from './components/validation-messages/validation-messages.component';
import { defaultValidationMessagesConfigProvider } from './resources';

@NgModule({
imports: [CommonModule],
declarations: [ValidationMessagesComponent],
exports: [ValidationMessagesComponent]
exports: [ValidationMessagesComponent],
providers: [defaultValidationMessagesConfigProvider],
})
export class ValidationMessagesModule {}