nebular: NbSelect doesn't display the [selected] option if data is dynamic

Issue type

I’m submitting a … (check one with “x”)

  • bug report
  • feature request

Issue description

Current behavior: I have a NbSelect populated with options from the database. Neither using [selected] or ([ngModel]) makes it display the selected value. As soon as I pass static data it works as intended.

Expected behavior: It should automatically select the selected option.

Steps to reproduce: Put NbSelect on page, have a simple MySQL database to support the exchange of data, take the data and put them as options.

Related code: Unfortunately since it requires my localhost mySQL here, I can’t provide a stackblitz, but I can include some snippets.

header.component.html

<nb-select (selectedChange)="changeLang($event)" ([ngModel])="selectedLang">
    <nb-option *ngFor="let lang of languages" [value]="lang">{{ lang.short }}</nb-option>
  </nb-select>

header.component.ts

service.exp.subscribe((exp: any) => {
      this.languages = exp.langs.map(x => {
        return {
          short: x.short,
          name: x.name
        }
      });

      this.selectedLang.next(this.languages.filter(x => x.short === translate.currentLang)[0]);
    });

header.service.ts

this.server.getLangsList().subscribe((langs: any) => {
        this.exp.next({
          langs: langs
      });
    });

server.service.ts

getLangsList() {
    return this.request('GET', `http://localhost:8080/langs`);
    //return of([{"idLang":1,"short":"en","name":"English","dateFormat":"YYYY-MM-DD","dateTimeFormat":"YYYY-MM-DD HH:mm:ss"}]);
  }

Now, in the last snippet, the two rows return the same Observable. In the caso of the second (static data) it displays the [selected] value correctly, in the first it doesn’t.

I’m not sure it’s related to the issue 2088, so I started a new issue #2088

Other information:

npm, node, OS, Browser

Node version 10.16.0
npm version 6.10.3
Windows 10, on Chrome

Angular, Nebular

ngx-admin v4.0.1, nebular v4.1.2

About this issue

  • Original URL
  • State: open
  • Created 5 years ago
  • Reactions: 19
  • Comments: 21 (1 by maintainers)

Most upvoted comments

I have used @shireefadel’s workaround as follows:

  1. in the html, add a #select on the nb-select element.
<nb-select fullWidth multiple formControlName="mySelected" #select>
  1. Add an angular viewchild to the nb-select element
@ViewChild('select') selectElem: NbSelectComponent;
  1. Add a workaround timeout and call it when the dynamic content changes
changeMyDynamicContent() {
  ...Logic here...

  setTimeout(() => {
    if (this.selectElem) {
      const selectedOptions: NbOptionComponent[] = [];
      for (const option of this.selectElem.options['_results']) {
        if (mySelected.includes(option.value)) {
          selectedOptions.push(option);
        }
      }
      for (const option of selectedOptions) {
        this.selectElem['selectOption'](option);
      }
      this.selectElem['cd'].detectChanges();
    }
  }, 500);
}

This is extremely dirty, but it will work until this bug is fixed.

I have used @shireefadel’s workaround as follows:

  1. in the html, add a #select on the nb-select element.
<nb-select fullWidth multiple formControlName="mySelected" #select>
  1. Add an angular viewchild to the nb-select element
@ViewChild('select') selectElem: NbSelectComponent;
  1. Add a workaround timeout and call it when the dynamic content changes
changeMyDynamicContent() {
  ...Logic here...

  setTimeout(() => {
    if (this.selectElem) {
      const selectedOptions: NbOptionComponent[] = [];
      for (const option of this.selectElem.options['_results']) {
        if (mySelected.includes(option.value)) {
          selectedOptions.push(option);
        }
      }
      for (const option of selectedOptions) {
        this.selectElem['selectOption'](option);
      }
      this.selectElem['cd'].detectChanges();
    }
  }, 500);
}

This is extremely dirty, but it will work until this bug is fixed.

thanks, it helped me a lot to create a generic function

static setOptionNbSelect(selectComponent: NbSelectComponent, optToCompare: any) {
    setTimeout(() => {
        if (selectComponent) {
            const selectedOptions: NbOptionComponent[] = [];
            for (const option of selectComponent.options['_results']) {
                if (_.isEqual(optToCompare, option['value'])) {
                    selectedOptions.push(option);
                    break;
                }
            }
            for (const option of selectedOptions) {
                selectComponent['selectOption'](option);
            }
            selectComponent['cd'].detectChanges();
        }
    }, 500);
    
}

I hope it works for someone else. Thanks again @ironsm4sh

The issue is still present in nebular 8. Any news? I created a repo nb-select-issue with the issue and the solution proposed by @ironsm4sh and @rardila-uniajc and works fine. Thanks

until it´s fixed: setting “selected” in ngAfterViewInit works for me…

This will work

in the html, add a #select on the nb-select element.

<nb-select fullWidth multiple formControlName="mySelected" #select>

in the ts, add a ViewChild to you variables.

  @ViewChild('select') selectElem: NbSelectComponent;

add this function

setOptionNbSelect(selectComponent: NbSelectComponent, optToCompare: any) {
        if (selectComponent) {
            const selectedOptions: NbOptionComponent[] = [];
            for (const option of selectComponent.options['_results']) {
                if (_.isEqual(optToCompare, option['value'])) {
                    selectedOptions.push(option);
                    break;
                }
            }
            for (const option of selectedOptions) {
                selectComponent['selectOption'](option);
            }
            selectComponent['cd'].detectChanges();
        }
    }

and call the function giving it the default value when you subscribe

this.api.getData().subscribe(
        (data:any) =>{
            this.setOptionNbSelect(this.selectElem, data.pack);
          });

@michabbb that works when initially setting the select right after component loads, however after a while I also have to change its value from outside (from another server request) and at that time the component doesn’t show the updated value (even if the correct item is selected when opening the dropdown)

The issue seems to be due to the component not re-evaluating the selected value when the options are updated dynamically after the value is assigned somehow. My workaround was to clear the value and then set it again with a deferred call (setTimeout with 0 duration) and calling changeDetectorRef.markForCheck().

Something like this:

// preserve value and clear it from component
const value = this.value;
this.value = "";

// deferred code to re-assign value and trigger CD cycle
setTimeout(() => {
  this.value = value;
  this.changeDetector.markForCheck();
});

Works for me, many thanks

Thank you jrasm91, I think this is the best explanation of the problem, took me a while to reach the same conclusion. I had the same problem but with template based forms, and was able to “solve” it by making sure that the variable that contains the options list for <nb-option> is set before changing <nb-select> attached ngModel (Of course mine is not a solution, only a weak workaround)

Explanation Using formControlName or ngModel uses angular’s value accessor, which calls writeValue on the NbSelectComponent. I believe the problem is that this happens before the underlying options are re-rendered. Basically, the NbSelectComponent runs compare/matching logic against the wrong (old) NbOption list, which is why it doesn’t show as “selected”. Adding a setTimeout just delays the “selected” change until after the new NbOption components are updated. Now when it runs, it finds the matching NbOption and marks it as selected.

I ran into this issue when I was dynamically updating the underlying nb-options, while simultaneously changing the selected value.

<nb-select formControlName="selectedOption">
    <nb-option *ngFor="let option of options" [value]="option">{{ option }}</nb-option>
</nb-select>
this.options = newOptions;
this.formGroup.controls.selected.setValue(newOptions[0]);

Solution I fixed the issue by wrapping the selected update in a setTimeout.

this.options = newOptions;
setTimeout(() => this.formGroup.controls.selected.setValue(newOptions[0]));

Explanation Using formControlName or ngModel uses angular’s value accessor, which calls writeValue on the NbSelectComponent. I believe the problem is that this happens before the underlying options are re-rendered. Basically, the NbSelectComponent runs compare/matching logic against the wrong (old) NbOption list, which is why it doesn’t show as “selected”. Adding a setTimeout just delays the “selected” change until after the new NbOption components are updated. Now when it runs, it finds the matching NbOption and marks it as selected.

The issue seems to be due to the component not re-evaluating the selected value when the options are updated dynamically after the value is assigned somehow. My workaround was to clear the value and then set it again with a deferred call (setTimeout with 0 duration) and calling changeDetectorRef.markForCheck().

Something like this:

// preserve value and clear it from component
const value = this.value;
this.value = "";

// deferred code to re-assign value and trigger CD cycle
setTimeout(() => {
  this.value = value;
  this.changeDetector.markForCheck();
});