I'm using angular 5.1.0, and I have an issue with the routing system, let me explain:
In my app-routing module I have an url /api that lazy loads another module, in that lazy loaded module I have the next routing implementation:
api-routing.module.ts
const routes: Routes = [
{
path: '',
component: ApisComponent,
data: {
breadcrumbs: 'APIs',
},
children: [
{
path: '',
component: ApiListComponent,
},
{
path: ':id',
component: ApiDetailComponent,
resolve: {
api: ApiResolverService
},
data: {
breadcrumbs: '{{ api.title }}',
},
},
],
},
];
The important thing here is the data param that the Router receives.
In my app I have a generic error behaviour that when an exception is throwed I have a errorHandler class that catch the error and redirects to another url: /error, this is the handler code:
import { ErrorHandler, Injectable, Injector } from '#angular/core';
import { Router } from '#angular/router';
#Injectable()
export class AppErrorHandler implements ErrorHandler {
constructor(private injector: Injector) { }
handleError(error: any): void {
const routerService = this.injector.get(Router);
routerService.navigateByUrl('/error', { skipLocationChange: true });
}
}
The problem is, when an exception is throwed inside /api and handleError is executed, I see my generic error page rendered with the breadcrumb loaded in the last route: /api by data param.
Is there any way to set the Router to reset data when is loaded? or maybe I'm doing something wrong?
UPDATE
At this point I thought the problem was due to data param, but now I see that it's not the problem. Let me show my error.component that is rendered when Router loads /error:
import { Component, OnInit } from '#angular/core';
import { Router, ActivatedRoute } from '#angular/router';
#Component({
selector: 'app-error',
templateUrl: './error.component.html',
styleUrls: ['./error.component.scss']
})
export class ErrorComponent implements OnInit {
constructor(private router: Router, private route: ActivatedRoute) { }
ngOnInit() {
console.log('snapshot trace');
console.log(this.route.snapshot);
}
}
I have included in the onInit component method, a trace of ActivatedRoute snapshot to see what it has, and the thing is that trace is not showing when errorHandler navigates from /api to /error.
But if I load directly /error the trace is showed, so for any reason the error component is not instanciated correctly in the first scenario (navigate from /api to /error)
UPDATE
I have upgraded to angular 5.2.9 and the problem still happens.
I have solved the problem using NgZone, I think the "timing" routing problem that angular has involve the render error component out of angular zone, so, the AppErrorHandler class looks like this:
import { ErrorHandler, Injectable, Injector, NgZone } from '#angular/core';
import { Router } from '#angular/router';
#Injectable()
export class AppErrorHandler implements ErrorHandler {
constructor(private injector: Injector) { }
handleError(error: any): void {
const routerService = this.injector.get(Router);
const ngZone = this.injector.get(NgZone);
ngZone.run(() => {
routerService.navigate(['/error'], { skipLocationChange: true });
});
}
}
Here a github issue related to my problem
Related
I am working on an app in Angular 14 that requires authentication/authorization, reason for witch I use Keycloak Angular. I need to guard certain routes.
As per the instructions, I have first installed Keycloak Angular with:
npm install keycloak-angular keycloak-js
In shared/auth-guard.service.ts I have:
import { Injectable } from '#angular/core';
import {
ActivatedRouteSnapshot,
Router,
RouterStateSnapshot
} from '#angular/router';
import { KeycloakAuthGuard, KeycloakService } from 'keycloak-angular';
#Injectable({
providedIn: 'root'
})
export class AuthGuard extends KeycloakAuthGuard {
constructor(
protected override readonly router: Router,
protected readonly keycloak: KeycloakService
) {
super(router, keycloak);
}
public async isAccessAllowed(
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot
) {
if (!this.authenticated) {
await this.keycloak.login({
redirectUri: window.location.origin + state.url
});
}
}
}
I have imported the above service in my application's routing module:
import { AuthGuard } from './shared/auth-guard.service';
const routes: Routes = [
{ path: '', component: HomeComponent },
{ path: 'login', component: LoginComponent },
{ path: 'profile', component: ProfileComponent }
];
The problem
Importing the AuthGuard service throws the error:
Property 'isAccessAllowed' in type 'AuthGuard' is not assignable to the same property in base type 'KeycloakAuthGuard'.
The error refers to the isAccessAllowed() method in the auth-guard.service.ts file.
Questions
What is causing this error?
What is the easiest and most reliable way to fix it?
You method implementation does not match the type definition of the base class because you do not return anything if the user is authenticated.
The method definition from keycloak-angular for isAccessAllowed():
abstract isAccessAllowed(route: ActivatedRoute, state: RouterStateSnapshot): Promise<boolean | UrlTree>;
The method has a return type of Promise<boolean | UrlTree>.
You need to return a value if the user is already authenticated. They can access the route and the guard should indicate that. Try making this change:
public async isAccessAllowed(
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot
) {
if (!this.authenticated) {
await this.keycloak.login({
redirectUri: window.location.origin + state.url
});
}
return true;
}
EDIT:
You add it to your routes like so:
import { AuthGuard } from './shared/auth-guard.service';
const routes: Routes = [
{ path: '', component: HomeComponent },
{ path: 'login', component: LoginComponent },
{ path: 'profile', component: ProfileComponent, canActivate: [AuthGuard] }
];
See the Angular docs on route guards.
I have a lazy loaded module. I want to load some data before the module load. I tried app_initializer but it didn't work because i have already an app_initializer in AppModule. Is there any way to do this?
You can use an Resolver that will handle your data before the module will load and navigate to your specific route as that loaded completly
resolver
import { Injectable } from '#angular/core';
import { APIService } from './api.service';
import { Resolve } from '#angular/router';
import { ActivatedRouteSnapshot } from '#angular/router';
#Injectable()
export class APIResolver implements Resolve<any> {
constructor(private apiService: APIService) {}
resolve(route: ActivatedRouteSnapshot) {
return this.apiService.getItems(route.params.date);
}
}
in your route
{
path: 'items/:date',
component: ItemsComponent,
resolve: { items: APIResolver }
}
and to acces the resolved data in the desired component
import { ActivatedRoute } from '#angular/router';
constructor(private route: ActivatedRoute) { }
items: any[];
ngOnInit() {
this.items= this.route.snapshot.data.items;
}
using a component with a resolver in a given module will not init that module until all the data is resolved
you can check also https://angular.io/api/router/Resolve
Using Asp Core on Angular 4 with the default template from asp core. The guard works, but on page refresh I get unwanted behavior. When refreshing a guarded route it briefly shows my login page when canActivate is true. Image below shows this behavior. Notice on refresh the screen flashes red (my login page).
Steps to reproduce issue:
Create project with dotnet new angular
Run the dotnet restore and npm install
Add file auth.guard.ts (code below)
Add file auth.service.ts (code below)
Add login component
Add service and guard to routes in app.modal.shared.ts (code below)
Add login button on home component
Run program and click login button
Navigate to the counter route
Press F5 to refresh, the login page will appear before showing counter route (should not show login page as canActivate should be true)
Please let me know if you wish to see any additional code or if you have any questions. I have been pulling my hair out the past two days trying all kinds of things with Observables, maps and subscriptions with no results. Any help will be greatly appreciated. Thanks in advance!
auth.guard.ts
import { Injectable } from '#angular/core';
import { Router, CanActivate } from '#angular/router'
import { AuthService } from './auth.service';
#Injectable()
export class AuthGuard implements CanActivate {
constructor(
private authService: AuthService,
private router: Router) {
}
canActivate() {
if (!this.authService.isLoggedIn()) {
this.router.navigate(['/login']);
return false;
}
return true;
}
}
auth.service.ts
import { Injectable, Inject, PLATFORM_ID } from '#angular/core';
import { Router } from '#angular/router';
import { Response, Headers, RequestOptions } from '#angular/http';
import { isPlatformBrowser, isPlatformServer } from '#angular/common';
import { Observable } from 'rxjs/Rx';
import { BehaviorSubject } from 'rxjs/Rx';
#Injectable()
export class AuthService {
private baseUrl: string = '';
private loggedIn = false;
uid: string | null;
constructor(
private router: Router,
#Inject(PLATFORM_ID) private platformId: Object
) {
if (isPlatformBrowser(this.platformId)) {
this.loggedIn = !!localStorage.getItem('auth_token');
}
}
login() {
this.loggedIn = true;
localStorage.setItem('auth_token', 'test');
}
logout() {
localStorage.removeItem('auth_token');
localStorage.removeItem('uid');
window.location.replace('/home'); // redirect as we want all var and obj to reset
}
isLoggedIn() {
return this.loggedIn;
}
}
app.module.shared.ts
...
RouterModule.forRoot([
{ path: '', redirectTo: 'home', pathMatch: 'full' },
{ path: 'home', component: HomeComponent },
{ path: 'login', component: LoginComponent },
{ path: 'counter', component: CounterComponent, canActivate: [AuthGuard] },
{ path: 'fetch-data', component: FetchDataComponent, canActivate: [AuthGuard] },
{ path: '**', redirectTo: 'home' }
])
...
EDIT: Added gif of issue.
EDIT: Found out this is an issue with server-side prerendering. I am currently looking into how to setup a token storage service and pass this to the server.
I am attempting to use Server-Side Rendering in Angular (v4) to allow for better SEO.
Things work as expected until I add resolve on my route. Adding resolve causes HTML title to retain it's initial value when viewing source.
My Module:
import {
Injectable,
ModuleWithProviders,
NgModule
} from '#angular/core';
import {
ActivatedRouteSnapshot,
Resolve,
Router,
RouterModule,
RouterStateSnapshot
} from '#angular/router';
import {
Observable
} from 'rxjs/Rx';
import {
ArticleComponent
} from './article.component';
import {
Article,
ArticlesService,
UserService,
SharedModule
} from '../shared';
#Injectable()
export class ArticleResolver implements Resolve < Article > {
constructor(
private articlesService: ArticlesService,
private router: Router,
private userService: UserService
) {}
resolve(
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot
): any {
return this.articlesService.get(route.params['slug'])
.catch((err) => this.router.navigateByUrl('/'));
}
}
const articleRouting: ModuleWithProviders = RouterModule.forChild([{
path: 'article/:slug',
component: ArticleComponent,
resolve: {
article: ArticleResolver
},
data: {
preload: true
}
}]);
#NgModule({
imports: [
articleRouting,
SharedModule
],
declarations: [
ArticleComponent
],
providers: [
ArticleResolver
]
}) export class ArticleModule {}
My Component:
import {
Component,
OnInit
} from '#angular/core';
import {
ActivatedRoute,
Router,
} from '#angular/router';
import {
Title,
Meta
} from '#angular/platform-browser';
import {
AppComponent
} from '../app.component';
import {
Article,
} from '../shared';
#Component({
selector: 'article-page',
templateUrl: './article.component.html'
})
export class ArticleComponent implements OnInit {
article: Article;
constructor(
private route: ActivatedRoute,
private meta: Meta,
private title: Title
) {}
ngOnInit() {
this.route.data.subscribe(
(data: {
article: Article
}) => {
this.article = data.article;
}
);
this.title.setTitle(this.article.title);
}
}
I am new to Angular SSR so any guidance is greatly appreciated.
Instead of subscribing to route data, retrieve your results from the snapshot like this:
this.route.snapshot.data['article']
You also might need to register ArticlesService in your providers for the module.
As a side note, this import:
import {
Observable
} from 'rxjs/Rx';
is an RxJS antipattern. Please use the following import instead:
import {Observable} from 'rxjs/Observable';
I found that my primary service was referencing a secondary service that was attempting to return an authentication token from window.localStorage.
Attempting to access the client storage caused Angular SSR to omit the generation of source code for my component.
Thanks #Adam_P for helping me walk through it!
I'm developing an Angular 2 app with multiple components that rely on some data that is loaded from the server via the http-service (it's data about the user and his roles).
Most of my route components throw errors within their ngOnInit() methods if this data is not already loaded. The data is loaded and stored within a service that is injected in all components.
Is there a way to delay the rendering of the current route within my root-component until the http call is finished?
Otherwise I would have to implement some kind of check and retry mechanism within the ngOnInit of all the route components, which would be very awkward.
I already tried to hide the router-outlet element until the call finished but this leads to an error saying "Cannot find primary outlet to load xxx"
There is an option to delay of router initial navigation when creating Router module:
RouterModule.forRoot([
// routes here
], {
initialNavigation: false // the propery to delay navigation
}),
Later the initial navigation can be triggered via Angular Router, like this:
this.router.initialNavigation();
I accomplished this with a CanActivate guard. The key to making it work is returning an Observable from the canActivate method. That way, you can delay as long as you need to.
import { CanActivate } from '#angular/router';
import { Injectable } from '#angular/core';
import { Observable } from 'rxjs/Rx';
import { StateService } from '../../_services/state.service';
import { Subject } from 'rxjs/Rx';
import { Subscription } from 'rxjs/Subscription';
#Injectable()
export class LoadingGuard implements CanActivate {
constructor(private state: StateService) {}
public canActivate(): Observable<boolean> {
if (!this.state.loading$.getValue()) {
return Observable.of(true);
}
let subject = new Subject<boolean>();
let subscription = this.state.loading$.subscribe(value => {
if (!value) {
subject.next(true);
subject.complete();
subscription.unsubscribe();
}
});
return subject;
}
}
Above, StateService is a service that evaluates the current user and pre-caches some data for the rest of the app. It has a subject named loading$ that returns false when loading has completed.
All that's left is to declare the guard in the app module.
import { LoadingGuard } from './app/loading-guard/loading-guard';
// other imports omitted
#NgModule({
// other module properties omitted
providers: [LoadingGuard]
})
export class AppModule {}
Then declare the guard in your routing.
import { LoadingGuard } from './app/loading-guard/loading-guard';
// other imports omitted
export const rootRouterConfig: Routes = [
{ path: 'app', component: AppComponent,
canActivate: [LoadingGuard],
children: [
{ path: 'index', component: IndexComponent },
// child routes omitted
] },
{ path: 'sign-in', component: SignInComponent },
{ path: '**', redirectTo: 'sign-in' }
];
For reference, here's the documentation on CanActivate:
https://angular.io/docs/ts/latest/api/router/index/CanActivate-interface.html