In Angular how to create a custom validator that validates a http request?

3k Views Asked by At

I'm working on a application that uses gitLab Issues to display a chart. To authenticate with the api I want to use a access token that gets appended to the get request like that:

https://gitlab.de/api/v4/issues?private_token=************

I have a form where the user enters the personal access token. I want to validate the token with a custom input validator and add a error message below the input field (I'm using material angular).

I use a service to make the http requests:

  private makeGetRequest(endpoint: string, params: HttpParams) {
    return this.http.get<Issue[]>(this.buildURL(endpoint), {params}).pipe(catchError(this.handleError));
  }

  public getIssues(): Observable<Issue[]> {
    let params = new HttpParams().set('private_token', this.access_token);
    return this.makeGetRequest('/issues', params)
  }

  private handleError(error: HttpErrorResponse) {
    if (error.error instanceof ErrorEvent) {
      // A client-side or network error occurred. Handle it accordingly.
      console.error('An error occurred:', error.error.message);
    } else {
      // The backend returned an unsuccessful response code.
      // The response body may contain clues as to what went wrong.
      console.error(
        `Backend returned code ${error.status}, ` +
        `body was: ${error.error}`);
    }
    // Return an observable with a user-facing error message.
    return throwError(
      'Something bad happened; please try again later.');
  }

In the component I added a validator function. I'm trying to make some call to the api and then check if it worked.

  // Form group with validation
  authForm = new FormGroup({
    url: new FormControl('gitlab.de'),
    token: new FormControl('', [this.validateToken])
  });


  // Add error message below input fields
  getErrorMessage() {
    if (this.authForm.controls.token.hasError('required')) {
      return 'You must enter a token';
    }
    return this.authForm.controls.token.hasError('tokenInvalid') ? 'Not a valid token' : '';
  }

// Check if token is valid using api
  validateToken(control: AbstractControl): { [key: string]: any } | null {
    if (control.dirty || control.touched) {
      this.apiService.getIssues().subscribe((response) => {}, (error) => {return {'tokenInvalid': true}})
    } else {
      return null
    }
  }

There are several tutorials but I can't wrap my head around them. When I type something in the input I just get this output in console: ERROR TypeError: this is undefined

2

There are 2 best solutions below

2
On

The validator function is executed by Angular and not our code which is the reason why the 'this' keyword inside your validator function does not point to your class.

You can simply bind your validateToken function in the Validators array like this:

token: new FormControl('', [this.validateToken.bind(this)])

Also, I would recommend you to use custom async validator function if you are performing async operations in your validator function like this by returning a promise/observable:

usernameValidator(): AsyncValidatorFn {
  return (control: AbstractControl): Observable<ValidationErrors | null> => {
    return this.checkIfUsernameExists(control.value).pipe(
      map(res => {
        // if res is true, username exists, return true
        return res ? { usernameExists: true } : null;
        // NB: Return null if there is no error
      })
    );
  };
}
0
On

you do not need to create service, you can access http module with the depenceny injection. first set up the http module:

in app.module.ts:

import { HttpClientModule } from '@angular/common/http';
@NgModule({
  declarations: [AppComponent],
  imports: [BrowserModule, HttpClientModule],
  providers: [],
  bootstrap: [AppComponent],
})

create a class to write a custom async validator:

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { AsyncValidator, FormControl } from '@angular/forms';
import { map, catchError } from 'rxjs/operators';
import { of } from 'rxjs';

// this class needs to use the dependency injection to reach the http client to make an api request
// we can only access to http client with dependecny injection system
// now we need to decorate this class with Injectable
@Injectable({
  providedIn: 'root',
})
export class HttpRequestValidation implements AsyncValidator {
  // with this code inside the constructor, we access to "http"
  constructor(private http: HttpClient) {}
  
  // validate() will be called by the component that implments the 
     form which has a  different context. "this" refers to the context 
     that calls the function. so in other context, "this" will refer 
     to that context, so "this.http" will be undefined in that context 
     cause that context does not have http.
 // that is why we use arrow function here. Because, wherever you use 
    the arrow function, "this" inside arrow will always refer to the 
    object that it was created in. in this case "this" will always 
    refer to the current class (HttpRequestValidation). so "this.http" 
    will work.
  validate = (control: FormControl) => {
    // if this validator would be used by the FormGroup, you could use "FormGroup" type.
    //if you are not sure you can  use "control: AbstractControl)"
    // looks like, in your case you need for the FormControl
    const { value } = control;
    return this.http
      .post<any>('https://domain/', {
      //looks like you are checking for the token
        token: value,
      })
      .pipe(
        //   errors skip the map(). if we return null, means we got 200 response code
        map(() => {
          return null;
        }),
        catchError((err) => {
          //check the err obj to see its properties
          console.log(err);
          if (err.error.token) {
          //catchError has to return a new Observable and "of" is a shortcut
            return of({ write a meaningful obj});
          }
          return of({ write a meaningful obj});
        })
      );
  };
}

now we you wrote this in a separate class, it is time to use it for your form.

authForm = new FormGroup({
    url: new FormControl('gitlab.plri.de'),
    token: new FormControl('', [arrays of sync validators],asyncValidator)
  });

FormControl()has 3 args. 1st is the initial value, second is the array of sync validators, 3rd is the async validator. Since implementing async validator is expensive, angular will first resolve the sync validators. If all of the sync validators validated the input and then async validation will be initated.

import { Component, OnInit } from '@angular/core';
import { FormGroup, FormControl, Validators } from '@angular/forms';
import { HttpRequestValidation } from '../validators/unique-username';

export class SignupComponent implements OnInit {
authForm = new FormGroup({
    url: new FormControl('gitlab.plri.de'),
    token: new FormControl('', [ 
                             Validators.required,
                             Validators.minLength(3)],
                             this.httpRequestValidation.validate)
  });
constructor(
    private httpRequestValidation: HttpRequestValidation
  ) {}

  ngOnInit(): void {}
}