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?

3 Answers 3

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}),
Sign up to request clarification or add additional context in comments.

2 Comments

Sorry, I might have missed it in the code here but I had already tried that. It just doesn't work.
Ok then please amend the code you've posted to reflect that.
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.

Comments

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

});

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.