I am working on a simple Angular login dialog that uses HttpClient to post() to my backend API. I am strictly following the approach laid out in the angular httpclient docs.
Everything is working fine, except in the case where an error occurs (I purposely turn off the backend API to generate a 504). Even in this case, all the error handling code gets executed exactly as I expect, but the problem is that the angular UI fails to update.
In the error handling code that's being correctly executed, I am updating a simple string variable in my component that binds to the dialog UI through standard interpolation; but the change never shows up in the UI.
Here's my service code that makes the HttpClient post call (note: I am using pipe() and catchError() and returning a new ErrorObservable as recommended in the docs):
import { Injectable } from '@angular/core';
import { HttpClient, HttpHeaders, HttpErrorResponse } from '@angular/common/http';
import 'rxjs/add/operator/timeout';
import { catchError} from 'rxjs/operators';
import { Observable } from 'rxjs/Observable';
import { ErrorObservable } from 'rxjs/observable/ErrorObservable';
// My models
import { SignupRequest } from '../models/signuprequest';
import { LoginRequest } from '../models/loginrequest';
import { LoginResponse } from '../models/loginresponse';
import { SignupResponse } from '../models/signupresponse';
import { ErrorResponse } from '../models/errorresponse';
@Injectable()
export class LoginService {
private readonly loginUrl = 'http://localhost:4200/api/login';
private readonly signupUrl = 'http://localhost:4200/api/signup';
private readonly timeout = 20000;
constructor(private http: HttpClient) {}
LoginUser(loginRequest: LoginRequest) {
console.log('LoginService: login request ', loginRequest);
return this.http.post<LoginResponse>(this.loginUrl, loginRequest, { observe: 'response' })
.timeout(this.timeout)
.pipe(catchError(this.HandleError));
}
private HandleError(error: HttpErrorResponse, caught: Observable<any>) {
const er: ErrorResponse = {
response: error,
url: error.url ? error.url : '',
status: error.status ? error.status : '',
statusText: error.statusText ? error.statusText : '',
message: error.message ? error.message : '',
error: error.error ? error.error : ''
};
console.log('LoginService: error: ', er);
return new ErrorObservable(er);
}
}
And here is the component code that invokes the service call and subscribes to the resulting Observable:
import { Component, OnInit, Injectable, ApplicationRef, NgZone } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { HttpResponse } from '@angular/common/http';
import { LoginService } from '../../services/login.service';
import { SignupRequest } from '../../models/signuprequest';
import { LoginRequest } from '../../models/loginrequest';
import { LoginResponse } from '../../models/loginresponse';
import { SignupResponse } from '../../models/signupresponse';
import { ErrorResponse } from '../../models/errorresponse';
@Component({
selector: 'app-login',
templateUrl: './login.component.html',
styleUrls: ['../../app.component.css']
})
export class LoginComponent implements OnInit {
...
constructor( private fb: FormBuilder, private loginService: LoginService ) {
...
}
public Login() {
const loginRequest = this.ExtractLoginRequest();
if (this.ValidLogin(loginRequest)) {
console.log('LoginComponent: login request ', loginRequest);
this.loginService.LoginUser(loginRequest).subscribe(
this.HandleLoginResponse,
// The line below should work fine based on everything I've read
this.HandleError
// But the UI will not update unless I use the zone.run() line below instead!?
// (error) => { this.zone.run( () => { this.HandleNetworkError(error); } ); }
);
console.log('finished the login request flow');
}
private HandleLoginResponse(httpResponse: HttpResponse<LoginResponse>) {
const loginResponse: LoginResponse = httpResponse.body;
if (!this.BackEndLoginError(loginResponse)) {
console.log('LoginComponent: Storing access token in localstorage', loginResponse.token);
window.localStorage.setItem('littlechatToken', loginResponse.token);
}
}
private HandleError(errorResponse: ErrorResponse) {
console.log('LoginComponent: handling error');
// set the error text that is displayed on a div in the UI
this.backendLoginErrorText = 'Network error: ' + errorResponse.message;
}
}
And in the component template I'm interpolating to display the 'backendLoginErrorText' variable...
<!-- Login button and error text -->
<div class="centered">
<button type="button" [disabled]="LoginDisabled()" name="login" (click)="Login()">Login</button>
<div class="alert alert-danger" [hidden]="backendLoginErrorText.length === 0"> {{backendLoginErrorText}} </div>
</div>
As I've noted in the code comments in the component code, simply passing this.HandleError as the error-handling function to subscribe() does not work -- the UI never gets updated! Note: all code is executed properly (i.e. the update is definitely happening, the change detection is just not working).
If I instead use this.zone.run(...) as noted above, the update happens just fine.
After much reading on SO and otherwise, I understand the basics of what's going on here -- something about the fact that the 504 error is happening after a few seconds asynchronously is somehow causing the error handling to be executed outside of Angular, necessitating the zone.run()?
I am just learning Angular, and a more experienced pal has told me "if you ever have to use zone.run() in a situation like this, you're doing something wrong". I find it really hard to believe such a simple scenario (basic login form with backend API and a post() call) -- using the suggested approach from the angular docs -- would require such a hacky-seeming thing....
What am I missing here?