I need to run a method with 2 parameters, each parameter is gotten through some form of subscribe function. the first is the collection which is gotten through the url from angular's page routing. The second is the dokument, this is the firebase's firestore document.
export class FirebaseDocument implements OnInit {
collection: string;
dokument: any;
//== CONSTRUCTORS
constructor(
private route: ActivatedRoute,
private _db: AngularFirestore
) {}
//== Initialize
ngOnInit() {
console.log("__loading page component");
this.route.params.subscribe(params => {
this.collection = params["collection"];
});
console.log(this.collection);//collection populated correctly
//load the document from AngularFirestore
console.log("loading the document from firebase");
let itemsCollection = this._db.collection(url).valueChanges();
//subscribe to get the dok of the first document in the collection
itemsCollection.subscribe(docArr => {
this.dokument = docArr[0];
console.log(this.dokument);//dokument is populated
});
console.log(this.dokument);//dokument is undefined
this.doMultiParameterMethod(this.collection, this.dokument);
}
}
this.collection populates perfectly fine;
this.dokument is only populated inside the subscribe method
I need this to be populated by the time the next line is run. the console.log(this.dokument);
I have been dumbstruck by this because essentially the same code is used by the 2 subscribe methods but they don't behave the same way.
Sometimes a subscribe can be synchronous. This happens when the Observable is a ReplaySubject a BehaviorSubject or an Observable which has a shareReplay() pipe. (probably other options as well.
This will make the observable immediately fire on subscription. However, you should never count on this behavior, and always continue within your subscribe.. Or use pipes like mergeMap and create other observables which you can access in your template using the async pipe.
In your case. The this.route.params is obviously a 'replaying' Observable from which you get the latest value after subscribing. Otherwise you would have to wait for the params to change again until you get a value.
Your Database call cannot return an immediate response, because it's essentially a network request.
In your example code, you can update it to this, and use the async pipe in your template
export class FirebaseDocument implements OnInit {
readonly collection$: Observable<string> = this.route.params.pipe(
map((params) => params.collection)
);
readonly doc$: Observable<any[]> = this.db.collection(this.url).valueChanges().pipe(
shareReplay({ refCount: true, bufferSize: 1 })
);
constructor(private route: ActivatedRoute, private db: AngularFirestore) {}
ngOnInit() {
// don't forget to unsubscribe
combineLatest([
this.collection$,
this.doc$
]).subscribe((collection, document) => {
this.doMultiParameterMethod(collection, document);
});
}
}
Maybe you should make the Observable a Promise, in your case would be the following :
export class FirebaseDocument implements OnInit {
collection: string;
dokument: any;
//== CONSTRUCTORS
constructor(
private route: ActivatedRoute,
private _db: AngularFirestore
) {}
//== Initialize
ngOnInit() {
console.log("__loading page component");
this.route.params.subscribe(params => {
this.collection = params["collection"];
});
console.log(this.collection); //collection populated correctly
this.getDokument().then(docArr => {
this.dokument = docArr[0];
this.doMultiParameterMethod(this.collection, this.dokument);
});
}
getDokument(): Promise<any> {
let itemsCollection = this._db.collection(url).valueChanges();
return new Promise((resolve, reject) => {
itemsCollection.subscribe((response: any) => {
resolve(response);
}, reject);
});
}
}
Related
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()
}
Below are my code snippets
Angular service
export class CarService {
constructor(private httpClient: HttpClient) {}
// -------------------------------------------------
// define observable Subject(s) which will be subscribed by callers
private observableList = new Subject<CarModel>();
public getObservableList() {
return this.observableList.asObservable();
}
// -------------------------------------------------
// fetch required required data, then add into observable
public fetchCarInfo(carId: string) {
// call service
const url = '.../api/car/' + carId;
this.httpClient
.get<CarModel[]>(url)
.subscribe(
data => {
this.observableList.next(data);
}
);
}
}
Angular component
export class ListPage implements OnInit, OnDestroy {
car1: CarModel[];
car2: CarModel[];
constructor(private carService: CarService) {}
ngOnInit() {
// 1. subscribe Observable Subject
const carsSubscription = this.carService
.getObservableList()
.subscribe(serviceResult => {
// Here we can use returned serviceResult
// but our service will be called 2 times with 2 different parameters!
});
}
// in my business, I need to call the same service 2 times with different parameters
// The issue here, BOTH calls will refresh the same Observable
// so i can not know the returned serviceResult is related to the first or second call?
public coreBusiness(carId1: string, carId2 string) {
// 2. call service method by parameters to fetch/refresh Observable
this.carService.fetchCarInfo(carId1);
this.carService.fetchCarInfo(carId2);
}
}
The normal solution
To add into Service 2 Observables and in Component subscribe both!
But I think this is not a clean solution to add multiple Observables in the service equivalent to the number of service calls with different parameters.
Any suggestions?
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 am new to Angular, JS, and observables. I have a typescript class called DataService. I want it to load a list of URLs from a JSON formatted local file, and then have some way to call those URLs (to a handful of REST APIs) and return observables. The problem I am having is my code is not waiting for the config file to be loaded before the REST API functions get called.
I thought I could have the DataService constructor load the configuration file, and then have unique functions for each REST API call, but that isn't working
my code:
export class DataService {
configFile
constructor(private http: HttpClient) {
this.http.get('/assets/restApiUrlListConfig.json').subscribe(config => {
this.configFile = config;
});
}
getUrlFromConfigFile(name: string): string {
...
this returns the URL from the config file
...
}
getUrlAData(): Observable {
return this.http.get( getUrlFromConfigFile('A') )
}
}
My other components have code like this:
export class SomeComponent implements OnInit {
someComponentAData
constructor(private data: DataService) { }
ngOnInit() {
this.data.getUrlAData().subscribe(
data => {
this.someComponentAData = data
}
)
}
I am getting an error that the observable returned from the dataservice is undefined. Which I believe is because the constructor hasn't finished loading the config file, which I think is why the function getUrlAData isn't returning anything.
I feel like I'm not correctly handling these async calls, but I'm at a loss for how to tell my code to :
create the data service object
load the data file before anything else can be done
allow the other functions to be called asyncronously AFTER the config file is loaded
Angular CLI: 6.2.3
Node: 8.12.0
OS: win32 x64
Angular: 6.1.8
Edit 1: attempting to implement suggested solution
My DataService
configFile
configObservable: Observable<any>;
someSubscribeObj
constructor(private http: HttpClient) {
this.someSubscribeObj = this.http.get('/assets/restApiUrlListConfig.json').subscribe(config => {
this.someSubscribeObj = undefined;
this.configFile = config;
});
}
getObsFromConfigFile(name: string): Observable<any> {
//...
if (this.configFile != undefined) {
console.log('this.restApiUrlListConfig[name]',this.configFile[name])
return of(this.configFile[name])
}
else
return of(this.someSubscribeObj.pipe(map(c => c[name])))
//this.configObservable
//...
}
getUrlAData(): Observable<any> {
return this.getObsFromConfigFile('A').pipe(mergeMap(url => this.http.get(url)))
}
My other component:
constructor( private data: DataService ) { }
ngOnInit() {
//this.data.loggedIn.pipe((p) => p);
this.data.getUrlAData().subscribe(
data => {
this.urlAData = data
}
)
}
I was unable to store the "subscribe" into the observable, so I created a generic Any type varable, but at runtime I get a problem with the pipe command:
TypeError: this.someSubscribeObj.pipe is not a function
at DataService.push../src/app/services/data.service.ts.DataService.getObsFromConfigFile
(data.service.ts:67)
at DataService.push../src/app/services/data.service.ts.DataService.getUrlAData
(data.service.ts:74)
Edit 2: the unfortunate workaround
I am currently using two nested subscriptions to get the job done basically
http.get(config_file_url).subscribe(
config => {
http.get( config['A'] ).subscribe( adata => { do things };
http.get config['B'].subscribe( bdata => {do things };
}
)
I feel like I should be able to use a mergeMap of some sort, but I couldn't get them to work as I thought they would.
You need to wait on that async call, I would use a flatmap to get the value out of an observable.
export class DataService {
configFile
configObservable: Observable<any>;
constructor(private http: HttpClient) {
this.configObservable = this.http.get('/assets/restApiUrlListConfig.json').pipe(
map(config => {
this.configObservable = undefined;
this.configFile = config;
return configFile;
})
);
}
getUrlFromConfigFile(name: string): Observable<string> {
...
return of(configFile[name]) if configFile is set else return configObservable.pipe(map(c => c[name]));
...
}
getUrlAData(): Observable<string> {
return this.getUrlFromConfigFile('A').pipe(map(url => this.http.get(url)))
}
}
Basically you want to store the observable and keep using it till it completes, after it completes you can just wrap the config in an observable. The reason for wrapping it is to make the interface consistent, otherwise you have to have an if before every get.
in my Angular App i make a simple call to a node.js server. the HttpClient "get"
function returns the right answer. This answer I want to store in a variable of my component "interfaces". But in the "subscribe" function of the get request my "this" pointer doesn't point to my component. Instead it tells me that it is of type "SafeSubscriber". Any call to my member "interfaces" lead to the following error:
TypeError: Cannot read property 'interfaces' of undefined
export class SettingsComponent implements OnInit {
public interfaces : string[];
constructor(private http: HttpClient) {
this.interfaces = [];
this.interfaces.push("huhu");
}
ngOnInit() : void {
this.http.get('http://localhost:3000/settings/interfaces').subscribe((data) => {
// Read the result field from the JSON response.
console.log(data);
this.interfaces.push("xxx");
Object.keys(data).forEach(function(k) {
console.log(k);
this.interfaces.push("xxx");
});
}),
err => {
console.log("error " + err);
};
}
}
As you can see I also tried to enter some values manually into the array just to make sure, that not the server response is causing the problem.
Any help is appreciated.
I used this code as a blueprint which is from:
https://angular.io/guide/http
#Component(...)
export class MyComponent implements OnInit {
results: string[];
// Inject HttpClient into your component or service.
constructor(private http: HttpClient) {}
ngOnInit(): void {
// Make the HTTP request:
this.http.get('/api/items').subscribe(data => {
// Read the result field from the JSON response.
this.results = data['results'];
});
}
}
You're losing reference to the correct this in this statement:
Object.keys(data).forEach(function(k) {..})
Inside the function block code this refers to the calling context , which is the subscribe method itself, that's why interfaces is undefined, since it's not a property of the subscribe method.
You can change the function for a lambda en it should be fine:
Object.keys(data).forEach((k) => {..})