npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

list-selection-input

v15.0.5

Published

This is an Angular Module containing Components/Services using Material

Readme

List Selection Input Component

Overview

The list-selection-input library provides a Material Design dropdown selection component that allows users to select from a list of options in a space-efficient manner. It supports both single and multiple selection modes, validation limits, disabled items, and works with both string and object data formats. Built with Angular Material select components and implementing ControlValueAccessor for seamless form integration.

Core Capabilities

📋 Dropdown List Selection Interface

  • Single/Multiple Selection: Support for both single selection and multi-select modes
  • Form Validation: Built-in min/max selection validation with error handling
  • Flexible Data Support: Handles both string arrays and complex object arrays
  • Disabled State Support: Mark individual items as non-selectable
  • Material Design: Built on Angular Material select foundation
  • Form Control Integration: Implements ControlValueAccessor for reactive forms
  • Validation Integration: Native Angular validation system compatibility
  • Space Efficient: Dropdown interface saves screen space

🔧 Features

ControlValueAccessor Implementation - Works with Angular forms
Material Design Integration - Uses Angular Material components
Single & Multiple Selection - Flexible selection modes
Min/Max Validation - Configurable selection limits
Disabled Items - Mark individual options as non-selectable
Flexible Data Types - Support for strings and objects
Pre-selection - Initialize with selected items
Form Validation - Native validation integration
Dropdown Interface - Space-efficient selection display

Key Benefits

| Feature | Description | |---------|-------------| | Space Efficient | Compact dropdown interface vs. visible lists | | Flexible Selection | Support for single or multiple item selection | | Rich Validation | Built-in min/max selection limits with error states | | Data Format Support | Works with simple strings or complex objects | | State Management | Disabled states and pre-selection capabilities | | Form Integration | Seamless Angular form control and validation integration |


Summary

The list-selection-input library provides a space-efficient dropdown selection component with comprehensive form integration, validation support, and multiple selection capabilities for Angular applications.


Quick Start Guide

Installation & Setup (2 minutes)

1. Import Module

// app.module.ts
import { ListSelectionInputModule } from 'list-selection-input';

@NgModule({
  imports: [
    ListSelectionInputModule
  ]
})
export class AppModule { }

2. No Module Configuration Required

The ListSelectionInputModule does not require global configuration. Components can be used immediately after module import.

Quick Examples

Example 1: Basic Single Selection

import { Component } from '@angular/core';
import { FormControl } from '@angular/forms';

@Component({
  selector: 'app-basic-list-select',
  template: `
    <app-list-selection-input
      [formControl]="selectionControl"
      [data]="options">
    </app-list-selection-input>
    
    <div>Selected: {{ selectionControl.value }}</div>
  `
})
export class BasicListSelectComponent {
  selectionControl = new FormControl();
  
  options = ['Option 1', 'Option 2', 'Option 3', 'Option 4'];
}

Example 2: Multiple Selection with Validation

import { Component } from '@angular/core';
import { FormControl, Validators } from '@angular/forms';

@Component({
  selector: 'app-multi-list-select',
  template: `
    <app-list-selection-input
      [formControl]="multiControl"
      [data]="skills"
      [multiple]="true"
      [minSelection]="2"
      [maxSelection]="4"
      label="Select Skills"
      placeholder="Choose your skills">
    </app-list-selection-input>
    
    <div class="errors" *ngIf="multiControl.errors && multiControl.touched">
      <div *ngIf="multiControl.hasError('minRequired')">
        Please select at least 2 skills
      </div>
      <div *ngIf="multiControl.hasError('maxExceeded')">
        You can select maximum 4 skills
      </div>
    </div>
  `,
  styles: [`
    .errors {
      color: #f44336;
      font-size: 0.875rem;
      margin-top: 0.5rem;
    }
  `]
})
export class MultiListSelectComponent {
  multiControl = new FormControl([], [
    Validators.required,
    Validators.minLength(2),
    Validators.maxLength(4)
  ]);
  
  skills = [
    'JavaScript', 'TypeScript', 'Python', 'Java', 'C#', 'PHP', 'Ruby', 'Go',
    'Rust', 'Swift', 'Kotlin', 'Dart', 'Scala', 'Clojure', 'Elixir', 'Haskell'
  ];
}

Example 3: Object Data with Disabled Items

import { Component } from '@angular/core';
import { FormControl } from '@angular/forms';

@Component({
  selector: 'app-object-list-select',
  template: `
    <app-list-selection-input
      [formControl]="userControl"
      [data]="userOptions"
      label="Assign User"
      placeholder="Select a user"
      displayField="label">
    </app-list-selection-input>
    
    <div *ngIf="userControl.value" class="user-info">
      <h4>{{ userControl.value.label }}</h4>
      <p>{{ userControl.value.email }}</p>
      <p>Role: {{ userControl.value.role }}</p>
    </div>
  `,
  styles: [`
    .user-info {
      margin-top: 1rem;
      padding: 1rem;
      border: 1px solid #ddd;
      border-radius: 4px;
      background: #f9f9f9;
    }
    .user-info h4 {
      margin: 0 0 0.5rem 0;
      color: #333;
    }
    .user-info p {
      margin: 0.25rem 0;
      font-size: 0.9rem;
      color: #666;
    }
  `]
})
export class ObjectListSelectComponent {
  userControl = new FormControl();
  
  userOptions = [
    { value: 'user1', label: 'John Doe', email: '[email protected]', role: 'Admin', disabled: false },
    { value: 'user2', label: 'Jane Smith', email: '[email protected]', role: 'Editor', disabled: false },
    { value: 'user3', label: 'Bob Johnson', email: '[email protected]', role: 'Viewer', disabled: true },
    { value: 'user4', label: 'Alice Brown', email: '[email protected]', role: 'Editor', disabled: false },
    { value: 'user5', label: 'Charlie Wilson', email: '[email protected]', role: 'Admin', disabled: false }
  ];
}

Example 4: Multi-Select with Pre-selection

import { Component } from '@angular/core';
import { FormControl } from '@angular/forms';

@Component({
  selector: 'app-pre-selected-list',
  template: `
    <app-list-selection-input
      [formControl]="preSelectedControl"
      [data]="preSelectedOptions"
      [multiple]="true"
      label="Favorite Languages"
      placeholder="Select your favorite programming languages">
    </app-list-selection-input>
    
    <div class="selection-info">
      Selected ({{ preSelectedControl.value?.length || 0 }}): 
      {{ preSelectedControl.value?.join(', ') || 'None' }}
    </div>
  `,
  styles: [`
    .selection-info {
      margin-top: 1rem;
      padding: 0.75rem;
      background: #e8f5e8;
      border: 1px solid #4caf50;
      border-radius: 4px;
      color: #2e7d32;
      font-size: 0.9rem;
    }
  `]
})
export class PreSelectedListComponent {
  preSelectedControl = new FormControl(['JavaScript', 'TypeScript']);
  
  preSelectedOptions = [
    { value: 'JavaScript', label: 'JavaScript', selected: true },
    { value: 'TypeScript', label: 'TypeScript', selected: true },
    { value: 'Python', label: 'Python', selected: false },
    { value: 'Java', label: 'Java', selected: false },
    { value: 'C#', label: 'C#', selected: false },
    { value: 'PHP', label: 'PHP', selected: false },
    { value: 'Ruby', label: 'Ruby', selected: false },
    { value: 'Go', label: 'Go', selected: false }
  ];
}

Example 5: Form Integration with Dynamic Options

import { Component } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';

@Component({
  selector: 'app-dynamic-list-form',
  template: `
    <form [formGroup]="dynamicForm">
      <app-list-selection-input
        formControlName="category"
        [data]="categoryOptions"
        label="Product Category"
        placeholder="Select a category"
        [required]="true">
      </app-list-selection-input>
      
      <app-list-selection-input
        formControlName="subcategories"
        [data]="subcategoryOptions"
        label="Subcategories"
        placeholder="Select subcategories"
        [multiple]="true"
        [minSelection]="1"
        [maxSelection]="3"
        [disabled]="!dynamicForm.get('category')?.value">
      </app-list-selection-input>
    </form>
    
    <div class="form-status">
      <div>Form Valid: {{ dynamicForm.valid }}</div>
      <div>Category: {{ dynamicForm.get('category')?.value }}</div>
      <div>Subcategories: {{ dynamicForm.get('subcategories')?.value?.length || 0 }} selected</div>
    </div>
    
    <div class="controls">
      <button (click)="resetForm()">Reset</button>
      <button (click)="setDefaults()">Set Defaults</button>
      <button (click)="submitForm()" [disabled]="dynamicForm.invalid">Submit</button>
    </div>
  `,
  styles: [`
    form {
      display: flex;
      flex-direction: column;
      gap: 1rem;
      max-width: 400px;
    }
    .form-status {
      margin-top: 1rem;
      padding: 1rem;
      background: #f5f5f5;
      border-radius: 4px;
      font-family: monospace;
      font-size: 0.9rem;
    }
    .controls {
      margin-top: 1rem;
      display: flex;
      gap: 0.5rem;
    }
    button {
      padding: 0.5rem 1rem;
      border: 1px solid #ccc;
      border-radius: 4px;
      background: white;
      cursor: pointer;
    }
    button:disabled {
      opacity: 0.5;
      cursor: not-allowed;
    }
  `]
})
export class DynamicListFormComponent {
  dynamicForm: FormGroup;
  
  categoryOptions = [
    'Electronics', 'Clothing', 'Books', 'Home & Garden', 'Sports & Outdoors',
    'Automotive', 'Health & Beauty', 'Toys & Games', 'Food & Beverages'
  ];
  
  subcategoryOptions: string[] = [];

  constructor(private fb: FormBuilder) {
    this.dynamicForm = this.fb.group({
      category: ['', Validators.required],
      subcategories: [[], [Validators.required, Validators.minLength(1), Validators.maxLength(3)]]
    });

    // Watch for category changes to update subcategories
    this.dynamicForm.get('category')?.valueChanges.subscribe(category => {
      this.updateSubcategories(category);
      this.dynamicForm.get('subcategories')?.reset();
    });
  }

  updateSubcategories(category: string) {
    const subcategoryMap: { [key: string]: string[] } = {
      'Electronics': ['Smartphones', 'Laptops', 'Tablets', 'Headphones', 'Cameras'],
      'Clothing': ['Men\'s Wear', 'Women\'s Wear', 'Kids\' Wear', 'Accessories', 'Footwear'],
      'Books': ['Fiction', 'Non-Fiction', 'Educational', 'Children', 'Comics'],
      'Home & Garden': ['Furniture', 'Decor', 'Kitchen', 'Tools', 'Plants'],
      'Sports & Outdoors': ['Exercise', 'Outdoor Gear', 'Sports Equipment', 'Team Sports', 'Water Sports']
    };
    
    this.subcategoryOptions = subcategoryMap[category] || [];
  }

  resetForm() {
    this.dynamicForm.reset();
    this.subcategoryOptions = [];
  }

  setDefaults() {
    this.dynamicForm.patchValue({
      category: 'Electronics',
      subcategories: ['Smartphones', 'Laptops']
    });
  }

  submitForm() {
    if (this.dynamicForm.valid) {
      console.log('Form submitted:', this.dynamicForm.value);
      alert('Form submitted successfully!');
    }
  }
}

Component API

Inputs

| Input | Type | Description | Default | | :--- | :--- | :--- | :--- | | data | any[] \| string[] | Array of options to select from | (Required) | | multiple | boolean | Allow multiple selections | false | | label | string | Label text for the select field | undefined | | placeholder | string | Placeholder text for the select field | undefined | | displayField | string | Property name to display for object arrays | undefined | | minSelection | number | Minimum number of selections required | 0 | | maxSelection | number | Maximum number of selections allowed | 0 | | required | boolean | Whether selection is required | false | | disabled | boolean | Whether the select is disabled | false |

Outputs

| Output | Type | Description | |--------|------|-------------| | selectionChange | EventEmitter<any[]> | Emits array of selected values when selection changes |


Form Integration (ControlValueAccessor)

The component implements Angular's ControlValueAccessor interface for seamless form integration.

ControlValueAccessor Implementation

// writeValue(value: any[]): void
// Sets the selected values
writeValue(value: any[]): void {
  this.selectedValues = value || [];
}

// registerOnChange(fn: any): void
// Registers a callback for value changes
registerOnChange(fn: any): void {
  this.onChange = fn;
}

// registerOnTouched(fn: any): void
// Registers a callback for touch events
registerOnTouched(fn: any): void {
  this.onTouch = fn;
}

// setDisabledState(isDisabled: boolean): void
// Sets disabled state
setDisabledState?(isDisabled: boolean): void {
  this.disabled = isDisabled;
}

Form Integration Examples

Reactive Forms

import { Component } from '@angular/core';
import { FormControl, FormGroup, Validators } from '@angular/forms';

@Component({
  selector: 'app-reactive-list-form',
  template: `
    <form [formGroup]="listForm">
      <app-list-selection-input
        formControlName="country"
        [data]="countries"
        label="Country"
        placeholder="Select a country"
        [required]="true">
      </app-list-selection-input>
      
      <app-list-selection-input
        formControlName="languages"
        [data]="languages"
        [multiple]="true"
        label="Programming Languages"
        placeholder="Select languages"
        [minSelection]="1"
        [maxSelection]="5">
      </app-list-selection-input>
    </form>
  `
})
export class ReactiveListFormComponent {
  listForm = new FormGroup({
    country: new FormControl('', Validators.required),
    languages: new FormControl([], [Validators.required, Validators.minLength(1), Validators.maxLength(5)])
  });
  
  countries = ['United States', 'Canada', 'United Kingdom', 'Germany', 'France', 'Japan'];
  languages = ['JavaScript', 'Python', 'Java', 'C#', 'PHP', 'Ruby', 'Go', 'Rust'];
}

Template-Driven Forms

import { Component } from '@angular/core';

@Component({
  selector: 'app-template-list-form',
  template: `
    <app-list-selection-input
      [(ngModel)]="selectedValue"
      name="templateSelection"
      [data]="templateOptions"
      label="Template Selection"
      placeholder="Choose an option"
      [multiple]="true"
      required>
    </app-list-selection-input>
    
    <div *ngIf="selectedValue">
      Selected: {{ selectedValue.join(', ') }}
    </div>
  `
})
export class TemplateListFormComponent {
  selectedValue: string[] = [];
  
  templateOptions = ['Option A', 'Option B', 'Option C', 'Option D', 'Option E'];
}

Model Structures

Selection Option Interface

export interface SelectionOptionInterface {
  value: any;           // The value to be selected/returned
  label?: string;       // Optional display label
  disabled?: boolean;   // Whether this option is disabled
  selected?: boolean;   // Whether this option is pre-selected
}

Selection Option Class

export class SelectionOption implements SelectionOptionInterface {
  constructor(
    public value: any,
    public label?: string,
    public disabled?: boolean = false,
    public selected?: boolean = false,
  ) {}

  static adapt(item?: any): SelectionOption {
    return new SelectionOption(
      item?.value ?? item,
      item?.label,
      item?.disabled || false,
      item?.selected || false,
    );
  }
}

Usage Examples

// String array data (automatically converted)
const stringData = ['Option 1', 'Option 2', 'Option 3'];

// Object array data
const objectData = [
  { value: 'opt1', label: 'Option 1', disabled: false, selected: true },
  { value: 'opt2', label: 'Option 2', disabled: true, selected: false },
  { value: 'opt3', label: 'Option 3', disabled: false, selected: false }
];

// Manual SelectionOption creation
const manualOptions = [
  new SelectionOption('value1', 'Label 1', false, true),
  new SelectionOption('value2', 'Label 2', true, false),
  new SelectionOption('value3', 'Label 3', false, false)
];

// Using adapt method for flexible data
const adaptedOptions = [
  SelectionOption.adapt({ value: 'item1', label: 'Item 1', selected: true }),
  SelectionOption.adapt({ value: 'item2', label: 'Item 2', disabled: true }),
  SelectionOption.adapt('item3') // String fallback
];

Validation System

Built-in Validation

The component provides built-in validation for:

Min Selection Validation

// Requires at least X selections
[minSelection]="2"  // Must select at least 2 items

Max Selection Validation

// Allows maximum X selections
[maxSelection]="3"  // Cannot select more than 3 items

Required Validation

// Marks field as required
[required]="true"   // At least one selection required

Validation Error Types

| Error Type | Condition | Description | |------------|-----------|-------------| | minRequired | selectedCount < minSelection | Not enough selections made | | maxExceeded | selectedCount >= maxSelection | Too many selections made | | required | Control has required validator and no selections | Control is required but empty |

Custom Validation Examples

// Complex validation scenarios
const complexForm = new FormGroup({
  permissions: new FormControl([], [
    Validators.required,
    Validators.minLength(1),  // At least 1 selection
    Validators.maxLength(5),  // Maximum 5 selections
    customPermissionValidator() // Custom validation logic
  ])
});

// Custom validator example
function customPermissionValidator() {
  return (control: AbstractControl): ValidationErrors | null => {
    const value = control.value;
    if (value && value.includes('admin') && value.includes('delete')) {
      return { adminDeleteConflict: true };
    }
    return null;
  };
}

Module Configuration

ListSelectionInputModule

No Global Configuration Required

The ListSelectionInputModule does not provide a forRoot() method or global configuration options. All configuration is done at the component level through input properties.

Dependencies

  • @angular/core: Core Angular functionality
  • @angular/forms: Form control integration (FormsModule, ReactiveFormsModule)
  • @angular/material: Material Design components (MatSelectModule, MatFormFieldModule, etc.)

Advanced Usage Patterns

Cascading Selections

import { Component } from '@angular/core';
import { FormBuilder, FormGroup } from '@angular/forms';

@Component({
  selector: 'app-cascading-selects',
  template: `
    <form [formGroup]="cascadingForm">
      <app-list-selection-input
        formControlName="country"
        [data]="countries"
        label="Country"
        placeholder="Select a country">
      </app-list-selection-input>
      
      <app-list-selection-input
        formControlName="state"
        [data]="states"
        label="State/Province"
        placeholder="Select a state"
        [disabled]="!cascadingForm.get('country')?.value">
      </app-list-selection-input>
      
      <app-list-selection-input
        formControlName="city"
        [data]="cities"
        label="City"
        placeholder="Select a city"
        [disabled]="!cascadingForm.get('state')?.value">
      </app-list-selection-input>
    </form>
  `
})
export class CascadingSelectsComponent {
  cascadingForm: FormGroup;
  states: string[] = [];
  cities: string[] = [];
  
  countries = ['United States', 'Canada', 'United Kingdom'];
  
  stateData = {
    'United States': ['California', 'New York', 'Texas', 'Florida'],
    'Canada': ['Ontario', 'Quebec', 'British Columbia', 'Alberta'],
    'United Kingdom': ['England', 'Scotland', 'Wales', 'Northern Ireland']
  };
  
  cityData = {
    'California': ['Los Angeles', 'San Francisco', 'San Diego', 'Sacramento'],
    'New York': ['New York City', 'Buffalo', 'Rochester', 'Albany'],
    'Ontario': ['Toronto', 'Ottawa', 'Mississauga', 'Hamilton']
  };

  constructor(private fb: FormBuilder) {
    this.cascadingForm = this.fb.group({
      country: [''],
      state: [''],
      city: ['']
    });

    // Watch for country changes
    this.cascadingForm.get('country')?.valueChanges.subscribe(country => {
      this.states = this.stateData[country as keyof typeof this.stateData] || [];
      this.cascadingForm.get('state')?.reset();
      this.cascadingForm.get('city')?.reset();
    });

    // Watch for state changes
    this.cascadingForm.get('state')?.valueChanges.subscribe(state => {
      this.cities = this.cityData[state as keyof typeof this.cityData] || [];
      this.cascadingForm.get('city')?.reset();
    });
  }
}

Async Data Loading

import { Component } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { debounceTime, distinctUntilChanged, switchMap } from 'rxjs/operators';
import { FormControl } from '@angular/forms';
import { Observable } from 'rxjs';

@Component({
  selector: 'app-async-list-select',
  template: `
    <app-list-selection-input
      [formControl]="searchControl"
      [data]="searchResults$ | async"
      label="Search Products"
      placeholder="Start typing to search..."
      displayField="name"
      [loading]="loading">
    </app-list-selection-input>
    
    <div *ngIf="loading" class="loading-indicator">
      Loading products...
    </div>
  `
})
export class AsyncListSelectComponent {
  searchControl = new FormControl();
  searchResults$!: Observable<any[]>;
  loading = false;

  constructor(private http: HttpClient) {
    this.searchResults$ = this.searchControl.valueChanges.pipe(
      debounceTime(300),
      distinctUntilChanged(),
      switchMap(query => this.searchProducts(query))
    );
  }

  private searchProducts(query: string): Observable<any[]> {
    this.loading = true;
    return this.http.get<any[]>(`/api/products/search?q=${encodeURIComponent(query)}`).pipe(
      // Simulate loading state
      switchMap(results => {
        this.loading = false;
        return [results];
      })
    );
  }
}

Performance Optimization

import { Component, ChangeDetectionStrategy } from '@angular/core';
import { FormControl } from '@angular/forms';

@Component({
  selector: 'app-optimized-list-select',
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    <app-list-selection-input
      [formControl]="optimizedControl"
      [data]="largeDataset"
      label="Large Dataset Selection"
      placeholder="Select from large dataset..."
      [filterFunction]="customFilter">
    </app-list-selection-input>
  `
})
export class OptimizedListSelectComponent {
  optimizedControl = new FormControl();
  
  // Large dataset with efficient filtering
  largeDataset = Array.from({ length: 10000 }, (_, i) => ({
    id: i,
    name: `Product ${i}`,
    category: `Category ${Math.floor(i / 100)}`,
    price: Math.random() * 1000
  }));

  customFilter(data: any[], filterText: string): any[] {
    if (!filterText) return data;
    
    const lowerFilter = filterText.toLowerCase();
    return data.filter(item => 
      item.name.toLowerCase().includes(lowerFilter) ||
      item.category.toLowerCase().includes(lowerFilter)
    );
  }
}

Integration Examples

With Other UI Components

import { Component } from '@angular/core';

@Component({
  selector: 'app-integrated-list-select',
  template: `
    <app-display-card title="User Assignment">
      <app-list-selection-input
        [formControl]="userControl"
        [data]="users"
        label="Assign to User"
        placeholder="Select a user"
        displayField="name">
      </app-list-selection-input>
      
      <div *ngIf="userControl.value" class="assignment-info">
        <h4>Assigned to: {{ userControl.value.name }}</h4>
        <p>{{ userControl.value.email }}</p>
        <p>Role: {{ userControl.value.role }}</p>
        <p>Department: {{ userControl.value.department }}</p>
      </div>
    </app-display-card>
  `
})
export class IntegratedListSelectComponent {
  userControl = new FormControl();
  
  users = [
    { 
      id: 1, 
      name: 'John Doe', 
      email: '[email protected]', 
      role: 'Admin', 
      department: 'Engineering' 
    },
    { 
      id: 2, 
      name: 'Jane Smith', 
      email: '[email protected]', 
      role: 'Editor', 
      department: 'Marketing' 
    }
  ];
}

With State Management

import { Component } from '@angular/core';
import { Store } from '@ngrx/store';

@Component({
  selector: 'app-state-list-select',
  template: `
    <app-list-selection-input
      [formControl]="categoryControl"
      [data]="categories$ | async"
      label="Product Category"
      placeholder="Select a category"
      (selectionChange)="onCategorySelected($event)">
    </app-list-selection-input>
  `
})
export class StateListSelectComponent {
  categoryControl = new FormControl();
  categories$ = this.store.select(state => state.categories);
  
  constructor(private store: Store) {}
  
  onCategorySelected(category: any) {
    this.store.dispatch(selectCategory({ category }));
  }
}

Testing

Unit Testing Example

import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ListSelectionInputComponent } from './list-selection-input.component';
import { ReactiveFormsModule } from '@angular/forms';

describe('ListSelectionInputComponent', () => {
  let component: ListSelectionInputComponent;
  let fixture: ComponentFixture<ListSelectionInputComponent>;

  beforeEach(async () => {
    await TestBed.configureTestingModule({
      declarations: [ ListSelectionInputComponent ],
      imports: [ ReactiveFormsModule ]
    }).compileComponents();

    fixture = TestBed.createComponent(ListSelectionInputComponent);
    component = fixture.componentInstance;
  });

  it('should create', () => {
    expect(component).toBeTruthy();
  });

  it('should display options from data input', () => {
    component.data = ['Option 1', 'Option 2', 'Option 3'];
    fixture.detectChanges();
    
    const compiled = fixture.nativeElement;
    expect(compiled.textContent).toContain('Option 1');
    expect(compiled.textContent).toContain('Option 2');
    expect(compiled.textContent).toContain('Option 3');
  });

  it('should handle multiple selection mode', () => {
    component.multiple = true;
    component.data = ['Option 1', 'Option 2', 'Option 3'];
    
    expect(component.multiple).toBe(true);
  });

  it('should emit selection changes', () => {
    spyOn(component.selectionChange, 'emit');
    
    component.onSelectionChange(['Option 1', 'Option 2']);
    
    expect(component.selectionChange.emit).toHaveBeenCalledWith(['Option 1', 'Option 2']);
  });

  it('should validate minimum selections', () => {
    component.minSelection = 2;
    component.data = ['Option 1', 'Option 2', 'Option 3'];
    
    // Simulate selecting only 1 item
    component.writeValue(['Option 1']);
    
    const validationResult = component.validate({} as AbstractControl);
    expect(validationResult?.minRequired).toBe(true);
  });
});

Troubleshooting

Common Issues

  1. Form control not working: Ensure ReactiveFormsModule is imported
  2. Validation not triggering: Check that validators are properly configured
  3. Selection not updating: Verify data format matches expected structure
  4. Multiple selection not working: Check that multiple input is set to true
  5. Object display issues: Set displayField property for object arrays
  6. Styling issues: Ensure Material theme is properly configured

Debug Mode

@Component({
  template: `
    <div class="debug-info">
      Form Control Value: {{ formControl.value | json }}<br>
      Form Control Valid: {{ formControl.valid }}<br>
      Form Control Errors: {{ formControl.errors | json }}<br>
      Data Length: {{ data?.length || 0 }}<br>
      Multiple Mode: {{ multiple }}<br>
      Min Selection: {{ minSelection }}<br>
      Max Selection: {{ maxSelection }}
    </div>
    
    <app-list-selection-input
      [formControl]="formControl"
      [data]="data"
      [multiple]="multiple"
      [minSelection]="minSelection"
      [maxSelection]="maxSelection"
      (selectionChange)="onSelectionChange($event)">
    </app-list-selection-input>
  `
})
export class DebugListSelectComponent {
  formControl = new FormControl();
  data: any[] = [];
  multiple = false;
  minSelection = 0;
  maxSelection = 0;
  
  constructor() {
    this.formControl.valueChanges.subscribe(value => {
      console.log('List selection value changed:', value);
    });
    
    this.formControl.statusChanges.subscribe(status => {
      console.log('Form control status:', status);
    });
  }

  onSelectionChange(selection: any[]) {
    console.log('Selection changed:', selection);
  }
}

Performance Debugging

@Component({
  template: `
    <div class="performance-info">
      Data Size: {{ data?.length || 0 }} items<br>
      Render Time: {{ renderTime }}ms<br>
      Filter Operations: {{ filterOperationCount }}
    </div>
    
    <app-list-selection-input
      [formControl]="formControl"
      [data]="data"
      (selectionChange)="onSelectionChange($event)">
    </app-list-selection-input>
  `
})
export class PerformanceDebugComponent {
  formControl = new FormControl();
  data: any[] = [];
  renderTime = 0;
  filterOperationCount = 0;

  onSelectionChange(selection: any[]) {
    const start = performance.now();
    
    // Perform selection operation
    this.processSelection(selection);
    
    const end = performance.now();
    this.renderTime = end - start;
    this.filterOperationCount++;
  }

  private processSelection(selection: any[]) {
    // Your selection processing logic
  }
}