Angular rxjs Subject : subscribing to a subject after emitting values - javascript

I read an article in which it states that, if you subscribe to subject when the event has been already emitted You would lose the emitted value. his demonstrated example is below.
let subject: Subject<string> = new Subject();
subject.next('test');
subject.subscribe((event) => {
console.log(event);
});
But ,I have a similar approach implemented, In which first subject will emit the value , then subscribes the subject. Value is there. not lost. code is below.
In the code, we are saving authentication information in a subject. we use that in header component to show logout button and links. Question is why my code is working even though article says it won't?
import { HttpClient, HttpErrorResponse } from "#angular/common/http";
import { Injectable } from "#angular/core";
import { Subject} from "rxjs";
import { User } from "./user.model";
#Injectable({providedIn : "root"})
export class AuthService{
constructor(private httpClient:HttpClient){}
user = new Subject<User>(); //emit new user when login or logout or token expired
login(email:string,password:string){
return this.httpClient
.post<SignUpResponse>('https://identitytoolkit.googleapis.com/v1/accounts:signInWithPassword?key=AIzaSyBCK9B2RcL0D4Bn8YaFCxE9rBXejTLNYQY',
{
email:email,
password:password,
returnSecureToken:true
})
.pipe(tap( response => this.handleAuthentication(response.email,response.idToken,response.idToken,response.expiresIn)))
}
private handleAuthentication(email:string,userId:string,token:string,expiresIn:string){
const expirationDate = new Date(new Date().getTime() + +expiresIn * 1000)
const user = new User(email,userId,token,expirationDate);
this.user.next(user);
}
}
Header.component.ts
import { Component,Output, EventEmitter, OnInit, OnDestroy } from "#angular/core";
import { Subscription } from "rxjs";
import { AuthService } from "../auth/auth/auth.service";
import { DataStorageService } from "../shared/data-storage.service";
#Component({
selector : 'app-header',
templateUrl:'./header.component.html'
})
export class HeaderComponent implements OnInit,OnDestroy{
collapsed = true;
private subscription : Subscription;
isAuthenticated : boolean = false;
constructor(private dataStorageService:DataStorageService,
private authService:AuthService){
}
ngOnInit(){
this.subscription = this.authService.user.subscribe( user => {
this.isAuthenticated = !!user;
console.log("User Object"); console.log(user);
console.log("IsAuthenticated value"); console.log(!!user);
})
}
ngOnDestroy(){
this.subscription.unsubscribe();
}
}

The only reason your example subscribe is running is because you load the Header component before you start the login process/method. ngOnInit of component is called before login method in service.
Your code run like this not the way you have described in the quesiton:
user = new Subject<User>();
subject.next('test');
// then this runs in component ngOnInit
this.authService.user.subscribe((event) => {
console.log(event);
});
// when you try to login from inside the component then it runs the login method with next.
login(email:string,password:string){.....
this.user.next(user); // now this is triggered hence you don't loose the user response.

Related

BehaviourSubject.getValue() returning default values

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

In Angular 9, how do I update a component's data field to show in the DOM without re-instantiating it?

I'm fairly new to Angular 9. I have a program where a user enters in a name - which, upon submitting - a POST HTTP request is sent and the name is stored. I then have an unrelated component for a sub-header that lists the names that have been stored using a GET HTTP request using ngOnInit(). However, I need the sub-header to update that list of names dynamically each time a new list is entered rather than just whenever the component instantiates.
I'm unsure how to proceed. I'm sure I could simply add a button that fetches and updates said list, but trying for something more dynamic. Thanks in advance!
//SERVICE.TS...
import { Injectable } from '#angular/core';
import { HttpClient } from '#angular/common/http';
import { NewList } from './new-list.model';
import { map } from 'rxjs/operators';
#Injectable({
providedIn: 'root'
})
export class ListService {
createdLists: NewList[] = [];
constructor(private http: HttpClient) { }
createList(postData) {
return this.http
.post(
'API_KEY',
postData
);
}
getLists() {
return this.http
.get<NewList>(
'API_KEY'
).pipe(map(responseData => {
const responseArray: NewList[] = [];
for (const key in responseData) {
responseArray.push(responseData[key])
}
return responseArray;
})
);
}
}
// NEW-LIST-MENU.TS (USER ENTERS A NAME)...
import { Component, OnInit } from '#angular/core';
import { NgForm } from '#angular/forms';
import { Router } from '#angular/router';
import { ListService } from 'src/app/shared/list.service';
import { NewList } from 'src/app/shared/new-list.model';
import { UIService } from 'src/app/shared/ui.service';
#Component({
selector: 'app-new-list-menu',
templateUrl: './new-list-menu.component.html',
styleUrls: ['./new-list-menu.component.css']
})
export class NewListMenuComponent implements OnInit {
constructor(private listService: ListService,
private uiService: UIService,
private router: Router) { }
ngOnInit(): void {
}
onSubmit(form: NgForm) {
const listName = form.value.listname;
const newListObj = new NewList(listName, []);
this.listService.createList(newListObj)
.subscribe(() => {
this.router.navigate(['']);
});
const lists = this.listService.updateLists(newListObj);
form.reset();
}
onCancel() {
this.router.navigate(['']);
}
}
// SUB-HEADER.TS...
import { Component, OnInit, Output } from '#angular/core';
import { Router } from '#angular/router';
import { ListService } from 'src/app/shared/list.service';
import { NewList } from 'src/app/shared/new-list.model';
import { faWindowClose } from '#fortawesome/free-solid-svg-icons';
import { faPlusCircle } from '#fortawesome/free-solid-svg-icons';
import { faList } from '#fortawesome/free-solid-svg-icons';
import { faSignOutAlt } from '#fortawesome/free-solid-svg-icons';
import { Subject } from 'rxjs';
#Component({
selector: 'app-sub-header',
templateUrl: './sub-header.component.html',
styleUrls: ['./sub-header.component.css']
})
export class SubHeaderComponent implements OnInit {
createdLists: NewList[];
faWindowClose = faWindowClose;
faPlusCircle = faPlusCircle;
faList = faList;
faSignOutAlt = faSignOutAlt;
#Output() closeSub = new Subject();
constructor(private listService: ListService,
private router: Router) { }
ngOnInit(): void {
this.listService.getLists().subscribe((responseData) => {
this.createdLists = responseData;
});
}
onCloseSelect() {
this.closeSub.next();
}
onNewListSelect() {
this.onCloseSelect();
this.router.navigate(['new-list-menu']);
}
onLogOutSelect() {
}
}```
You can accomplish this in many ways, as these components are not related to each other, you can introduce a state service and use observables. see below possible solution
Create a new state service ListStateService
export class ListStateService {
private listData = new BehaviorSubject<NewList >({} as NewList);
listData$ = this.listData .asObservable();
}
Inject ListStateService into NewListMenuComponent
In the onSubmit, after you update,
const lists = this.listService.updateLists(newListObj);
this.listData .next(lists );
Inject ListStateService into SubHeaderComponent
In the ngOnInit(), subscribe to the ListStateService.listData$ and here you will get the value on changes
In your service, use an event emitter (very useful):
import { EventEmitter } from "#angular/core";
#Output() myEvent: EventEmitter<any> = new EventEmitter();
then emit new data to your sub header component through your service like so:
emitEvent (newData: Array<string>) {
this.myEvent.emit({
data: newData,
});
}
Subscribe to new data in your sub header component ngOnInit and use it:
this.myService.myEvent.subscribe((newData: Array<string>) => {
console.log(JSON.stringify(newData.data));
});
Note: Subscriptions will cause memory leaks if constantly re-subscribed in the component, so you can save the subscription and call unsubscribe() on it in the ngOnDestroy callback.
It's a little unclear what you are trying to do, but if you are trying to pass data from a parent component to a child component, you can do this either with Input fields or a ViewChild
to use Input fields your parent might looks like this:
<app-sub-header [names]="names"></app-sub-header>
then use an "Input" field in the child. Updating names in the parent should update the same named variable in the child in real time.

Angular4 - let multiple unrelated components notify each other of the problem of updating data, and whether there is a cleaner coding method?

I have encountered a project in progress, let multiple unrelated components notify each other of the update data, is there a cleaner coding method?
There are 3 components (more likely later) and a common-data component. They have no parent-child relationship with each other and only show on the same screen.
The desired effect is to press the button of any component, update the contents of common-data, and notify yourself and other components to fetch new messages from common-data.
At present, my approach is to use Rx's Observable and Subscription, but they must be imported in the component.ts and service.ts files of each component, and a lot of duplicate code appears, it is very messy, I don't know what is better. practice?
Thanks!
My code :
The sample name is test-a-comp (a.b.c and so on, the code is the same)
test-a-comp.html
<p>
{{ownMessage}}
</p>
<button (click)="sendChange()">update</button>
test-a-comp.component
import { Component, OnInit } from '#angular/core';
import { Subscription } from 'rxjs/Subscription';
import { CommonData } from '../common-data/common-data';
import { TestACompService } from './test-a-comp.service';
import { TestBCompService } from '../test-b-comp/test-b-comp.service';
import { TestCCompService } from '../test-c-comp/test-c-comp.service';
#Component({
selector: 'app-test-a-comp',
templateUrl: './test-a-comp.component.html',
styleUrls: ['./test-a-comp.component.css']
})
export class TestACompComponent implements OnInit {
subscription: Subscription;
ownMessage;
constructor(
private testAService: TestACompService,
private testBService: TestBCompService,
private testCService: TestCCompService,
) {
this.subscription = this.testAService.getMessage()
.subscribe((test) => {
CommonData.message = test;
});
this.subscription = this.testBService.getMessage()
.subscribe(() => {
this.ownMessage = CommonData.message;
});
this.subscription = this.testCService.getMessage()
.subscribe(() => {
this.ownMessage = CommonData.message;
});
}
ngOnInit() {
}
sendChange() {
this.testAService.sendMessage();
}
}
test-a-comp.service:
import { Injectable } from '#angular/core';
import {Subject} from 'rxjs/Subject';
import {Observable} from 'rxjs/Observable';
import {Subscription} from 'rxjs/Subscription';
#Injectable()
export class TestACompService {
subscription: Subscription;
private subject = new Subject<any>();
constructor() {
}
getMessage(): Observable<any> {
return this.subject.asObservable();
}
sendMessage(): void {
this.subject.next('update message from A');
}
}
As far as i understand & you've mentioned in the above, there is a button in one of the component (test-a-component.html). If you update the button, you need to send message to other components which are subscribed.
The Components which have no Parent-Child relationship can communicate via a service:
Create a single service file (In your case: test-a-comp.service)
Create a Subject on what data you need to communicate via this service:
export class testMessageService {
constructor() {}
// Observable string sources
private message = new Subject<string>();
//Observable string streams
testMessage$ = this.message.asObservable();
constructor() {}
// Method to send message when a button is clicked
sendMessage(message: string) {
this.message.next(message);
}
/* You don't need "getMessage()" method as you've already subscribed to
the observables. There subscribed Observable string streams are
injected in your components (As below point 3) to display / do other
operation on the message. */
}
In your other Components, where you want to receive messages, do the following:
export class TestComponent 1 {
myMessage1: string;
constructor(private TestMessageService: testMessageService) {}
TestMessageService.testMessage$.subscribe(message => {
this.myMessage1 = message;
});
}
export class TestComponent 2 {
myMessage2: string;
constructor(private TestMessageService: testMessageService) {}
TestMessageService.testMessage$.subscribe(message => {
this.myMessage2 = message;
});
}
export class TestComponent 3 {
myMessage3: string;
constructor(private TestMessageService: testMessageService) {}
TestMessageService.testMessage$.subscribe(message => {
this.myMessage3 = message;
});
}
For more information/guidance refer Component interaction via a common
service: https://angular.io/guide/component-interaction
Hope this helps!

AngularFire2 - How to maintain logged in state after page refresh

I'm using AngularFire2 for an app and I've gotten the registration/login functionality working with Firebase, however, every time I refresh the page, the logged in state is reset and won't persist. I can't quite find functionality to do this, though I feel I'm missing something very small.
Can I use the AngularFireAuth to check on page load somewhere?
Here is my auth provider code:
import { Injectable } from '#angular/core';
import {Observable, Subject, BehaviorSubject} from "rxjs/Rx";
import {AngularFireAuth, FirebaseAuthState} from "angularfire2";
import {AuthInfo} from "./auth-info";
import {Router} from "#angular/router";
#Injectable()
export class AuthService {
static UNKNOWN_USER = new AuthInfo(null);
authInfo$: BehaviorSubject<AuthInfo> = new BehaviorSubject<AuthInfo>(AuthService.UNKNOWN_USER);
constructor(private auth: AngularFireAuth, private router:Router) {
}
login(email, password):Observable<FirebaseAuthState> {
return this.fromFirebaseAuthPromise(this.auth.login({email, password}));
}
signUp(email, password) {
return this.fromFirebaseAuthPromise(this.auth.createUser({email, password}));
}
fromFirebaseAuthPromise(promise):Observable<any> {
const subject = new Subject<any>();
promise
.then(res => {
const authInfo = new AuthInfo(this.auth.getAuth().uid);
this.authInfo$.next(authInfo);
subject.next(res);
subject.complete();
},
err => {
this.authInfo$.error(err);
subject.error(err);
subject.complete();
});
return subject.asObservable();
}
logout() {
this.auth.logout();
this.authInfo$.next(AuthService.UNKNOWN_USER);
this.router.navigate(['/login']);
}
}
Thankyou in advance!
AngularFireAuth is an observable and emits FirebaseAuthState values. If a user is signed in and the page is refreshed, AngularFireAuth will emit an authenticated FirebaseAuthState; otherwise, it will emit null.
So something like this should come close to solving your problem:
constructor(private auth: AngularFireAuth, private router:Router) {
auth.subscribe((authState) => {
if (authState) {
const authInfo = new AuthInfo(authState.uid);
this.authInfo$.next(authInfo);
}
});
}

Auth0 service unable to find container using Angular 2

I'm creating an Angular 2 SPA for learning purposes and integrating Auth0 for handeling the authentication. I have an auth.service.ts that is going to be called from difference places in my application, for example in the top-navbar to logout and on the auth-page to handle logins and registrations.
When trying to place the Auth0 container in a div by setting the container option I get the following error: Can't find element with id auth-container
How can I let the auth.service know how/where to look for the auth-container div? Placing all the logic inside the auth.component.ts is assumably not an option because the auth.service will be used for other functionality in other places where the lock variable is also used.
auth.service.ts
import { Injectable } from '#angular/core';
import { tokenNotExpired } from 'angular2-jwt';
import { myConfig } from './auth.config';
declare var Auth0Lock: any;
var options = { container: 'auth-container' };
#Injectable()
export class Auth {
lock = new Auth0Lock(myConfig.clientID, myConfig.domain, options);
constructor() {
this.lock.on('authenticated', (authResult) => {
localStorage.setItem('id_token', authResult.idToken);
});
}
public authenticated() {
return tokenNotExpired();
};
public logout() {
localStorage.removeItem('id_token');
};
}
auth.component.ts
constructor(public auth: Auth) {
auth.lock.show();
}
auth.component.html
<div id="auth-container"></div>
Well they did not make your life easy but by mistake I made it work.
Try this:
auth.component.ts
ngOnInit() {
this.auth.login()
}
Delete this from your constructor
auth.lock.show();
The auth.service is not a container, it's a service that provides a popup when the login function is invoked.
So, to reuse it wherever you like, you need to inject the auth service into the component where you want to call the auth service from. Then, you just call the method. For example, here is the html for my Start component. You can see that the click event for the signin button is bound to the "submitLogin()" method of the component (the Start component):
<div class="splash-back" *ngIf="!authService.authenticated()">
<div id="splash">
<div id="logo"><span class="silver">GCO</span>TeamKeeper
<p class="silver tagline">The other teams could make trouble for us if they win.</p>
<p class="silver attribution">~ Yogi Berra</p></div>
<div class="call">
<br>
<button class="btn-sign-in" (click) = "submitLogin()">Sign up or Log in</button>
</div>
<!--<mtm-authentication></mtm-authentication>-->
</div>
</div>
And here is the start component code (note the injection of the authentication service in the constructor):
#Component({
selector: 'tk-start',
templateUrl: './start.component.html',
styleUrls: ['./start.component.css']
})
export class StartComponent implements OnInit {
constructor(private authService: UserAuthenticationService) { }
ngOnInit() {
}
submitLogin(){
this.authService.login();
}
}
And to make this example complete, here is my auth service code:
import {Injectable} from "#angular/core";
import { tkConfig } from './user-authentication.config';
import {Router} from "#angular/router";
import {tokenNotExpired} from "angular2-jwt";
let Auth0Lock = require('auth0-lock').default;
#Injectable()
export class UserAuthenticationService {
// Configure Auth0
userProfile: Object;
lock = new Auth0Lock (tkConfig.clientID, tkConfig.domain, {
avatar: null,
theme: {
primaryColor: "#69BE28",
foregroundColor: "#000000"
},
languageDictionary: {
title: "GCO TeamKeeper"
}
}
);
constructor(
private router: Router) {
this.userProfile = JSON.parse(localStorage.getItem('profile'));
// Add callback for lock `authenticated` event
this.lock.on('authenticated', (authResult) => {
localStorage.setItem('id_token', authResult.idToken);
this.lock.getProfile(authResult.idToken, (error, profile) => {
if (error) {
alert(error);
return;
}
profile.user_metadata = profile.user_metadata || {};
localStorage.setItem('profile', JSON.stringify(profile));
this.userProfile = profile;
this.router.navigate(['/organization']);
});
})
}
public login() {
// Call the show method to display the widget.
this.lock.show();
};
public authenticated() {
// Check if there's an unexpired JWT
// It searches for an item in localStorage with key == 'id_token'
return tokenNotExpired();
};
public logout() {
// Remove token from localStorage
localStorage.removeItem('id_token');
localStorage.removeItem('profile');
this.userProfile = undefined;
this.router.navigate(['/start']);
};
}

Categories

Resources