angularrxjsrxjs6angular11async-pipe

How to cause observables to re-emit when ngModel value changes


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.


Solution

  • 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.