2

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.

BeniaminoBaggins
  • 11,202
  • 41
  • 152
  • 287
  • Could you please look at the doc, if there is something that could be handled when there is a change in the input field – vsnikhilvs Oct 08 '21 at 01:41
  • 1
    Does it work if you replace the entire mappedItem? `this.mappedItem = {…this.mappedItem, selectedBusinessEntities: entities}` – BizzyBob Oct 08 '21 at 04:14
  • @BizzyBob That works only if I do *ngIf="mappedItem" at the top of the html. And although that does work, it causes a flash where the controls go blank for half a second or so. I guess it is something to do with detect changes but `this.cdRef.detectChanges()` doesn't fix it. – BeniaminoBaggins Oct 08 '21 at 04:58

1 Answers1

2

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.

H3AR7B3A7
  • 4,366
  • 2
  • 14
  • 37
  • Thanks. I updated the bottom of my question with that about 2 minutes before you posted. It is not working. – BeniaminoBaggins Oct 08 '21 at 02:25
  • I updated the answer, if you create your observables where you declared them it should work fine. Cheers – H3AR7B3A7 Oct 08 '21 at 02:50
  • Thanks! What happens to the constructor services if I declare the observables which use those services above the constructor? I'm getting errors that they aren't defined. – BeniaminoBaggins Oct 08 '21 at 03:07
  • That makes no sense ... The class constructor always get executed first, that's how you inject services. Even if the constructor is at the bottom of your class it should still work. Could you paste the whole class or better yet, create a [StackBlitz](https://stackblitz.com/) pls? – H3AR7B3A7 Oct 08 '21 at 04:22
  • Sorry I must have had a syntax error. It now works. But inside `handleNgModelChangedEvent`, how do I push that new value into `selectedBusinessEntitiesOptionsAsDtos$`. See the bottom of my question, I do `this.businessEntitiesOptions$.subscribe();` inside `handleNgModelChangedEvent` which is the only code that makes `selectedBusinessEntitiesOptionsAsDtos$` execute again and update the control below it with the new options. Instead or resubscribing inside `handleNgModelChangedEvent`, can I just do something the equivalent of `selectedBusinessEntitiesOptionsAsDtos$.next(entities)`? – BeniaminoBaggins Oct 08 '21 at 04:45
  • I am not entirely sure what you are trying to do. But either you transform an existing stream, like you are doing now (by piping or merging streams). Or you create a new Subject to observe and push values to. Then you can return the subject as an observable with .asObservable(). – H3AR7B3A7 Oct 08 '21 at 04:59
  • 1
    Thanks, I will mark as solution. When I do `this.mappedItem = {...this.mappedItem, selectedBusinessEntities: entities};` inside `handleNgModelChangedEvent` it seems to cause the observables to re-run and make it work, but that is a very key part of making it work. – BeniaminoBaggins Oct 08 '21 at 05:48