Select Picker

A multi-select that shows chips on one line and picks in a searchable checkbox dialog.

import { … } from '@inlaylab/ivorie/extras/select-picker'
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { IV_SELECT_PICKER_IMPORTS } from '@inlaylab/ivorie/extras/select-picker';

@Component({
  selector: 'demo-select-picker',
  imports: [FormsModule, IV_SELECT_PICKER_IMPORTS],
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    <div class="w-72">
      <iv-select-picker
        [items]="fruits"
        [(ngModel)]="selected"
        placeholder="Select fruits"
        dialogTitle="Select fruits"
        searchPlaceholder="Search fruits"
        valueColumnHeader="Fruit"
      />
    </div>
  `,
})
export default class SelectPickerDemo {
  readonly fruits = ['Apple', 'Banana', 'Cherry', 'Grape', 'Mango', 'Orange'];
  readonly selected = signal<string[]>(['Apple', 'Mango']);
}

Installation

CLInpm
npx ivorie add select-picker

Use it for multi-select from a known list. For free-text tag entry, use the combobox with searchSubmit.

Usage

import { IV_SELECT_PICKER_IMPORTS } from '@inlaylab/ivorie/extras/select-picker';
<iv-select-picker
  [items]="groups"
  [valueFn]="groupName"
  [labelFn]="groupName"
  [(ngModel)]="selected"
  placeholder="Select groups"
/>
readonly groupName = (group: Group): string => group.name;

IV_SELECT_PICKER_IMPORTS holds only the trigger. Never add IvSelectPickerDialog to an NgModule imports. IvDialog creates it at runtime, and listing it fails the AOT build with NG3004.

Examples

Form control

Value: Editor

import { ChangeDetectionStrategy, Component } from '@angular/core';
import { FormControl, ReactiveFormsModule } from '@angular/forms';
import { IV_SELECT_PICKER_IMPORTS } from '@inlaylab/ivorie/extras/select-picker';

@Component({
  selector: 'demo-select-picker-form-control',
  imports: [ReactiveFormsModule, IV_SELECT_PICKER_IMPORTS],
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    <div class="flex w-72 flex-col gap-2">
      <iv-select-picker [items]="roles" [formControl]="roles$" placeholder="Select roles" ariaLabel="Roles" />
      <p class="text-sm text-muted-foreground">Value: {{ roles$.value.join(', ') || 'none' }}</p>
    </div>
  `,
})
export default class SelectPickerFormControlDemo {
  readonly roles = ['Admin', 'Editor', 'Viewer', 'Billing'];
  readonly roles$ = new FormControl<string[]>(['Editor'], { nonNullable: true });
}

Description column

import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { IV_SELECT_PICKER_IMPORTS } from '@inlaylab/ivorie/extras/select-picker';

interface Team {
  id: string;
  name: string;
  about: string;
}

@Component({
  selector: 'demo-select-picker-description-column',
  imports: [FormsModule, IV_SELECT_PICKER_IMPORTS],
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    <div class="w-72">
      <iv-select-picker
        [items]="teams"
        [valueFn]="id"
        [labelFn]="name"
        [descriptionFn]="about"
        [(ngModel)]="selected"
        placeholder="Select teams"
        dialogTitle="Select teams"
        valueColumnHeader="Team"
        descriptionColumnHeader="About"
      />
    </div>
  `,
})
export default class SelectPickerDescriptionColumnDemo {
  readonly teams: Team[] = [
    { id: 'eng', name: 'Engineering', about: 'Builds the product' },
    { id: 'design', name: 'Design', about: 'Shapes the experience' },
    { id: 'support', name: 'Support', about: 'Helps customers' },
  ];
  readonly selected = signal<string[]>(['eng']);

  readonly id = (team: Team) => team.id;
  readonly name = (team: Team) => team.name;
  readonly about = (team: Team) => team.about;
}

Minimum selection

Save stays disabled until one is checked.

import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { IV_SELECT_PICKER_IMPORTS } from '@inlaylab/ivorie/extras/select-picker';

@Component({
  selector: 'demo-select-picker-minimum-selection',
  imports: [FormsModule, IV_SELECT_PICKER_IMPORTS],
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    <div class="flex w-72 flex-col gap-2">
      <iv-select-picker
        [items]="channels"
        [(ngModel)]="selected"
        [minSelection]="1"
        placeholder="Select channels"
        dialogDescription="Pick at least one channel."
      />
      <p class="text-sm text-muted-foreground">Save stays disabled until one is checked.</p>
    </div>
  `,
})
export default class SelectPickerMinimumSelectionDemo {
  readonly channels = ['Email', 'SMS', 'Push', 'Slack'];
  readonly selected = signal<string[]>(['Email']);
}

API

Every part also accepts class.

iv-select-picker

The value is string[]. It works with ngModel and formControlName.

Name Type Default Description
items T[] [] Full option list. Filtering is client-side.
valueFn (item: T) => string String(item) Item identity. This is what gets stored.
labelFn (item: T) => string String(item) Display text for chips and the table.
descriptionFn (item: T) => string none Adds a second, searchable table column.
placeholder string 'Select…' Shown when nothing is selected.
dialogTitle string 'Select items' Dialog title.
dialogDescription string '' Optional dialog sub-header.
searchPlaceholder string 'Search' Placeholder of the search box.
valueColumnHeader string 'Name' Header of the label column.
descriptionColumnHeader string '' Header of the description column. Needs descriptionFn.
emptyMessage string 'No data found' Shown when no row matches.
minSelection number 0 Save stays disabled below this count.
disabled boolean false Also set by the form through setDisabledState.
ariaLabel string '' Set it when there is no <label ivLabel>.
id string | null null Goes on the inner button, so <label for> works.

Outputs:

Name Payload Description
selectionChange string[] Emits the new selection on Save. The form control gets the same value.

Accessibility

  • The whole control is one <button> with aria-haspopup="dialog" and aria-expanded.
  • Set ariaLabel, or link a <label> with id.
  • The dialog traps focus. Escape acts as Cancel; a backdrop click does nothing (disableBackdropClose), so an accidental click outside doesn't discard the selection.
  • The offscreen chip measuring row is aria-hidden.

Notes

  • Assign a new items array. An in-place push is not seen.
  • Chips are measured, not guessed. At least one chip always shows. The rest collapse into +N more.
  • Cancel discards. The dialog reports back only on Save.
  • Save order follows items order, not click order.
  • Select-all applies to the filtered rows only.
  • A stored value with no matching item still shows as a chip with its raw value.
  • It opens as a nested dialog when the host is a dialog.