GovAlta / ui-components

ui-components contains the code you need to start building a user interface for Government of Alberta platforms and services.
Apache License 2.0
15 stars 24 forks source link

Dropdown: Dynamic options are not populated correctly #1980

Closed francisagyapong-goa closed 2 months ago

francisagyapong-goa commented 3 months ago

Describe the bug

The options for a dropdown with dynamic options are not populated correctly. Previous entries are retained when the options list is updated.

Provide Code

This example has two dropdowns, the options in the second dropdown depend on the selected option in the first:

<script lang="ts">
const childItems = {
  red: [
    { label: "Wine-Red", value: "wine" },
    { label: "Rose Red-Red", value: "rose-red" },
    { label: "Scarlet-Red", value: "scarlet" },
  ],
  green: [
    { label: "Seaweed-Green", value: "Seaweed" },
    { label: "Emerald-Green", value: "emerald" },
    { label: "Olive-Green", value: "olive" },
    { label: "Lime-Green", value: "lime" },
    { label: "Mint-Green", value: "mint" },
  ],
  blue: [
    { label: "Sky-Blue", value: "sky" },
    { label: "Cerulean-Blue", value: "Cerulean" },
    { label: "Lapis-Blue", value: "Lapis" },
    { label: "Denim-Blue", value: "Denim" },
  ],
}

let selections = {
  parent: null,
  child: null,
};

function itemSelected(item) {
  selections[item.detail.name] = item.detail.value;
}
</script>

<goa-form-item label="Colors">
  <goa-dropdown name="parent" on:_change={itemSelected} value={selections.parent}>
    <goa-dropdown-item value="red" label="Red"></goa-dropdown-item>
    <goa-dropdown-item value="green" label="Green"></goa-dropdown-item>
    <goa-dropdown-item value="blue" label="Blue"></goa-dropdown-item>
  </goa-dropdown>
</goa-form-item>

{#if selections.parent}
  <goa-form-item label="Shades" style="display: block; margin-top: 1rem;">
    <goa-dropdown name="child" on:_change={itemSelected} value={selections.child}>
      {#each childItems[selections.parent] as child}
        <goa-dropdown-item value={child.value} label={child.label}></goa-dropdown-item>
      {/each}
    </goa-dropdown>
  </goa-form-item>
{/if}

To Reproduce

To reproduce (using the provided example):

  1. Select an option from the first dropdown
    • the second dropdown appears with the correct options
  2. Select another option from the first dropdown
    • the the second dropdown's options are updated, but incorrectly. The second dropdown should now have 5 options (which it does, but they are not correct). The new options list still has the previous 3 options, and the last 2 options from the new options list is added on top

dropdown-dynamic-options-2024-07-03_10.36.41.webm

Relevant console log output

No response

What browsers are you seeing the problem on?

Firefox, Chrome, Edge, Mobile Android, Mobile iOS

Additional context

Using @agbov/web-components v1.22.0

Spark450 commented 3 months ago

I discussed this issue with @francisagyapong-goa to clarify:

This issue is not currently affecting their product because they are not on the "latest" version of the components. However, if they upgrade to the latest version, it would break the current functionality of their app.

ataboo commented 2 months ago

We're seeing similar issues with dropdown options being duplicated when generated dynamically:

"@abgov/angular-components": "^3.0.3",
"@abgov/design-tokens": "^1.2.0",
"@abgov/web-components": "^1.23.1",
import { Component } from '@angular/core';

@Component({
  selector: 'ui-test-dropdown',
  templateUrl: './test-dropdown.component.html',
  styleUrl: './test-dropdown.component.scss',
})
export class TestDropdownComponent {
  oneTwoThree = [
    { label: 'One', value: '1' },
    { label: 'Two', value: '2' },
    { label: 'Three', value: '3' },
  ];

  fourFiveSix = [
    { label: 'Four', value: '4' },
    { label: 'Five', value: '5' },
    { label: 'Six', value: '6' },
  ];

  ddOneOptions = [...this.oneTwoThree];

  ddTwoOptions = [...this.oneTwoThree];

  private switched = false;

  handleReverseClick() {
    this.ddTwoOptions = [...this.ddOneOptions.reverse()];
  }

  switchList() {
    this.ddTwoOptions = [...(this.switched ? this.oneTwoThree : this.fourFiveSix)];
    this.switched = !this.switched;
  }
}
<div class="container">
  <div>
    <label>This one has static items</label>
    <goa-dropdown>
      <goa-dropdown-item label="Item 1" value="1" />
      <goa-dropdown-item label="Item 2" value="2" />
      <goa-dropdown-item label="Item 3" value="3" />
    </goa-dropdown>
  </div>

  <div>
    <label>This one is foreach from a static array</label>
    <goa-dropdown>
      <goa-dropdown-item *ngFor="let o of ddOneOptions" [label]="o.label" [value]="o.value" />
    </goa-dropdown>
  </div>

  <div class="col">
    <label>This one has weird behaviour when changing the options dynamically</label>
    <goa-dropdown>
      <goa-dropdown-item *ngFor="let o of ddTwoOptions" [label]="o.label" [value]="o.value" />
    </goa-dropdown>

    <goa-button (_click)="handleReverseClick()">Reverse</goa-button>
    <goa-button (_click)="switchList()">Switch</goa-button>

    <ul>
      <li *ngFor="let o of ddTwoOptions">Label: {{ o.label }}, Value: {{ o.value }}</li>
    </ul>
  </div>
</div>

In this example, clicking reverse does not update the dropdown options though the ul will show properly.
image

Switching the list causes the options to be appended instead of replacing them. image

Change detector doesn't seem to do anything. As a workaround, we've added an ngIf to the dropdown and toggled it off and on withe a timeout after changing the options to force the re-render.

<goa-dropdown *ngIf="showTheDropdown" ...>
changeToNewDropdownOptions() {
  dropdownOptions = newOptions;
  showTheDropdown = false;
  setTimeout(() => {showTheDropdown = true}, 10);
}