How to push to Observable of Array in Angular 4? RxJS - javascript

I have a property on my service class as so:
articles: Observable<Article[]>;
It is populated by a getArticles() function using the standard http.get().map() solution.
How can I manually push a new article in to this array; One that is not yet persisted and so not part of the http get?
My scenario is, you create a new Article, and before it is saved I would like the Article[] array to have this new one pushed to it so it shows up in my list of articles.
Further more, This service is shared between 2 components, If component A consumes the service using ng OnInit() and binds the result to a repeating section *ngFor, will updating the service array from component B simultaneously update the results in components A's ngFor section? Or must I update the view manually?
Many Thanks,
Simon

As you said in comments, I'd use a Subject.
The advantage of keeping articles observable rather than storing as an array is that http takes time, so you can subscribe and wait for results. Plus both components get any updates.
// Mock http
const http = {
get: (url) => Rx.Observable.of(['article1', 'article2'])
}
const articles = new Rx.Subject();
const fetch = () => {
return http.get('myUrl').map(x => x).do(data => articles.next(data))
}
const add = (article) => {
articles.take(1).subscribe(current => {
current.push(article);
articles.next(current);
})
}
// Subscribe to
articles.subscribe(console.log)
// Action
fetch().subscribe(
add('article3')
)
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/5.5.2/Rx.js"></script>

Instead of storing the whole observable, you probably want to just store the article array, like
articles: Article[]
fetch() {
this.get(url).map(...).subscribe(articles => this.articles)
}
Then you can manipulate the articles list using standard array manipulation methods.
If you store the observable, it will re-run the http call every time you subscribe to it (or render it using | async) which is definitely not what you want.
But for the sake of completeness: if you do have an Observable of an array you want to add items to, you could use the map operator on it to add a specified item to it, e.g.
observable.map(previousArray => previousArray.concat(itemtToBeAdded))

ex from angular 4 book ng-book
Subject<Array<String>> example = new Subject<Array<String>>();
push(newvalue:String):void
{
example.next((currentarray:String[]) : String[] => {
return currentarray.concat(newValue);
})
}
what the following says in example.next is take the current array value Stored in the observable and concat a new value onto it and emit the new array value to subscribers. It is a lambda expression.I think this only works with subject observables because they hold unto the last value stored in their method subject.getValue();

Related

Modify data in stream used with angular async pipe reactive way

iam trying to use async pipes instead of manually subscribing in components, in case, iam just displaying data which i get from server, everything works fine. But what if i need to change displayed part of displayed data later, based on some event?
For example, i have a component which displays timetable, which can be later accepted by user. I create timesheet$ observable and i use it in my template with async pipe. For accepting i created subject, so i can emit, when user accepts the timesheet. But how do i combine these two streams(approvementChange$ and timesheet$), so the $timesheet gets updated? I was trying combineLatest, but it returns the latest values, so i cant decide, if the value from approvementChange stream is new or old. Any ideas how to solve this?
export class TimesheetComponent {
errorMessage: string = '';
timesheet$: Observable<Timesheet>;
private approvementSubject = new Subject<TimesheetApprovement>();
approvementChange$ = this.approvementSubject.asObservable();
constructor(
private planService: PlanService,
private statusService: StatusService,
private notifService: NotificationService
) {}
this.timesheet$ = this.statusService.appStatus.pipe(
switchMap((status) => {
return this.planService.getTimesheet(
status.selectedMonth,
status.selectedAgency.agNum
);
})
); //get data every time user changed selected month
approveTimesheet(isApproved: boolean = true) {
const approvement: TimesheetApprovement = {
isApproved: isApproved,
approveTs: new Date(),
isLock: true,
};
this.approvementSubject.next(approvement);
}
}
But what if i need to change displayed part of displayed data later, based on some event?
RxJS provides lots of operators for combining, filtering, and transforming observables.
You can use scan to maintain a "state" by providing a function that receives the previous state and the newest emission.
Let's look at a simplified example using a generic Item interface:
interface Item {
id : number;
name : string;
isComplete : boolean;
}
We will use 3 different observables:
initialItemState$ - represents initial state of item after being fetched
itemChanges$ - represents modifications to our item
item$ - emits state of our item each time a change is applied to it
private itemChanges$ = new Subject<Partial<Item>>();
private initialItemState$ = this.itemId$.pipe(
switchMap(id => this.getItem(id))
);
public item$ = this.initialItemState$.pipe(
switchMap(initialItemState => this.itemChanges$.pipe(
startWith(initialItemState),
scan((item, changes) => ({...item, ...changes}), {} as Item),
))
);
You can see we define item$ by piping the initialItemState$ to the itemChanges$ observable. We use startWith to emit the the initialItemState into the changes stream.
All the magic happens inside the scan, but the set up is really simple. We simply provide a function that accepts the previous state and the new change and returns the updated state of the item. (In this case, I'm just naively apply the changes to the previous state; it's possible this logic would need to be more sophisticated for your case.)
This solution is completely reactive. The end result is a clean item$ observable that will emit the updated state of the current item (based on id), whenever the id changes or the changes occur on the item.
Here's a StackBlitz where you can see this behavior.
you need to track only approvementChange
after that you can pick the latest timesheet via withLatestFrom
approvementChange$
.pipe(withLatestFrom(this.timesheet$))
.subscribe(([accepted, latestTimesheet]) => accepted ? save(latestTimesheet) : void 0)

variable inside Observable subscription gets empty value

I know that Observables take some time to get data while javascript keeps running the others codes and that is troubling me a lot.
I have used ngrx in my angular project. Here, I am trying to fetch some data from the store which is working fine. Then, I convert this data stream into string[] which is also working fine.
To use this string[] me subscribeto this observable. And inside subscription I try to assign the value to other values named filterSizeValues.
Here, the problem comes. If I console.logthis filterSizeValuesinitially I got and empty array. When the observable finishes his job filterSizeValues variable is filled with data.
But I can not effort filterSizeValues variable to be empty array initially. What can I do?
I have already searched the solution in the internet but nothing is working out.
Help me out please. And Many Many Thanks in advance.
Here is my code;
this.sizeTargetingStore$.dispatch(SizeTargetingActions.getSizeTargeting({
campaignId: this.campaignId,
lineItemId: this.lineItemId
}));
Here I am accessing the store to get data.
this.sizeTargeting$
.pipe(switchMap(sizes=>{
let temporary:string[] = [];
sizes.forEach(eachSize=>{
temporary.push(eachSize.name);
})
this.filterSizeValues$ = of(temporary);
return this.filterSizeValues$;
}))
.subscribe(size_name=>{
this.filters.set('size_name', size_name);
})
Here, I am trying to set the filter values.
I also tried this way also.
this.sizeTargeting$
.pipe(switchMap(sizes=>{
let temporary:string[] = [];
sizes.forEach(eachSize=>{
temporary.push(eachSize.name);
})
this.filterSizeValues$ = of(temporary);
return this.filterSizeValues$;
}))
.subscribe(size_name=>{
this.filterSizeValues = size_name
})
this.filters.set('size_name', this.filterSizeValues);
But all ways filters set to an empty array.
Anyone can help me out please?
From my understanding, you have 2 possibilities, either filter out the empty values or skip the first value. You can do so with the filter and skip rxjs operator respectively.
Also I believe that you are misusing the switchMap operator, since you are not using asynchronous operations within your switchMap we can use the map operator instead, so below I have a simplified version of your code with your 2 options to fix your problem.
Option 1:
this.sizeTargeting$.pipe(
filter(sizes => sizes.length > 0), // filter out empty array values
map(sizes => sizes.map(size => size.name)) // perform your remap
).subscribe(sizes => {
this.filterSizeValues = size_name; // Only arrays with values will reach this step
});
Option 2:
this.sizeTargeting$.pipe(
skip(1), // skip the first value
map(sizes => sizes.map(size => size.name)) // perform your remap
).subscribe(sizes => {
this.filterSizeValues = size_name; // Only arrays with values will reach this step
});
Normally when I subscribe to something that I am waiting on to return what I do is I set up a Subject:
private componentDestroyed$ = new Subject<void>();
then in the Observable piping and subscription I do it as:
this.sizeTargeting$
.pipe(takeUntil(this.componentDestroyed$))
.subscribe((sizes: YourTypeHere[]) => {
if(sizes) {
//Do what I need to do with my sizes here, populate what I need,
//dispatch any other actions needed.
}
})

how to handle same value from multiple datastreams in Angular RxJs

I have two different data streams which gives same object with modified property values. I want to write a single subscription so that whenever any of the two DataStream notifies about property modification I can reuse the same code.
const selectedItems$ = this.grid.onSelect.pipe(.... this gives me the selected object);
const updatedItem$ = this.fetchData.onUpdate.pipe(.....this gives me the updated object);
displayItem$(selection$, updatedItem$) {
(..here i want to get notified whenever there is change in).subscribe(item => {
this.displayLatest(item)
})
}
selectedItem$ and updatedItem$ can return same object with different property values when the already selected item is modified.
This is the first time I am working on RxJs and bit confused about this part. I searched in RxJS operators list (like concat, merge, combine) but most of the working examples are for different data structures. Also is there any other better way to achieve this?
You can use merge to create an observable that emits values from both source streams:
import { merge } from 'rxjs';
...
merge(selection$, updatedItem$)
.subscribe(item => {
this.displayLatest(item)
})

Angular: Ensure Services is Complete before Running Next Step

We are currently using Angular.
Component is receiving data from API. After getting API Data, it goes through Data Services which transform and customize the data, concatenate First Last Name, rounds dollar amounts, makes calculations, etc.
The last step tries to populate the Sales year in a Dropdown, after parsing all the data.
this.webStoreSearchHttpService.GetAllCustomerSalesData(this.customerId).subscribe((response) => {
this.customerList= customerDataService.createCustomerList(response);
this.productList = customerDataService.createProductAnalysis(response);
this.salesList= customerDataService.createSalesList(response);
this.salesYearList= customerDataService.createYearList(response);
this.salesYearItemCurrent = _.cloneDeep(this.salesYearList[0]); <--- this goes into a Mat Select Dropdown
However, correlating data does not appear after selecting web dropdown, because the Data Services is not finished parsing/created yet, even though its in original API subscribe.
What I am trying to do, is make sure all 4 Data services are totally complete, and Then populate salesYear. How can this be done with Angular typescript ?
The data services can be run in Parallel, however last step is salesYear population in dropdown.
The methods return class arrays, not promises or observables.
Update
You added the sentece The methods return class arrays, not promises or observables.. This implies that you have no possibility from outside to wait for asynchroneous calls to finish. Hence you have to change the return value of the customerDataService methods. I am assuming that inside this methods some asynchroneous stuff is done, because you say What I am trying to do, is make sure all 4 Data services are totally complete.
Old version
To answer your question one have to know what the customerDataService methods return type is. Do the method return Promise or Observable? Depending on that you can use Promise.all or forkJoin operator to wait for all methods to finish and then execute the select population. This is an example using observables:
this.webStoreSearchHttpService.GetAllCustomerSalesData(this.customerId).subscribe(response => {
forkJoin([
customerDataService.createCustomerList(response),
customerDataService.createProductAnalysis(response),
customerDataService.createSalesList(response),
customerDataService.createYearList(response)
]).subscribe(([customerList, productList, salesList, salesYearList]) => {
this.customerList = customerList;
this.productList = productList;
this.salesList = salesList;
this.salesYearList = salesYearList;
this.salesYearItemCurrent = _.cloneDeep(this.salesYearList[0]);
});
});
or even better to avoid the inner subscription and has only one subscription:
this.webStoreSearchHttpService.GetAllCustomerSalesData(this.customerId).pipe(
flatMap(response =>
forkJoin([
customerDataService.createCustomerList(response),
customerDataService.createProductAnalysis(response),
customerDataService.createSalesList(response),
customerDataService.createYearList(response)
])
)
).subscribe(([customerList, productList, salesList, salesYearList]) => {
this.customerList = customerList;
this.productList = productList;
this.salesList = salesList;
this.salesYearList = salesYearList;
this.salesYearItemCurrent = _.cloneDeep(this.salesYearList[0]);
});

Wait for Observable to complete in order to submit a form

I have a 'new trip' form, where the user can write the names of the participants and then submit the form to create the trip.
On submit, I query a Firebase database with the names, in order to get the IDs of the participants (/users). I then add the IDs to the participantsID field of the trip object and then I push the new trip to Firebase.
The problem is that the Firebase query is async and returns an Observable, therefore my function will proceed to push the object before the Observable has completed, so the participantsID field of the new object is empty.
Is there any method to wait for the observable to complete (in a kind of synchronous way) so that i can manipulate the data and then proceed? All my attempts to fix this have failed so far.
Here's my simple code.
getUserByAttribute(attribute, value) {
return this.db.list('/users', {
query: {
orderByChild: attribute,
equalTo: value,
limitToFirst: 1
}
});
}
createTrip(trip) {
for(let name in participantsName.split(',')) {
getUserByAttribute('username', name)
.subscribe( user => trip.participantsID.push(user[0].$key) );
}
this.db.list('/trips').push(trip);
}
You could treat all Observables into a single Observable by doing forkJoin
createTrip(trip) {
var observableArray: any = participantsName.split(',')
.switchMap((name)=> getUserByAttribute('username', name))
Observable.forkJoin(observableArray).subscribe(
trips => trips.forEach((trip) => {
this.db.list('/trips').push(trip);
})
);
}
In the end I used part of #Pankaj Parkar's answer to solve the problem.
I forkJoin all the Observables returned by mapping the splitted names and I subscribe to that Observable which result contains an array of arrays, where the inner arrays contain a user object.
getUserByAttribute(attribute, value) {
return this.db.list('/users', {
query: {
orderByChild: attribute,
equalTo: value,
limitToFirst: 1
}
}).first();
}
createTrip(trip) {
Observable.forkJoin(
trip.participantsName.split(',')
.map(name => getUserByAttribute('name', name))
).subscribe(
participants => {
trip.participants = participants.map( p => p[0].$key);
this.tripService.createTrip(trip);
}
);
}
}
You have a difficult problem. You have to get users info before push a new trip.
You can't just make new subscriptions every time because of the memory leak problem (or be careful with unsubscribes). If you are using Firebase, you can use AngularFire subject support.
You can update a subscription by using a subject in your query (with the equal to) and then push a user to retrieve with .next(user).
Then you still have to wait for all users. For that, you can have only one subscription and get all IDs synchronously or have multiple subscriptions to get multiple results faster (but it's difficult).
To solve this problem, I created:
a queue of callbacks (just arrays but use push() and unshift() methods)
a queue of values
one subject for one subscription.
If you want an ID, you have to:
push the value
push the callback that will retrieve the value returned.
You should use functions to push because you'll have to call .next() if there is no value in the stack (to start !).
And in your subscription, in its callback, i.e when you receive the distant user object, you can call the first callback in the stack. Don't forget to pop your value and callback of the stacks and call the next() for the next value if there is one.
This way, you can push your trip in the last callback for the last user. And it's all callbacks, it means your app is not interrupted.
I still not decided if we should do that in a cloud function. Because the user have to stay connected, and this use his data / processor. But it's good to have all the code in the same place, and cloud functions are limited for a free version of Firebase. What would a Firebase developer advice?
I made a lot of searches to find a better solution, so please share it if you have one. It's a little complicated I think, but it's working very fine. I had the same problem when a user want to add a new flight, I need to get the airports information before (coords) and push multiple objects (details, maps, etc.)

Categories

Resources