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.
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
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