nebular icon indicating copy to clipboard operation
nebular copied to clipboard

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

Open meisbest opened this issue 5 years ago • 18 comments

Issue type

I'm submitting a ... (check one with "x")

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

meisbest avatar Dec 28 '19 09:12 meisbest

Same here. If I use the standard HTML select, it updates without problems.

augustocb23 avatar Jan 09 '20 16:01 augustocb23

until it´s fixed: setting "selected" in ngAfterViewInit works for me....

michabbb avatar Apr 28 '20 21:04 michabbb

I was facing exactly the same issue also yesterday. If you are using FormControls you can set the value of the nb-select in your component and it will work. step1: this.fb.group({ shop: [this.getDefaultElement(this.shoe.shop), [Validators.required]],....

fofiedelly avatar May 01 '20 17:05 fofiedelly

any news on this?

gonzalo42 avatar May 02 '20 19:05 gonzalo42

I was facing exactly the same issue also yesterday. If you are using FormControls you can set the value of the nb-select in your component and it will work. step1: this.fb.group({ shop: [this.getDefaultElement(this.shoe.shop), [Validators.required]],....

Can you please your code snippet?? because I have tried anything and it still doesn't work

gonzalo42 avatar May 03 '20 13:05 gonzalo42

I was facing exactly the same issue also yesterday. If you are using FormControls you can set the value of the nb-select in your component and it will work. step1: this.fb.group({ shop: [this.getDefaultElement(this.shoe.shop), [Validators.required]],....

Are you using nb-select & nb-option with dynamic data ? I have tried but it still doesn't work !

cacc559230 avatar May 04 '20 08:05 cacc559230

Is there any workaround on this?

alex88 avatar Nov 29 '20 16:11 alex88

@alex88 doesn´t this work for you?

michabbb avatar Nov 30 '20 09:11 michabbb

@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)

alex88 avatar Nov 30 '20 09:11 alex88

The only workaround that worked for me is using the protected member selectOption which takes an instance of NbOptionComponent object, look at the following example:

this.ddlCoutry.selectOption(this.ddlCoutry.options.find( (item, _index, _options) => { return item.value.id === this.shop.country.id; }, ));

this will work for both single and multiple select as follows: this.shop.classifications.forEach((classification) => { this.ddlClassification.selectOption(this.ddlClassification.options.find( (item, _index, _options) => { return item.value.id === classification.id; }, ));});

The weirdest thing is that the only working function is protected!

and to overcome this issue you can use dynamic property access with bracket notation, like so this.ddlCoutry['selectOption'](this.ddlCoutry.options.find( (item, _index, _options) => { return item.value.id === this.shop.country.id; }, ));

shireefadel avatar Dec 13 '20 14:12 shireefadel

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.

ironsm4sh avatar Jan 17 '21 21:01 ironsm4sh

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

rardila-uniajc avatar Apr 14 '21 18:04 rardila-uniajc

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

visyone avatar Sep 15 '21 13:09 visyone

Still no solution? The workarounds don't seem to work.

MincDev avatar Dec 28 '21 14:12 MincDev

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);
          });

ettouzany avatar Jan 17 '22 00:01 ettouzany

@ettouzany That does not work (or at least it did not for me) if the data is set using Angular's @Input property.

ironsm4sh avatar Jan 17 '22 08:01 ironsm4sh

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();
});

colinvella avatar Jun 30 '22 14:06 colinvella

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.

jrasm91 avatar Jul 22 '22 15:07 jrasm91

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.

mdefelicegenio avatar Nov 11 '22 07:11 mdefelicegenio

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

ddt313 avatar Dec 20 '22 09:12 ddt313

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

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

My hero ! this works!


    setTimeout(() => {
      let correctFormField: AbstractControl = this.userSchemaDataForm.controls['role'];
      correctFormField.setValue("admin")
    });

nikita-fuchs avatar Feb 21 '23 13:02 nikita-fuchs