Select Menu

The <smart-select-menu> component renders a single-select menu with a placeholder, list of items (label/value, optional avatar/secondary text/status/disabled), an optional empty state slot, and a two-way value binding. It follows the Base + Standard + Wrapper pattern with an InjectionToken-based extension mechanism. The abstract SelectMenuBaseComponent defines the shared API — value (two-way ModelSignal<SelectMenuValue>), disabled, optional ISelectMenuOptions, cssClass (alias class), and a select(next) method that updates value while respecting the disabled state. SelectMenuStandardComponent is a barebones placeholder concrete implementation using a native <select> element. SelectMenuComponent is the public wrapper that renders SelectMenuStandardComponent by default and accepts a custom replacement via SELECT_MENU_STANDARD_COMPONENT_TOKEN.


Usage

<smart-select-menu [options]="options" [disabled]="disabled" [(value)]="plan" />

Components

SelectMenuComponent (<smart-select-menu>)

Main wrapper component. Renders SelectMenuStandardComponent by default. When SELECT_MENU_STANDARD_COMPONENT_TOKEN is provided, renders the injected component via NgComponentOutlet.

SelectMenuStandardComponent (<smart-select-menu-standard>)

Barebones placeholder concrete implementation using a native <select>. Renders an outer wrapper <div>, a <div class="select-menu">, and a <select> with one <option> per item; an optional placeholder is rendered as a disabled first option. Disabled items get the disabled attribute on their <option>. The <select> is disabled when disabled() is true. On change, the standard reads the raw string value, looks up the matching item by String(value) === String(item.value) (preserving numeric types), and calls select(). When items is empty and emptyTpl is provided, the empty template is rendered inside <div class="empty">. The external cssClass is applied to the root wrapper. It does not include any Tailwind UI styling — it exists solely as the default structural placeholder until a custom implementation is registered through the token.

SelectMenuBaseComponent (abstract)

Abstract base directive for extending custom select menu implementations. Exposes:

  • value: ModelSignal<SelectMenuValue> (default null) — two-way bindable
  • disabled: InputSignal<boolean> (default false)
  • options: InputSignal<ISelectMenuOptions | undefined>
  • cssClass: InputSignal<string> (alias class)
  • select(next: SelectMenuValue): void — updates value only when not disabled

SelectMenuValue is string | number | null.

API

Inputs

InputTypeDefaultDescription
valueModelSignal<SelectMenuValue>nullCurrently selected value (two-way bindable)
disabledInputSignal<boolean>falseWhether the menu is disabled
optionsInputSignal<ISelectMenuOptions | undefined>-Optional configuration (items, placeholder, variant, slots)
classInputSignal<string>''External CSS classes (alias for cssClass)

ISelectMenuOptions

The default SelectMenuStandardComponent renders value, disabled, placeholder, items and emptyTpl. variant, avatarUrl, iconTpl, secondary, and status are hints/data for custom implementations registered via the token.

SELECT_MENU_STANDARD_COMPONENT_TOKEN

InjectionToken that allows replacing the default SelectMenuStandardComponent with a custom implementation. Provide a Type<SelectMenuBaseComponent> to override.

Extending the base class

import {
  ChangeDetectionStrategy,
  Component,
  computed,
  input,
  signal,
  ViewEncapsulation,
} from '@angular/core';

import {
  ISelectMenuOptions,
  SELECT_MENU_STANDARD_COMPONENT_TOKEN,
  SelectMenuBaseComponent,
  SelectMenuComponent,
  SelectMenuValue,
} from '@smartsoft001/angular';

@Component({
  selector: 'docs-custom-select-menu',
  template: `
    <div [class]="containerClasses()">
      <button
        type="button"
        class="docs-select-menu__trigger"
        [disabled]="disabled()"
        [attr.aria-expanded]="expanded()"
        [attr.aria-label]="options()?.ariaLabel"
        (click)="toggle()"
      >
        {{ currentLabel() ?? options()?.placeholder ?? 'Select' }}
      </button>

      @if (expanded()) {
        <ul class="docs-select-menu__list" role="listbox">
          @for (item of items(); track item.value) {
            <li
              role="option"
              class="docs-select-menu__option"
              [attr.aria-selected]="value() === item.value"
              [attr.aria-disabled]="item.disabled ?? false"
              (click)="pick(item.value, item.disabled ?? false)"
            >
              {{ item.label }}
            </li>
          }
        </ul>
      }
    </div>
  `,
  encapsulation: ViewEncapsulation.None,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class CustomSelectMenuComponent extends SelectMenuBaseComponent {
  // NgComponentOutlet passes 'cssClass' by canonical name, not the 'class' alias.
  override cssClass = input<string>('');

  readonly expanded = signal(false);

  readonly items = computed(() => this.options()?.items ?? []);

  readonly currentLabel = computed(
    () => this.items().find((item) => item.value === this.value())?.label,
  );

  readonly containerClasses = computed(() => {
    const classes = ['docs-select-menu'];
    const extra = this.cssClass();
    if (extra) classes.push(extra);
    return classes.join(' ');
  });

  toggle(): void {
    if (this.disabled()) return;
    this.expanded.update((open) => !open);
  }

  pick(next: SelectMenuValue, itemDisabled: boolean): void {
    if (itemDisabled) return;
    // select() is the base class API: it ignores the call while disabled.
    this.select(next);
    this.expanded.set(false);
  }
}

@Component({
  selector: 'docs-select-menu-custom-example',
  changeDetection: ChangeDetectionStrategy.OnPush,
  imports: [SelectMenuComponent],
  // The token swaps the standard native select for the custom listbox
  // everywhere below this component, so consumers keep writing
  // `<smart-select-menu>`.
  providers: [
    {
      provide: SELECT_MENU_STANDARD_COMPONENT_TOKEN,
      useValue: CustomSelectMenuComponent,
    },
  ],
  // NgComponentOutlet forwards `value` as a plain input, so the initial value
  // reaches the custom component but its own selections stay inside it.
  template: ` <smart-select-menu [(value)]="selected" [options]="options" /> `,
})
export class SelectMenuCustomExampleComponent {
  readonly selected = signal<SelectMenuValue>(null);

  readonly options: ISelectMenuOptions = {
    placeholder: 'Choose a country',
    ariaLabel: 'Country',
    items: [
      { value: 'pl', label: 'Poland' },
      { value: 'de', label: 'Germany' },
      { value: 'us', label: 'United States' },
    ],
  };
}

Source

The component lives in packages/shared/angular/src/lib/components/select-menu and is documented for Claude Code by the angular-components-select-menu skill.