Cjolsen06
Cjolsen06

Reputation: 99

Angular - Dynamic ngDropdown not expanding when clicked

I have component that has N amount of filters, and for each of those filters, there are M amount of values. I am trying to dynamically display both of these, but it doesn't seem to be working.

I was able to test the values and see if I was using the correct syntax to retrieve the data and the code is able to get the data from the objects. However, when using the dynamic filters, it looks like nothing is being ran past the dropdown <button> tag.

You can look here at the StackBlitz project I made as a replica of my application. I have printed the filters object to the console in the form it is returned when I call getFilters from the HTML. As you can see, it is returning data, but none of the filterValues can be seen when the user clicks the dropdown.

I expect it to display each of the filterValues, but instead nothing happens when I click the dropdown button. If I remove the dynamic features from it, I am able to see the values in the dropdown, but I would like for it to be dynamic. Any help would be greatly appreciated.

The relevant template code:

<div ngbDropdown class="d-inline-block" *ngFor="let filter of getFilters()">
    <button class="btn btn-outline-primary" id="{{filter[0]-dropdown}}" ngbDropdownToggle>{{filter[1].displayName}}</button>
    <div ngbDropdownMenu attr.aria-labelledby="{{filter[0]-dropdown}}">
        <button ngbDropdownItem *ngFor="let val of filter[1].filterValues">{{val}}</button>
    </div>
</div>

and the relevant TypeScript code:

  getFilters() : [string, FilterItem][] {
    ...
    return Array.from(this.filters.entries()); 
  }

Upvotes: 2

Views: 452

Answers (1)

DWilches
DWilches

Reputation: 23015

The issue you're witnessing is caused because your getFilters() function is not idempotent.

Angular expects that if it invokes a method twice in a row, without user intervention, it will return the same value. If your function returns an array, it needs to return the same array twice. But in your code, you are returning each time a different array (even if it has the same content, it's still a different reference).

Try this implementation for the getFilters() method and you'll see the issue go away (I don't recommend you solve your problem like this, but it illustrates where the issue is):

  private cached;
  getFilters() : [string, FilterItem][] {
    if (!this.cached)
      this.cached = Array.from(this.filters.entries())
    return this.cached; 
  }

My recommendation

I wouldn't recommend caching the value like that, instead, you should place the code that changes the state of your application in a different part than the code that retrieves the state of your application, like so:

  constructor () {
    this.filters = ...
    this.filterEntries = Array.from(this.filters.entries());
  }

And then in your template:

<div ngbDropdown class="d-inline-block" *ngFor="let filter of filterEntries">

From the docs:

Angular docs tell you the following about side effects, I have bolded the most relevant text:

No visible side effects

A template expression should not change any application state other than the value of the target property.

In Angular terms, an idempotent expression always returns exactly the same thing until one of its dependent values changes.

If an idempotent expression returns a string or a number, it returns the same string or number when called twice in a row. If the expression returns an object, including an array, it returns the same object reference when called twice in a row.

Upvotes: 1

Related Questions