1

I am working on an app in Angular 14.

The app is designed to contain multiple forms in various components which is why I thought it was a good idea to create a reusable component for error messages.

In form.component.ts I have:

import { Component } from '@angular/core';
import { FormGroup, FormControl, Validators } from '@angular/forms';
import { FormService } from '../services/form.service';

@Component({
  selector: 'app-form',
  templateUrl: './form.component.html',
  styleUrls: ['./form.component.css'],
})
export class FormComponent {
  public errorMessage: any = null;

  // More code

  public sendFormData() {
    this.formService.sendFormData(this.formService.value).subscribe(
      (response) => {},
      (error) => {
        this.errorMessage = error.message;
      }
    );
  }
}

In errors.component.ts I have:

import { Component } from '@angular/core';

@Component({
  selector: 'app-errors',
  template: `<div class="alert-error">{{ errorMessage }}</div>`,
  styles: [
    `
    .alert-error {
      color: #721c24;
      background-color: #f8d7da;
      border-color: #f5c6cb;
      padding: 0.75rem 1.25rem;
      margin-bottom: 1rem;
      text-align: center;
      border: 1px solid transparent;
      border-radius: 0.25rem;
    }
    `,
  ],
})
export class ErrorsComponent {}

I call the abve component in app.component.html on the condition that there are errors:

<app-errors *ngIf="errorMessage"></app-errors>
<app-form></app-form>

There is a Stackblitz HERE.

The problem

In reality, even though there are errors (the errorMessage is different from null), the ErrorsComponent component is no rendered.

Questions

  1. What is my mistake?
  2. What is the most realizable way to fix this problem?
Razvan Zamfir
  • 4,209
  • 6
  • 38
  • 252

2 Answers2

2

Your problem is due to errorMessage is defined inside of the FormComponent. And it exists only there, you cant reach it that simple outside of this exact component. You need to wire all of them using @Output() and @Input() and probably using eventEmitters.

But, one of the classic approaches is to create a service, that will hold this errorMessage and will provide it to ErrorsComponent.

Very rough example:

Service (provideIn: root as singleton):

import { Injectable } from '@angular/core';
import { BehaviorSubject } from 'rxjs';

@Injectable({
  providedIn: 'root',
})
export class ErrorService {
  private readonly _errorMessage$ = new BehaviorSubject<string | undefined>(
    undefined
  );

  public readonly errorMessage$ = this._errorMessage$.asObservable();

  public setErrorMessage(value: string | undefined) {
    this._errorMessage$.next(value);
  }

  constructor() {}
}

Error component:

import { Component } from '@angular/core';
import { Observable } from 'rxjs';
import { ErrorService } from '../services/error.service';

@Component({
  selector: 'app-errors',
  template: `<div class="alert-error" *ngIf="errorMessage$ | async as errorMessage">{{ errorMessage }}</div>`,
  styles: [
    `
    .alert-error {
      color: #721c24;
      background-color: #f8d7da;
      border-color: #f5c6cb;
      padding: 0.75rem 1.25rem;
      margin-bottom: 1rem;
      text-align: center;
      border: 1px solid transparent;
      border-radius: 0.25rem;
    }
    `,
  ],
})
export class ErrorsComponent {
  public readonly errorMessage$: Observable<string | undefined>;

  constructor(private readonly errorService: ErrorService) {
    this.errorMessage$ = errorService.errorMessage$;
  }
}

Forms component:

import { Component, OnDestroy } from '@angular/core';
import { FormGroup, FormControl, Validators } from '@angular/forms';
import { ErrorService } from '../services/error.service';
import { FormService } from '../services/form.service';

@Component({
  selector: 'app-form',
  templateUrl: './form.component.html',
  styleUrls: ['./form.component.css'],
})
export class FormComponent implements OnDestroy {
  public accountTypes!: any;
  public selectedAccountType: any;

  public form: FormGroup = new FormGroup({
    first_name: new FormControl('', Validators.required),
    last_name: new FormControl('', Validators.required),
    email: new FormControl('', [Validators.required, Validators.email]),
    accountInfo: new FormGroup({
      account_type: new FormControl(''),
    }),
  });

  constructor(
    private readonly formService: FormService,
    private readonly errorService: ErrorService
  ) {}

  ngOnDestroy(): void {
    this.errorService.setErrorMessage(undefined);
  }

  ngOnInit(): void {
    this.getAccountTypes();
  }

  public getAccountTypes() {
    this.formService.getAccountTypes().subscribe((response) => {
      this.accountTypes = response;
      this.form.controls['accountInfo'].patchValue({
        account_type: response[0].value, // use index as per your choice.
      });
    });
  }

  public sendFormData() {
    this.errorService.setErrorMessage(undefined);
    this.formService.sendFormData(this.formService.value).subscribe(
      (response) => {},
      (error) => this.errorService.setErrorMessage(error.message)
    );
  }
}

StackBlitz: click

Sergey Sosunov
  • 4,124
  • 2
  • 11
  • 15
  • How would you make the alert dismissible? – Razvan Zamfir Jan 25 '23 at 07:09
  • @RazvanZamfir Hi, can you please clarify about "dismissable"? When and how do you want to dismiss it? – Sergey Sosunov Jan 25 '23 at 13:41
  • @RazvanZamfir Sorry, having some issues with ethernet and mobile network and electricity here, will answer as soon as everything stabilized, but please, explain me what you want to happen with alert and i'll elaborate. But basically you need to call `this.errorService.setErrorMessage(undefined);` to dismiss the error component, as it is shown in the Forms component. Or you can add a "X" button to the Error component and bind onClick to it that will do the same – Sergey Sosunov Jan 25 '23 at 13:52
  • Alerts that the user can close by clicking the "x" are [dismissible alerts](https://www.w3schools.com/bootstrap4/bootstrap_alerts.asp) in Bootstrap terminology. – Razvan Zamfir Jan 25 '23 at 14:44
  • @RazvanZamfir Honestly you can just inspect a bit [angular-bootstrap lib](https://ng-bootstrap.github.io/#/components/alert/examples). Or [ngx-toast](https://www.npmjs.com/package/ngx-toastr), as you can see - ngBootstrap is using input() and output() bindings and toast - is using service. Nothing new here, same approaches :) – Sergey Sosunov Jan 25 '23 at 14:51
1

Your error component needs some work. If you want to make the error component re-usable, you should create a pub/sub method, so that any component can publish an error message, and error component can capture the error and display it. I'd suggest making following changes:

  1. Add a subject in your service file.
  2. When you get an error, emit the error message to the subject.
  3. Subscribe to the subject in error component. You can control the display from error.component, so your app.component is not responsible for rendering the error alert.

Stackblitz demo

service.ts


  public subject = new Subject<any>();
  dataObservable$ = this.subject.asObservable();

...
...

  updateErrorMsg(error: any) {
    this.subject.next(error);
  }

form.component.ts:

public sendFormData() {
    this.formService.sendFormData(this.form.value).subscribe(
      (response) => {
      },
      (error) => {
        this.formService.updateErrorMsg(error.message);
      }
    );
  }

error.component.ts:

<div *ngIf="errorMessage" class="alert-error">{{ errorMessage }}
  <div><button (click)="closeError()">Close</button></div>
</div>`
export class ErrorsComponent implements OnInit {
  errorMessage;

  constructor(private formService: FormService) {}

  ngOnInit() {
    this.subscribeToData();
  }

  subscribeToData() {
    this.formService.dataObservable$.subscribe((data) => (this.errorMessage = data));
  }

  closeError() {
    this.errorMessage = null;
  }
}
Nehal
  • 13,130
  • 4
  • 43
  • 59