0

I use bootstrap-selectpicker in my project, when I try to put the data dynamically the dropdown doesn't work. I think selectpicker() gets called too soon. I want it to be called after I fetch the data.

export class MyComponent implements AfterViewInit {
   accounts = Observer<Account[]>;

   // Account service is just a http service returning Observer with accounts.
   constructor(private accountService : AccountService) {
       this.accounts = this.accountService.getAccounts();
   }

   ngAfterViewInit() {
       $('.selectpicker').selectpicker();
   }
}

Template:

<select class="selectpicker" multiple>
    <option *ngFor="let account of accounts | async" >{{account.username}}</option>             
</select>

It only works When I use predefined list like this:

<select class="selectpicker" multiple>
    <option>One</option>             
    <option>two</option>
    <option>three</option>     
</select>

This is my AccountService:

@Injectable()
export class AccountService {
    constructor(private http: Http) { }

    // TODO: Error handling
    getAccounts() : Observable<Account[]> {
        return this.http.get("/api/accounts").map(response => response.json());
    }
}
4
  • 1
    can you try to use ngAfterViewChecked ? Commented Apr 22, 2017 at 22:40
  • @KD I have just tried it. It works when I switch between routes, but it doesn't after first page load. Commented Apr 22, 2017 at 22:41
  • make sure you are defining provider for service correctly. Commented Apr 22, 2017 at 22:44
  • @KD I define it in my main module (AppModule) like this: providers: [AccountService] Commented Apr 22, 2017 at 22:48

4 Answers 4

3

Although your technique will work fine, since ngAfterViewChecked, as the name suggests, is called during every change detection run, there is a risk that this technique is a potential performance issue. This is because your selectpicker('refresh') function will be called regardless of if your list has actually been changed or not.

One way around this is to adjust your observable slightly so that it triggers the selectpicker('refresh') function only when your service returns a new list of Accounts.

constructor(private accountService : AccountService) { }

ngOnInit() {

   $('.selectpicker').selectpicker(); // <-- i don't know if this first call is necessary before the version with 'refresh' parameter will work - try removing it
   this.accounts = this.accountService.getAccounts().do(() => {
     setTimeout(() => {
       $('.selectpicker').selectpicker('refresh');
   }, 150);
   });
}

Explanation

Basically, the key is the do operator that allows you to run some side effect code whenever a new value is emitted from your observable, without changing the value coming out of the observable. So, use this to run your refresh function with each new list of accounts. I also moved your logic into ngOnInit - you should limit your constructor logic to just be simple initialization of properties through dependency injection. Finally, you need to wrap your selectpicker('refresh') call in a setTimeout to allow change detection to run (and the DOM to update) before trying to find the new elements on the DOM for the select picker. The exact timeout delay isn't as important as ensuring that you use setTimeout so the selectpicker('refresh') call runs in a separate tick of the event loop, after change detection has updated the DOM.

To be clear, I honestly doubt the performance penalty of your existing technique is anything to be concerned about, but the technique above can be added to your toolbox for situations when performance might actually be affected.

Sign up to request clarification or add additional context in comments.

6 Comments

That's a good point and looks cleaner. I also need to import 'rxjs/add/operator/do'.
Just tested it and it didn't work. Probably the view hasn't been initialized at the time of sending request.
@midnightStar, your theory might be right - the view might not be initialized yet. try moving that logic from ngOnInit to ngAfterViewInit
didn't work. I've checked this logic with every lifecycle hook. That's really weird, my solution works though might cause performance issues. Right now it doesn't.
actually, i think i understand what's happening. Your selectpicker() function works directly with the DOM and requires that the DOM be updated with the new list before it can work. At the point where we execute it in the do handler, we just got the new Account list, and change detection hasn't run yet to update the DOM with the new list. Try wrapping the selectPicker() in a setTimeout of 100ms, which will move the call to another tick of the event loop and hence give change detection time to run. if it works, i'll update the answer
|
2

Just simply add ngIf="accounts" in the upper <div> of the <select> tag like :

<div *ngIf="accounts">
   <select class="selectpicker" multiple>
      <option *ngFor="let account of accounts" >{{account.username}}</option>             
   </select>
</div>

Comments

1

You might use Subject.

let subject = new Rx.Subject();

constructor(private accountService : AccountService) {
  this.accountService.getAccounts().subscribe(subject);
  this.accounts = subject.asObservable();
}

 ngAfterViewInit() {
    subject.asObservable()
     .subscribe(x=> {
        $('.selectpicker').selectpicker();
    });
 }

<select class="selectpicker" multiple>
    <option *ngFor="let account of accounts | async" >
   {{account.username}}</option>             
 </select>

Comments

0

I figured I can call selectpicker('refresh') after I dynamically update the selectpicker. This is so far the easiest way to solve this problem.

ngAfterViewInit() {
    $('.selectpicker').selectpicker();
}

ngAfterViewChecked() {
    $('.selectpicker').selectpicker('refresh');
}

I have found it by looking at the wrapper library for React here.

Edit: This method will seem to work at first sight but it doesn't and it will refresh at the time you try to check some item in the select box and won't allow selecting more items. For working solution see snorkpete's anwser.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.