Translation validation for Angular - automatic validation messages translated

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
validation status 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 to validation 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

33