0

I'm doing a web application in Angular 8 and I want to have a reusable component to only show my FormControl's error to avoid repeat this logic in a lot of places. I'm using bootstrap v4.4.1.

<form [formGroup]="form" (ngSubmit)="onSubmit()">

      <div class="input-group">
        <label for="emailInput" class="sr-only">Email</label>
        <input id="emailInput"
               formControlName="email"
               type="email"
               class="form-control"
               [ngClass]="{
                  'is-invalid': form.get('email').invalid && (form.get('email').dirty || form.get('email').touched)
                  }"
               placeholder="Email"
               required
               autofocus>
        // Reusable component to show errors
        <app-error-message-container [control]="form.get('email')" errorMessagesKeyName="email"></app-error-message-container>
      </div>

      <button type="submit" class="btn btn-primary btn-block mt-3">Log in</button>

    </form>

Reusable error message component:

import {Component, Input, OnInit} from '@angular/core';
import {AbstractControl} from '@angular/forms';

const FORM_VALIDATION_MESSAGES = {
    email: [
      {type: 'required', message: 'Is required.'},
      {type: 'pattern', message: 'No pattern.'}
    ],
  };

@Component({
  selector: 'app-error-message-container',
  templateUrl: './error-message-container.component.html',
  styleUrls: ['./error-message-container.component.scss']
})
export class ErrorMessageContainerComponent implements OnInit {

  @Input() control: AbstractControl;
  @Input() errorMessagesKeyName: string;
  errorMessages: { type: string, message: string }[];

  constructor() {
  }

  ngOnInit() {
    this.errorMessages = FORM_VALIDATION_MESSAGES[this.errorMessagesKeyName];
  }

}

Template

<ng-container *ngFor="let validation of errorMessages">
  <div *ngIf="control.hasError(validation.type) && (control.dirty || control.touched)"
       class="invalid-feedback">
    {{validation.message}}
  </div>
</ng-container>

Demo on stackblitz: https://stackblitz.com/edit/angular-ivy-dnmnpk

If there are errors, the input's class is-invalid is activated and I see a red border, but, the reusable error component is not showing the message. I think that the cause is related to bootstrap and the class="invalid-feedback".

If I don't use a reusable component to show the error message, everything works as expected, like this:

<form [formGroup]="form" (ngSubmit)="onSubmit()">

      <div class="input-group">
        <label for="emailInput" class="sr-only">Email</label>
        <input id="emailInput"
               formControlName="email"
               type="email"
               class="form-control"
               [ngClass]="{
                  'is-invalid': form.get('email').invalid && (form.get('email').dirty || form.get('email').touched)
                  }"
               placeholder="Email"
               required
               autofocus>
  // Is showing correctly
  <div *ngIf="form.get('email').hasError('required') && (form.get('email').dirty || form.get('email').touched)" class="invalid-feedback">
    There is an error!
  </div>
</div>

My goal is to be able to use a reusable component to show the errors correctly with bootstrap.

RRGT19
  • 1,437
  • 3
  • 28
  • 54
  • I have implemented something similar on Angular Material Reactive forms. You need to pass the validation function as a parameter. I will work with the stackblitz and post it – T. Sunil Rao May 10 '20 at 21:30

2 Answers2

1

If there are errors, the input's class is-invalid is activated and I see a red border, but, the reusable error component is not showing the message. I think that the cause is related to bootstrap and the class="invalid-feedback".

That is exactly the cause.

Removed class="invalid-feedback" from template

<ng-container *ngFor="let validation of errorMessages">
  <div *ngIf="control.hasError(validation.type) && (control.dirty || control.touched)">
    {{validation.message}}
  </div>
</ng-container>

Added the same to the component directive

@Component({
  selector: 'app-error-message-container',
  host: { class: 'invalid-feedback' },
  templateUrl: './error-message-container.component.html',
  styleUrls: ['./error-message-container.component.scss']
})
export class ErrorMessageContainerComponent implements OnInit {
  ...
}

The issue is resolved

Happy coding!

T. Sunil Rao
  • 1,167
  • 5
  • 14
  • Thanks for you help. Could you add more details about what did you do and why adding the host property make it work? I don't get it. Also, I don't know the reason behind why having the `class="invalid-feedback"` in the error template don't work. Just a note: TSlint is saying "Use @HostBinding or @HostListener rather than the `host` metadata property", any idea? – RRGT19 May 10 '20 at 22:43
  • Welcome! Had to dig the `invalid-feedback` class. The CSS properties are coded in such a way that it needs to be direct child of `input-group` to be positioned at the correct place inside it. Having it elsewhere renders `display:none` hiding the content. – T. Sunil Rao May 10 '20 at 22:52
  • As for TSlint, you can read https://stackoverflow.com/questions/45172565/use-hostbindings-instead-host-in-angular-4 – T. Sunil Rao May 10 '20 at 22:53
0

In your ErrorMessageContainerComponent, your invalid-feedback class has display property set to none since it is not the sibling of an input element. If you would still like to apply the invalid-feedback class style from Bootstrap 4, you can set the display manually by using class="invalid-feedback d-block".

ipekkr
  • 1