1

I have an angular component which gets data from an @Input variable and uses this to filter additional data from a service. The (simplified) structure looks like this:

<phones-component>
 <phone-details>
  <..>
  <phone-history-component [phoneNumber$]="phoneNumber$"></phone-history-component>
 <phone-details>
</phones-component>

PhoneDetailsComponent:

 changeLogs$: Observable<AuditLog[]>;

 constructor(
   private auditLogService: AuditLogService, //ngrx-data service
 ){

  ngOnInit(): void {
      this.changeLogs$ = combineLatest([this.phoneNumber$, this.auditLogService.entities$])
           .pipe(
                 map(pn => pn[1].filter(logs => logs.pkId === pn[0].id))); //filters the logs to show only log entries from the corresponding phonenumber
   }

The routes are configured like this:

{
  path: 'phones',
  component: PhonesComponent,
},
{
  path: 'phones/:id',
  component: PhoneDetailsComponent,
},

Everything works as expected as long as I 'enter' the PhoneDetailsComponent from the parent container. When the user manually reloads the PhoneDetails View, the changeLogs$ array is empty, UNLESS I manually inject a property of the unwrapped observable like this:

  <phone-history-component [phoneNumberId]="(phoneNumber$ | async).id" [phoneNumber$]="phoneNumber$"></phone-history-component>

If I just inject the phoneNumber$ like this

[phoneNumber]="(phoneNumber$ | async)"

it doesn't work. (I don't use these additional inputs in the component)

Now I know that there are other ways to implement this functionality, but I'm merely interested in why the code behaves like this?

It feels like I'm lacking some fundamental understanding of the basic rxjs pipeline. So..what is happening here exactly?

Any help is appreciated.

UPDATE BizzyBob was kinda right. When directly reloading the component, the phoneNumber$ Observable didn't have any value when the filterfunction got fist executed. I fixed this with a quick check:

  map(pn => {
        return !!pn[0] ? pn[1].filter(logs => logs.pkId === pn[0].id) : null;
      }));

Now.. is this the recommended way to do it, or are there better ways?

opp
  • 1,010
  • 10
  • 17
  • Looks like when you reload the PhoeDetails, `this.auditLogService.entities$` is not emitting any value.Please make sure that `this.auditLogService.entities$` emits some value. When you come from the parent, `this.auditLogService.entities$` has some value which makes the details works fine. – user2216584 May 11 '20 at 18:18
  • where does `this.phoneNumber$` come from? It seems like maybe when you go directly to your "phone details" component, the phoneNumber$ isn't defined. important note about `combineLatest` is that it won't emit any values until all source observables have emitted. – BizzyBob May 12 '20 at 05:10
  • thx for your replies.`this.phoneNumber$` is a @Input from the parent component. Both observables emit values when I bind to them directly in the view e.g `{{(phoneNumber$ | async) | json}}` `{{(auditLogService.entities$ | async) | json}}` – opp May 12 '20 at 07:10

2 Answers2

2

To create a proper rxjs pipline, you can make use of startWith operator along with switchMap. Then your code would look something like this:

ngOnInit(): void {
  this.changeLogs$ = this.phoneNumber$
                          .pipe(
                             startWith({}),
                             switchMap(item => this.filterData(item))
                              );
 }

filterData(value = {}): Observable<any>{
 return this.auditLogService.entities$
            .pipe(
                map(data => data.filter(logs => logs.pkId === value.id))
                );
 }
YogendraR
  • 2,144
  • 12
  • 17
1

How is phoneNumber$ defined? is it perhaps formControl.valueChanges observable? This doesn't invoke initial value so you could use rxjs startWith() operator. If you cant use that ( for some weird reason ) well, BehaviorSubject is next option.

I reproduced your problem in snippet and provided 2 recommended solutions. Take a look!

let changeLogs$ = null;
let phoneNumber$ = null;
let auditLogService = {
  entities$: rxjs.of([{pkId: 1}])
}

function ngOnInit() {
      changeLogs$ = rxjs.combineLatest([phoneNumber$, auditLogService.entities$])
           .pipe(
                 rxjs.operators.map(pn => pn[1].filter(logs => logs.pkId === pn[0].id)))
   }
   
console.log('empty observable');
phoneNumber$ = rxjs.of();
ngOnInit();
let subscribtion = changeLogs$.subscribe(data => console.log(data));
subscribtion.unsubscribe();

console.log('observable with data');
phoneNumber$ = rxjs.of({id: 1})
ngOnInit();
subscribtion = changeLogs$.subscribe(data => console.log(data));
subscribtion.unsubscribe();

console.log('empty observable with startWith');
phoneNumber$ = rxjs.of().pipe(rxjs.operators.startWith({id: 1}));
ngOnInit();
subscribtion = changeLogs$.subscribe(data => console.log(data));
subscribtion.unsubscribe();

console.log('behaviorsubject with data');
phoneNumber$ = new rxjs.BehaviorSubject({id: 1})
ngOnInit();
subscribtion = changeLogs$.subscribe(data => console.log(data));
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/6.5.5/rxjs.umd.js"></script>
Józef Podlecki
  • 10,453
  • 5
  • 24
  • 50