Dynamically add an item to a dropdown list

type Task = {
  value: string;
  label: string;
  mount: GoabDropdownItemMountType;
};

const DEFAULT_TASKS: Task[] = [
  { label: "Finish Report", value: "finish-report", mount: "append" },
  { label: "Attend Meeting", value: "attend-meeting", mount: "append" },
  { label: "Reply Emails", value: "reply-emails", mount: "append" },
];

const [tasks, setTasks] = useState<Task[]>(DEFAULT_TASKS);
const [newTask, setNewTask] = useState<string>("");
const [mountType, setMountType] = useState<string>("append");
const [selectedTask, setSelectedTask] = useState<string>("");
const [taskError, setTaskError] = useState<boolean>(false);
const [isReset, setIsReset] = useState<boolean>(false);

function onMountTypeChange(value: string | undefined) {
  setMountType(value as string);
}

function addTask() {
  if (newTask === "") {
    setTaskError(true);
    return;
  }
  setTaskError(false);

  const task: Task = {
    label: newTask,
    value: newTask.toLowerCase().replace(" ", "-"),
    mount: mountType as GoabDropdownItemMountType,
  };
  setTasks([...tasks, task]);
  setNewTask("");
  setIsReset(false);
}

function reset() {
  setTasks(DEFAULT_TASKS);
  setMountType("append");
  setNewTask("");
  setSelectedTask("");
  setTaskError(false);
  setIsReset(true);
}
<GoabFormItem
  requirement="required"
  label="Name of item"
  error={taskError ? "Please enter item name" : undefined}
  helpText="Add an item to the dropdown list below"
>
  <GoabInput
    onChange={(event: GoabInputOnChangeDetail) => setNewTask(event.value)}
    name="item"
    value={newTask}
  />
</GoabFormItem>

<GoabFormItem mt="l" label="Add to">
  <GoabRadioGroup
    name="mountType"
    onChange={(event: GoabRadioGroupOnChangeDetail) =>
      onMountTypeChange(event.value)
    }
    value={mountType}
    orientation="horizontal"
  >
    <GoabRadioItem value="prepend" label="Start" />
    <GoabRadioItem value="append" label="End" />
  </GoabRadioGroup>
</GoabFormItem>

<GoabButtonGroup alignment="start" gap="relaxed" mt="l">
  <GoabButton type="primary" onClick={addTask}>
    Add new item
  </GoabButton>
  <GoabButton type="tertiary" onClick={reset}>
    Reset list
  </GoabButton>
</GoabButtonGroup>

<GoabDivider mt="l" />

<GoabFormItem mt="l" label="All items">
  <div style={{ width: isReset ? "320px" : "auto" }}>
    <GoabDropdown
      key={tasks.length}
      onChange={(event: GoabDropdownOnChangeDetail) =>
        setSelectedTask(event.value as string)
      }
      value={selectedTask}
      name="selectedTask"
    >
      {tasks.map((task) => (
        <GoabDropdownItem
          key={task.value}
          value={task.value}
          mountType={task.mount}
          label={task.label}
        />
      ))}
    </GoabDropdown>
  </div>
</GoabFormItem>
defaultTasks: Task[] = [
  { label: "Finish Report", value: "finish-report", mount: "append" },
  { label: "Attend Meeting", value: "attend-meeting", mount: "append" },
  { label: "Reply Emails", value: "reply-emails", mount: "append" },
];

tasks: Task[] = [...this.defaultTasks];
newTask = "";
mountType: GoabDropdownItemMountType = "append";
selectedTask = "";
taskError = false;
renderTrigger = true;

onMountTypeChange(event: GoabRadioGroupOnChangeDetail): void {
  this.mountType = event.value as GoabDropdownItemMountType;
}

onNewTaskChange(event: GoabInputOnChangeDetail): void {
  this.newTask = event.value;
  this.taskError = false;
}

onSelectedTaskChange(event: GoabDropdownOnChangeDetail): void {
  this.selectedTask = event.value as string;
}

addTask(): void {
  if (this.newTask === "") {
    this.taskError = true;
    return;
  }
  this.taskError = false;

  const task: Task = {
    label: this.newTask,
    value: this.newTask.toLowerCase().replace(" ", "-"),
    mount: this.mountType,
  };
  this.tasks =
    this.mountType === "prepend" ? [task, ...this.tasks] : [...this.tasks, task];
  this.newTask = "";
}

reset(): void {
  this.newTask = "";
  this.selectedTask = "";
  this.taskError = false;
  this.tasks = [...this.defaultTasks];
  this.forceRerender();
}

forceRerender(): void {
  this.renderTrigger = false;
  setTimeout(() => {
    this.renderTrigger = true;
  }, 0);
}

trackByFn(index: number, item: Task): string {
  return item.value;
}
<goab-form-item
  requirement="required"
  label="Name of item"
  [error]="taskError ? 'Please enter item name' : undefined"
  helpText="Add an item to the dropdown list below"
>
  <goab-input name="item" [value]="newTask" (onChange)="onNewTaskChange($event)">
  </goab-input>
</goab-form-item>

<goab-form-item mt="l" label="Add to">
  <goab-radio-group
    name="mountType"
    [value]="mountType"
    orientation="horizontal"
    (onChange)="onMountTypeChange($event)"
  >
    <goab-radio-item value="prepend" label="Start"></goab-radio-item>
    <goab-radio-item value="append" label="End"></goab-radio-item>
  </goab-radio-group>
</goab-form-item>

<goab-button-group alignment="start" gap="relaxed" mt="l">
  <goab-button type="primary" (onClick)="addTask()"> Add new item </goab-button>
  <goab-button type="tertiary" (onClick)="reset()"> Reset list </goab-button>
</goab-button-group>

<goab-divider mt="l"></goab-divider>

<goab-form-item mt="l" label="All items">
  <ng-container *ngIf="renderTrigger">
    <goab-dropdown
      [value]="selectedTask"
      name="selectedTask"
      (onChange)="onSelectedTaskChange($event)"
    >
      @for (task of tasks; track task.value) {
      <goab-dropdown-item
        [value]="task.value"
        [mountType]="task.mount"
        [label]="task.label"
      >
      </goab-dropdown-item>
      }
    </goab-dropdown>
  </ng-container>
</goab-form-item>
const itemInput = document.getElementById("item-input");
const itemFormItem = document.getElementById("item-form-item");
const mountTypeGroup = document.getElementById("mount-type");
const addBtn = document.getElementById("add-btn");
const resetBtn = document.getElementById("reset-btn");
const dropdown = document.getElementById("task-dropdown");

let mountType = "append";
let newTask = "";

const defaultItems = [
  { value: "finish-report", label: "Finish Report" },
  { value: "attend-meeting", label: "Attend Meeting" },
  { value: "reply-emails", label: "Reply Emails" },
];

mountTypeGroup.addEventListener("_change", (e) => {
  mountType = e.detail.value;
});

itemInput.addEventListener("_change", (e) => {
  newTask = e.detail.value;
  itemFormItem.removeAttribute("error");
});

addBtn.addEventListener("_click", () => {
  if (newTask === "") {
    itemFormItem.setAttribute("error", "Please enter item name");
    return;
  }

  const newItem = document.createElement("goa-dropdown-item");
  newItem.setAttribute("value", newTask.toLowerCase().replace(" ", "-"));
  newItem.setAttribute("label", newTask);
  newItem.setAttribute("mount", mountType);
  dropdown.appendChild(newItem);

  itemInput.value = "";
  newTask = "";
});

resetBtn.addEventListener("_click", () => {
  dropdown.innerHTML = "";
  defaultItems.forEach((item) => {
    const dropdownItem = document.createElement("goa-dropdown-item");
    dropdownItem.setAttribute("value", item.value);
    dropdownItem.setAttribute("label", item.label);
    dropdown.appendChild(dropdownItem);
  });
  itemInput.value = "";
  newTask = "";
  itemFormItem.removeAttribute("error");
});
<goa-form-item
  version="2"
  id="item-form-item"
  requirement="required"
  label="Name of item"
  helptext="Add an item to the dropdown list below"
>
  <goa-input version="2" id="item-input" name="item"></goa-input>
</goa-form-item>

<goa-form-item version="2" mt="l" label="Add to">
  <goa-radio-group
    version="2"
    id="mount-type"
    name="mountType"
    value="append"
    orientation="horizontal"
  >
    <goa-radio-item value="prepend" label="Start"></goa-radio-item>
    <goa-radio-item value="append" label="End"></goa-radio-item>
  </goa-radio-group>
</goa-form-item>

<goa-button-group alignment="start" gap="relaxed" mt="l">
  <goa-button version="2" id="add-btn" type="primary">Add new item</goa-button>
  <goa-button version="2" id="reset-btn" type="tertiary">Reset list</goa-button>
</goa-button-group>

<goa-divider mt="l"></goa-divider>

<goa-form-item version="2" mt="l" label="All items">
  <goa-dropdown version="2" id="task-dropdown" name="selectedTask">
    <goa-dropdown-item value="finish-report" label="Finish Report"></goa-dropdown-item>
    <goa-dropdown-item value="attend-meeting" label="Attend Meeting"></goa-dropdown-item>
    <goa-dropdown-item value="reply-emails" label="Reply Emails"></goa-dropdown-item>
  </goa-dropdown>
</goa-form-item>

Allow users to add new items to a dropdown list dynamically.

When to use

Use this pattern when:

  • Users need to add custom options to a predefined list
  • The list of options can grow based on user input
  • You want to provide flexibility while maintaining structure

Considerations

  • Use the mountType prop to control where new items appear (prepend or append)
  • Validate input before adding to prevent empty or duplicate entries
  • Provide a reset option to restore the original list
  • Show clear feedback when items are added successfully
View old example docs