I'm using these observables in my HTML but they are not re-emitting each time the input values of my HTML controls change (and they depend on those values so they become out of sync). For instance when this updates ([(ngModel)]="mappedItem.selectedBusinessEntities"
) then selectedBusinessEntitiesOptionsAsDtos$ | async
needs to re-emit. See the options of the second HTML control are the selected options of the first HTML control. Do I need to make the [(ngModel)]="mappedItem.selectedBusinessEntities"
value be an observable somehow?
HTML:
<cb-selection-list name="selectedBusinessEntities"
[(ngModel)]="mappedItem.selectedBusinessEntities"
[options]="businessEntitiesOptions$ | async"
[readonly]="isView()"
maxHeight="400px"
[slim]="true">
</cb-selection-list>
<cb-select label="Primary Business Entity"
name="primaryBusinessEntity"
[required]="true"
[(ngModel)]="mappedItem.primaryBusinessEntity"
[options]="selectedBusinessEntitiesOptionsAsDtos$ | async"
[readonly]="isView()">
</cb-select>
Typescript:
@Component({
selector: 'cb-user-details',
templateUrl: './user-details.component.html',
styleUrls: ['./user-details.component.scss']
})
export class UserDetailsComponent implements OnInit {
public teamsOptions$: Observable<ITeamDto[]>;
public userRoleTagsOptions$: Observable<ITagDto[]>;
public userRoleTagsOptions: ITagDto[];
public businessEntitiesOptions$: Observable<IBusinessEntityDto[]>;
public isMobileNumberMandatory$: Observable<boolean>;
public selectedBusinessEntitiesOptionsAsDtos$: Observable<IBusinessEntityDto[]>;
public ngOnInit(): void {
this._initSelectOptions();
}
private _initSelectOptions(): void {
this.teamsOptions$ = this.teamsLogicService.$getList();
this.userRoleTagsOptions$ = this.tagsLogicService.$getList();
this.businessEntitiesOptions$ = this.businessEntitiesLogicService
.$getList()
.pipe(
map(businessEntities => {
return orderBy(businessEntities, businessEntity => businessEntity?.name?.toLowerCase());
})
);
this.selectedBusinessEntitiesOptionsAsDtos$ = this.businessEntitiesOptions$.pipe(
map(businessEntities => {
return businessEntities
.filter(businessEntity => includes(
this.mappedItem.selectedBusinessEntities, businessEntity.id)
);
}));
this.isMobileNumberMandatory$ = this.selectedBusinessEntitiesOptionsAsDtos$.pipe(
map(businessEntities => {
const buildingConsultantTag = this.userRoleTagsOptions?.find(
tag => tag.key === USER_TAG_CONSTANTS_CONST.BUILDING_CONSULTANT);
return this.mappedItem?.selectedTags
.some(tag => tag === buildingConsultantTag?.id);
})
);
this.isMobileNumberMandatory$.subscribe();
this.teamsOptions$.subscribe();
this.userRoleTagsOptions$.subscribe(res => this.userRoleTagsOptions = res);
}
EDIT:
Have tried breaking the ngModel up like so:
<cb-selection-list name="selectedBusinessEntities"
[ngModel]="mappedItem.selectedBusinessEntities"
(ngModelChange)="selectedBusinessEntitiesChanged($event)"
[options]="businessEntitiesOptions$ | async"
[readonly]="isView()"
maxHeight="400px"
[slim]="true">
</cb-selection-list>
The ts:
public selectedBusinessEntitiesChanged(entities: number[]): void {
this.mappedItem.selectedBusinessEntities = entities;
this.businessEntitiesOptions$.subscribe();
this.cdRef.detectChanges();
}
And annoyingly, that does cause the this.selectedBusinessEntitiesOptionsAsDtos$
to run and emit the new list (checked with debugger). But the UI doesn't update. That's why I added this.cdRef.detectChanges();
but it didn't work.
You can use the ngModelChange event:
(ngModelChange)="handleNgModelChangedEvent($event)
It will get triggered every time there are changes in ngModel. The $event payload will hold the current value of the form field.
Update:
Also, not sure if it will help, but you shouldn't define your observables inside of the private function, but rather where you declared them, as class members:
E.g.:
businessEntitiesOptions$ = this.businessEntitiesLogicService.getList()
.pipe(
map(businessEntities => {
orderBy(businessEntities, businessEntity => businessEntity?.name?.toLowerCase());
})
);
Instead of:
public businessEntitiesOptions$: Observable<IBusinessEntityDto[]>;
The same goes for all your observables...
You should even be able to use the OnPush change detection strategy in this class.
You can try adding tap(x => console.log(x))
in your piped observables to see if they are working as intended...
E.g.:
.pipe(
tap(x => console.log(x)),
map(businessEntities => {
orderBy(businessEntities, businessEntity => businessEntity?.name?.toLowerCase()),
tap(x => console.log(x))
})
You might also want to remove the returns... Unless you are catching errors or something, you don't want to return anything. Observables are just streams.