I have an angular application which has a number of steps to complete. Each step can only be done once and must have all previous steps complete. To achieve this I have added route guards to each route. The application makes a http request on start to check the status. However the route guard canActivate method doesn't seem to be subscribing to changes.
In the below example statusService updates the status which should trigger an update in the guards.
statusService
#Injectable({
providedIn: 'root'
})
export class StatusService {
private stepOneComplete: BehaviorSubject<boolean> = new BehaviorSubject(false);
private stepTwoComplete: BehaviorSubject<boolean> = new BehaviorSubject(false);
constructor(
private http: HttpClient
) { }
public getStepOneComplete(): Observable<boolean> {
return this.stepOneComplete;
};
public updateStepOneComplete(newValue: boolean): void {
this.stepOneComplete.next(newValue);
};
public initialize(): void {
this.http.get(`${apiUrl}status`)
.subscribe((data: any) => {
this.stepOneComplete(data.stepOne);
});
};
};
stepOneGuard
#Injectable()
export class StepOneGuard implements CanActivate {
constructor(
private service: StatusService,
private router: Router
) {}
canActivate(): Observable<boolean> {
return this.service.getStepOneComplete().pipe(
tap(complete => {
if(complete){
this.router.navigate(['/step-two']);
}
}),
map(complete => {
return !complete;
})
);
}
}
What I expect to happen is that after the initialize method runs and updates stepOneComplete then the router should navigate to step two. However no navigation occurs. If I put a console.log in the tap method of the guard it fires on initial load but not when stepOneComplete.next is called.
I think the answer is here:
public getStepOneComplete(): Observable<boolean> {
return this.stepOneComplete.asObservable();
}
This is what I have in my own production code, works just fine.
You should not see a guard as a singleton that controls navigation. Its only purpose is to control whether the user can access a page or not.
I suggest you to have a state service that holds the state of your "wizard", and then you would simply check it in every guard. You shouldn't even need Subjects at all.
Related
In an Angular app, we're using a Base Component to unsubscribe from most of our app's observable subscriptions. If a component subscribes to an observable, that component will extend the Base Component. My thought is that this keeps observable subscriptions alive until the entire application is destroyed, rather than until each component is destroyed:
base.component.ts:
import { Subject } from 'rxjs';
import { OnDestroy, Component } from '#angular/core';
export abstract class BaseComponent implements OnDestroy {
protected unsubscribe$ = new Subject<void>();
ngOnDestroy(): void {
this.unsubscribe$.next();
this.unsubscribe$.complete();
}
}
the-rest-of-our-components.ts:
import { Component, OnInit } from '#angular/core';
import { MyService } from 'src/app/services/my.service';
import { BaseComponent } from '../base/component/base-component';
export class myComponent extends BaseComponent implements OnInit {
myProperty: string;
constructor(
private myService: MyService,
) {
super();
}
ngOnInit(): void {
this.myService.doStuff$
.pipe(takeUntil(this.unsubscribe$)) // take until baseComponent's unsubscribe$
.subscribe((data) => {
this.myProperty = data;
});
}
If many components extend BaseComponent and utilize its unsubscribe$ Subject, does that mean all of my subscriptions only get unsubscribed when the entire application is destroyed (aka user closes the tab or navigates away from web app, thus Base Component is destroyed), rather than when individual components get destroyed?
Is this a strategy you've seen before, and is it advisable? If it works as I'm assuming, this means all of our subscriptions across the application stay active until the whole app is destroyed. I see how, depending on our needs, that might be a bad thing or a good thing.
Bonus question: is Base Component going to act like a singleton? AKA if multiple components simultaneously extend BaseComponent, will they all be using the same instance of unsubscribe$ or will there be multiple instances of unsubscribe$ (one per component)?
I assumed this would work, but we all know where assumptions get you, so I made a test: https://stackblitz.com/edit/angular-ivy-ueshwz?file=src/app/extended/extended.component.ts
It works, as in subscriptions get destroyed when individual components get destroyed.
We make a service that holds a subject we can subscribe to, and a value we can change with the subscription, to show that the subscription exists:
import { Injectable } from '#angular/core';
import { Subject } from 'rxjs/internal/Subject';
#Injectable({ providedIn: 'root' })
export class UpdateService {
subject = new Subject<void>();
value = 0;
}
In the root we'll fire the subject every second, and have a component that we can toggle on and off
export class AppComponent implements OnInit {
extCompOpen = true;
constructor(public update: UpdateService) {}
ngOnInit() {
interval(1000).subscribe(() => this.update.subject.next());
}
}
<app-extended *ngIf="extCompOpen"></app-extended>
<button (click)="extCompOpen = !extCompOpen">Toggle Component</button>
<p>This counter will keep going up as long as the subscription exists:</p>
<p>{{ update.value }}</p>
Then we'll use an extended component to tick that value up by 1 with a subscription
export class ExtendedComponent extends BaseComponent implements OnInit {
constructor(private update: UpdateService) {
super();
}
ngOnInit() {
this.update.subject.pipe(takeUntil(this.unsubscribe$)).subscribe(() => {
this.update.value++;
});
}
}
<p>Extended component exists!</p>
Neat, closing the component stops the ticker, so the subscription has been unsubscribed.
Bonus question: BaseComponent does not act like a singleton, when you create an instance of an object it does not create shared instances of parent classes. Extending a class just adds properties and methods to that instance.
I'm not sure if I would recommend this, if someone overrides ngOnDestroy() they need to call super.ngOnDestroy(), which may be easy to forget. It's only four lines of code, probably better to explicitly put it in every component that needs it. Manual subscriptions should be pretty rare anyway, if you're using the async pipe.
I solved this in a project doing the following:
In base.component:
private sub: any = {};
ngOnDestroy() {
Object.keys(this.sub).map(item => {
this.sub[item].unsubscribe();
})
}
Then in any component that extends:
this.sub.myService = this.myService.doStuff$.subscribe(......
With this method, the subscription stay active until the component is destroyed.
I am fairly new to angular. I have two components namely header and profile component. The header component handles the login functionality and maintains two information- the user details which is json object and a isLoggedIn which is a boolean that saves current state of login. The general layout of the profile page is-
<header-component>
<profile-component>
Now since the header component handles the login. I want to avoid writing the logic for getting userDetails and the isLoggedIn status again for profile component. So i decided writing a shared service called profile service so that i can upload userDetails and isLogged from header and access that info in the profile component. The input in the loginlogout method comes from the header component.
SharedService code -
import { Injectable } from '#angular/core';
import { HttpService } from './https.service';
import { Observable, BehaviorSubject, of as observableOf } from 'rxjs';
import * as _ from 'lodash';
import { HttpHeaders, HttpParams } from '#angular/common/http';
import { BaseService } from './base.service';
#Injectable()
export class ProfileServices{
constructor(){};
userDetailsBS = new BehaviorSubject<any>('original value');
userDetails= this.userDetailsBS.asObservable();
isLoggedIn:boolean;
loginlogout(userDetails:any , isLoggedIn:boolean){
this.userDetails=userDetails;
this.userDetailsBS.next(this.userDetails);
console.log("Value of user details set in profile service",this.userDetails); //debug
console.log(".getValue() method:",this.userDetailsBS.getValue()); //debug
this.isLoggedIn=isLoggedIn;
}
getUserDetails(){
return this.userDetailsBS.getValue();
}
}
Post login from the header-component.ts i call the loginlogout method in the profile service to set the values. I also tried to access the value passed to the shared Service using the getUserDetails which shows that the userDetails object is passed correctly to the shared service.
The issue arises when i try to access the data from the profile component-
export class ProfileT1Component implements OnInit {
userDetails:any;
constructor(
public profileService: ProfileServices){
this.profileService.userDetails.subscribe((result)=>{
console.log(result);
this.userDetails=result;
console.log("received user details in profile component constructor: ", this.userDetails);
})
}
}
the result still shows "original value" and not the updated value. Is this wrong approach altogether or am i handling the observables incorrectly. Help would be much appreciated.
You need to make a couple of changes in your service to make it work. Add providedIn: root and remove all declarations from other modules. Secondly, you do not need this.userDetailsBS.asObservable() and you can use the subscribe directly on userDetailsBS. Your code will look something like the following.
Service:
#Injectable({
providedIn: 'root'
})
export class ProfileServices {
constructor() {}
userDetailsBS = new BehaviorSubject<any>('original value');
isLoggedIn: boolean;
loginlogout(userDetails: any, isLoggedIn: boolean) {
this.userDetailsBS.next(userDetails);
this.isLoggedIn = isLoggedIn;
}
getUserDetails() {
return this.userDetailsBS.getValue();
}
}
Component:
export class ProfileT1Component implements OnInit {
userDetails: any;
constructor(public profileService: ProfileServices) {
this.profileService.userDetailsBS.subscribe((result) => {
console.log(result);
this.userDetails = result;
console.log('received user details in profile component constructor: ', this.userDetails);
});
}
}
the implementation seems to be OK
(except you should make the BehaviorSubject private and expose only the observable)
probably you have multiple instance of the service.
try to add :
#Injectable({
providedIn: 'root',
})
and remove the service declaration from all the modules provider array
https://angular.io/guide/singleton-services
There is such structure of components:
Desired Behavior
child1_component - is a header.
child2_component - is a body.
There is a button inside child1_component.
Clicking on that button I want to invoke a method inside child2_component.
Question
What is the best way to implement this?
One way to approach this would be to use a service with rxjs subjects and observables.
When the user clicks on the button in child1_component then it calls a method that in turn calls a method inside the shared service.
When the method in the service is called it can emit a value as an observable via a subject.
child2_component then subscribes to the observable within the shared service and can operate some logic based on when it receives data from the service.
More on services here: https://angular.io/tutorial/toh-pt4
Great tutorial on subjects and rxjs: https://blog.angulartraining.com/rxjs-subjects-a-tutorial-4dcce0e9637f
On your general.component.html :
<app-child1 (clicked)="app1Clicked($event)"></app-child1>
<app-child2 #child2></app-child2>
On your general.component.ts:
#ViewChild('child2', {static: true}) child2: Child2Component;
app1Clicked($event) {
this.child2.doSomething()
}
On the child1.components.ts:
#Output() clicked = new EventEmitter<any>();
onClick() {
this.clicked.emit();
}
Finally on the child2.component.ts:
doSomething() {
alert('ok');
}
There are 2 ways to do it:
1.Service:
export class ActionService {
private someAction = new Subject();
someActionEmitted$(): Observable<unknown> {
return this.someAction.asObservable();
}
emitSomeAction(): void {
this.someAction.next();
}
}
//childComponent1
export class ChildComponent1 {
constructor(private actionService: ActionService) {
}
emitAction(): void {
this.actionService.emitSomeAction();
}
}
//childComponent2
export class ChildComponent2 implements OnInit, OnDestroy {
private destroy$ = new Subject();
constructor(private actionService: ActionService) {
}
ngOnInit(): void {
this.actionService.someActionEmitted$()
.pipe(takeUntil(this.destroy$)) // dont forget to unsubscribe, can cause memory leaks
.subscribe(() => this.doSomething());
}
doSomething(): void {
// your logic here
}
ngOnDestroy(): void {
this.destroy$.next();
}
}
2. Using Parent Component
<child-component1 (btnClicked)="childComponentBtnClick()"></child-component1>
<child-component2 [clickBtnSubject]="childBtnClicked"></child-component1>
Ts logic:
export class ParentComponent {
childBtnClicked = new Subject();
childComponentBtnClick(): void {
this.childBtnClicked.next();
}
}
//childComponent1
export class ChildComponent1 {
#Output() btnClicked = new EventEmitter();
emitAction(): void {
this.btnClicked.emit(); // you can pass value to emit() method
}
}
//childComponent2
export class ChildComponent2 implements OnInit, OnDestroy {
#Input() clickBtnSubject: Subject;
ngOnInit(): void {
this.clickBtnSubject
.pipe(takeUntil(this.destroy$)) // dont forget to unsubscribe, can cause memory leaks
.subscribe(() => this.doSomething());
}
doSomething(): void {
// your logic here
}
ngOnDestroy(): void {
this.destroy$.next();
}
}
What is the appropriate place for adding a call to initialize a global listener in Angular app?
Here is the code:
export class AuthService {
constructor(
private store: Store<fromAuth.State>,
private afAuth: AngularFireAuth
) {
this.afAuth.auth.onAuthStateChanged(payload => {
if (payload) {
const user: UserBeta = {
uid: payload.uid,
displayName: payload.displayName,
email: payload.email,
emailVerified: payload.emailVerified
};
this.store.dispatch(AuthActions.authenticated({ user }));
} else {
this.store.dispatch(AuthActions.notAuthenticated());
}
});
}
As you could see I've added it to the constructor of the AuthService but it doesn't seem right for me.
What I'm also concerning about is that the following code has two dependencies: Ngrx and AngularFireAuth.
In this case, would it be correct to move somewhere to the FirebaseModule (i.e. firebase.module.ts) and if yes, how is the call will look like?
You can add it inside ngOnInit(), from the docs:
A callback method that is invoked immediately after the default change detector has checked the directive's data-bound properties for the first time, and before any of the view or content children have been checked. It is invoked only once when the directive is instantiated.
Check here for more info:
https://angular.io/api/core/OnInit
Thank all of you for replies.
I've finally decided to introduce a new initialize() method inside the AuthService and call it inside the ngOnInit() method of the AppComponent.
auth.service.ts:
#Injectable({ providedIn: 'root' })
export class AuthService {
constructor(
private http: HttpClient,
private store: Store<fromAuth.State>,
private afAuth: AngularFireAuth
) { }
initialize() {
this.afAuth.auth.onAuthStateChanged(payload => {
if (payload) {
const user: UserBeta = {
uid: payload.uid,
displayName: payload.displayName,
email: payload.email,
emailVerified: payload.emailVerified
};
this.store.dispatch(AuthActions.authenticated({ user }));
} else {
this.store.dispatch(AuthActions.notAuthenticated());
}
});
}
}
app.component.ts:
export class AppComponent implements OnInit {
constructor(private authService: AuthService) { }
ngOnInit(): void {
this.authService.initialize();
}
}
Update: At my project I'm using ngrx for state management. Since AngularFireAuth also manages user information I've faced difficulty in managing the same state in multiple places, which increased the complexity, so the final solution became quite complicated. In the end, I've decided to stop using the onAuthStateChanged listener and start persisting the ngrx state locally.
working with Angular (v7.2) and Firebase Auth to build a simple web app with a members area. I've set the dashboard up in a separate module with its own router. I've got a service which does most of the heavy lifting for the auth which is shared between all the components.
I'm not convinced this is the best way of doing things but my auth.service.ts file looks like this:
export class AuthService {
private userData$ = new Subject();
private userData;
public userDataObs = this.userData$.asObservable();
constructor(public afs: AngularFirestore, public afAuth: AngularFireAuth, public functions: AngularFireFunctions, public router: Router, public ngZone: NgZone) {
this.afAuth.authState.subscribe((user) => {
if(user) {
this.userData = user;
this.getCurrentUserAccessLevel().subscribe((result) => {
let userAccessLevel = result.result;
let userObj = {
displayName: this.userData.displayName,
email: this.userData.email,
emailVerified: this.userData.emailVerified,
phoneNumber: this.userData.phoneNumber,
photoURL: this.userData.photoURL,
uid: this.userData.uid,
accessLevel: userAccessLevel
}
this.userData$.next(userObj);
});
localStorage.setItem('user', JSON.stringify(this.userData));
JSON.parse(localStorage.getItem('user'));
} else {
localStorage.setItem('user', null);
JSON.parse(localStorage.getItem('user'));
}
})
}
...
In short the service fetches the user data from the Firebase auth service and stores it into a userData property. It then fetches the accessLevel which is a custom claim in Firebase auth and merges this with some fields in the userData to create a custom userObj object. The userObj is then submitted to the userData$ subject for which there is an observable userDataObs.
Each component subscribes to userDataObs.
In the main dashboard.component.ts there is:
export class DashboardComponent {
private userDataObs;
private userData;
private loaded: boolean = false;
constructor(public authService: AuthService, public router: Router, public ngZone: NgZone) {
this.authService.userDataObs.subscribe((data) => {
this.userData = data;
this.loaded = true;
})
}
}
which simply subscribes to the userDataObs in the authService. In the main dashboard component there is a menu with various routerLinks to other components which are served through a router-outlet in the component. If I click the link to another component the route changes and the component initialises but the observable does not load. There is no error message or anything - just nothing:
dashboard-profile.component.ts:
export class DashboardProfileComponent {
private userDataObs;
private userData;
private loaded: boolean = false;
private fileUploadEvent;
private fileUploadProgress;
private fileUploadURL;
constructor(private authService: AuthService, private router: Router, private uploadService: UploadService) {
console.log('constructor');
this.authService.userDataObs.subscribe((data) => {
this.userData = data;
this.loaded = true;
console.log('loaded');
});
}
If, however, I just navigate to the route, without clicking the routerLink it works fine.
I assume it's something to do with sharing the observable? Ideally I'd just like the child components to somehow inherit the observable from the dashboard.component but from what I can tell this isn't possible, hence I need to set up a new subscription.
Would appreciate any help at all! Thank you!