I'm using Angular 2 and I have noticed an unexpected behaviour.
I have a datasource class, which extends DataSource, with two variables:
private archives = new BehaviorSubject<MyArchive[]>([]);
public archives$: Observable<MyArchive[]> = this.archives.asObservable();
private filteredArchive: MyArchive[];
I update archives this way within the class:
this.archives.next(this.filteredArchive);
Outside in another class I try to subscribe to the observable but it doesn't work:
ngAfterViewInit(): void {
this.dataSource.archives$.subscribe((archive: Array<MyArchive>) => {
console.log(archive.length);
console.log(archive);
console.log(archive.length);
}
}
In the console log it prints:
0
<THE ARCHIVE WITH AN OBJECT INSIDE AS IT SHOULD BE, WITH LENGTH = 1>
0
So that I can't iterate on the archive variable because its length is 0. What's going on here?
the issue this the value is already emitted before you subscribe. Think of observables like tv stream and your data is like a show on this stream if you open the tv after the show ended (subscribe after you pushed the data) you never see the show. if you want your observable to keep the last value you can use the Scan operator like this :
export const reducer = () =>
scan<MyArchive[]>((archive, update) => {
//update your Archive or simply overwrite the value
archive = update
return archive
}, []);
export class datasource {
archives$ : Observable<MyArchive[]>;
archives : Subject<MyArchive[]> = new Subject([]);
update(newMyArchive: MyArchive[]) {
this.archives.next(newMyArchive);
}
constructor(public http: HttpClient) {
this.archives$ = this.archives.pipe(
reducer(),
//use the shareReplay so all your subscribers get the same values
shareReplay(1)
);
this.archives$.subscribe();
}
and you can update the Arcjive useing the update method in the datasource class like:
this.update(filteredArchive)
I'm not sure why you're redundantly creating archives$ and archives. You can do it with just a single BehaviorSubject.
I think you should be doing this:
// Create a BehaviorSubject for MyArchive[]
public archives$: BehaviorSubject<MyArchive[]> = new BehaviorSubject<MyArchive[]>([]);
// Create a private filtered Array for MyArchive.
private filteredArchive: MyArchive[];
And then,
this.archives$.next(this.filteredArchive);
When you use this statement:
console.log(archive);
a reference to the archive object is shown in the console (see the MDN documentation). The content of the object can be different when you inspect it in the console (by clicking on the arrow) from what it was when the console.log statement was executed.
In order to see what the actual content is at a specific moment, you should output a string representation of the object:
console.log(archive.toString());
console.log(JSON.stringify(archive));
You can expermiment with this stackblitz to see the various console outputs.
I solved the problem.
I forgot that this.filteredArchive is updated by an HTTP GET request. I was trying to push data within this.filteredArchive using forEach, but I forgot to call then() when all the data are received.
Hence I was trying to access, using subscribe, a result that was not yet ready.
Thanks everyone for the replies!
Related
Consider the code:
res: any;
getData(url: any) {
this.res = this.http.get(url);
}
ngOnInit(): void {
getData("url.com/file.json");
console.log(this.res)
}
In the console I get
Observable {source: Observable, operator: ƒ}
How do I get the contents of the json file instead of an observable?
First, the way you define the property and use the httpClient verbs in unconventional. If you want to assign the Http verb (i.e GET) to a property this property will hold an Observable, because HttpClient verbs return Observables in Angular and they are powered by RxJS under the hood.
Second, with this being said, if you want to get into the JSON part you need to subscribe to the Observable i.e listen to the values that will be broadcasted by the Observer.
You have two ways to do that;
1 - As per Andrew description by assinging the JSON to the property within the subscription.
2- You keep the property assigned an Observable and use async pipe in your template.
Now, why you are getting undefined ? because during initialization of the component your asynchronous data has not been assinged yet to your property. Here, you have multiple options, including using a different lifecycle hook such as ngAfterViewInit or use a guard condition to check
if(res) { //stuff}
In all cases, you need a small refactoring for the unconventional way of handling Http calls as in your code snippets above.
You need to subscribe to the observable.
getData(url: any) {
this.http.get(url).subscribe(result => { this.res = result; });
}
In my Angular service I am providing a public Observable called usageDoc$.
service.ts:
usageDoc$: Observable<IUsage>;
initializeUsageDoc() {
//gets called in app.component.ts on app load
...
//some async db querying
this.usageDoc$ = this.firestore.getUsageDoc(user.uid); //getUsageDoc(..) returns Observable<IUsage>
}
component.ts
localDoc: any;
ngOnInit() {
this.service.usageDoc$.subscribe(doc=>this.localDoc=doc);
}
This leads to the following error: cannot read property subscribe of undefined... as usageDoc$ is not yet set on the component init. Currently I am using a workaround by creating a second observable usageDocReady$ = new Subject<boolean> in service that emits as soon as usageDoc$ is set.
Is there better to solve this issue? Can I somehow initialize usageDoc$ with a default value?
I know I would not have this issue if I'd subscribe in template using the async pipe, but I need a normal variable for displaying matters, hence the use of localDoc.
I'd suggest using a ReplaySubject with buffer 1. It'd ensure future subscribers would get the last emission whilst avoiding any undefined errors.
private usageDocSrc = new ReplaySubject<IUsage>(1);
public usageDoc$ = this.usageDocSrc.asObservable();
initializeUsageDoc() {
//gets called in app.component.ts on app load
...
//some async db querying
this.firestore.getUsageDoc(user.uid).subscribe({
next: value => this.usageDocSrc.next(value)
});
}
If you don't wish to use a ReplaySubject, then I don't understand the need for the Observable. You could directly return the observable from getUsageDoc() function.
initializeUsageDoc(): Observable<IUsage> {
//gets called in app.component.ts on app load
...
//some async db querying
return this.firestore.getUsageDoc(user.uid);
}
Although this would invoke all the statements prior to getUsageDoc() for each subscription to initializeUsageDoc().
I want to retrieve data from api and assign it to some value inside the angular component. In subscribe I'm trying to assign the data to loggedUser and then call function inside this subscribe to navigate to another component with this received object. Unfortunately I got the error : The requested path contains undefined segment at index 1. I want to have this object set outside the subscribe too. How can I achieve this?
logIn() {
this.portfolioAppService.logIn(this.loggingUser).subscribe((data) => {
this.loggedUser = data;
console.log(this.loggedUser);
console.log(data);
this.navigateToProfile(this.loggedUser.Id);
});
}
navigateToProfile(id: number) {
this.router.navigate(['/profile', id]);
}
console output
You are using an incorrectly named property when calling navigateToProfile.
From your console output, I can see that the data object in the subscribe looks like this:
{
id: 35,
// ..
}
But you are calling the function like this:
this.navigateToProfile(this.loggedUser.Id);
Instead, use the property id (lower case)
this.navigateToProfile(this.loggedUser.id);
To narrow this problem down in the future, try being more specific in your testing. Humans are good at seeing what they want to see and will assume the problem is more complicated than it is. If you had tried console.log(this.loggedUser.Id), you would have seen the result undefined, and worked out the problem yourself.
I've wasted about 2hrs on this stupid thing. All I see when I log things is a DocumentSnapshot object and nothing every shows up in my template view. What am I doing wrong? Should I just be passing a whole document to my component rather than trying to get single document based on an ID?
//component.ts
constructor(db: AngularFirestore) {
this.contactCollection = db.collection('contacts');
this.contact = this.contactCollection.doc('YYlKh7Gg7khN8L636iYE')
}
//viewTemplate.ts
{{this.contact | async}}
You are missing the .valueChanges() function. Added it below, now you will get the entire document back in the viewTemplate.ts and you can call any of the fields on it.
//component.ts
constructor(db: AngularFirestore) {
this.contactCollection = db.collection('contacts');
this.contact = this.contactCollection.doc('YYlKh7Gg7khN8L636iYE').valueChanges();
}
//viewTemplate.ts
{{this.contact | async}}
.valueChanges();
Here is a formal answer to my comment:
contact is a Promise which must be unwrapped to get the underlying value. In the template, we need a way to convey this:
AsyncPipe
Safe Navigation Operator ? (ensures we don't try to access the unwrapped values untimely causing a runtime error)
Putting it all together in the template:
{{(contact | async)?.some_prop}}
This appropriately waits to access some_prop (from the Promise return value) after unwrapping contact.
I'm trying to figure out an rxjs way of doing the following:
You have two observables, one onAddObs and onRemoveObs.
let's say onAddObs.next() fires a few times, adding "A", "B", "C".
I would like to then get ["A", "B", "C"].
.toArray requires the observable be completed...yet more could come.
That's the first part. The second part is probably obvious...
I want onRemoveObs to then remove from the final resulting array.
I don't have a plunkr cuz I can't get anything close to doing this...
Thanks in advance!
UPDATE
Based on user3743222's advice, I checked out .scan, which did the job!
If anyone else has trouble with this, I've included an angular2 service which shows a nice way of doing this. The trick is to use .scan and instead of streams of what was added/removed, have streams of functions to add/remove, so you can call them from scan and pass the state.
#Injectable()
export class MyService {
public items: Observable<any>;
private operationStream: Subject<any>;
constructor() {
this.operationStream = Subject.create();
this.items = this.operationStream
// This may look strange, but if you don't start with a function, scan will not run....so we seed it with an operation that does nothing.
.startWith(items => items)
// For every operation that comes down the line, invoke it and pass it the state, and get the new state.
.scan((state, operation:Function) => operation(state), [])
.publishReplay(1).refCount();
this.items.subscribe(x => {
console.log('ITEMS CHANGED TO:', x);
})
}
public add(itemToAdd) {
// create a function which takes state as param, returns new state with itemToAdd appended
let fn = items => items.concat(itemToAdd);
this.operationStream.next(fn);
}
public remove(itemToRemove) {
// create a function which takes state as param, returns new array with itemToRemove filtered out
let fn = items => items.filter(item => item !== itemToRemove);
this.operationStream.next(fn);
}
}
You can refer to the SO question here : How to manage state without using Subject or imperative manipulation in a simple RxJS example?. It deals with the same issue as yours, i.e. two streams to perform operations on an object.
One technique among other, is to use the scan operator and a stream of operations which operates on the state kept in the scan but anyways go have a look at the links, it is very formative. This should allow you to make up some code. If that code does not work the way you want, you can come back and ask a question again here with your sample code.