I'm trying to get the current value of a BevhaviorSubject in Angular. I printed each, the whole thing and the value only, to the console to check its content by using these two lines:
console.log(this._isNumeric)
console.log(this._isNumeric.getValue())
...but what I am receiving is this:
closed: false
hasError: false
isStopped: false
observers: []
thrownError: null
_isScalar: false
_value: true
value: true
__proto__: Subject
for the subject (note that the value parameter is set to true) and
false
if I am just printing the value. Maybe I am making an obvious mistake, but does someone has a clue how to get the actual value of the BehaviorSubject? Using .value instead of .getValue() does not change the outcome. Thank you! :)
In your service you can create and expose the BehaviorSubject like this:
private _isNumeric$ = new BehaviorSubject<boolean>(false); // first value is false
/** Protects the _isNumeric$ BehaviorSubject from outside access */
public get IsNumeric$(): Observable<boolean> {
return this._isNumeric$.asObservable();
}
// NOTE: This is how you can access the value within the service
private get IsNumeric(): boolean {
return this._isNumeric$.getValue();
}
To clarify the use of the '$' at the end of the variable name. It does not do anything, it is just sometimes used by convention to indicate that a variable holds an Observable or that a function will return an Observable.
If you wanted to emit a new value you can do so using '.next()' on the BehaviorSubject
this._isNumeric$.next(true); // Emit a value of true
If you wanted to access the data from a component you can retrieve the data by subscribing to it like so. Remember to unsubscribe from the BehaviorSubject as well.
this.yourService.IsNumeric$
.pipe(takeUntil(this.onDestroy$)) // This is just a subject used to unsubscribe later
.subscribe((value: boolean) => {
// Use the result from the BehaviorSubject
});
Every Behavior Subject will require 3 things: a) initial value, b) a source that can be turned into an observable c) a public variable that can be subscribed. You can use Behavior Subject like this:
// In your service file, do this
#Injectable({ provideIn: 'root' })
export class YourServiceName {
// Set the initialvalues in this format ==>
// initialValuesForBehaviorSubject: YourModel = initialValue;
// In your case:
initialValue: boolean = false;
// Make a source using Behavior Subject as a type and above initial
// values as the initial value as a private variable
// This source is required to SET the data
// Your case:
private behaviorSubjectSource: BehaviorSubject<boolean> =
new BehaviorSubject<boolean>(this.initialValue);
// Create a public observable to GET the data
// Your case:
observableForBehaviorSubject: Observable<boolean> =
this.behaviorSubjectSource.asObservable();
// Create a method to set the data from anywhere in your application
setData(data: boolean) {
this.behaviorSubjectSource.next(data);
}
// Create a method to get data from anywhere in your application
getData(): Observable<boolean> {
return this.observableForBehaviorSubject;
}
}
///// In your component-a.ts file, set the value like this:
ngOnInit() {
this.yourService.setData(true);
}
//// In your component-b.ts, get the value like this:
ngOnInit() {
localVariableToStoreObservableData: boolean;
this.yourService.getData().subscribe(data => {
if(data) {
this.localVariableToStoreObservableData = data;
}
})
}
Related
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!
Im currently getting the new updated user value this way:
this.Service.user$.subscribe(data => {
this.userData = data;
this.userId = data._id;
});
but the updateUser is only executed every 5 secs.
So before its loaded the userData and UserId is empty.
is there a way i can get the stored user data from whats already in the service, instead of waiting 5 secs to it beeing executed again?
something like:
this.Service.user$().GET((data:any) => { // gets the value already stored
});
How would i accomplish this?
Service code:
user$: Observable<any>;
constructor(private http: HttpClient, private router: Router) {
this.user$ = this.userChangeSet.asObservable();
}
updateUser(object) {
this.userChangeSet.next(object);
}
Edit:
Also, how would i destory all subscribes on ngOnDestroy event?
What you can do in your service is internally use a BehaviourSubject to
store the values but expose this as an Observable.
Here is a quote from the docs detailing what a BehaviourSubject is
One of the variants of Subjects is the BehaviorSubject, which has a notion of "the current value".
It stores the latest value emitted to its consumers, and
whenever a new Observer subscribes, it will immediately receive the "current value" from the BehaviorSubject
See here for more.
Service code:
private _user$ = new BehaviourSubject<any>(null); // initially null
constructor(private http: HttpClient, private router: Router) {
this.userChangeSet.subscribe(val => this._user$.next(val))
}
get user$ () {
return this._user$.asObservable();
}
Then you can use it like normal in your component.
this.service.user$.subscribe(v => {
// do stuff here
})
Note that the first value
that the component will get will be null since this is the inital value of
the BehaviourSubject.
EDIT:
In the component
private _destroyed$ = new Subject();
public ngOnDestroy (): void {
this._destroyed$.next();
this._destroyed$.complete();
}
And then for the subscription
this.service.user$.pipe(
takeUntil(this._destroyed$)
).subscribe(v => {
// do stuff here
})
The way this works is that when the destroyed$ subject emits, the observables that have piped takeUntil(this._destroyed$) will unsubscribe from their respective sources.
Use BehaviorSubject for userChangeSet. It emits value immediately upon subscription.
Example:
userChangeSet = new BehaviorSubject<any>(this.currentData);
I have a callback function which returns some data to the component.
export class AppComponent {
constructor(
private service: AppService
) {
this.processSomething(true);
this.processSomething(false);
}
private processSomething(isZoom: boolean = false) {
this.service.handleAsyncResponses(
this,
this.processDataReceived
);
}
private processDataReceived(
attributeValueList: any,
isZoom?: boolean
) {
console.log("isZoom:", isZoom);
}
}
I need to send some value isZoom parameter from the component and access the same in console.log("isZoom:", isZoom). Now console.log is loggin undefined.
A working sample is here: https://stackblitz.com/edit/angular-service-oqkfmf?file=app/app.component.ts
I think you're getting a little lost.
I took the freedom to clean your stackblitz from non-used code and show you how to use callbacks : you can check it there.
Let's start with the component :
constructor(
private service: AppService
) {
this.processSomething(true);
this.processSomething(false);
}
private processSomething(isZoom: boolean = false) {
this.service.handleAsyncResponses(isZoom, this.processDataReceived);
}
private processDataReceived(isZoom: boolean) {
console.log("isZoom:", isZoom);
}
You don't need to define your parameters as optional, since you give your isZoom value a default value, hence making it always defined.
As you can see, you don't need to pass your full object as an argument : the function can be called without it.
In your service, all you have left is
public handleAsyncResponses(zoom: boolean, callback: Function) {
callback(zoom);
}
Simply call the function as you would in any other context. simply rename this.processDataReceived(zoom) with the name of the parameter (here it being callback).
This is how callbacks are handled.
In your case, you need to wrap the function call in local closure:
private processSomething(isZoom: boolean = false) {
this.service.handleAsyncResponses(
this, (attributeValueList: any) => {
this.processDataReceived(attributeValueList, isZoom);
}
);
}
changed example
So in normal javascript if I wanted to assign a value to a variable and then use that value outside of a function it would be done by declaring the variable first and then define it's value in the function. I'm brand new to typescript and angular so I am missing how to do this.
In the code below I am trying to get the value from a method in a service and then pass that value into my return. (I hope that makes sense). However I keep getting undefined on console.log(url) with no other errors.
emailsAPI() {
let url: any
this.apiUrlsService.urlsAPI().subscribe(
data => {
this.results = data
url = this.results.emails
}
);
console.log(url)
return this.http.get('assets/api/email_list.json')
}
api-urls service:
import { Injectable } from '#angular/core';
import { HttpClient, HttpErrorResponse } from '#angular/common/http';
#Injectable()
export class ApiUrlsService {
constructor(
private http: HttpClient
) { }
urlsAPI () {
return this.http.get('assets/api/api_urls.json')
}
}
That's because you're calling async method subscribe and then trying to log the coming value before subscription is resolved. Put last two statements (console.log and return) inside the curly braces just after assigning this.results.emails to the url variable
emailsAPI(): Observable<any> {
let url: any
return this.apiUrlsService.urlsAPI()
.flatMap(data => {
this.results = data
url = this.results.emails
// you can now access url variable
return this.http.get('assets/api/email_list.json')
});
}
As per reactive programming, this is the expected behaviour you are getting. As subscribe method is async due to which you are getting result later on when data is received. But your console log is called in sync thread so it called as soon as you are defining subscribe method. If you want the console to get printed. Put it inside your subscribe data block.
UPDATE:
As per your requirement, you should return Subject instead of Observable as Subject being data consumer as well as data producer. So it will consume data from httpget request from email and act as a producer in the method from where you called emailsAPI method.
emailsAPI(): Subject<any> {
let emailSubject:Subject = new Subject();
this.apiUrlsService.urlsAPI()
.flatMap(data => {
this.results = data
return this.results.emails;
}).
subscribe(url=> {
this.http.get(your_email_url_from_url_received).subscribe(emailSubject);
});
return emailSubject;
}
The subject can be subscribed same as you will be doing with Observable in your calee method.
I am facing a weird issue in assigning response to a class's global variable from inside a observable. So my program logic is as follows:
Get latest playlists ID's from elastic search (i use elastic search from a type definition file). This returns me a PromiseLike to which i hook a then operator.
Inside the promise resolution, i make another http get call (i.e an observable)
In Observable subscription, i assign my global array with the response from the server.
Code is working correctly, I am getting responses as they should be but i cant assign the variable to the global one.
Here is my code:
import {Component, OnInit} from '#angular/core';
import {PlaylistService} from '../api/services'
#Component({
selector: 'app-playlists',
templateUrl: './playlists.component.html',
styleUrls: ['./playlists.component.css']
})
export class PlaylistsComponent implements OnInit {
public playlists: any[] = [];
constructor(private playlistService: PlaylistService) {
}
ngOnInit() {
let that = this;
this.playlistService.listIds().then((val) => { // <-- promise resolution
return this.playlistService.getByIds(val).toPromise(); // <-- http get call which i then convert to promise for simplicity
}).then((res) => { // <-- resolution of the http get call
console.log(this.playlists); <-- in this log, i get my desired results
// here is my problem, this assignment doesn't happens
this.playlists = res.data;
});
}
}
The listIds function is as follows:
listIds() {
return this.api.listing('playlist').then((body) => {
let hits = body.hits.hits;
return _.keys(_.groupBy(hits, '_id'));
});
}
and here is my api.listing function (elastic search client)
listing(type: string) {
let es = this.prepareES();
return es.search({
index: 'test',
_source: ["_id"],
type: type
});
}
The return type of es.search is
search(params: SearchParams): PromiseLike>;
Any ideas why i am not being able to assign value to global variable?
It looks like the promise returned by this.playlistservice.listIds() doesn't run inside Angulars zone. This is why Angular2 doesn't run change detection and doesn't recognize the change.
You can invoke change detection explicitly after the change:
constructor(private playlistService: PlaylistService, private cdRef:ChangeDetectorRef) {
...
ngOnInit() {
let that = this;
this.playlistService.listIds().then((val) => { // <-- promise resolution
return this.playlistService.getByIds(val).toPromise(); // <-- http get call which i then convert to promise for simplicity
}).then((res) => { // <-- resolution of the http get call
console.log(this.playlists); <-- in this log, i get my desired results
// here is my problem, this assignment doesn't happens
this.playlists = res.data;
this.cdRef.detectChanges();
});
}
Can you try passing
this.playlistService.listIds()
call inside your
return this.playlistService.getByIds(val)
replace val with first service call and see if your view gets updated. Just for testing purpose like
return this.playlistService.getByIds(this.playlistService.listIds())
.then((results)=>{/*rest of logic here*/});