I am trying to implement a shared service for managing Roles on my app, with an Observable so that, from other components, you can either change the current role and be notified when it changes. The problem I have is that when I publish a new value through the service, the components that subscribe to the observable always recieve the same value (the initial one). Then, I never receive the new role number and I can't update the component state.
Apparently
I have the following set of components:
RolesService: The shared Service, which manages role change, gets the available roles from the user token, manages persistence of the current role for the logged in user. It uses localStorage to persist the role index. It receives changes
HeaderComponent: This is an example of a component receiving changes for the role change, because it needs to update the title of the user. It subscribes to the observable and changes the title accordingly
EscullRolComponent: And this is an example of a component that changes the role the user is currently using (by action of the user, of course). It has some buttons and sends to the service the new index.
Here is the relevant code for this issue:
// RolesService file
#Injectable()
export class RolesService {
private _currentRole: BehaviorSubject<Rol> = new BehaviorSubject(null);
currentRole = this._currentRole.asObservable();
private get currentIndex(): number {
const ras = localStorage.getItem('current_role');
// console.log("Guardat aixo: ", ras);
if (ras === '' || ras === null || ras === 'NaN' || ras === '-1' || parseInt(ras, 10) === NaN) {
return 0;
} else {
return parseInt(ras, 10);
}
}
private set currentIndex(val) {
localStorage.setItem('current_role', val.toString());
}
currentToken: NbAuthJWTToken;
constructor(private authService: NbAuthService,
private http: HttpClient) {
// console.log(this.currentRole);
this.authService.getToken().subscribe((token: NbAuthJWTToken) => {
if (token.isValid()) {
this.currentToken = token;
console.log("Executing token change");
this.setRolActual(0);
}
});
}
protected publishCurrentRol(i: number): void {
console.log("Publishing rol id: ", i); // LOG 1
this._currentRole.next(this.getUserRoles()[i]);
}
setRolActual(i: number) {
this.publishCurrentRol(i);
this.currentIndex = i;
}
}
The following is the component the user has to change the role, and that calls the service with the new role.
#Component({
templateUrl: 'escull-rol.component.html',
styleUrls: ['escull-rol.component.scss'],
})
export class EscullRolComponent {
rols: Array<Rol> = [];
actual: number;
constructor( private rolesService: RolesService,
private route: ActivatedRoute,
private router: Router,
private location: Location ) {
this.rols = this.rolesService.getUserRoles();
this.actual = this.rolesService.getRolActualIndex();
}
buttonRolClicked(index: number) {
this.rolesService.setRolActual(index);
this.router.navigate(['inici']);
// console.log('Boto del rol ' + index + ' clicat');
}
}
And here the header, which changes its state depending on the role:
#Component({
selector: 'ngx-header',
styleUrls: ['./header.component.scss'],
templateUrl: './header.component.html',
})
export class HeaderComponent implements OnInit {
#Input() position = 'normal';
user: any = {};
picture: string;
client: stream.Client;
logoPath = '';
logoEra = '';
rol: string;
ids: Array<string>;
constructor(private sidebarService: NbSidebarService,
/* ... more injections .. */
private imatgesService: ImatgesService,
private notificacionsService: NotificacionsService) {
this.logoEra = 'assets/images/logoEra.png';
this.authService.onTokenChange()
.subscribe((token: NbAuthJWTToken) => {
if (token.isValid()) {
if (token.getValue()) {
this.user = token.getPayload(); // Posem les dades del token a l'objecte user
// console.log('token changed, changing user in header', token);
}
}
}, error => {
console.error('Error en carregar el token en el header');
throw error;
});
this.rolesService.currentRole.subscribe((rol: Rol) => {
// This subscription here should receive the change from the service
if(rol) {
console.log("Changing rol on header to ", rol.getIndex()); // LOG 2
this.rol = rol.getTitle();
this.ids = rol.getRolIDs();
}
});
this.imatgesService.getProfileImagePath().subscribe((path: string) => {
this.picture = path;
}, (err) => {
this.picture = '';
});
}
}
The behaviour that I'm seeing is, the EscullRol component calling the setRolActual(id) method with the new id, and then the service calling its internal method publishCurrentRole(id) with the same id, so at LOG 1 I can see the expected outoput. But then immediately next I can see the output form LOG 2 at the Header Component with the wrong id, which is always the number that we had initially saved at the localStorage when the app started up.
I don't really know if the issue is with how I use the observables, with the service-component communication model or with how components and observables are initailsed and treated in angular.
Few thing to try
First make your service as a singleton using
#Injectable({ providedIn: "root" })
Improvement
Also, make sure that the service is not provided on child modules, as that would create their own instance and it wouldn't be a singleton anymore. If there are more than one instance of the service, the Subject-Observer pattern will not work across all the app.
Then this code
currentRole = this._currentRole.asObservable();
You should create a function to return the data not defined as an variable like
getCurrentRole() {
return this._currentRole.asObservable();
}
Related
In my angular application, after successful login, the user data is stored in the local storage. So in order to implement the auto login feature, that is to not loss the data while reloading, I implemented the below function in auth.service.ts
user = new Subject<User>();
// refresh and stays logged in
autoLogin() {
const userData: {
email: string;
id: string;
_token: string;
_tokenExpirationDate: string;
} = JSON.parse(localStorage.getItem('userData') || '{}');
if (!userData)
return;
const loadedUser = new User(userData.email, userData.id, userData._token, new Date(userData._tokenExpirationDate));
if (loadedUser.token) {
this.user.next(loadedUser);
const expirationDuration = new Date(userData._tokenExpirationDate).getTime() - new Date().getTime();
this.autoLogout(expirationDuration);
}
}
private handleAuthentication(email: string, userId: string, token: string, expiresIn: number) {
const expirationDate = new Date(new Date().getTime() + expiresIn * 1000);
const user = new User(email, userId, token, expirationDate);
this.user.next(user);
//this.autoLogout(expiresIn * 1000);
localStorage.setItem('userData', JSON.stringify(user));
}
Now I have a navbar component, where my logout function is present. The code I tried is :
nav-bar.component.ts
#Component({
selector: 'app-nav-bar',
templateUrl: './nav-bar.component.html',
styleUrls: ['./nav-bar.component.css']
})
export class NavBarComponent implements OnInit, OnDestroy {
isAuthenticated = false;
private userSub!: Subscription;
constructor(private authService : AuthService) { }
onLogout() {
this.authService.logout();
}
ngOnInit(): void {
this.userSub = this.authService.user.subscribe(u => {
// this.isAuthenticated = !u ? true: false;
this.isAuthenticated = !u ? false : true;
console.log(this.isAuthenticated);
//this.isAuthenticated = !!u;
console.log(this.isAuthenticated);
});
}
and my html code for the logout button
<button class="btn btn-outline-success" type="button" (click)="onLogout()" *ngIf="isAuthenticated">Logout</button>
When I refresh the page, only the logout button disappears but every other details after login remains.
Before refresh:
After refresh :
It's hard to say without full stackblitz repro, but my initial guess would be that your subscription in you nav-bar happens after that the user subject have already emitted the data.
To overcome this, I'd change it to a:
user = new ReplaySubject<User>(1);
Replay subject keeps a buffer (with size defined in it's constructor) and upon new subscription will replay the emissions in the buffer (in this case - always last value).
This way, when you subscribe after the login has happened, it will pass the latest value to your component.
I've made a facade service to avoid multiple calls to the API.
It call retrieveMyUser each time the request is made.
If the request has never been made it store the value usingBehaviorSubject. If it has already been made it take the value stored.
I want to clear the data of my BehaviorSubject in auth.service.ts when a user logout. My try to do that is that I call a clearUser() method from facade-service.ts.
facade-service.ts :
...
export class UserServiceFacade extends UserService {
public readonly user = new BehaviorSubject(null);
retrieveMyUser() {
console.log(this.user.value);
return this.user.pipe(
startWith(this.user.value),
switchMap(user => (user ? of(user) : this.getUserFromServer())),
take(1)
)
}
private getUserFromServer() {
return super.retrieveMyUser(null, environment.liveMode).pipe(tap(user => this.storeUser(user)));
}
public clearUser() {
console.log("cleared");
this.storeUser(null)
console.log(this.user.value); // Output null
}
private storeUser(user: V2UserOutput) {
this.user.next(user);
}
}
auth.service.ts :
...
logout() {
var cognitoUser = this.userPool.getCurrentUser();
if (cognitoUser) {
this.userServiceFacade.clearUser()
cognitoUser.signOut();
}
this._router.navigate(['/login']);
}
...
The method clearUser() in auth.service.ts is well called and print cleared correctly.
But when I login, after I logout the console.log(this.user.value); in retrieveMyUser still output the previous value. It was null when at logout though.
So, how do I clear BehaviorSubject cache or to reset BehaviorSubject from another service ?
There are many things in your code which sound weird at reading:
You shouldn't access immediately to the value of a BehaviorSubject without using the asObservable() as recommended by ESLint here.
Instead, you could use another variable which will keep the latest value for the user.
You should use the power of TypeScript in order to help you with types definition and quality code (in my opinion).
The use of a BehaviorSubject with a startWith operator can be simplified using a ReplaySubject with a bufferSize of 1 (replay the latest change)
Your subject acting like a source storage should be private in order to limit the accessibility from outside.
I took your code and make some updates from what I said above:
export class UserServiceFacade extends UserService {
private _user: V2UserOutput;
private readonly _userSource = new ReplaySubject<V2UserOutput>(1);
public get user(): V2UserOutput { // Use for accessing to the user data without the use of an observable.
return this._user;
}
constructor() {
super();
this.clearUser(); // It will make your ReplaySubject as "alive".
}
public retrieveMyUser$(): Observable<V2UserOutput> {
return this._userSource.asObservable()
.pipe(
switchMap(user => (user ? of(user) : this.getUserFromServer())),
take(1)
);
}
private getUserFromServer(): Observable<V2UserOutput> {
return super.retrieveMyUser(null, 'environment.liveMode')
.pipe(
tap(user => this.storeUser(user))
);
}
public clearUser() {
console.log('cleared');
this.storeUser(null);
}
private storeUser(user: V2UserOutput) {
this._user = user;
this._userSource.next(user);
}
}
Cheers!
I have made a route guard in Angular, that checks for a certain value in firestore before allowing access to said route.
The component is routed to once a HTTP cloud function has completed. This cloud function creates an object in a firestore document called agreement, which the route then listens to the status within this object, to decide whether to allow access or not. The issue we are having though, is that maybe 9/10 times, the guard seems to think that the agreement object does not exist in the document, and therefore throws an error. (Though I can see the agreement object there in firestore!).
I have logged the flow, and it correctly logs within the subscription, then within the guard, so I know that the HTTP function is completing before trying to route and calling the guard class.
This is the guard:
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
this.tenancyOffer$ = this._tenancyOffer.getTenancyOfferById(route.params.offerId).get({source: 'server'})).valueChanges();
return this.tenancyOffer$.pipe(
map((tenancyOffer) => {
if (tenancyOffer.status === 'incomplete'
&& tenancyOffer.agreement.status === 'incomplete') {
return true;
} else {
this._router.navigate(['/' + Landlord.base, Landlord.manage.base, Landlord.manage.applications.base, route.params.propId, Landlord.manage.applications.summary, route.params.offerId]);
}
})
);
}
GetTenancyOfferById:
this.tenancyOfferCollection = afs.collection<any>('tenancy_offers');
getTenancyOfferById(id: string) {
return this.tenancyOfferCollection.doc(id);
}
the error is coming from this part: tenancyOffer.agreement.status === 'incomplete', saying TypeError: Cannot read property 'status' of undefined.
The HTTP function I'm calling is here, with the routing to this component in the subscribe:
landlordAcceptReferences() {
this.hasAcceptedRefs = true;
this.processingAcceptRefs = true;
this._references.landlordAcceptReferences(this.offerId).pipe(
finalize(() => {
this.processingAcceptRefs = false;
})
).subscribe(e => {
console.log({e});
this.dref.close();
this._toastr.success(`You have accepted tenant references`);
this._router.navigate(['../../', Landlord.manage.applications.agreement.base, this.offerId], {relativeTo: this._activatedRoute});
}, () => this.hasAcceptedRefs = false);
}
I made the response of the cloud function the entire firestore document, and that HAS the agreement object within it which is logged console.log({e});. I then log (AFTER) in the guard, the value of the document, and it does not exist, even though this is where I make another call to firestore?
Does anyone have any ideas why the getTenancyOfferById(route.params.offerId) function within the guard, is not returning the latest version of this document from firestore, even though the cloud function needs to end before routing in the subscription?
I guess this.tenancyOfferCollection = afs.collection<any>('tenancy_offers'); is defined somewhere in your service. If this is true, the collection will be loaded as soon as your application is loaded.
I recommend to store tenancy_offers values in your service and read it from your guard.
Service:
export class YourService {
private tenancyOfferMap: Map<string, any> = new Map();
constructor(
private afs: AngularFirestore,
) {
afs.collection<any>('tenancy_offers').snapshotChanges()
.subscribe( actions => {
actions.map( action => {
this.tenancyOfferMap.set(action.payload.doc.id, action.payload.doc.data());
});
});
}
getTenancyOfferById(id: string) {
return this.tenancyOfferMap.get(id);
}
}
Guard:
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree {
const tenancyOffer = this.yourService.getTenancyOfferById(route.params.offerId);
if (tenancyOffer.status === 'incomplete'
&& tenancyOffer.agreement.status === 'incomplete') {
return true;
} else {
return this._router.navigate(['/' + Landlord.base, Landlord.manage.base, Landlord.manage.applications.base, route.params.propId, Landlord.manage.applications.summary, route.params.offerId]);
}
}
It may depend on the fact that your stream does not complete.
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
this.tenancyOffer$ = this._tenancyOffer.getTenancyOfferById(route.params.offerId).get({source: 'server'})).valueChanges()
.pipe(take(1)); // <-- ensure your stream completes in guards
return this.tenancyOffer$.pipe(
map((tenancyOffer) => {
if (tenancyOffer && tenancyOffer.status === 'incomplete'
&& tenancyOffer.agreement.status === 'incomplete') {
return true;
} else {
this._router.navigate(['/' + Landlord.base, Landlord.manage.base, Landlord.manage.applications.base, route.params.propId, Landlord.manage.applications.summary, route.params.offerId]);
return false; // <-- always return something in guards
}
})
);
}
I'm developping a single app and at the moment the only good behavior is that I'm getting an user from an API with HttpClient method.
The method is store in a service.
Getting the user is a success but now I want to get a specific array from that user to re-use it by my will.
Should I make another service since this value will be use in 2 components ?
How should I procced to get this array in a var ?
Exemple of user object :
{
firstName: '',
lastName: '',
arrayIWant: []
}
My user is in a subject and here is the way I use it in a component
user: User;
userSubscription: Subscription;
constructor(
public userService: UserService
) {
}
ngOnInit() {
this.userSubscription = this.userService.userSubject.subscribe(
(user: User) => {
this.user = user;
}
);
this.userService.getSingleUserFromServer();
this.userService.emitUser();
}
ngOnDestroy() {
this.userSubscription.unsubscribe();
}
Should I put this code in every component where I want to use the user or is there a way to definie globaly the user ?
You can use a BehaviourSubject which will hold the last value of whatever that service populates the userSubject with
public userSubject: BehaviourSubject<User> = new BehaviourSubject(null);
getSingleUserFromServer(): void {
//get your user from http
userSubject.next(result);
}
In you HTML you can use the async pipe to display the values of the inner array you want. Or just use it in your component by subscribing to the last emission of the behaviourSubject
//X.Component
public subscriptionKiller: Subject<void> = new Subject();
ngOnInit(): void {
this.userService.userSubject
.pipe(takeUntil(this.subscriptionKiller))
.subscribe((lastUser: User) => {
someMethod(this.userService.userSubject.value.arrayIWant);
}
}
ngOnDestroy(): void {
this.subscriptionKiller.next()
}
Let me just preface this by saying yes, i'm using arrow functions to retain the scope of "this" (as far as I can tell anyway).
I have two properties on my component:
IsAdmin (boolean)
currentRole (string)
I make an api call to fetch user roles from my backend via Angular's HttpClient, and I have a callback subscribe-method which updates above mentioned properties with the result.
However, while I can assign the role value to currentRole, the other property IsAdmin remains undefined even as I assign it, and I get no error in my f12 debugger or visual studio code via the chrome plugin.
import { Component, OnInit } from "#angular/core";
import { AuthorizeService, IUser } from "../authorize.service";
import { Observable } from "rxjs";
import { map, tap } from "rxjs/operators";
import { HttpClient } from "#angular/common/http";
#Component({
selector: "app-login-menu",
templateUrl: "./login-menu.component.html",
styleUrls: ["./login-menu.component.scss"]
})
export class LoginMenuComponent implements OnInit {
isAuthenticated: Observable<boolean>;
public userName: Observable<string>;
IsAdmin : boolean;
currentRole : string;
constructor(private authorizeService: AuthorizeService, private http : HttpClient) {
}
ngOnInit() {
this.isAuthenticated = this.authorizeService.isAuthenticated();
this.userName = this.authorizeService.getUser().pipe(map(u => u && u.name));
const endpoint = '.../api/User/User/GetRoles';
this.authorizeService.getUser()
.subscribe(data => {
this.userNameSignedIn = data.name;
});
this.http.get<string[]>(endpoint).
subscribe(result => {
this.currentRole = result[0];
console.log("this.currentRole ", this.currentRole); //prints "admin"
this.IsAdmin == result.includes("admin");
console.log("this.IsAdmin", this.IsAdmin); //prints "undefined"
}, error => console.error(error));
}
}
Console output is as following:
logon-menu.component.ts:37 this.currentRole admin
logon-menu.component.ts:39 this.IsAdmin undefined
What on earth is going on here? What am I doing wrong?
the problem in your subscribe is that you are using ==(comparison) instead of = (assignation)
subscribe(result => {
this.currentRole = result[0];
console.log("this.currentRole ", this.currentRole); //prints "admin"
this.IsAdmin == result.includes("admin"); //<-- here is an error
console.log("this.IsAdmin", this.IsAdmin); //prints "undefined"
},
your code should be:
subscribe(result => {
this.currentRole = result[0];
this.IsAdmin = result.includes("admin");
},