Atomic Angular — Doc interne

Select

A single- or multiple-choice dropdown built on the accessible @angular/aria combobox/listbox primitives, with a fully custom trigger and option rendering when the default chrome doesn't fit.

A dropdown that behaves like a native <select> — keyboard navigable, aria-selected, popup positioned near the field — but accepts option and trigger content of any shape. SelectMenu (SelectComponent) covers both single and multiple selection from one component.

Concept — FormValueControl

Angular Signal Forms recognizes a component as a form control when it exposes a value model matching the FormValueControl<TValue> contract. SelectMenu implements it through its own value model, so [formField] works exactly like it would on a native input — no adapter needed.

Minimal example

A single-choice dropdown over a plain string list
fruit-select.component.ts
import { Component, signal } from "@angular/core";
import { SelectComponent } from "@sinequa/galactik";

@Component({
  selector: "fruit-select",
  imports: [SelectComponent],
  template: `
    <SelectMenu
      [options]="fruits"
      [(value)]="fruit"
      fieldLabel="Fruit"
      placeholder="Choose a fruit…"
      hint="Pick your favorite." />
  `,
})
export class FruitSelectComponent {
  fruits = ["Apple", "Banana", "Cherry", "Date", "Elderberry"];
  fruit = signal<string | null>(null);
}

How it works

click / Enter / Space expand arrow keys / typeahead click / Enter on an option onSelectionChange value.set(...) / [(value)] touch.emit autoUpdate / computePosition User Trigger (ngCombobox / SelectTriggerDirective) ngComboboxPopup + ngListbox (role=listbox) SelectComponent Host component state form.field (marked touched) @floating-ui/dom

The trigger and popup are the accessible @angular/aria combobox/listbox primitives — every role and ARIA state (role="combobox", aria-expanded, aria-haspopup="listbox" on the trigger; role="listbox", aria-activedescendant, aria-selected on the popup) comes from there, nothing to wire by hand. The popup is positioned with @floating-ui/dom (offset(4), flip(), shift({ padding: 8 })), its width bound to the trigger's, and reopening it scrolls the current selection into view automatically.

The row under the mouse becomes the active row (pointermove on the ngListbox, via activateOnHover), so only one row is highlighted at a time and the arrow keys carry on from it; a still pointer under a scrolling list changes nothing.

Single vs. multiple only changes the shape of value and whether the popup closes on pick: single mode (default) is TOption | null and closes the popup once a choice is made; multiple makes value an array, keeps the popup open across picks, and shows a check mark via the option template's selected context.

Recipes

Multiple selection

Toggling several options without closing the popup
fruits-multi-select.component.ts
import { Component, signal } from "@angular/core";
import { SelectComponent } from "@sinequa/galactik";

@Component({
  selector: "fruits-multi-select",
  imports: [SelectComponent],
  template: `
    <SelectMenu
      multiple
      [options]="fruits"
      [(value)]="selected"
      fieldLabel="Fruits"
      placeholder="Select fruits…"
      hint="Pick one or more — the popup stays open.">
      <ng-template #selectOption let-fruit let-selected="selected">
        <span class="select-option-body">
          <span class="select-option-label">{{ fruit }}</span>
        </span>
        @if (selected) { <span class="select-option-check">✓</span> }
      </ng-template>
    </SelectMenu>
  `,
})
export class FruitsMultiSelectComponent {
  fruits = ["Apple", "Banana", "Cherry", "Date", "Elderberry"];
  selected = signal<string[]>([]);
}

Object options — custom key, label and row

key/label default to identity/String(option), correct only for primitive options. Any object-shaped option needs explicit functions — otherwise the trigger prints [object Object] and selection comparisons break (see Pitfalls).

A country picker with a sub-label per row
country-select.component.ts
import { Component, signal } from "@angular/core";
import { SelectComponent } from "@sinequa/galactik";

interface Country {
  id: string;
  label: string;
  capital: string;
}

const COUNTRIES: Country[] = [
  { id: "fr", label: "France", capital: "Paris" },
  { id: "de", label: "Germany", capital: "Berlin" },
  { id: "es", label: "Spain", capital: "Madrid" },
];

@Component({
  selector: "country-select",
  imports: [SelectComponent],
  template: `
    <SelectMenu
      [options]="countries"
      [(value)]="selected"
      [key]="keyOf"
      [label]="labelOf"
      fieldLabel="Country"
      placeholder="Select a country…">
      <ng-template #selectOption let-country let-selected="selected">
        <span class="select-option-body">
          <span class="select-option-label">{{ country.label }}</span>
          <span class="select-option-sublabel">{{ country.capital }}</span>
        </span>
        @if (selected) { <span class="select-option-check">✓</span> }
      </ng-template>
    </SelectMenu>
  `,
})
export class CountrySelectComponent {
  countries = COUNTRIES;
  selected = signal<Country | null>(null);

  protected readonly keyOf = (c: Country) => c.id;
  protected readonly labelOf = (c: Country) => c.label;
}

A fully custom trigger

Project a [selectTrigger] element to replace the default trigger entirely — useful for a compact, chrome-less trigger such as a sort selector. placeholder and #selectValue no longer apply once a custom trigger is projected; render the current value yourself.

A chevron-only sort trigger
sort-select.component.ts
import { Component, signal } from "@angular/core";
import { SelectComponent, SelectTriggerDirective } from "@sinequa/galactik";

interface SortOption {
  id: string;
  label: string;
}

const SORT_OPTIONS: SortOption[] = [
  { id: "relevance", label: "Relevance" },
  { id: "date-desc", label: "Newest first" },
  { id: "date-asc", label: "Oldest first" },
];

@Component({
  selector: "sort-select",
  imports: [SelectComponent, SelectTriggerDirective],
  template: `
    <SelectMenu [options]="sortOptions" [(value)]="sort" [key]="keyOf" [label]="labelOf">
      <button selectTrigger type="button" class="select-trigger">
        Sort: {{ labelOf(sort()) }}
      </button>
      <ng-template #selectOption let-o let-selected="selected">
        <span class="select-option-label">{{ o.label }}</span>
        @if (selected) { <span class="select-option-check">✓</span> }
      </ng-template>
    </SelectMenu>
  `,
})
export class SortSelectComponent {
  sortOptions = SORT_OPTIONS;
  sort = signal<SortOption>(SORT_OPTIONS[0]);

  protected readonly keyOf = (o: SortOption) => o.id;
  protected readonly labelOf = (o: SortOption) => o.label;
}

Options

Prop

Type

Pitfalls

What's next

On this page