Angular 5: guard forward retrieved data to next component/route - javascript

In an angular 5 app, there is a route guard that check from an API if an object exists:
//guard.ts excerpt
canActivate(
next: ActivatedRouteSnapshot,
state: RouterStateSnapshot
): Observable<boolean> | Promise<boolean> | boolean {
return this.clientService.get(next.params.id).switchMap( data => {
return Observable.of(true);
})
.catch( err => Observable.of(false))
}
//route.ts excerpt
{ path: ':id', canActivate: [ ClientDetailGuard ], component: ClientDetail }
this works perfect, but I am wondering if is there a way to pass the data retrieved from my service to next the route/component (ClientDetail), so I won't need to call the service again this again.
I tried to add
next.data.client = data;
before the return of Observable(true) but in the component, the ActivatedRoute's data does not have this value set.
Or should I use something like Resolve?
I know I can achieve this using some state container or a shared service to store/retrieve data, but I wouldn't like to do this at this time, as long as the app is not complex.

I could do this using a Resolver instead of a guard
//route.ts
{ path: ':id', resolve: { client: ClientDetailResolver }, component: ClientDetail }
//resolver.ts
#Injectable()
export class ClientDetailResolver implements Resolve {
constructor(private clientService: ClientService, private router: Router, public location: Location) {
}
resolve(
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot
): Observable<any>|Promise<any>|any {
return this.clientService.get(route.params.id)
.catch( err => {
//handle error
const path = this.location.path();
this.router.navigate(["error", err.status], { skipLocationChange: true })
.then( () => {
this.location.replaceState(path);
});
return Observable.empty();
})
}
}

You seem to be under-estimating the power of services. Services are the best way to save/store data or states between components. You can set the data from any component, pull the data from any component. You don't have to worry about putting data in for the next route, instead you go to the next route and subscribe to your data on ngOnInit and boom, got everything you need. Here is an example of just how simple it really is.
Example of service
import { Injectable } from '#angular/core';
import { BehaviorSubject, Subscription } from 'rxjs';
#Injectable()
export class AppService {
alertData = new BehaviorSubject({
type: '',
message: '',
timer: 0,
showing: false
});
constructor() {}
setAlertData(data: AlertModel) {
this.alertData.next(data);
}
}
Example of using service
this.subscription$.push(this._as.alertData.subscribe((data: AlertModel) => {
this.alertData = data;
if (data.showing) {
this.alertIsShowing = true;
}
else {
this.alertIsShowing = false;
}
}));

Related

load data in ngrx effect with dispatch an action before using route guard

My auth is based on NGRX
so when the page starts loading I got all roles and
then get logged in.
but when I start using route guard,
route gourd start working before user data get loading
how can I wait for user load action to be done then start using canActivate
I try below solution but it's not working
export class AuthGuard implements CanActivate, OnDestroy {
private unsubscribe: Subject<any>;
constructor(private store: Store<AppState>, private router: Router,
private alertService: ToastrService, private authService: AuthService) {
this.unsubscribe = new Subject();
}
ngOnDestroy(): void {
this.unsubscribe.next();
this.unsubscribe.complete();
}
getFromStoreOrAPI(): Observable<any> {
return this.store.pipe(
select(isUserLoaded),
tap((data: boolean) => {
if (!data) {
this.store.dispatch(new UserRequested());
}
}),
take(1)
);
}
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> {
return this.getFromStoreOrAPI().pipe(
switchMap(() =>
this.store.pipe(
select(isLoggedIn),
map(loggedIn => {
if (!loggedIn) {
this.router.navigateByUrl('/auth/login');
return false;
} else {
this.store.pipe(
select(currentUserRoles),
map((userRoles: Role[]) => {
//.......
}),
takeUntil(this.unsubscribe)
).subscribe();
}
}),
)
),
catchError(() => this.router.navigateByUrl('/auth/login'))
);
}
}
You can use filter to wait until loaded flag is true.
Here is approach I took with my auth.guard.ts :
canActivate(
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot
): Observable<boolean> {
return this.authFacade.loaded$.pipe(
filter(loaded => !!loaded),
mergeMap(() => this.authFacade.userAccount$),
map(userAccount => {
if (!userAccount) this.authFacade.redirectLoginPage(state.url);
return !!userAccount;
}),
first()
);
}
In my case, main app component is dispatching an action CheckAuth to check if user is already authenticated, and then set loaded flag.
It should work with some adaptation for your need. But main difference is the use of filter which avoid to continue the workflow if user checking is not done, and force waiting for the value.
Of course, be sure to set loaded value in all the case after receiving response (authenticated or not), or in case of any error.
Here is a potential adaptation for your case :
authLoaded$ = this.store.pipe(select(authLoaded));
authAccount$ = this.store.pipe(select(authAccount));
canActivate(...) {
return userLoaded$.pipe(
tap(loaded => {
if (!loaded) {
this.store.dispatch(new UserRequested());
}
}),
filter(loaded => !!loaded),
mergeMap(() => authAccount$),
map(authAccount => {
if (!authAccount.loggedIn) {
this.router.navigateByUrl('/auth/login');
return false;
}
if (!authAccount.roles?.length) {
this.router.navigateByUrl('/auth/forbidden');
return false;
}
// do some extra stuff...
return true;
}),
first()
);
}
I renamed isUserLoaded to authLoaded to clearly indicate the status of authentication loading (you can use also ready or not for instance). But not necessary user.
I created also a new selector authAccount which returns an object with at least 2 things :
loggedIn : true/false if user is logged in
roles: array of user roles.
But you can add of course user property, with user details.
This is a composed selector from different parts of your state.
With it, your code is more clear and maintable, you receive a complete status of your current authentication user.
Maybe some typos is possible, I wrote the code directly in my answer without testing it.
Hope this will help you.

Check Ionic storage for item

I want to show intro slider when user just installed the app (only once)
Logic
When user install the app store item named intro with value of false
When user open the app check intro item, if value is false show intro page if value is true continue with routing and load main page route.
Issue
I have tried couple of ways to achieve this goal but all failed as result of storage not having any item named intro.
Code
What I've tried so far: (all placed in app.component.ts file)
1
import { NativeStorage } from '#ionic-native/native-storage/ngx';
constructor(
public navCtrl: NavController,
private storage: NativeStorage,
) {
this.letsTest();
}
letsTest(){
console.log('fired');
if (this.storage.getItem('intro') == undefined) { // if not getting intro add and redirect to intro route
this.storage.setItem('intro', false);
this.navCtrl.navigateRoot('/intro');
} else { // otherwise do normal routing and load main page
console.log('exists', this.storage.getItem('intro'));
}
}
Result: Failed.
2
import { NativeStorage } from '#ionic-native/native-storage/ngx';
constructor(
public navCtrl: NavController,
private storage: NativeStorage,
) {
this.letsTest();
}
letsTest(){
console.log('fired');
//get intro
const intro = this.storage.getItem('intro');
// if not exist add it and reload to intro route
if (!intro){
console.log('intro was undefined so we set new false value in storage');
this.storage.setItem('intro', false);
this.navCtrl.navigateRoot('/intro');
} else {//if found get it and check value of it
this.storage.getItem('intro').then(
data=>{
console.log('just data', data);
//if value is true means user already saw intro do normal routing and load main page
if(data == true){
console.log('exists', data);
}else{
// if value is not true (false) then load intro page first
this.navCtrl.navigateRoot('/intro');
console.log('ionViewWillEnter checked', data);
}
},
error => {
console.log('some error happened', error);
}
)
}
}
Result: Failed.
Console debug
This is what I get in console (Code 2 = ITEM_NOT_FOUND)
__zone_symbol__state: 0
__zone_symbol__value: NativeStorageError
code: 2
exception: null
source: "Native"
__proto__: Error
constructor: ƒ (code, source, exception)
ITEM_NOT_FOUND: 2
JSON_ERROR: 5
NATIVE_WRITE_FAILED: 1
NULL_REFERENCE: 3
UNDEFINED_TYPE: 4
WRONG_PARAMETER: 6
arguments: null
caller: null
length: 3
name: "NativeStorageError"
prototype: Error {constructor: ƒ}
__proto__: ƒ ()
[[FunctionLocation]]: NativeStorageError.js:6
[[Scopes]]: Scopes[1]
__proto__: Object
__proto__: Object
Question
How can I check intro item from native local storage and redirect user based on it's value?
import {Storage} from "#ionic/storage";
constructor( public storage: Storage){
this.storage.get('intro').then(value => {
if(value){
//nav to home page
}else{
this.setIntro(true)
}
},reason => {
this.setIntro(true)
}).catch(err=>{
console.log(err)
})
}
setIntro(bool:boolean){
this.storage.set('intro',bool).then(()=>{
//nav to intro page
}).catch(err=>{
console.log(err)
})
}
The problem with your code is likely something to do with you using NativeStorage instead of Ionic Storage, which is the recommended option.
There are several tutorials out there for this.
I think this is a good one:
Ionic Intro Slider for New Users | AngularFirebase
That content is pro but you can get 3 free lessons, it was a bit confusing but refreshing the page got it loaded for me normally.
The idea is to use an Route Guard instead of doing it in the page. So you would generate a guard and use something like this inside of it:
// ...omitted
import { Storage } from '#ionic/storage';
#Injectable({
providedIn: 'root'
})
export class TutorialGuard implements CanActivate {
constructor(private storage: Storage, private router: Router) {}
async canActivate(
next: ActivatedRouteSnapshot,
state: RouterStateSnapshot
): Promise<boolean> {
const isComplete = await this.storage.get('tutorialComplete');
if (!isComplete) {
this.router.navigateByUrl('/tutorial');
}
return isComplete;
}
}
Then you can just put that guard on your homepage route like:
import { Routes, RouterModule } from '#angular/router';
import { TutorialGuard } from './guards/tutorial.guard';
const routes: Routes = [
{
path: '',
loadChildren: './tabs/tabs.module#TabsPageModule',
canActivate: [TutorialGuard] // <-- apply here
},
{
path: 'tutorial',
loadChildren: './tutorial/tutorial.module#TutorialPageModule'
}
];
#NgModule(...)
export class AppRoutingModule {}
And in the tutorial / intro code you would set the isComplete value once the user reached the end of the intro slides:
async finish() {
await this.storage.set('tutorialComplete', true);
this.router.navigateByUrl('/');
}

ERROR Error: Cannot insert a destroyed View in a ViewContainer

I have designed a page generator by configuration app and it works fine and generate components and render them on the page as good as it should.
But when i try to render a new page by new configuration, i get this error ERROR Error: Cannot insert a destroyed View in a ViewContainer! right when the generator service try to render the first component on the page after cleaning the page.
The pages configuration arrives from pageConfigService at ngOnInit in NemoContainerComponent and error appears when pageGenerator try to render the WidgetContainerComponent.
** UPDATE **
Page will be generate after changing the rout, all of the routes base component is the NemoContainerComponent and when route changes, the NemoContainerComponent destroyed and created again.
** UPDATE **
This is NemoContainerComponent:
#Component({
selector: 'nemo-container',
templateUrl: './nemo-container.component.html',
encapsulation: ViewEncapsulation.None
})
export class NemoContainerComponent {
private subscription: Subscription = new Subscription();
#ViewChild(ChildReferenceDirective) childReference: ChildReferenceDirective;
constructor(
private pageGenerator: PageGeneratorService,
private route: ActivatedRoute,
private routeService: RouteService,
private router: Router,
private storeEngine: StoreEngineService,
private pageConfigService: PageConfigService
) {
this.subscription.add(route.params.subscribe((params) => {
this.routeService.setRouteService = params;
}));
let activeRoute = router.url.split('/').join(' ');
document.body.className += ' ' + activeRoute;
}
ngOnInit() {
this.pageGenerator.containerRef = this.childReference.viewReference;
this.subscription.add(this.pageConfigService
.get(this.route.data['value'].page)
.subscribe(data => {
this.pageGenerator.renderNewPageByConfigurationFile(data.json)
}));
}
ngOnDestroy() {
this.subscription.unsubscribe();
}
}
WidgetContainerComponent is here:
#Widget({
typeName: 'widget-container',
displayName: '',
type: 'parent',
settings: []
})
#Component({
selector: 'widget-container',
templateUrl: "widget-container.component.html",
encapsulation: ViewEncapsulation.None
})
export class WidgetContainerComponent {
#ViewChild(ChildReferenceDirective, { read: ViewContainerRef }) childRef;
get childReference(): ViewContainerRef {
return this.childRef;
}
private data: ObjectInterface = {};
set widgetData(value: ObjectInterface) {
for (let item in value) {
this.data[item] = value[item];
}
}
get widgetData(): ObjectInterface {
return this.data;
}
public id: string = '';
}
** UPDATE **
angular pack version: 4.4.6
** UPDATE **
thanks in advance for your helps :)

How to send data from resolver in other route param in Angular 4

this is my resolver:
export class ComponentResolveService implements Resolve<Component> {
constructor(private componentService: ComponentService) {}
async resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise<Component> {
const id = Number(route.paramMap.get('id'));
const type = route.data['type'];
return this.componentService.getComponent(id, type);
}
}
and this is my routing object
{
path: 'component/:id',
component: Component,
resolve: {
data: dataResolver
},
data: {
type: 'type',
title: `{{data.title}}`,
breadcrumbs: `{{data.title}}`
},
}
and i try to send the data from resolver in title and breadcrums, you can see {{data.title}} but it doesn't. And can't find in official docs something related with that. It's possible somehow to send the data from resolver in title and breadcrumbs? Thanks a lot
I assume router data is static and it's just merged with resolved data then available in component

Can I save data in guard before loading page

Current routing configuration:
//...
{
path: 'foo/:id',
component: SomeComponent,
canActivate: [SomeGuard]
},
//...
Then in guard I call permission service to get access for component:
#Injectable()
export class SomeGuard implements CanActivate {
constructor(private service: Service) {
}
canActivate(next: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> | Promise<boolean> | boolean {
const id = parseInt(next.params['id']);
return this.service.getPermissions(id).then(permissions => {
if (permissions.canView) {
return true;
}
return false;
});
}
}
But in the component I utilize the same permissions endpoint, which means I call it twice in a row to get to one page:
//...
constructor(private route: ActivatedRoute,
private service: Service) {
}
ngOnInit() {
this.id = parseInt(this.route.snapshot.params['id']);
this.service.getPermissions(this.id).then(permissions => {
// ...
});
}
//...
So it would be great to just save the permissions data in the route and utilize it by both guard and the component. I tried using resolve, but it turns out resolve only activates after the guards, which is no good. So how can i save permissions data?
This looks like the kind of task for a caching service. Permissions do not change often so they are the perfect candidate for caching. That way even multiple visits to the same resource would not trigger multiple HTTP requests for permission checks.
Edit: Since you need permissions to be loaded each time, you could listen for RouteNavigationStart and clear the cache. If this becomes too cumbersome to maintain in the PermissionsService you could extract the logic into a separate service.
You could something like this in the service you use to get your permissions:
// Permissions service
private permissionCache;
constructor(
router: Router,
) {
// clear cache when a route navigation starts
router.events
.filter(event => event instanceof NavigationStart)
.subscribe(event => this.permissionCache = {})
}
getPermissions(id) {
if (permissionCache[id]) {
return Observable.of(permissionCache[id]);
} else {
// get the permissions
permissionCache[id] = response;
return Observable.of(response);
}
});

Categories

Resources