Within my Angular app i ve the following treatment :
OnInit on lauching a subscibe from a subject call (SubjectOne)
when there is a new data coming from SubjectOne ,
and if some condition is verified ; i reeuse this data to launch a second call whitch is a http call from a service call .
Here is my code
MyComponent.ts :
ngOnInit() {
this.getFirstTreatment();
}
getFirstTreatment() {
this.subscriptionOne = this.myService.subjectOne.subscribe((data) => {
this.myValue = data['myValue'];
this.getSecondTreatment(data['myValue'])
})
}
getSecondTreatment(thatValue) {
if(thatValue >= 100){
this.subscriptionTwo = this.myService.sendToBackend(thatValue).subscribe((response)=>{}
}
}
MyService.ts
sendToBackend(thatValue){
let newValue = someFormatingnMethod(thatValue)
return this.httpClient.post(url , newValue );
}
My Purpose is how may i dynamically close the subscribtionTwo so it won't be called n times after each time i got new data from the subject .
NB : mySubject can notice some new Data even before the destroy of the compoment
I ve tried to use switchMap , but it seems to not work correctly
Suggestions ?
You are starting with one observable
That observable stays open after it has emitted a value, so we need to unsubscribe
You then want to conditionally run a second observable based on the result of the first observable
I would take this approach:
Set up your first observable as you are currently doing
Use takeUntil to unsubscribe on destroy
Use filter to only continue based on a condition
Use switchMap to run the second observable
The second observable is an HttpClient request, which self-completes, so we don't need to unsubscribe
private destroyed$ = new Subject();
ngOnInit() {
getFirstTreatment();
}
ngOnDestroy() {
this.destroyed$.next();
this.destroyed$.complete();
}
getFirstTreatment() {
this.myService.subjectOne.pipe(
takeUntil(this.destroyed$),
tap(data => this.myValue = data['myValue']),
filter(data => data['myValue'] >= 100),
switchMap(data => this.getSecondTreatment(data['myValue']))
).subscribe(data => {
console.log(data); // the output of the second observable
});
}
getSecondTreatment(myValue): Observable<any> {
return this.getSecondTreatment(myValue);
}
Related
Am fairly new to RxJs, and trying to wrap my head around what the proper pattern is to simply create an Observable array of Observables.
I want to retrieve a list of User's Posts. The Posts themselves should be Observables, and I want to keep them all in an Observable array, so that when the array changes the calling code should be notified and update anything subscribed to the post "list". This is simple enough, but I also would like each of the Posts to be Observables, so if I retrieve a specific posts[i] from it, I should also be able to subscribe to these individual objects.
What is the proper way to do this?
Am using Angular 9, I have:
public getPosts(): Observable<Array<Post>> {
return new Promise((resolve, reject) = {
let posts: Observable<Array<Post>> = new Observable<Array<Post>>();
this.get<Array<Post>>('posts').subscribe(r => {
posts = from(r);
return resolve(posts);
});
});
}
This gives me an Observable<Array<Post>>, but how should I create an Observable<Array<Observable<Post>>>?
Is this an anti-pattern?
It all comes to convenience, if your server serves you differential data of what changed in post, then go ahead and create Observable<Observable<Post>[]>.
In your post, however, there are multiple problems. You cannot mix Observables with Promises. The method getPosts will return only the first post you get from API.
This is the solution ask for, but I am not sure, it is what you actually wanted...
public getPosts(): Observable<Array<Observable<Post>>> {
return this.get('posts').pipe(
switchMap(posts => combineLatest(
posts.map(post => this.get('post', post.id))
)),
);
}
it's unclear what you're trying to accomplish here, but you might want something more like this:
#Injectable({providedIn:'root'})
export class PostService {
// private replay subject will cache one value
private postSource = new ReplaySubject<Post[]>(1)
// public list of posts observable
posts$ = this.postSource.asObservable();
// function to select item by id out of list
post$ = (id) => this.posts$.pipe(map(posts => posts.find(p => p.id === id)))
getPosts() {
// function to get remote posts
return this.get<Post[]>('posts');
}
loadPosts() {
// function to load posts and set the subject value
this.getPosts().subscribe(posts => this.postSource.next(posts));
}
}
you'll have to define that get function and call loadPosts everytime you want to update the list.
Given informations:
!If any of this statements is wrong, please tell me and I will update the answer!
get function that returns an observable with one array that filled with posts
the get observable emits always when the posts are changing
the value inside the observable (Array>) is no observable and does not change over time
this.get<Array<Post>>('posts')
Possible functions
() => getPostById$
// This function returns you an observable with the post related to your id.
// If the id is not found the observable will not emit
// If the id is found the observable will only emit if the interface values have been changed
function getPostById$(id: string): Observable<Post> {
// Returns you either the post or undefined if not found
const findId = (id: string) => (posts: Array<Post>): Post | undefined =>
posts.find(post => post.id === id);
// Allows you only to emit, if id has been found
const existingPost = (post: Post | undefined): boolean => post != null;
// Allows you only to emit if your id has been changed
const postComparator = (prevPost: Post, currPost: Post): boolean =>
prevPost.value === currPost.value && prevPost.name === currPost.name;
return this.get('posts').pipe(
map(findId(id)),
filter(existingPost),
distinctUntilChanged(postComparator)
);
}
() => getPosts$
function getPosts$(): Observable<Array<Post>> {
return this.get('posts');
}
() => getStatePosts$
// This function allows to manage your own state
// 1. posts$: overwrites all posts
// 2. clear$: empties your posts$ observable
// 3. add$: adds one observable to the end of your posts
function statePosts$(posts$: Observable<Array<Posts>>, clear$: Observable<void>, add$: Observable<Post>): Observable<Array<Post>> {
const updatePosts = (newPosts: Array<Posts>) => (oldPosts: Array<Posts>) => newPosts;
const clearPosts = () => (oldPosts: Array<Posts>) => [];
const addPost = (post: Post) => (oldPosts: Array<Posts>) => [...oldPosts, post];
return merge(
// You can add as much update functions as you need/want (eg: deleteId, addPostAtStart, sortPosts, ...)
posts$.pipe(map(updatePosts)),
clear$.pipe(map(clearPosts)),
add$.pipe(map(addPost))
).pipe(
// The fn in you scan is the (oldPosts: Array<Posts>) function from one of your three update functions (updatePosts, clearPosts and addPosts).
// Whenever one of those three observables emits it first calls the left side of the function inside the map (post: Post) and returns a new function
// When this function reaches the scan it gets the oldPosts and is able to update it
scan((oldPosts, fn) => fn(oldPosts), [])
)
}
// Usage
private posts$: Observable<Array<Post>> = this.get('posts');
private clear$: Subject<void> = new Subject();
private add$: Subject<Post> = new Subject();
public statePosts$ = getStatePosts(posts$, clear$, add$);
Hint: Try to read the functions from the return statement first. And then check what is happening in the mapping/filtering or other operations. Hopefully I did not confuse you too much. If you have questions, feel free to ask.
I have a subject that consumers are subscribed to:
private request$: Subject<Service> = new BehaviorSubject(null);
This is the function that my components call upon initialisation:
public service(id: number): Observable<Service> {
return this.request$
.pipe(
switchMap((request) => request && request.serviceId ? of(request) : this.requestById(id)));
}
and the service call:
private requestById(serviceId: number): Observable<Service> {
// http call
}
Different components call this function with different ids. I'd like to renew/update the subject if the incoming id parameter value doesn't match of the id value of the current subject.
Is that possible to do? I have seen an iif function, but I don't think it suits me fully.
Thanks
This is what you can do:
public service(id: number): Observable<Service> {
return this.request$
.pipe(
//this is needed to ensure to avoid second evalution after doing next once
//we got the new service value from the API
take(1),
switchMap((request) => {
if(request && request.serviceId === id) {
return of(request);
} else {
return this.requestById(id)
.pipe(
//i am assuming that response of this.requestById is an instance of 'Service';
//update the subject
tap(s => this.requests$.next(s))
);
}
})
);
}
In a parent component I have a stream of Tour[] tours_filtered: Observable<Tour[]> which I assign in the subscribe function of an http request
this.api.getTours().subscribe(
result => {
this.tours_filtered = of(result.tours);
}
)
in the view I display the stream using the async pipe
<app-tour-box [tour]="tour" *ngFor="let tour of tours_filtered | async"></app-tour-box>
Up to here all works as expected. In a child component I have an input text which emits the value inserted by the user to filtering the array of Tour by title.
In the parent component I listen for the emitted values in a function, I switch to new stream of Tour[] filtered by that value using switchMap
onSearchTitle(term: string) {
this.tours_filtered.pipe(
switchMap(
(tours) => of( tours.filter((tour) => tour.name.toLowerCase().includes(term)) )
)
)
}
I thought that the async pipe was constantly listening to reflect the changes to the array to which it was applied and so I thought I didn't have to subscribe in the function above, but nothing change in the view when I type in the input to filtering the results.
The results are updating correctly if I assign the new stream to the original array in the subscribe function
onSearchTitle(term: string) {
this.tours_filtered.pipe(
switchMap((tours) => of(tours.filter((tour) => tour.name.toLowerCase().includes(term))))
).subscribe( val => { this.tours_filtered = of(val); })
}
Is this procedure correct? Could I avoid to subscribe because I already use the async pipe? There is a better way to reach my goal?
EDITED:
Maybe I found a solution, I have to reassing a new stream to the variable just like this
onSearchTitle(term: string) {
this.tours_filtered = of(this.city.tours).pipe(
switchMap((tours) => of(tours.filter((tour) => tour.name.toLowerCase().includes(term))))
);
}
and I don't need to subscribe again, the results in the view change according to the search term typed by the user. Is this the correct way?
I think in your situation the solution should work as follows:
onSearchTitle(term: string) {
this._searchTerm = term;
this.tours_filtered = of(
this.city.tours.filter((tour) => tour.name.toLowerCase().includes(term))
)
}
Because in your example you don't change the observable which is used in ngFor. Thus it's not working.
However, I don't see the reason of using observables here unless this is the first step and you're going to fetch this data from server in future
UPDATE
The best solution for you would be to consider your input as an observable and watch for the changes:
// your.component.ts
export class AppComponent {
searchTerm$ = new BehaviorSubject<string>('');
results = this.search(this.searchTerm$);
search(terms: Observable<string>) {
return terms
.pipe(
debounceTime(400),
distinctUntilChanged(),
switchMap(term => {
return of(this.city.tours.filter((tour) => tour.name.toLowerCase().includes(term)))
}
)
)
}
}
// your.template.html
...
<input type="" (input)="searchTerm$.next($event.target.value)">
...
Additionally it would be great to add debounceTime and distinctUntilChanged for better user experience and less search requests.
See full example for the details. Also please, refer to this article for more detailed explanations
Every time I load the webpage, I'd have to click the logo in-order my data to fully populate the local array in my component. The data fetched is located in a local JSON file. Having to refresh the page every-single-time is fairly unprofessional/annoying.
Using Angular CLI 1.3.2
Here's where my problem lies:
#Injectable()
export class LinksService implements OnInit{
siteFile : IFile[];
constructor(private http: Http) {
this.getJSON().subscribe(data => this.siteFile = data, error =>
console.log(error));
}
public getJSON(): Observable<any> {
return this.http.get('./assets/docs/links.json')
.map((res:any) => res.json());
}
getAllIpageLinks() : IPageLink[]{
var selectedIPageLinks: IPageLink[] = new Array();
var selectedFileLinks : IFile[] = new Array();
selectedFileLinks = this.siteFile;
for (var i=0; i<selectedFileLinks.length; i++)
{
selectedIPageLinks =
selectedIPageLinks.concat(selectedFileLinks[i].files);
}
return selectedIPageLinks.sort(this.sortLinks);
}
Component:
constructor(private elRef: ElementRef, private linksService: LinksService) {
this._file = this.linksService.getAllIpageLinks();
}
Edit
The title has to be clicked in order for array of IFile[] to completely render. I've tried setting IFile to an empty array (IFile[] = []) The error goes away, however, it will render empty data.
The problem seems to be in the For loop, it can't recognize .length.
Problem :
The codes are correct but the approach is wrong. Subscribing to an Observable getJSON() is async task. Before any data is being returned by getJSON(), you already calls getAllIpageLinks() and therefore you get null value on very first run. I believe since you have injected the service as singleton in component, the data gets populated in subsequent call( on refresh by clicking logo).
Solution:
Apply the changes (that you are making in getAllIpageLinks ) by using map operator on observable.
return the instance of that observable in the component.
subscribe to that observable in the component(not in .service)
Welcome to StackOverflow. Please copy paste your codes in the question instead of giving screenshot of it. I would be able than to give you along the exact codes
Reference Codes :
I haven't tested the syntax but should be enough to guide you.
1. Refactor getAllIpageLinks() as below
public getAllIpageLinks(): Observable<any> {
return this.http.get('./assets/docs/links.json')
.map((res:any) => res.json());
.map(res => {
var selectedIPageLinks: IPageLink[] = new Array();
var selectedFileLinks : IFile[] = new Array();
selectedFileLinks = res;
for (var i=0; i<selectedFileLinks.length; i++)
{
selectedIPageLinks =
selectedIPageLinks.concat(selectedFileLinks[i].files);
}
return selectedIPageLinks.sort(this.sortLinks);
});
}
call above getAllIpageLinks() in your component
and subscribe to it there
I have a sandbox which subscribes to a stream of messages and I want to filter that stream to find messages that have been sent to or received from a specific user using route params specified in another component.
messages.sandbox.ts:
messages$: Observable<Array<Message>> = this.store.select(state => state.data.messages);
fetchReceived(id: string): Observable<Array<Message>> {
return this.messages$.map((messages: any) => {
return messages.filter((message: Message) => {
return message.recipientId == id;
});
});
}
fetchSent(id: string): Observable<Array<Message>> {
return this.messages$.map((messages: any) => {
return messages.filter((message: Message) => {
return message.userId == id;
})
})
}
messages.detail.container.ts
sentMessages$ = new Observable<Array<Message>>();
receivedMessages$ = new Observable<Array<Message>>();
matchingMessages$ = new Observable<Array<Message>>();
ngOnInit() {
this.route.params.subscribe((params: Params) => {
this.sentMessages$ = this.sb.fetchReceived(params['id']);
this.receivedMessages$ = this.sb.fetchSent(params['id']);
this.matchingMessages$ = Observable.merge(this.sentMessages$, this.receivedMessages$);
});
}
this.matchingMessages$ seems to only include this.receivedMessages$ however I know that this.sentMessages$ is not null as I can use it in my template without a problem.
Am I missing something with merging Observables? Would it be better to create a single fetchMessages method which filters for either the userId or recipientId equalling the route param id? If so how would I go about that?
Thanks!!
You have the right general idea. Just a few flaws.
Never use new Observable<T>(). It does not do what you think it does. It pretty much does not do anything useful. Always construct observables from factory methods or other observables
You need to transform the params observable into a new observable using an operator. Your problem is you subscribe to the params observable, and then construct new observables each time. But other code will have already subscribed to the initial observables so they will never see the changes.
So you want to do something like this:
sentMessages$ : Observable<Array<Message>>;
receivedMessages$ : Observable<Array<Message>>;
matchingMessages$ : Observable<Array<Message>>;
ngOnInit() {
const params$ = this.route.params;
// use switchMap to map the new params to a new sent observable
// each time params change, unsubscribe from the old fetch and subscribe
// to the new fetch. Anyone subscribed to "sentMessages" will see the
// change transparently
this.sentMessages$ = params$.switchMap((params: Params) => this.sb.fetchReceived(params['id']));
// same for received
this.receivedMessages$ = params$.switchMap((params: Params) => this.sb.fetchSent(params['id'])));
// merge the 2 streams together
this.matchingMessages$ = Observable.merge(this.sentMessages$, this.receivedMessages$);
}
Edit:
to answer your other question: is it better to create a single observable that matches senders and receivers: depends upon your use case. But here is how you could go about it:
messages.sandbox.ts:
fetchEither(id: string): Observable<Array<Message>> {
return this.messages$.map((messages: any) => {
return messages.filter((message: Message) => {
return message.recipientId == id || message.userId === id;
});
});
}
container:
matchingMessages$ : Observable<Array<Message>>;
ngOnInit() {
const params$ = this.route.params;
// use switchMap to map the new params to a new either observable
// each time params change, unsubscribe from the old and subscribe
// to the new fetch. Anyone subscribed to "matchingMessages" will see the
// change transparently
this.matchingMessages$ = params$.switchMap((params: Params) => this.sb.fetchEither(params['id']));
}