简体   繁体   中英

Using BehaviorSubject to share data between sibling components

I am still learning Angular and trying to see how BehaviorSubject works but got stuck where to start. Here is waht I am trying to do:

Create a search box component called HeroDetailComponent where I enter some text and based on this text I will search a list of heroes and show them to user using another component HeroesComponent . Now this communication should be done using a service component with help of BehaviorSubject .

Here is my main component:

app.component.html

<app-hero-detail></app-hero-detail>
<app-heroes></app-heroes>

Here is my hero-detail.component.html this i am using it as search box:

  <div>
    <label>name:
      <input [(ngModel)]="hero.name" placeholder="name"/>
    </label>
  </div>

and the corresponding hero-detail.component.ts

import { Component, OnInit, Input } from '@angular/core';
import { Hero } from '../hero';

@Component({
  selector: 'app-hero-detail',
  templateUrl: './hero-detail.component.html',
  styleUrls: ['./hero-detail.component.css']
})
export class HeroDetailComponent implements OnInit {

  constructor() { }

  ngOnInit() {
  }

}

This is my heroes.component.html , here I am displaying the search results:

<h2>My Heroes</h2>

<ul class="heroes">
  <li *ngFor="let hero of heroes"
    <span class="badge">{{hero.id}}</span> {{hero.name}}
  </li>
</ul>

and its heroes.component.ts :

import { Component, OnInit } from '@angular/core';
import { Hero } from '../hero';
import { HEROES } from '../mock-heroes';
import { HeroService } from '../hero.service';
@Component({
  selector: 'app-heroes',
  templateUrl: './heroes.component.html',
  styleUrls: ['./heroes.component.css']
})

export class HeroesComponent implements OnInit {

  heroes = HEROES;

  constructor(private heroService: HeroService) { }

    getHeroes(): void {
      // Logic to access heroes list matching search text and display to user
    }

  ngOnInit() {
    this.getHeroes();
  }
}

Here is my service component:

import { Injectable } from '@angular/core';
import { BehaviorSubject } from 'rxjs/BehaviorSubject';

@Injectable({
  providedIn: 'root'
})
export class HeroService {

    private searchSource = new BehaviorSubject<string>("");
    searchText = this.searchSource.asObservable();

    constructor() { }

    searchByName(name: string) {
        this.searchSource.next(name);
    }

}

This is my mock data of heroes list:

import { Hero } from './hero';

export const HEROES: Hero[] = [
  { id: 11, name: 'Mr. Nice' },
  { id: 12, name: 'Narco' },
  { id: 13, name: 'Bombasto' },
  { id: 14, name: 'Celeritas' },
  { id: 15, name: 'Magneta' },
  { id: 16, name: 'RubberMan' },
  { id: 17, name: 'Dynama' },
  { id: 18, name: 'Dr IQ' },
  { id: 19, name: 'Magma' },
  { id: 20, name: 'Tornado' }
];

Can you please help me how to have this should be implemented?

Updated:

I followed steps given in trichetriche answer, but I am getting below errors:

ERROR in src/app/hero-detail/hero-detail.component.ts(17,16): error TS2339: Property 'formValue$' does not exist on type 'HeroService'.
src/app/hero-detail/hero-detail.component.ts(17,61): error TS2304: Cannot find name 'startWith'.
src/app/heroes/heroes.component.ts(17,13): error TS2304: Cannot find name 'combineLatest'.
src/app/heroes/heroes.component.ts(19,22): error TS2339: Property 'formValue$' does not exist on type 'HeroService'.
src/app/heroes/heroes.component.ts(21,5): error TS2552: Cannot find name 'map'. Did you mean 'Map'?

How to solve these errors?

Update: The above errors are solved now. Now I started facing issue with observable:

Observable.js:54 TypeError: You provided 'undefined' where a stream was expected. You can provide an Observable, Promise, Array, or Iterable.
    at subscribeTo (subscribeTo.js:41)
    at subscribeToResult (subscribeToResult.js:11)
    at CombineLatestSubscriber.push../node_modules/rxjs/_esm5/internal/observable/combineLatest.js.CombineLatestSubscriber._complete (combineLatest.js:62)
    at CombineLatestSubscriber.push../node_modules/rxjs/_esm5/internal/Subscriber.js.Subscriber.complete (Subscriber.js:66)
    at Observable._subscribe (subscribeToArray.js:8)
    at Observable.push../node_modules/rxjs/_esm5/internal/Observable.js.Observable._trySubscribe (Observable.js:43)
    at Observable.push../node_modules/rxjs/_esm5/internal/Observable.js.Observable.subscribe (Observable.js:29)
    at CombineLatestOperator.push../node_modules/rxjs/_esm5/internal/observable/combineLatest.js.CombineLatestOperator.call (combineLatest.js:32)
    at Observable.push../node_modules/rxjs/_esm5/internal/Observable.js.Observable.subscribe (Observable.js:24)
    at MapOperator.push../node_modules/rxjs/_esm5/internal/operators/map.js.MapOperator.call (map.js:18)

Can you please help me in solving this.

Subjects and BehaviorSubjects are proxies : they act both as observables, and observers.

You can make the observers of the proxies react when you cann subject.next(value) : the difference between a subject, and a behavior subject, is that the observer of a subject will receive the current value of the proxy (while a subject will only get the value of the next next ).

So, basically,

BehaviorSubject === Subject.pipe(startWith(someValue))

Now, you don't really need a behavior subject to do what you want. You have a cleaner way of doing so, use reactive forms and its valueChanges property.

In your form, use this :

  <div>
    <label>name:
      <input [formControl]="heroName" placeholder="name"/>
    </label>
  </div>
export class HeroDetailComponent implements OnInit {
  heroName = new FormControl('');
  constructor(
    private service: HeroService
  ) {
    this.service.formValue$ = this.heroName.valueChanges.pipe(startWith(''));
  }
}

You will create a form control, then inject your service into your component, and finally, you will use the valueChanges observable of the form control to sort your heroes in the sibling.

In the service, simply add a formValue$: Observable<string> to declare the variable. ( xx$ is just a naming convention for observables, to spot them quick).

You need the startWith operator to trigger a first value change on your control (otherwise, the value don't change unless you type something)

Finally, in the sibling :

export class HeroesComponent implements OnInit {

  private _heroesList$ = new BehaviorSubject(HEROES);

  heroes$;

  constructor(private heroService: HeroService) { }

    getHeroes(): void {
      this.heroes$ = combineLatest(
        this._heroesList$,
        this.heroService.formValue$
      ).pipe(
        map(([list, name]) => list.filter(hero => hero.name.includes(name)))
      );
    }

  ngOnInit() {
    this.getHeroes();
  }
}
<h2>My Heroes</h2>

<ul class="heroes">
  <li *ngFor="let hero of heroes$ | async"
    <span class="badge">{{hero.id}}</span> {{hero.name}}
  </li>
</ul>

By donig so, you create a new stream, that is made of both the form value and the list, and you map the result to return a filtered list of your heroes. You use the async pipe to let angular deal with the subscriptions, which is a good practice and prevents you from forgetting to unsubscribe.

getHeroes(): void {

  this.heroService.searchText
   .pipe(
     map((name) => HEROES.filter((hero) => hero.name.indexOf(name) !== -1))
   )
   .subscribe((results) => this.heroes = results);
}

Then just loop it like:

<ul class="heroes">
  <li *ngFor="let hero of heroes"
    <span class="badge">{{hero.id}}</span> {{hero.name}}
  </li>
</ul>

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM