0

I'm developing an Angular 8 application. I want to display form errors using NgRx store and reactive forms using a custom asynchronous validator.

login.component.ts

@Component({
  selector: 'auth-login',
  templateUrl: './login.component.html',
  styleUrls: ['./login.component.scss'],
})
export class LoginComponent implements OnInit {
  public transitionController: TransitionController = new TransitionController(
    true,
  );
  public form: FormGroup;
  public pageState$: Observable<ILoginPageState>;
  public formSubmitted: boolean = false;

  constructor(
    private _formBuilder: FormBuilder,
    private _store: Store<IAppState>,
  ) {
    this.pageState$ = this._store.select(selectLoginPageState) as Observable<
      ILoginPageState
    >;
    this._buildForm();
  }

  ngOnInit() {
    this._animatePage();
  }

  public onFormSubmit() {
    this.formSubmitted = true;

    if (this.form.invalid) {
      return;
    }

    this._store.dispatch(Login(this.form.value));
  }

  private _buildForm() {
    this.form = this._formBuilder.group({
      email: this._formBuilder.control(
        null,
        [Validators.required, Validators.email],
        [this.test.bind(this)],
      ),
      password: this._formBuilder.control(null, Validators.required),
    });
  }

  private test(control: AbstractControl) {
    return this._store.select(selectLoginErrorMessage).pipe(
      tap(() => console.log('executing')),
      map(value => ({
          foo: true
      })),
    );
  }

  private _animatePage() {
    this.transitionController.animate(
      new Transition(EAnimationType.FadeUp, 500, TransitionDirection.In),
    );
  }
}

login-page.effects.ts

@Injectable()
export class LoginEffects {
  constructor(
    private _actions$: Actions,
    private _authService: AuthenticationSevice,
    private _router: Router,
    private _modalService: SuiModalService,
    private _store: Store<IAppState>,
  ) {}

  Login$ = createEffect(() => {
    return this._actions$.pipe(
      ofType(AuthActions.Login),
      tap(() => this._store.dispatch(ShowPageLoader())),
      switchMap((credentials: ILoginCredentials) =>
        this._authService.login(credentials).pipe(
          map((response: ILoginResponse) => AuthActions.LoginSuccess(response)),
          catchError((response: HttpErrorResponse) => {
            let validationErrors: ValidationErrors;

            switch (response.status) {
              case HttpStatusCode.BAD_REQUEST:
                validationErrors = {
                  error: {
                    validationErrors: response.error,
                    generalError:
                      'Oops! We found some errors with your provided details.',
                  },
                };
                break;
              case HttpStatusCode.NOT_FOUND:
                validationErrors = {
                  error: {generalError: 'Email or password is incorrect.'},
                };
                break;
            }

            return of(AuthActions.LoginFailure(validationErrors));
          }),
          finalize(() => this._store.dispatch(HidePageLoader())),
        ),
      ),
    );
  });

  LoginSuccess$ = createEffect(
    () => {
      return this._actions$.pipe(
        ofType(AuthActions.LoginSuccess),
        tap(() => {
          this._modalService.open(
            new ModalComponent<IModalContext>(undefined, {
              title: 'Login successful',
              imageSrc: 'assets/images/modal/login-successful.png',
            }),
          );

          this._router.navigateByUrl('/home');
        }),
      );
    },
    {dispatch: false},
  );
}

The main problem here is inside my test method. I want the { foo : true} to be set on the error field but it never happens. I searched a ton on google and 1 solution I found was to add first() method inside the pipe() so that my observable gets completed. It worked, but only for the very first time. Plus, the async validator never called when the form was submitted.

All of the examples that I found on the internet were using Http call. I understand that it completes the observable when the request is complete but in my case, that Http call is being handled inside my login-page.effects.ts

Is there any better way to do it? or do I need some RxJs operator which I'm not familiar of?

Zahid Saeed
  • 152
  • 2
  • 9

3 Answers3

0

The map rxjs operator in your test() function is not returning what you think it is, because foo is interpreted as a label, not as a key in an object literal as you intend. See the docs for more explanation.

There are two ways you can get the right syntax.

1.

map(value => {
  return {foo: true};
}),

2.

map(value => ({foo: true}),
Collierre
  • 946
  • 8
  • 16
0

Instead of returning this._store.select(), try hooking into actions stream listening for multiple action completion events AuthActions.LoginFailure and AuthActions.LoginSuccess

this.actions$.pipe(
        ofType(AuthActions.LoginFailure, AuthActions.LoginSuccess),
        switchMap(x => {
            if (x.type == AuthActions.LoginFailure)
                return this._store.pipe(
                    select(selectLoginErrorMessage),
                    map(msg => ({ foo: true })),
                    take(1)
                );
            return true;
        })
    )

store.select fires immediately and does not wait for the effects to get over.

Now for the other question -

the async validator never called when the form was submitted.

The validation is configured on control level (email), so it won't be triggered implicitly. Ideally, your form should not be allowed to be submitted if there are errors in any control validation.

Pankaj
  • 538
  • 4
  • 13
0

This is a very fundamental error I would say. What you need to first of all do is to understand the difference between pipe() and subscribe().

Here is a link that will be helpful: Difference between the methods .pipe() and .subscribe() on a RXJS observable

Basically, the problem with your code is that you are piping the operation on observable which means that your operations will execute only once and it will not monitor for changes.

You need to actually subscribe to the select action using code shown below:

this.error$ = this.store.select(selectLoadingErrors);
this.error$.subscribe(data=>{
//do your code here

});
The Cloud Guy
  • 963
  • 1
  • 8
  • 20