Handling validation messages in Angular like a boss

Sergey Gultyayev
5 min readNov 8, 2021

--

Tired of writing validation messages for each control and applying conditions to show them? In this post, I will explain how to create a reusable component for printing error messages and styling them easily.

An example of code where the old approach is cumbersome and rigid and the new approach takes just a few lines to define a control with its validation messages

The agenda:

  1. Write a wrapper component that will project a form control and print the error messages
  2. Define an error config to show the error messages
  3. Show an error in the template dynamically
  4. Add ngx-translate to enable translations and augment them with error details

A little of the background. I’ve done a project where a user would start with just 20 required fields and could add many more depending on their need. The forms were fluid and extensible in width and depth.

The simplest form group started from 7 controls each of which had validation messages to show. Then the quick realization came into mind that the basic approach that is usually being shown in tutorials where you hard code each message in the template for every single field isn’t practical.

This is where the idea of having a shared error printing logic was born. Now let’s get to the code.

Defining an anchor

First, we will start with an anchor. For an anchor, we will use a directive that is put exclusively onto input fields and which we will use as a selector to register the control in our wrapper.

For now, let’s keep it simple

@Directive({
selector: 'input[myInput]'
)
export class MyInputDirective {
constructor(@Optional() @Self() public ngControl: NgControl) {}
}

Here, we define a directive, from the host element injectNgControl which is optional and make it public so we can access it from our wrapper.

Creating a wrapper

Now it’s time to create a basic wrapper that will project input and print some messages after it. The wrapper will get a reference to the input through the @ContentChild decorator using previously created directive as a selector.

@Component({
selector: 'my-form-field',
templateUrl: './form-field.component.html',
styleUrls: ['./form-field.component.css'],
})
export class FormFieldComponent implements OnInit {
@ContentChild(MyInputDirective, { static: true })
myDirective: MyInputDirective;

ngOnInit() {
if (!this.myDirective) {
throw new Error('MyInputDirective is required!')
}
}
}

Notice the {static: true} in the ContentChild arguments, that allows us to access the directive right at the component’s initialization and warn the developers that the directive is required. The template, for now, is simple
<ng-content></ng-content> .

Configuring the error messages

Now, let’s define our error messages. We will be using an object where the keys will correspond to the control error keys, and the values are the messages to print.

export const ERROR_MESSAGES = {
required: 'This field is required',
minlength: 'The entered value is too short',
maxlength: 'The entered value is too long',
}

Displaying validation error messages

For this one, we will need to add a getter to the wrapper which will return an error message if any.

get errorMessage(): string | null {
const errors = Object.entries(
this.myDirective?.ngControl?.control?.errors || {}
);

if (!errors.length) {
return null;
}

const [key, value] = errors[0];

return ERROR_MESSAGES[key];
}

And update the wrapper’s template so as to show the error message.

<ng-content></ng-content>
<div *ngIf="errorMessage">{{ errorMessage }}</div>

Now, let’s update the app.component.html file with the following content

<my-form-field>
<input [(ngModel)]="name" myInput type="text" required minlength="4" />
</my-form-field>

Now, it’s time to check how it works

Validation message for the empty required field
Validation message which tells us that the entered value is too short

Great! At last, we can observe things getting right!

Adding translations to the equation

Nowadays, most of our apps are shipped with multiple locales. One of the most popular libraries for this is ngx-translate . It enables us to change the translations in real-time with only one app build.

Let’s define a couple of translations, for brevity, I’ll define them in a TS file

export default {
errorMessages: {
required: 'This field is required',
minlength: 'The entered value should be longer than {{requiredLength}}',
maxlength: 'The entered value should be shorter than {{requiredLength}}',
},
};

and update our ERROR_MESSAGES config

export const ERROR_MESSAGES: Record<string, string> = {
required: 'errorMessages.required',
minlength: 'errorMessages.minlength',
maxlength: 'errorMessages.maxlength',
};

also, we would need to update our wrapper to display translated messages rather than hardcoded strings.

constructor(private translateService: TranslateService) {}

get errorMessage(): { key: string; options: any } | null {
const errors = Object.entries(
this.myDirective?.ngControl?.control?.errors || {}
);

if (!errors.length) {
return null;
}

const [key, value] = errors[0];

return {
key: ERROR_MESSAGES[key],
options: value,
};
}
<ng-content></ng-content>
<div *ngIf="errorMessage as config">
{{ config.key | translate: config.options }}
</div>

Now, that we updated returned value we use the “key” as a translation string and its “options” as an object which will be passed as translation options.

Let’s check what we have now

Input validation tells us that the entered value is too short, but also specifies the minimum required length

Yay! Finally, we can provide our users with something more specific than “too long” or “too short”!

Styling time

Here we will use the directive’s selector, this will help us encapsulate input styles.

.error-message {
color: red;
}

:host ::ng-deep {
input[myInput] {
display: block;
padding: 10px;
margin-top: 5px;
margin-bottom: 10px;
border: 1px solid rgba(0, 0, 0, 0.3);
border-radius: 5px;
color: #4a4a4a;

&:focus {
border-color: rgba(0, 0, 0, 0.7);
outline: 0;
}
}
}

Everything is encapsulated in the form-field styles. This enables us to keep all form styles close, also we won’t be wandering across the project searching if we have dead styles, because all classes here are either applied in the form-field’s template or attached to the host by MyInputDirective (these are the rules you must apply to make your development easier and more organized).

Input with a bit of styling applied to it and its validation message

Summarizing what we’ve done today

We have separated translations definitions from the controls they are used for, made our templates clean and DRY as we no longer need to copy-paste the same validation rules.

The error messages are not only translated but also are independent of whether we use FormsModule or ReactiveFormsModule.

Beware, that these won’t be shown if you don’t use Angular forms.

Ways to make it better even more:

  1. Introduce the OnPush strategy
  2. Add an optional input to the FormFieldComponent where we could pass an object with translations overrides
  3. Extend directive’s selector to accept different controls
  4. Adjust error message show condition to wait for input value being changed

The resulting code is available on GitHub

--

--

Sergey Gultyayev

A front-end developer who uses Angular as a main framework and loves it