Share and update Observable data in Angular 2 - javascript

I have 2 components - object-list and search-filter in my app. Also I have a fetch-service with getObjects(page, filter) method. I want to implement a scenario where object-list fetches all data via getObjects method and then search-filter will apply some filters to getObjects and object-list should automatically update. Here is some code:
FetchService
objects: Observable<any>;
getObjects(page: number, filter): void {
let headers = new Headers();
headers.append('Content-Type', 'application/json');
const offset = (page - 1)* this.pageSize;
let url = this.objectsBaseUrl;
//apply filter
if (filter) {
url = `${url}?filter=${JSON.stringify(filter)}`
}
this.objects = this.http.get(url)
.map((response: Response) => {
return response.json() || {}
})
.catch(this.handleError);
}
ObjectListComponent
constructor(private fetch: FetchService) {}
ngOnInit(): void {
this.fetch.getObjects(this.initialPage);
this.objects = this.fetch.objects; // I need an Observable object to use with async pipe with *ngFor
}
getPage(page: number) {
this.fetch.getObjects(page); //here this.objects variable probably should be update because this methods replaces this.fetch.objects
}
SearchFilter
constructor(private fetch: FetchService) {}
apply() {
//some filter calculations
this.fetch.getObjects(1, this.filter);
}
I don't use subscribe to objects because I put it to async pipe in *ngFor. But as far as I'm concerned async pipe uses subscribe inside. The problem is that the list updates only once when ngOnInit fires. What's going wrong?

The SearchFilter component should NOT be in charge of fetching the data.
Here's how I would do it:
1) Create 3 components
A parent component, aka "smart component", to handle all the logic of listening to filter changes and fetching the data with FetchService. The parent component has two children displayed in its template:
First child: SearchFilterComponent is a "dumb component" (or presentational component). Its only job is to emit an #Output event every time the filters change, passing the latest filter values to its parent.
Second child: ObjectListComponent is also a "dumb component". Its only job is to display the list of objects provided by its parent component via an #Input property.
2) Implement the following workflow
When filters change in SearchFilterComponent, emit an #Output event passing the latest filter values, e.g. this.filtersChanged.emit(filters).
In the parent component, listen to filter changes with <search-filter (filtersChanged)="getObjects($event)">. The getObjects(filters) method will re-fetch the objects on every filter change; it should store the fetched objects in a property of the parent component, let's call it results.
Then, the parent component passes results to its child component ObjectListComponent via an #Input property called objects: <object-list [objects]="results">. The object list should refresh automatically every time the input changes.
Hopefully my explanations will make sense. Read up on #Input and #Output on the angular.io site if you're not familiar with those. I don't have time to create a Plunkr, so please post questions if anythings seems unclear.

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)

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

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();

Subcription outside OnInit

I have two child components and i want pass data between them, to do this i use service.
In service i have
headerObject = new Subject<HeaderObject>();
In first component i have method
onClick() {
this.listImage = this.imageService.getImages();
let headers = new HeaderObject();
headers.file = this.listImage[0].data;
this.documentService.getOcrDocument(headers).subscribe(res => {
headers = res;
this.ocrService.headerObject.next(headers);
})
In second component
headerSubcribed: HeaderObject;
private headerSubscription: Subscription;
ngOnInit() {
this.headerSubscription =
this.ocrService.headerObject.subscribe((ocrHeader: HeaderObject) => {
this.headerSubcribed = ocrHeader;
}
But ngOnInit() only once after render view i want to move this code to outside ngOnit and exeute it but i don't know how ? Some tips how execute that code? I set breakpoint at next() method that emit value but later method subscribe isn't execute? What could I do wrong?
There are 2 parts which you observable/subscribe
Observing something and notifying.
In your service, you have a Subject type variable and you are pushing values to it by calling next(). when this method called, it notifies all the observers means who ever is subscribing it.
listening to the observer and do action
When you listen to an observer, you get an update when they change the value. It does not matter if you put your subscription in onNginit or constructor, if the code is there, it will notify you when there is a change into it.
You can also explore BevavourSubject if it fits to your situation.

ReplaySubject that doesn't complete on unsubscribe

I have a parent component in Angular which creates a ReplaySubject which is sourced from an API call. This changes over time as filters get changed and new calls are made for new data. This Subject is passed to child components so they can work on the data and stay updated if the filters change and new data is emitted.
// ParentComponent
public tasksList = new ReplaySubject(1);
queryParams.switchMap(params => apiCall(params)).subscribe(tasks => tasksList.next(tasks));
// ParentComponent template
<task-list [tasks$]="tasksList"></task-list>
// TaskListComponent
tasksSub = tasks$.subscribe();
ngOnDestroy() { tasksSub.unsubscribe(); }
The problem I have is that when one of the children components is destroyed and unsubscribes from the Subject it completes the Subject and new components cannot subscribe. How can I share the Subject and maintain replayability while also being able to destroy child components?

RxJS Register event sources and buffer events dynamically

I'm currently trying to get a Register/Subscribe system to work with RxJs.
The situation is that I have component A with several sub components A1, A2, A3, ... The amount has to be dynamic. What I want to do now is that whenever an event I will call "somethingChanged" occurs (which is already distributed through an Observable) all sub components A1, ... will do some processing and then return some information (a state) as an event I'll call newStates to the parent action A probably using another observable. For this to work the sub components first have to register themselves to the "event manager" as children of A so that these events can be processed accordingly.
First idea
My first idea for this was to use a bufferCount on the newStates observable with the count being the amount of registered sub components. The problem is that the sub component registering and the parent component subscribing to the newStates observable is happening at almost the same time, the parent even being slightly faster which means the amountSub is usually 0 which breaks this attempt.
registerSubComponent() {
amountSub++;
}
getParentObservable() {
return newStates.bufferCount(amountSub).mergeMap();
}
Second idea
The second attempt was to use the somethingChanged Event and use that to initialize a takeLast to get the last items when they should be thrown. The problem is again as i will run into race condition as sub components take longer to throw their newStates events meaning I'll get old values.
registerSubComponent() {
amountSub++;
}
getParentObservable() {
return somethingChanged.map(() => newStates.takeLast(amountSub);
}
Third idea
So currently my only idea would be to catch the newStates event in the event manager, store the states in an array and check everytime if all registered components send them by looking at the array length. When all states are in i could then send the saved states and reset the array.
registerSubComponent() {
amountSub++;
}
getParentObservable() {
return newParentObservable;
}
newStates.subscribe(state => {
savedStates.push(state);
if(savedStates.length == amountSub) {
newParentObservable.next(savedStates);
savedStates = [];
}
});
Is this the only way or am I missing something so it could be done easier/with observables?
Btw: This is all pseudo code as my actual code also has to support multiple parent components in one manager making it cumbersome to read through.
It sounds like you want change detection up the tree. Using the following method with an angular service sounds like it might be what you need:
I found a solution on this guy Jason Watmore's blog that describes using rxjs Observables and Subjects. Using this method allows data changes to easily propagate up and down the angular inheritance tree to any component you want
Jason's Post
Accompanying Plunkr
Briefly:
You declare a service as a provider at the module.ts level with 3 methods:
sendMessage
clearMessage
getMessage
import { Injectable } from '#angular/core';
import { Observable } from 'rxjs';
import { Subject } from 'rxjs/Subject';
#Injectable()
export class MessageService {
private subject = new Subject();
sendMessage(message: string) {
this.subject.next({ text: message });
}
clearMessage() {
this.subject.next();
}
getMessage(): Observable<any> {
return this.subject.asObservable();
}
}
This service needs imports of Observable and Subject from rxjs
In each component you want to share data with:
create a subscription object in the constructor which calls the service.getMessage() function
call rxjs subscription.unsubscribe() in ngOnDestroy for each component so you aren't leaking memory
you can hook in a function to handle the incoming subscription updates
When you have data you want to share with your other components:
Create a public method which calls the service.sendMessage() method
This will send your updated data to each component and fire those functions you've hooked in to handle the changed data
I believe the blog post I linked to and the plunkr from the post say it best and have really helped me move data around efficiently in my own app but if you have any questions I'll do my best to answer them

Categories

Resources