Search code examples
angularangular-materialngrxangular-guards

Angular 7 - mat-select input doesn't open sometimes when use guards


I'm starting with multiple applications structure in Angular and I found weird behavior on mat-select from Angular Material...

I can't open dropdown - overlay is not appeared and normal click event is dispatched instead of material open with e.preventDefault. To check this behavior i use (click) handler to log event.

If works - no event log (e.preventDefault called, select overlay opened normally) If it doesn't - click event logged - any change on UI

So... First I've checked that maybe this is Material issue, so I switched to NG-Zorro - recommended from angular.io framework and still the same...

After many hours of debugging, testing and searching I found that problem is with AuthGuard - still don't know why, but let me describe auth flow first.

Landing Page -> Login with Google with redirection to App -> Authorize action (ngrx) dispatched on init (in app.component) -> ngrx effect with watch for gapi token and dispatch auth success action -> effect and call to api for user data -> login success.

I prepared this auth.effect for testing

@Effect()
  authorize$: Observable<Action> = this.actions$.pipe(
    ofType(ActionTypes.Authorize),
    switchMap(() => this.authService.authState.pipe(
      map(user => {
        return user
          ? new AuthorizeSuccess(user)
          : new AuthorizeFailed();
      }),
      catchError(() => of(new AuthorizeFailed()))
    ))
  );

  @Effect()
  authorizeSuccess$: Observable<any> = this.actions$.pipe(
    ofType(ActionTypes.AuthorizeSuccess),
    switchMap((action: { type: string; payload: any }) => this.httpClient.get(`${environment.apiServerUrl}/users/me`)
      .pipe(take(1), map(response => {
        const user = new User(response ? { ...action.payload, ...ConvertingService.removeEmptyObjectValues(response) } : action.payload);
        return new LoginSuccess(user);
      }))
    ),
    tap(() => setTimeout(() => this.preloaderService.setInfinitely(true, false), 100)),
    catchError(() => of(new LoginFailed()))
  );

So you can see action flow.

Auth.reducer

export interface AuthenticationState {
  authorized: boolean
  profile: AuthUser
  user: User
}

export const initialState: AuthenticationState = {
  authorized: false,
  profile: null,
  user: null
};

export function authenticationReducer(
  state = initialState,
  action: Union
): AuthenticationState {
  switch (action.type) {

    case ActionTypes.AuthorizeSuccess:
      return {
        ...state,
        profile: action.payload
      };

    case  ActionTypes.LoginSuccess:
      return {
        ...state,
        user: action.payload,
        authorized: true
      };

   ...

    default:
      return state;
  }
}

And simple selector

export interface AppState {
  auth: AuthenticationState;
  router: RouterReducerState<RouterStateUrl>;
}

export const reducers: ActionReducerMap<AppState> = {
  auth: authenticationReducer,
  router: routerReducer
};

// SELECTORS
export const selectAuth = (state: AppState) => state.auth;
export const selectRouter = (state: AppState) => state.router;
export const authState = createSelector(selectAuth, (state) => state.profile);
export const authorized = createSelector(selectAuth, (state) => state.authorized);
export const currentUser = createSelector(selectAuth, (state) => state.user);

Finally guard that doesn't do anything except wait for user to be loaded

@Injectable({
  providedIn: 'root'
})
export class AuthGuard implements CanActivate {
  constructor(private store: Store<AppState>) { }

  canActivate(): Observable<boolean> {
    return this.checkStore().pipe(
      switchMap(() => of(true)),
      catchError(() => of(false))
    );
  }

  private checkStore(): Observable<boolean> {
    return this.store.pipe(
      select(authorized),
      filter(authenticated => authenticated),
      take(1)
    );
  }
}

This is added to routing with

{
    canActivate: [AuthGuard],
    loadChildren: './modules/home/home.module#HomeModule',
    path: 'home',
},

So... Everything looks fine for me (if someone has any good practice for this code - any feedback will be nice :) ). But when I run this and refresh page selector want work sometimes. Ok, most of time. But when I go to landing then login then after login I'm redirected to page all seems to work fine. Problem is only when I directly call this route when I'm logged in.

See animation below enter image description here

Another track was that when I just return true in that guard, or make it as much similar to this as it can, eg using some delay pipe or what is weird, when I map this to true, it works. But with expected check it won't.

I will be very thankful for any suggestions, because I spend on it 4 last days with refactoring, testing and debugging :(

Here is repo with sample project: https://github.com/wojtek1150/mat-select-error

Greetings, W


Solution

  • Change this._authState.next(this.getProfile(r.id_token)) to this.ngZone.run(() => this._authState.next(this.getProfile(r.id_token)));

    You need to pass lambda to ngZone