2
votes

I have two sibling components which need to access data retrieved in an observable that is dynamically created in a service based on a user input observable. I'm struggling to understand how I can get the search-list component to listen to the observable created by the search component. Should I store the last search observable creted by the service as another observable in the search service, should I store the search observable within the search component and access via a ref, or should I emit the data once the observer completes in the search component?

search.service.ts

export interface SearchApi {
 data: string[]
}

export class SearchService {
  loading: EventEmitter<boolean> = new EventEmitter();

  constructor(private httpClient: HttpClient) {

  }

  search(terms: Observable<string>){
     return terms.pipe(debounceTime(400))
     .pipe(distinctUntilChanged())
     .pipe(switchMap(term => {
        this.loading.emit(true);
        return this.searchEntries(term);
     }))
     .pipe((data) => {
        this.loading.emit(false);
        return data;
     });
  }

  searchEntries(term){
    return this.httpClient.post<SearchApi>(this.baseUrl,term);
  }
}

search.component.ts

import { Subject, Observable } from 'rxjs';

export class SearchComponent implements OnInit {

  searchTerm$ = new Subject<string>();

  constructor(private searchService: SearchService) {
    searchService.loading.subscribe(isLoading => {
      console.log(`Is loading: ${isLoading}`);
    });

    searchService.search(this.searchTerm$).subscribe(results => {
      console.log(`Search results ${results}`);
    });
  } 
}

search-list.component.ts

export class SearchListComponent implements OnInit {

  constructor(private searchService: SearchService) {
    searchService.loading.subscribe(isLoading => {
      console.log(`Is loading: ${isLoading}`);
    });

    /* Not sure how I would subscribe to observable that search.service.ts created */
    /*
    searchService.search().subscribe(results => {
      console.log(`Search results ${results}`);
    });
    */
  } 
}
2

2 Answers

2
votes

just use a subject to share state and seperate your "load state" action from your "receive state" action.

export class SearchService {
  private loading: Subject<boolean> = new Subject(); // use subjects, keep them private
  loading$: Observable<boolean> = this.loading.asObservable(); // public observable


  private searchResults = new BehaviorSubject(null);  // BehaviorSubjects solve timing problems
  searchResults$ = this.searchResults.asObservable(); 

  constructor(private httpClient: HttpClient) {

  }

  search(terms: Observable<string>){
     return terms.pipe(
         debounceTime(400), // cleaner syntax / less pipes
         distinctUntilChanged(),
         tap(v => this.loading.next(true)), // subjectsuse next, use tap for side effects
         switchMap(term => this.searchEntries(term)),
         tap(v => this.loading.next(false)) // use tap for side effects
     ).subscribe(this.searchResults); // send it to the subject
  }

  searchEntries(term){
    return this.httpClient.post<SearchApi>(this.baseUrl,term);
  }
}

then in your component, you subscribe to searchResults$ and just call search() when you want to populate the search results.

search.component.ts

import { Subject, Observable } from 'rxjs';

export class SearchComponent implements OnInit {

  searchTerm$ = new Subject<string>();

  constructor(private searchService: SearchService) {
    searchService.loading$.subscribe(isLoading => {
      console.log(`Is loading: ${isLoading}`);
    });

    searchService.search(this.searchTerm$); // call the state load action
  } 
}

search-list.component.ts

export class SearchListComponent implements OnInit {

  constructor(private searchService: SearchService) {
    searchService.loading$.subscribe(isLoading => {
      console.log(`Is loading: ${isLoading}`);
    });

    searchService.searchResults$.subscribe(results => { // receive state here
      console.log(`Search results ${results}`);
    });
  } 
}
1
votes

One of the options would be to move the searchTerm$ Observable one level up to the component that wraps both of them. And then you can pass it into both of them as @Input and it will listen for updates inside them. And if you need to update it, you can also pass an @Output function to update the value of the observable in the parent component.

Another option is to add this observable to a service and inject the service into both components. That will also work.

Here's a stackblitz example of both methods.