Automatic validation messages for Angular forms in any language
tl;dr
A library that can automatically display validation messages in any language. Check out the library here or the example here or if you want to have a look at the example code checkout the Stackblitz.
<label>Email:
<input formControlName="email">
</label>
<div *ngIf="myForm.controls['email'].invalid">
<div *ngIf="myForm.controls['email'].errors.required">
{{ translate('email.require') }}
</div>
</div>
Writing this code for one input can be okay. It is very clear and you know exactly what it does. However, lets say we also want to check if the email address is valid and that we have 10 other inputs that need validation. In that case this code will become very tedious and is cumbersome to write. Therefore we created ngx-translation-validation
. With this package you can write the following.
<label>Email:
<input formControlName="email">
</label>
The package will automatically take care of any form validation that is enabled on this form. You no longer have to write any html to display the errors.
Reasons for development
While developing RiskChallenger, an application for risk management, we had the need to show validation errors to the user. We have forms to setup the risk file, enter new risks, measures and all of those have quite a few inputs. As a developer should be, lazy, I did not want to write too much code and neither did I want to reinvent the wheel. So I googled on how to do this properly. At the time of writing I found the following solutions:
All of these examples have one thing in common; they require boilerplate code. While I'd say that it should not be needed. We can create a directive that binds to the formControl and when the formControl is invalid then it can show the error message. Even better, it can render the error message using a translation library like transloco or ngx-translate.
At the time of writing it does only support Transloco and not ngx-translate, but I plan to add this. Also other translation libraries should be easy to add.
Based on the form name and the form control name it can easily determine which validation message should be rendered to the user.
So what does the library need to do?
- Check if a form control is invalid
- Generate error component with error message
- Generate different messages for different forms
- Show errors on submit
Without going into too much depth I will try to explain how the library does its job. I will do this by showing some code snippets and explain how and why it works.
Detect form control validity, no more boilerplate
To prevent the boilerplate code as shown below we need to check the validity of every form control and when invalid inject a component with a validation message.
<div *ngIf="myForm.controls['email'].errors.required">
{{ translate('email.require') }}
</div>
We create a directive to do this and add the selector formControl
and formControlName
. This way the directive is automatically applied to all inputs having one of those. Inside the directive we subscribe to the statusChanges
of the formControl
. The statusChanges
is
A multicasting observable that emits an event every time the
validationstatus
of the control recalculates.
according to the Angular documentation. The validation status
of the control is recalculated according the updateOn
property of the AbstractControlOptions
.
constructor(@Self controlDir: NgControl){}
...
this.controlDir.statusChanges.subscribe(
() => {
const controlErrors = this.controlDir.errors;
if (controlErrors) {
const firstKey = Object.keys(controlErrors)[0];
this.setError(`${this.config.type}.${this.controlDir.name}.${firstKey}`);
} else if (this.ref) {
this.setError(null);
}
}
);
When the status
of a control changes we check if the control is valid. If there is an error we will call the setError
method with the translation key for the first error on the control. This function should create a component and display the error message. The translation key is formatted like this type
.controlName
.errorKey
.
-
type
is the type of validation, which defaults tovalidation
just to set it apart from any other translation strings -
controlName
is the name of the form control -
errorKey
is the name of the validation constraint
For example validation.name.required
is the translation key for the name
form control that has Validation.required
as constraint.
The component containing the error message will be created and the message is then set.
const factory = this.resolver.resolveComponentFactory(this.config.errorsComponent);
this.ref = this.container.createComponent(factory);
this.ref.instance.text = errorText;
Then the component that is generated has the following template.
<ng-container *transloco="let t">
<p class="input-errors">
{{ t(text) }}
</p>
</ng-container>
The text set with this.ref.instance.text
is an @Input() text
on the component after which the error text can be used in the template. The error text is an translation string and so we use it inside the transloco translation function.
Form name as scope to differentiate messages between forms
Let's say you have 2 forms that have an input for name, but the names have different meanings. In this case you might want a different validation message. In which case validation.name.required
probably won't do it, because name
will the a form control of many forms (eg. project name, user name, company name). You probably want something like validation.userForm.name.required
. To achieve this we must be able to set the scope for a form or a group of form controls.
<form [formGroup]="form" ngxTvScope="userForm">
<div>
<label for="name">{{ translate('name') }}</label>
<input formControlName="name" id="name" />
</div>
</form>
ngxTvScope
is a directive with the sole purpose of relaying the scope to generate the correct translation string. The translation string then becomes type
.scope
.controlName
.errorKey
.
To get the scope inside our error directive we simply inject the scope directive. It will inject the closest scope directive, searching in its parent nodes. Since providing a scope is not mandatory we do make it optional and should consider a default option. Also notice the @Host()
which limits the search up to the host element, which means that it searches inside the component where the form control lives angular docs.
constructor(
@Optional() @Host() controlScope?: NgxTvScopeDirective
){}
...
this.scope = this.controlScope?.scope ?? this.config.defaultScope;
...
this.setError(`${this.config.type}.${this.scope}.${this.controlDir.name}.${firstKey}`);
Result
We now get validation messages when a form control is at an invalid state... automatically! All you need to do is install the package, import it in your AppModule
, import it in the module your component lives in and then all your form elements will show validation messages. Also check out the example here or if you want to have a look at the code checkout the Stackblitz.
What's more?
These were some interesting example of what this library can do, but of course there is more to it than just this. You can checkout the project on GitHub. Please give it a try, the readme should provide a pretty decent getting started. If you feel that some features are missing or that there is a bug, please do not hesitate to create on issue or create a PR, I welcome contributions. ;-)
RiskChallenger
RiskChallenger is a company that delivers intuitive risk management software that encourages users for collaborate to explore risks and mitigate them by creating and managing measure. These risks can be project risks, for instance for large construction projects, or business risks. Want to know more about our company or our software? Please have a look at our website https://riskchallenger.nl
Top comments (0)