I'm using forkJoin to turn several observables into one which I then map over to transform into some view model that I use for my view in my component.
My model is as follows:
export interface MyModel {
itemOne: Observable<ItemOne[]>;
itemTwo: Observable<ItemTwo[]>;
itemThree: Observable<ItemThree[]>;
}
My forkJoin looks like:
this._subscriptions.push(this.myService.getData()
.flatMap(data => Observable.forkJoin(data
.map(data => {
let myModel: MyModel = {
itemOne: this.myService.getSomeData(),
itemTwo: this.myService.getSomeData(),
itemThree: this.myService.getSomeData()
};
return Observable.forkJoin(Observable.from([myModel]));
}))).subscribe(details => {
details.map(this.toMyModelViewModel.bind(this));
}));
Now, in my toMyModelViewModel method, I'd like to pick each value from these Observables and was wondering how I could accomplish this?
toMyModelViewModel(value: MyModel): any {
return {
itemOne: value.itemOne,
itemTwo: value.itemTwo,
itemThree: value.itemThree
};
}
Do I have to subscribe to each value to get the current value or is there another, possible better/cleaner way of doing this? Ultimately, I'd like to have an array of MyModel objects that way I don't have to worry about using data | async in my view.
Thanks
From a quick scan of your code, it seems that you have an initial api call (this.myService.getData()) from which you will construct your result, but that result is based on combining the results of several api calls (this.myService.getSomeData()).
I feel like you need the MyModel interface to have no Observables:
export interface MyModel {
itemOne: ItemOne[];
itemTwo: ItemTwo[];
itemThree: ItemThree[];
}
... toMyViewModel is a factory method:
function toMyModelViewModel(itemOne: any, itemTwo: any, itemThree: any): MyModel {
return {
itemOne: itemOne as ItemOne[],
itemTwo: itemTwo as ItemTwo[],
itemThree: itemThree as ItemThree[]
} as MyModel;
}
... then you'd use switchMap/forkJoin as follows:
this.myService.getData().switchMap(data => {
return Observable.forkJoin(
this.myService.getSomeData(),
this.myService.getSomeData(),
this.myService.getSomeData(),
toMyModelViewModel
}).subscribe(...);
You can convert the array of observables to an observable of an array, using whichever method you like -- the one that Miller shows in his answer will work. I personally like the combineLatest trick (quoting from redent84's answer to this question: How do I create an observable of an array from an array of observables?).
let arrayOfObservables: [Observable<E>] = ...
let wholeSequence: Observable<[E]> = Observable.combineLatest(arrayOfObservables)
Now, for your real question:
Do I have to subscribe to each value to get the current value or is there another, possible better/cleaner way of doing this? Ultimately, I'd like to have an array of MyModel objects that way I don't have to worry about using data | async in my view.
Unfortunately, the only way to get an Array<any> out of an Observable<Array<any>> is to subscribe to that observable and save the inner value to an array defined elsewhere, or (if you're using Angular as you are), to use the async pipe.
You can think of it this way: once an Observable, always an Observable. If the value is wrapped up in an Observable, you cannot get it out. The closest thing to this behavior might be using BehaviorSubject with the getValue method, but it's definitely not an option for Observables. This is a good thing because it prevents you from trying to use an asynchronous value "synchronously" in your program.
I also feel like I should mention that the async pipe is a really useful utility and should be preferred to manually subscribing in general. When you manually create a subscription in your component class, you will need to remember to save that subscription and clean it up in the ngOnDestroy lifecycle hook. The async pipe is great because it does all of this cleanup for you. Plus, you can use it with the safe navigation operator to handle values that are possible undefined.
<ul>
<li *ngFor="let obj of (possiblyUndefined$ | async)?.objects">
{{ obj.title }}
</li>
</ul>
That code would be a lot more complicated if you wrote it all in your component class.
To summarize:
1) You can use Observable.combineLatest(arrayOfObservables) to squish your array of observables into an observable of an array.
2) You cannot get an array "out of" an observable. You must either subscribe to that observable or use the async pipe.
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; });
}
Good afternoon,
I am struggling with placing functions in ngOnInit. If I understand what is going wrong correctly, the problem appears to be that when I place asynchronous functions in ngOnInit and the next function depends on the complete execution of the first, if the first has not been completed, then the data I require is still undefined and it will fail.
What I want to do is quite simple, really. ngOnInit populates two arrays by querying my API. One is groups associated with a user, and another is all groups (these come from a DB). It will then populate a third array by comparing these two (allgroups - groupsassociated = groupsavailable).
After this initial populating, I want to be able to do further comparisons between the arrays depending on user interaction. These functions work, but about 10% of the time I will receive an undefined problem which can be cleared up by refreshing the page (sometimes two or three times). This suggests to me that it's a problem of order of operations.
How can I move forward here? Can I make these function calls synchronous? Can I chain them together somehow?
I appreciate any help.
Relevant code below:
NGONINIT AND THREE FUNCTIONS:
ngOnInit(): void {
this.populatePickListUserAssociatedGroups();
this.populateOriginalUserAssociatedGroups();
this.populateAvailableGroups();
}
// METHODS
private populatePickListUserAssociatedGroups = () => {
this.userDataService.getSingleUserAssociatedGroups()
.then(groups => this.pickListUserAssociatedGroups = groups)
.then(groups1 => this.originalUserAssociatedGroups = groups1);
}
private populateOriginalUserAssociatedGroups = () => {
this.userDataService.getSingleUserAssociatedGroups().then(groups => this.originalUserAssociatedGroups);
}
private populateAvailableGroups = () => {
this.userDataService.getAllGroups()
.then(groups => this.allGroups = groups)
.then(groups => groups.filter(i1 => !this.originalUserAssociatedGroups
.some(i2 => i1.id === i2.id)))
.then(groups => this.availableGroups = groups);
}
I have described my main problem, but additionally I have another. For some reason the two arrays: pickListUserAssociatedGroups and originalUserAssociatedGroups always change together. This defeats the purpose. The pick list array represents what the user changes (I'm working with ngPrime pick list) and originalUserAssociatedGroups is supposed to be unchanged and represents what is in the DB so I can later make comparisons between the two.
Hopefully this is clear.
Thanks in advance.
You could achieve that by using rxjs. There is multiple solution to your question but I think the best is to use a forkJoin. Here is how you could achieve that :
ngOnInit() {
forkJoin(
this.populatePickListUserAssociatedGroups(),
this.populateOriginalUserAssociatedGroups(),
).subscribe(finalResult => {
// do your things with finalResult[0] and finalResult[1]
this.populateAvailableGroups();
});
}
populatePickListUserAssociatedGroups(): Observable<any> {
return this._myService.getPickListUserAssociatedGroups();
}
populateOriginalUserAssociatedGroups(): Observable<any> {
return this._myService.getOriginalUserAssociatedGroups();
}
populateAvailableGroups() {
// your code
}
Note that i use observable, not promise. You might need to add a value check in the subscribe to be sure you have the value you need, but the documentation says :
This operator is best used when you have a group of observables and only care about the final emitted value of each.
If your html depends on the data your methods retrieve, you'll have to adapt it, by adding a loader for example, to wait for the subscribe to end since it is asynchronous.
Finally, your service method should return an observable, and you can do it by simply retunring that response :
myServiceMethod(): Observable<any> {
return this._http.get<Whatever>(url);
}
For your other problem, if the changes of the pick list array is made locally in your constructor then you just have to store it in a variable in the forkJoin subscription : this.mypickListArray = finalResult[0] and just work with it.
In case anyone sees the above answer from #Quentin, I thought I would post the code that I ultimately used which worked as intended and to which I am thankful for Quentin's help.
ngOnInit(): void {
forkJoin(
[
this.populateAllGroups(),
this.populateAllUserGroup()
]
)
.subscribe(([x, y]) => {
this.allGroups = x;
this.allUserGroupByUser = y;
this.populateAvailableGroups();
this.populatePickListUserAssociatedGroups();
});
}
Where, for example, the first method looks like this:
private populateAllGroups(): Observable<any> {
return this.userDataService.getAllGroups();
}
And the data service has this method:
public getAllGroups(): Observable<any> {
return this.httpClient.get<Group[]>(`${Configuration.userApiURL}/getallgroups`);
}
The secondary methods are not relevant as they just manipulate the data provided.
The objective behind using BehaviouSubject was to use a single API call and pass the same data to multiple components in the same route.
I am able to do that. I am not able to filter the received data
Heres a stackblitz fiddle that i have created
https://stackblitz.com/edit/angular-xtne5y
In one component, i am displaying the table, in the other, i need to extract some info out of it based on the individual object key values. Like how many todos are complete / incomplete.
Since I am required to use the async pipe everywhere in the template, performing operations like filter are not possible.
Is there a better way to implement this?
I need to keep the data extracted as reusable
You're currently using the async pipe. To get the desired result, you can use (or chain) another custom pipe with your data to fetch specific properties.
I've forked your stackblitz example and modified the code with the solution.
Here's my working solution.
Essentially, all you need to do is use a custom pipe.
{{ todos$ | async | myCustomFilter }}
In my example (stackblitz), I'm doing:
<p>
No. of Completed Todos: {{ (todos$ | async | filterByCondition: {property: 'completed', value: true}).length }}
</p>
<p>
No. of Incomplete Todos: {{ (todos$ | async | filterByCondition: {property: 'completed', value: false}).length }}
</p>
Edit after your comments
There are two approaches to your desired result:
1) Use a custom pipe. You can parameterize the pipe the same as I've done or even create your own conditions and evaluate by passing a parameter to your pipe (as I've done in the example for the args property).
I.e
<div>{{$todos | async | filterTodo: 'byDate'}}</div>
Now, you can put the handler in your pipe filterTodo for this byDate value.
2) Use different observables for different data.
class MyComponent {
todos$;
completedTodos$;
constructor() {
this.todos$ = this.someService.getTodos(); // from behavior subject
this.completedTodos$ = this.todos$.pipe(
filter((item) => {
// your filter code here
})
)
}
}
So I've have worked on projects where when we create TODO's section, My approach would be when you subscribe to the URL to fetch the data, create a Redux Store
(Here's [a link] https://medium.com/supercharges-mobile-product-guide/angular-redux-the-lesson-weve-learned-for-you-93bc94391958), which has an interface like
interface ProductTodos {
product: {title:string,iscompleted:boolean,id:string/number},
todos?: any[]
}
When you fetch your Todos, you will dispatch an action like "PUSH" and every TODO will be an object of type product and will appended on to the todos array in the interface.
when you create an instance of the redux Store in the Component Class you will loop through the "todos" array and check for the id and isCompleted flag.
Based on this you can loop through each object and fill your tables based on their completed status.
Here is a link to my ReduxDemo in Angular 4, check it out https://github.com/nerdySingh/ReduxDemoAngular
First, I'd like to thank the community for all the support learners like me get while working with new technologies.
I've been using Angular for a while now, and there's something I still don't quite understand nor have I seen asked elsewhere.
Supposing I have a service that returns an Observable with the data I need, how should I use this data properly, in terms of performance?
I know I can use the async pipe and avoid having to sub/unsub, but this only happens in the template. What if I needed to use the same data in the component as well? Wouldn't subscribing again (from the template with the async pipe and from the component with .subscribe())?
How do I keep the observable up to date? For example, I have a table displaying data from an API. If I click on the 2nd page (pagination), I'd like to make another call and have the observable updated.
I'm sorry if this has been asked before, I personally couldn't find if on Stackoverflow. Thanks for your attention!
If you need the data in the component as well, you can just subscribe to it. BUT maybe you should not (see below)...
it's there that you use the operators, you can combine observables to define a custom data flow:
foo$: Observable < Foo[] > ;
randomClickEvent = new Subject < clickEvent > ();
ngOnInit() {
let initialFetch = this.fooService.getData().share()
this.foo$ = Observable.merge(
initialFetch, // need the initial data
initialFetch.flatMap(foos => {
this.randomClickEvent.switchMap(() => { //listen to click events
return this.fooService.getMore().map((moreFoos: Foo[]) => { //load more foos
return foos.concat(...moreFoos) //initial foos values + new ones
})
})
})
);
}
<span *ngFor="let foo of (foo$|async)">{{foo.name}}</span>
<button (click)="randomClickEvent.next($event)">Load More foos !</button>
Most of people just use simple operators like map(),do(), etc and manage their subscription imperatively, but it is usually better to not subscribe, so you avoid many side effects and some "Ooops I forgot to unsubscribe here". usually you can do everything you need without subscribing.
Observables exist to describe a data flow, nothing more, nothing less. It's a paradigm of functional programming: you don't define how things are done, but what they are. Here, this.foo$ is a combination of the initial fooService.getData() and every fooService.fetchMore() that may occur.
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.