Reputation: 1020
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 filter
function 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?
Upvotes: 1
Views: 328
Reputation: 2376
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))
);
}
Upvotes: 2
Reputation: 11283
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>
Upvotes: 1