npm package: @ng-vui/multi-select
Description: Angular Multi-Select Dropdown Component for VUI Library
Author: Varun Singh
Latest version: 1.0.0

# @ertpl-ui/multi-select

A fully-featured Angular multi-select dropdown component with built-in validation, error handling, and seamless form integration. Perfect for selecting multiple options with comprehensive validation and user-friendly feedback.

## 📚 **Quick Navigation**

| Section | Description | Perfect For |
|---------|-------------|------------|
| [🚀 Features](#-features) | Complete feature overview | Understanding capabilities |
| [📦 Installation](#-installation) | Setup and installation | Getting started |
| [🎯 Basic Usage](#-basic-usage) | Simple implementation examples | First-time setup |
| [🔧 Configuration](#-configuration) | Settings and options | Customization |
| [✨ Advanced Features](#-advanced-features) | Complex use cases | Advanced implementation |
| [🎨 Styling & Theming](#-styling--theming) | Visual customization | UI/UX design |
| [🧪 Testing](#-testing) | Unit testing examples | Quality assurance |
| [🚀 Performance](#-performance-tips) | Optimization strategies | Large datasets |

---

## 🎯 **Use Cases & Examples**

### **Basic Multi-Selection**
Perfect for simple multi-select dropdowns with basic validation.
```typescript
<ng-multiselect-dropdown [data]="fruits" [(ngModel)]="selectedFruits"></ng-multiselect-dropdown>
```

### **Form Integration**
Seamless integration with Angular reactive forms and validation.
```typescript
<ng-multiselect-dropdown [formControl]="categoriesControl" [data]="categories"></ng-multiselect-dropdown>
```

### **Search & Filter**
Built-in search functionality for large datasets.
```typescript
<ng-multiselect-dropdown [data]="cities" [settings]="{allowSearchFilter: true}"></ng-multiselect-dropdown>
```

### **Grouped Options**
Organize options into logical groups with validation.
```typescript
<ng-multiselect-dropdown [data]="employees" [settings]="{groupBy: 'department'}"></ng-multiselect-dropdown>
```

### **Custom Templates**
Customize how options and selections are displayed.
```typescript
<ng-multiselect-dropdown [data]="users" [itemTemplate]="userTemplate"></ng-multiselect-dropdown>
```

---

## 🚀 Features

- ✅ **Multi-Selection** - Select multiple items with checkboxes
- ✅ **Built-in Validation** - Supports all Angular validators with automatic error messages
- ✅ **Error Handling** - Visual error states with customizable error messages and icons
- ✅ **Form Integration** - Works seamlessly with FormControl and FormGroup
- ✅ **Search & Filter** - Built-in search functionality with filtering
- ✅ **Grouping** - Organize options into groups with validation
- ✅ **Custom Templates** - Customize option and selected item display
- ✅ **Accessibility** - Full ARIA support and keyboard navigation
- ✅ **Virtual Scrolling** - Handle large datasets efficiently
- ✅ **Selection Limits** - Min/max selection validation
- ✅ **Required Indicators** - Visual asterisk for required fields
- ✅ **TypeScript** - Full type safety and IntelliSense support

## 📦 Installation

```bash
npm install @ertpl-ui/multi-select
```

## ⚡ **Quick Start**

### **1. Small Dataset (< 100 options)**
```typescript
const basicSettings = {
  singleSelection: false,
  idField: 'id',
  textField: 'name',
  selectAllText: 'Select All',
  unSelectAllText: 'Clear All',
  itemsShowLimit: 3,
  allowSearchFilter: false
};
```

### **2. Medium Dataset (100-1000 options)**
```typescript
const mediumSettings = {
  singleSelection: false,
  idField: 'id',
  textField: 'name',
  allowSearchFilter: true,
  searchPlaceholderText: 'Search options...',
  itemsShowLimit: 5,
  enableCheckAll: true
};
```

### **3. Large Dataset (1000+ options)**
```typescript
const largeSettings = {
  singleSelection: false,
  idField: 'id',
  textField: 'name',
  allowSearchFilter: true,
  lazyLoading: true,
  virtualScrolling: true,
  itemHeight: 35,
  limitSelection: 20
};
```

## 🎯 Basic Usage

### Import

```typescript
import { MultiselectComponent } from '@ertpl-ui/multi-select';

// For standalone components
@Component({
  standalone: true,
  imports: [MultiselectComponent],
  // ... component config
})

// Or import in module
@NgModule({
  imports: [MultiselectComponent],
  // ... module config
})
```

### Simple Multi-Select

```typescript
@Component({
  selector: 'app-basic-example',
  template: `
    <ng-multiselect-dropdown
      [placeholder]="'Select items'"
      [data]="dropdownList"
      [(ngModel)]="selectedItems"
      [settings]="MultiSelectSettings">
    </ng-multiselect-dropdown>

    <div class="mt-3">
      <h5>Selected Items:</h5>
      <ul>
        <li *ngFor="let item of selectedItems">{{ item.item_text }}</li>
      </ul>
    </div>
  `
})
export class BasicExampleComponent {
  dropdownList = [
    { item_id: 1, item_text: 'Apple' },
    { item_id: 2, item_text: 'Banana' },
    { item_id: 3, item_text: 'Orange' },
    { item_id: 4, item_text: 'Mango' },
    { item_id: 5, item_text: 'Grapes' }
  ];
  
  selectedItems = [];
  
  MultiSelectSettings = {
    singleSelection: false,
    idField: 'item_id',
    textField: 'item_text',
    selectAllText: 'Select All',
    unSelectAllText: 'UnSelect All',
    itemsShowLimit: 3,
    allowSearchFilter: true
  };
}
```

### With Reactive Forms and Validation

```typescript
import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators, AbstractControl, ValidationErrors, ReactiveFormsModule } from '@angular/forms';
import { MultiselectComponent } from '@ertpl-ui/multi-select';

@Component({
  standalone: true,
  imports: [ReactiveFormsModule, MultiselectComponent],
  template: `
    <form [formGroup]="userForm" (ngSubmit)="onSubmit()">
      <div class="space-y-4">
        <div class="form-group">
          <label class="block text-sm font-medium mb-1">Skills (Required) *</label>
          <ng-multiselect-dropdown
            formControlName="skills"
            [placeholder]="'Select your skills'"
            [data]="skillsList"
            [settings]="skillsSettings"
            [class]="getInputClasses('skills')">
          </ng-multiselect-dropdown>
          
          <div *ngIf="getSkillsError()" class="mt-1 text-sm text-red-600">
            {{ getSkillsError() }}
          </div>
        </div>

        <div class="form-group">
          <label class="block text-sm font-medium mb-1">Interests *</label>
          <ng-multiselect-dropdown
            formControlName="interests"
            [placeholder]="'Choose interests (2-5)'"
            [data]="interestsList"
            [settings]="interestsSettings"
            [class]="getInputClasses('interests')">
          </ng-multiselect-dropdown>
          
          <div *ngIf="getInterestsError()" class="mt-1 text-sm text-red-600">
            {{ getInterestsError() }}
          </div>
          
          <div class="mt-1 text-xs text-gray-600">
            Selected: {{ getSelectionCount('interests') }}/5
          </div>
        </div>

        <button type="submit" class="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700">
          Save Profile
        </button>
      </div>
    </form>
  `
})
export class ReactiveFormComponent implements OnInit {
  userForm!: FormGroup;
  isSubmitted = false;

  skillsList = [
    { item_id: 1, item_text: 'JavaScript' },
    { item_id: 2, item_text: 'Angular' },
    { item_id: 3, item_text: 'React' },
    { item_id: 4, item_text: 'Node.js' },
    { item_id: 5, item_text: 'Python' },
    { item_id: 6, item_text: 'Java' },
    { item_id: 7, item_text: 'C#' },
    { item_id: 8, item_text: 'TypeScript' }
  ];

  interestsList = [
    { item_id: 1, item_text: 'Web Development' },
    { item_id: 2, item_text: 'Mobile Apps' },
    { item_id: 3, item_text: 'Machine Learning' },
    { item_id: 4, item_text: 'Data Science' },
    { item_id: 5, item_text: 'DevOps' },
    { item_id: 6, item_text: 'UI/UX Design' },
    { item_id: 7, item_text: 'Cloud Computing' },
    { item_id: 8, item_text: 'Cybersecurity' }
  ];

  skillsSettings = {
    singleSelection: false,
    idField: 'item_id',
    textField: 'item_text',
    selectAllText: 'Select All',
    unSelectAllText: 'Unselect All',
    itemsShowLimit: 3,
    allowSearchFilter: true,
    maxHeight: 200
  };

  interestsSettings = {
    singleSelection: false,
    idField: 'item_id',
    textField: 'item_text',
    selectAllText: 'Select All',
    unSelectAllText: 'Unselect All',
    itemsShowLimit: 2,
    allowSearchFilter: true,
    limitSelection: 5,
    maxHeight: 200
  };

  constructor(private fb: FormBuilder) {}

  ngOnInit() {
    this.userForm = this.fb.group({
      skills: [[], [
        Validators.required,
        this.minSelectedValidator(1),
        this.maxSelectedValidator(4)
      ]],
      interests: [[], [
        Validators.required,
        this.minSelectedValidator(2),
        this.maxSelectedValidator(5),
        this.uniqueSelectionValidator.bind(this)
      ]]
    });
  }

  minSelectedValidator(min: number) {
    return (control: AbstractControl): ValidationErrors | null => {
      const value = control.value || [];
      return value.length >= min ? null : { minSelected: { min, actual: value.length } };
    };
  }

  maxSelectedValidator(max: number) {
    return (control: AbstractControl): ValidationErrors | null => {
      const value = control.value || [];
      return value.length <= max ? null : { maxSelected: { max, actual: value.length } };
    };
  }

  uniqueSelectionValidator(control: AbstractControl): ValidationErrors | null {
    const selectedInterests = control.value || [];
    const selectedSkills = this.userForm?.get('skills')?.value || [];
    
    if (selectedInterests.length === 0) return null;
    
    // Check if interests overlap with skills (business rule)
    const skillIds = selectedSkills.map((skill: any) => skill.item_id);
    const interestIds = selectedInterests.map((interest: any) => interest.item_id);
    
    const hasOverlap = skillIds.some((id: number) => interestIds.includes(id));
    
    if (hasOverlap) {
      return { overlappingSelection: true };
    }
    
    return null;
  }

  getInputClasses(fieldName: string): string {
    const control = this.userForm.get(fieldName);
    const hasError = control?.invalid && (control?.touched || this.isSubmitted);
    
    return hasError ? 'border-red-500 ring-red-500' : 'border-gray-300';
  }

  getSelectionCount(fieldName: string): number {
    const control = this.userForm.get(fieldName);
    return control?.value?.length || 0;
  }

  getSkillsError(): string | null {
    const control = this.userForm.get('skills');
    
    if (!control || (!control.touched && !this.isSubmitted)) return null;
    
    if (control.errors?.['required']) {
      return 'Please select at least one skill';
    }
    
    if (control.errors?.['minSelected']) {
      const min = control.errors['minSelected'].min;
      return `Select at least ${min} skill(s)`;
    }
    
    if (control.errors?.['maxSelected']) {
      const max = control.errors['maxSelected'].max;
      return `Select at most ${max} skills`;
    }
    
    return null;
  }

  getInterestsError(): string | null {
    const control = this.userForm.get('interests');
    
    if (!control || (!control.touched && !this.isSubmitted)) return null;
    
    if (control.errors?.['required']) {
      return 'Please select your interests';
    }
    
    if (control.errors?.['minSelected']) {
      const min = control.errors['minSelected'].min;
      return `Select at least ${min} interests`;
    }
    
    if (control.errors?.['maxSelected']) {
      const max = control.errors['maxSelected'].max;
      return `Select at most ${max} interests`;
    }
    
    if (control.errors?.['overlappingSelection']) {
      return 'Interests cannot overlap with selected skills';
    }
    
    return null;
  }

  onSubmit() {
    this.isSubmitted = true;
    if (this.userForm.valid) {
      console.log('Form submitted:', this.userForm.value);
    }
  }
}
```

## 🚨 Error Handling & Validation

### Built-in Validation Support

The VUI multi-select component provides comprehensive validation capabilities for multiple selection scenarios with built-in error handling and visual feedback:

#### Required Validation

```typescript
@Component({
  template: `
    <form [formGroup]="form" (ngSubmit)="onSubmit()">
      <div class="form-group">
        <label class="block text-sm font-medium mb-1">Categories *</label>
        <ng-multiselect-dropdown
          formControlName="categories"
          [placeholder]="'Select categories'"
          [data]="categoryList"
          [settings]="categorySettings"
          [class]="getCategoryClasses()">
        </ng-multiselect-dropdown>
        
        <div *ngIf="getCategoryError()" class="mt-1 text-sm text-red-600">
          {{ getCategoryError() }}
        </div>
      </div>
    </form>
  `
})
export class RequiredValidationExample {
  form = this.fb.group({
    categories: [[], Validators.required] // Required multi-selection
  });
  
  categoryList = [
    { item_id: 1, item_text: 'Technology' },
    { item_id: 2, item_text: 'Business' },
    { item_id: 3, item_text: 'Design' },
    { item_id: 4, item_text: 'Marketing' }
  ];

  categorySettings = {
    singleSelection: false,
    idField: 'item_id',
    textField: 'item_text',
    selectAllText: 'Select All',
    unSelectAllText: 'Unselect All',
    itemsShowLimit: 2,
    allowSearchFilter: true
  };

  submitted = false;

  getCategoryClasses(): string {
    const control = this.form.get('categories');
    const hasError = control?.invalid && (control?.touched || this.submitted);
    return hasError ? 'border-red-500' : 'border-gray-300';
  }

  getCategoryError(): string | null {
    const control = this.form.get('categories');
    
    if (!control || (!control.touched && !this.submitted)) return null;
    
    if (control.errors?.['required']) {
      return 'Please select at least one category';
    }
    
    return null;
  }

  onSubmit() {
    this.submitted = true;
    if (this.form.valid) {
      console.log('Categories selected:', this.form.value);
    }
  }
}
```

#### Min/Max Selection Validation

```typescript
@Component({
  template: `
    <form [formGroup]="teamForm" (ngSubmit)="onSubmit()">
      <div class="space-y-4">
        <div class="form-group">
          <label>Team Members (2-5 required) *</label>
          <ng-multiselect-dropdown
            formControlName="teamMembers"
            [placeholder]="'Select 2-5 team members'"
            [data]="membersList"
            [settings]="membersSettings"
            [class]="getFieldClasses('teamMembers')">
          </ng-multiselect-dropdown>
          
          <div *ngIf="getTeamMembersError()" class="mt-1 text-sm text-red-600">
            {{ getTeamMembersError() }}
          </div>
          
          <div class="mt-1 text-xs text-gray-600">
            Selected: {{ getSelectionCount('teamMembers') }}/5
          </div>
        </div>

        <div class="form-group">
          <label>Project Tags (1-8 maximum) *</label>
          <ng-multiselect-dropdown
            formControlName="tags"
            [placeholder]="'Add project tags'"
            [data]="tagsList"
            [settings]="tagsSettings"
            [class]="getFieldClasses('tags')">
          </ng-multiselect-dropdown>
          
          <div *ngIf="getTagsError()" class="mt-1 text-sm text-red-600">
            {{ getTagsError() }}
          </div>
          
          <div class="mt-1 text-xs text-gray-600">
            Tags: {{ getSelectionCount('tags') }}/8 maximum
          </div>
        </div>
      </div>
    </form>
  `
})
export class MinMaxValidationExample implements OnInit {
  teamForm = this.fb.group({
    teamMembers: [[], [
      Validators.required,
      this.minSelectionValidator(2),
      this.maxSelectionValidator(5)
    ]],
    tags: [[], [
      Validators.required,
      this.minSelectionValidator(1),
      this.maxSelectionValidator(8)
    ]]
  });

  membersList = [
    { item_id: 1, item_text: 'Alice Johnson' },
    { item_id: 2, item_text: 'Bob Smith' },
    { item_id: 3, item_text: 'Carol Davis' },
    { item_id: 4, item_text: 'David Wilson' },
    { item_id: 5, item_text: 'Eva Brown' },
    { item_id: 6, item_text: 'Frank Miller' },
    { item_id: 7, item_text: 'Grace Lee' }
  ];

  tagsList = [
    { item_id: 1, item_text: 'urgent' },
    { item_id: 2, item_text: 'frontend' },
    { item_id: 3, item_text: 'backend' },
    { item_id: 4, item_text: 'database' },
    { item_id: 5, item_text: 'api' },
    { item_id: 6, item_text: 'testing' },
    { item_id: 7, item_text: 'deployment' },
    { item_id: 8, item_text: 'documentation' },
    { item_id: 9, item_text: 'refactoring' },
    { item_id: 10, item_text: 'optimization' }
  ];

  membersSettings = {
    singleSelection: false,
    idField: 'item_id',
    textField: 'item_text',
    selectAllText: 'Select All',
    unSelectAllText: 'Unselect All',
    itemsShowLimit: 2,
    allowSearchFilter: true,
    limitSelection: 5
  };

  tagsSettings = {
    singleSelection: false,
    idField: 'item_id',
    textField: 'item_text',
    selectAllText: 'Select All',
    unSelectAllText: 'Unselect All',
    itemsShowLimit: 3,
    allowSearchFilter: true,
    limitSelection: 8
  };

  submitted = false;

  ngOnInit() {
    // Watch for team member changes to validate business rules
    this.teamForm.get('teamMembers')?.valueChanges.subscribe(() => {
      this.teamForm.get('tags')?.updateValueAndValidity();
    });
  }

  minSelectionValidator(min: number) {
    return (control: AbstractControl): ValidationErrors | null => {
      const value = control.value || [];
      return value.length >= min ? null : { 
        minSelection: { required: min, actual: value.length } 
      };
    };
  }

  maxSelectionValidator(max: number) {
    return (control: AbstractControl): ValidationErrors | null => {
      const value = control.value || [];
      return value.length <= max ? null : { 
        maxSelection: { allowed: max, actual: value.length } 
      };
    };
  }

  getFieldClasses(fieldName: string): string {
    const control = this.teamForm.get(fieldName);
    const hasError = control?.invalid && (control?.touched || this.submitted);
    return hasError ? 'border-red-500 ring-red-500' : 'border-gray-300';
  }

  getSelectionCount(fieldName: string): number {
    const control = this.teamForm.get(fieldName);
    return control?.value?.length || 0;
  }

  getTeamMembersError(): string | null {
    const control = this.teamForm.get('teamMembers');
    
    if (!control || (!control.touched && !this.submitted)) return null;
    
    if (control.errors?.['required']) {
      return 'Please select team members for this project';
    }
    
    if (control.errors?.['minSelection']) {
      const min = control.errors['minSelection'].required;
      return `Select at least ${min} team members`;
    }
    
    if (control.errors?.['maxSelection']) {
      const max = control.errors['maxSelection'].allowed;
      return `Select at most ${max} team members`;
    }
    
    return null;
  }

  getTagsError(): string | null {
    const control = this.teamForm.get('tags');
    
    if (!control || (!control.touched && !this.submitted)) return null;
    
    if (control.errors?.['required']) {
      return 'Please add at least one project tag';
    }
    
    if (control.errors?.['minSelection']) {
      return 'Add at least one tag to categorize this project';
    }
    
    if (control.errors?.['maxSelection']) {
      return 'Too many tags selected (8 maximum)';
    }
    
    return null;
  }

  onSubmit() {
    this.submitted = true;
    if (this.teamForm.valid) {
      console.log('Team configuration:', this.teamForm.value);
    }
  }
}
```

#### Custom Business Logic Validation

```typescript
@Component({
  template: `
    <form [formGroup]="courseForm" (ngSubmit)="onSubmit()">
      <div class="space-y-4">
        <div class="form-group">
          <label>Prerequisites *</label>
          <ng-multiselect-dropdown
            formControlName="prerequisites"
            [placeholder]="'Select prerequisite courses'"
            [data]="coursesList"
            [settings]="prerequisiteSettings">
          </ng-multiselect-dropdown>
          
          <div *ngIf="getPrerequisitesError()" class="mt-1 text-sm text-red-600">
            {{ getPrerequisitesError() }}
          </div>
        </div>

        <div class="form-group">
          <label>Student Levels *</label>
          <ng-multiselect-dropdown
            formControlName="levels"
            [placeholder]="'Target student levels'"
            [data]="levelsList"
            [settings]="levelsSettings">
          </ng-multiselect-dropdown>
          
          <div *ngIf="getLevelsError()" class="mt-1 text-sm text-red-600">
            {{ getLevelsError() }}
          </div>
        </div>

        <div class="form-group">
          <label>Required Skills *</label>
          <ng-multiselect-dropdown
            formControlName="requiredSkills"
            [placeholder]="'Skills students must have'"
            [data]="skillsList"
            [settings]="skillsSettings">
          </ng-multiselect-dropdown>
          
          <div *ngIf="getSkillsError()" class="mt-1 text-sm text-red-600">
            {{ getSkillsError() }}
          </div>
        </div>
      </div>
    </form>
  `
})
export class BusinessLogicValidationExample implements OnInit {
  courseForm = this.fb.group({
    prerequisites: [[], [
      Validators.required,
      this.prerequisiteValidator.bind(this)
    ]],
    levels: [[], [
      Validators.required,
      this.levelCompatibilityValidator.bind(this)
    ]],
    requiredSkills: [[], [
      Validators.required,
      this.skillsAlignmentValidator.bind(this)
    ]]
  });

  coursesList = [
    { item_id: 1, item_text: 'Basic Programming', level: 'beginner' },
    { item_id: 2, item_text: 'Data Structures', level: 'intermediate' },
    { item_id: 3, item_text: 'Algorithms', level: 'intermediate' },
    { item_id: 4, item_text: 'Database Design', level: 'intermediate' },
    { item_id: 5, item_text: 'Advanced JavaScript', level: 'advanced' }
  ];

  levelsList = [
    { item_id: 1, item_text: 'Beginner', value: 'beginner' },
    { item_id: 2, item_text: 'Intermediate', value: 'intermediate' },
    { item_id: 3, item_text: 'Advanced', value: 'advanced' },
    { item_id: 4, item_text: 'Expert', value: 'expert' }
  ];

  skillsList = [
    { item_id: 1, item_text: 'JavaScript', category: 'programming' },
    { item_id: 2, item_text: 'HTML/CSS', category: 'web' },
    { item_id: 3, item_text: 'SQL', category: 'database' },
    { item_id: 4, item_text: 'Git', category: 'tools' },
    { item_id: 5, item_text: 'Problem Solving', category: 'soft' },
    { item_id: 6, item_text: 'Team Work', category: 'soft' }
  ];

  prerequisiteSettings = {
    singleSelection: false,
    idField: 'item_id',
    textField: 'item_text',
    allowSearchFilter: true,
    itemsShowLimit: 2
  };

  levelsSettings = {
    singleSelection: false,
    idField: 'item_id',
    textField: 'item_text',
    allowSearchFilter: false,
    limitSelection: 3
  };

  skillsSettings = {
    singleSelection: false,
    idField: 'item_id',
    textField: 'item_text',
    allowSearchFilter: true,
    groupBy: 'category'
  };

  submitted = false;

  ngOnInit() {
    // Cross-field validation updates
    this.courseForm.get('prerequisites')?.valueChanges.subscribe(() => {
      this.courseForm.get('levels')?.updateValueAndValidity();
    });

    this.courseForm.get('levels')?.valueChanges.subscribe(() => {
      this.courseForm.get('requiredSkills')?.updateValueAndValidity();
    });
  }

  prerequisiteValidator(control: AbstractControl): ValidationErrors | null {
    const prerequisites = control.value || [];
    
    if (prerequisites.length === 0) return null;
    
    // Check for circular dependencies
    const advancedPrereqs = prerequisites.filter((p: any) => 
      this.coursesList.find(c => c.item_id === p.item_id && c.level === 'advanced')
    );
    
    if (advancedPrereqs.length > 2) {
      return { tooManyAdvancedPrereqs: true };
    }
    
    // Ensure logical progression
    const hasBasic = prerequisites.some((p: any) => p.item_id === 1); // Basic Programming
    const hasIntermediate = prerequisites.some((p: any) => [2, 3, 4].includes(p.item_id));
    
    if (hasIntermediate && !hasBasic) {
      return { missingFoundation: true };
    }
    
    return null;
  }

  levelCompatibilityValidator(control: AbstractControl): ValidationErrors | null {
    const levels = control.value || [];
    const prerequisites = this.courseForm?.get('prerequisites')?.value || [];
    
    if (levels.length === 0) return null;
    
    // Check if selected levels match prerequisite complexity
    const hasAdvancedLevel = levels.some((l: any) => ['advanced', 'expert'].includes(l.value));
    const hasAdvancedPrereqs = prerequisites.some((p: any) => 
      this.coursesList.find(c => c.item_id === p.item_id && c.level === 'advanced')
    );
    
    if (hasAdvancedLevel && !hasAdvancedPrereqs) {
      return { levelMismatch: true };
    }
    
    // Cannot mix beginner with expert
    const hasBeginner = levels.some((l: any) => l.value === 'beginner');
    const hasExpert = levels.some((l: any) => l.value === 'expert');
    
    if (hasBeginner && hasExpert) {
      return { conflictingLevels: true };
    }
    
    return null;
  }

  skillsAlignmentValidator(control: AbstractControl): ValidationErrors | null {
    const skills = control.value || [];
    const levels = this.courseForm?.get('levels')?.value || [];
    
    if (skills.length === 0) return null;
    
    // Must have at least one technical and one soft skill
    const technicalSkills = skills.filter((s: any) => 
      ['programming', 'web', 'database', 'tools'].includes(
        this.skillsList.find(skill => skill.item_id === s.item_id)?.category
      )
    );
    
    const softSkills = skills.filter((s: any) => 
      this.skillsList.find(skill => skill.item_id === s.item_id)?.category === 'soft'
    );
    
    if (technicalSkills.length === 0) {
      return { noTechnicalSkills: true };
    }
    
    if (softSkills.length === 0) {
      return { noSoftSkills: true };
    }
    
    // Advanced levels require more technical skills
    const hasAdvancedLevels = levels.some((l: any) => ['advanced', 'expert'].includes(l.value));
    
    if (hasAdvancedLevels && technicalSkills.length < 2) {
      return { insufficientTechnicalSkills: true };
    }
    
    return null;
  }

  getPrerequisitesError(): string | null {
    const control = this.courseForm.get('prerequisites');
    
    if (!control || (!control.touched && !this.submitted)) return null;
    
    if (control.errors?.['required']) {
      return 'Please select prerequisite courses';
    }
    
    if (control.errors?.['tooManyAdvancedPrereqs']) {
      return 'Too many advanced prerequisites (maximum 2)';
    }
    
    if (control.errors?.['missingFoundation']) {
      return 'Basic Programming is required for intermediate courses';
    }
    
    return null;
  }

  getLevelsError(): string | null {
    const control = this.courseForm.get('levels');
    
    if (!control || (!control.touched && !this.submitted)) return null;
    
    if (control.errors?.['required']) {
      return 'Please select target student levels';
    }
    
    if (control.errors?.['levelMismatch']) {
      return 'Advanced levels require advanced prerequisites';
    }
    
    if (control.errors?.['conflictingLevels']) {
      return 'Cannot target both beginner and expert levels';
    }
    
    return null;
  }

  getSkillsError(): string | null {
    const control = this.courseForm.get('requiredSkills');
    
    if (!control || (!control.touched && !this.submitted)) return null;
    
    if (control.errors?.['required']) {
      return 'Please select required student skills';
    }
    
    if (control.errors?.['noTechnicalSkills']) {
      return 'At least one technical skill is required';
    }
    
    if (control.errors?.['noSoftSkills']) {
      return 'At least one soft skill is required';
    }
    
    if (control.errors?.['insufficientTechnicalSkills']) {
      return 'Advanced courses require at least 2 technical skills';
    }
    
    return null;
  }

  onSubmit() {
    this.submitted = true;
    if (this.courseForm.valid) {
      console.log('Course configuration:', this.courseForm.value);
    }
  }
}
```

#### Group-Based Validation

```typescript
@Component({
  template: `
    <form [formGroup]="permissionsForm">
      <div class="space-y-4">
        <div class="form-group">
          <label>User Permissions *</label>
          <ng-multiselect-dropdown
            formControlName="permissions"
            [placeholder]="'Select user permissions'"
            [data]="permissionsList"
            [settings]="permissionsSettings">
          </ng-multiselect-dropdown>
          
          <div *ngIf="getPermissionsError()" class="mt-1 text-sm text-red-600">
            {{ getPermissionsError() }}
          </div>
          
          <div class="mt-2 text-xs text-gray-600">
            <div>Read permissions: {{ getPermissionsByGroup('read').length }}</div>
            <div>Write permissions: {{ getPermissionsByGroup('write').length }}</div>
            <div>Admin permissions: {{ getPermissionsByGroup('admin').length }}</div>
          </div>
        </div>
      </div>
    </form>
  `
})
export class GroupValidationExample {
  permissionsForm = this.fb.group({
    permissions: [[], [
      Validators.required,
      this.permissionGroupValidator.bind(this)
    ]]
  });

  permissionsList = [
    { item_id: 1, item_text: 'View Users', group: 'read' },
    { item_id: 2, item_text: 'View Reports', group: 'read' },
    { item_id: 3, item_text: 'View Settings', group: 'read' },
    { item_id: 4, item_text: 'Edit Users', group: 'write' },
    { item_id: 5, item_text: 'Create Reports', group: 'write' },
    { item_id: 6, item_text: 'Edit Settings', group: 'write' },
    { item_id: 7, item_text: 'Delete Users', group: 'admin' },
    { item_id: 8, item_text: 'System Config', group: 'admin' },
    { item_id: 9, item_text: 'User Management', group: 'admin' }
  ];

  permissionsSettings = {
    singleSelection: false,
    idField: 'item_id',
    textField: 'item_text',
    groupBy: 'group',
    allowSearchFilter: true,
    itemsShowLimit: 3
  };

  submitted = false;

  permissionGroupValidator(control: AbstractControl): ValidationErrors | null {
    const permissions = control.value || [];
    
    if (permissions.length === 0) return null;
    
    const readPerms = this.getPermissionsByGroup('read', permissions);
    const writePerms = this.getPermissionsByGroup('write', permissions);
    const adminPerms = this.getPermissionsByGroup('admin', permissions);
    
    // Must have at least one read permission
    if (readPerms.length === 0) {
      return { noReadPermissions: true };
    }
    
    // Cannot have write permissions without corresponding read permissions
    if (writePerms.length > 0 && readPerms.length === 0) {
      return { writeWithoutRead: true };
    }
    
    // Cannot have admin permissions without write permissions
    if (adminPerms.length > 0 && writePerms.length === 0) {
      return { adminWithoutWrite: true };
    }
    
    // Admin permissions are limited
    if (adminPerms.length > 2) {
      return { tooManyAdminPermissions: { max: 2, actual: adminPerms.length } };
    }
    
    return null;
  }

  getPermissionsByGroup(group: string, permissions?: any[]): any[] {
    const perms = permissions || this.permissionsForm.get('permissions')?.value || [];
    return perms.filter((p: any) => 
      this.permissionsList.find(perm => perm.item_id === p.item_id && perm.group === group)
    );
  }

  getPermissionsError(): string | null {
    const control = this.permissionsForm.get('permissions');
    
    if (!control || (!control.touched && !this.submitted)) return null;
    
    if (control.errors?.['required']) {
      return 'Please select user permissions';
    }
    
    if (control.errors?.['noReadPermissions']) {
      return 'At least one read permission is required';
    }
    
    if (control.errors?.['writeWithoutRead']) {
      return 'Write permissions require corresponding read permissions';
    }
    
    if (control.errors?.['adminWithoutWrite']) {
      return 'Admin permissions require write permissions';
    }
    
    if (control.errors?.['tooManyAdminPermissions']) {
      const max = control.errors['tooManyAdminPermissions'].max;
      return `Too many admin permissions (maximum ${max})`;
    }
    
    return null;
  }
}
```

### Common Validation Patterns

#### Dynamic Selection Limits

```typescript
// Dynamic limit based on user role
getRoleBasedLimit(userRole: string): number {
  const limits = {
    'admin': 10,
    'manager': 5,
    'user': 3
  };
  return limits[userRole as keyof typeof limits] || 1;
}

// Usage in validator
roleLimitValidator(control: AbstractControl): ValidationErrors | null {
  const selections = control.value || [];
  const userRole = this.getCurrentUserRole(); // Your method to get role
  const limit = this.getRoleBasedLimit(userRole);
  
  return selections.length <= limit ? null : { 
    roleLimit: { role: userRole, limit, actual: selections.length }
  };
}
```

#### Conditional Required Validation

```typescript
// Conditional validator
conditionalRequiredValidator(dependentField: string) {
  return (control: AbstractControl): ValidationErrors | null => {
    const dependentValue = this.form?.get(dependentField)?.value;
    const currentValue = control.value || [];
    
    // Required only if dependent field has value
    if (dependentValue && currentValue.length === 0) {
      return { conditionallyRequired: { dependsOn: dependentField } };
    }
    
    return null;
  };
}
```

## 🔧 API Reference

### Component Properties

| Property | Type | Default | Description |
|----------|------|---------|-------------|
| `data` | `any[]` | `[]` | Array of options |
| `settings` | `IMultiSelectSettings` | `{}` | Configuration object |
| `placeholder` | `string` | `'Select'` | Placeholder text |
| `disabled` | `boolean` | `false` | Disable the component |
| `loading` | `boolean` | `false` | Show loading indicator |

### Events

| Event | Type | Description |
|-------|------|-------------|
| `onSelect` | `EventEmitter<any>` | Item selected |
| `onDeSelect` | `EventEmitter<any>` | Item deselected |
| `onSelectAll` | `EventEmitter<any[]>` | All items selected |
| `onDeSelectAll` | `EventEmitter<any[]>` | All items deselected |
| `onFilterChange` | `EventEmitter<any>` | Search filter changed |
| `onDropDownClose` | `EventEmitter<any>` | Dropdown closed |

### Settings Interface

```typescript
interface IMultiSelectSettings {
  singleSelection?: boolean;          // Single selection mode
  idField?: string;                   // ID property name
  textField?: string;                 // Display text property name
  enableCheckAll?: boolean;           // Show select/deselect all
  selectAllText?: string;             // Select all button text
  unSelectAllText?: string;           // Deselect all button text
  allowSearchFilter?: boolean;        // Enable search
  clearSearchFilter?: boolean;        // Clear search on selection
  maxHeight?: number;                 // Dropdown max height
  itemsShowLimit?: number;            // Max items to show in selection display
  limitSelection?: number;            // Max items that can be selected
  searchPlaceholderText?: string;     // Search input placeholder
  noDataAvailablePlaceholderText?: string; // No data message
  closeDropDownOnSelection?: boolean; // Close dropdown after selection
  showSelectedItemsAtTop?: boolean;   // Show selected items at top
  defaultOpen?: boolean;              // Open dropdown by default
  allowRemoteDataSearch?: boolean;    // Enable remote search
  lazyLoading?: boolean;              // Enable lazy loading
  tagToBody?: boolean;                // Append dropdown to body
  labelKey?: string;                  // Custom label key
  primaryKey?: string;                // Custom primary key
  position?: 'top' | 'bottom';       // Dropdown position
}
```

## 🎨 Grouping

```typescript
@Component({
  template: `
    <ng-multiselect-dropdown
      [data]="groupedData"
      [(ngModel)]="selectedItems"
      [settings]="groupSettings">
    </ng-multiselect-dropdown>
  `
})
export class GroupingExampleComponent {
  groupedData = [
    {
      category: 'Fruits',
      options: [
        { id: 1, name: 'Apple' },
        { id: 2, name: 'Banana' },
        { id: 3, name: 'Orange' }
      ]
    },
    {
      category: 'Vegetables',
      options: [
        { id: 4, name: 'Carrot' },
        { id: 5, name: 'Broccoli' },
        { id: 6, name: 'Spinach' }
      ]
    }
  ];

  selectedItems = [];
  
  groupSettings = {
    singleSelection: false,
    idField: 'id',
    textField: 'name',
    enableCheckAll: true,
    itemsShowLimit: 3,
    allowSearchFilter: true,
    // Group settings
    groupBy: 'category',
    enableGroup: true
  };
}
```

## 🎯 Advanced Examples

### Custom Templates

```html
<ng-multiselect-dropdown
  [data]="users"
  [(ngModel)]="selectedUsers"
  [settings]="settings">
  
  <!-- Custom option template -->
  <ng-template #optionTemplate let-option="option">
    <div class="d-flex align-items-center">
      <img [src]="option.avatar" class="rounded-circle me-2" width="24" height="24">
      <div>
        <div class="fw-medium">{{ option.name }}</div>
        <small class="text-muted">{{ option.email }}</small>
      </div>
    </div>
  </ng-template>
  
  <!-- Custom selected item template -->
  <ng-template #selectedTemplate let-item="item">
    <div class="badge bg-primary me-1">
      <img [src]="item.avatar" class="rounded-circle me-1" width="16" height="16">
      {{ item.name }}
      <button (click)="removeItem(item)" class="btn-close btn-close-white ms-1"></button>
    </div>
  </ng-template>
</ng-multiselect-dropdown>
```

### Remote Data Loading

```typescript
@Component({
  template: `
    <ng-multiselect-dropdown
      [data]="options"
      [(ngModel)]="selectedItems"
      [settings]="remoteSettings"
      [loading]="loading"
      (onFilterChange)="onSearch($event)">
    </ng-multiselect-dropdown>
  `
})
export class RemoteDataComponent {
  options: any[] = [];
  selectedItems = [];
  loading = false;
  
  remoteSettings = {
    idField: 'id',
    textField: 'name',
    allowSearchFilter: true,
    allowRemoteDataSearch: true
  };

  constructor(private dataService: DataService) {}

  onSearch(searchTerm: string) {
    if (searchTerm.length >= 2) {
      this.loading = true;
      this.dataService.search(searchTerm).subscribe(
        data => {
          this.options = data;
          this.loading = false;
        },
        error => {
          this.loading = false;
          console.error('Search failed:', error);
        }
      );
    }
  }
}
```

### Virtual Scrolling for Large Datasets

```typescript
@Component({
  template: `
    <ng-multiselect-dropdown
      [data]="largeDataset"
      [(ngModel)]="selectedItems"
      [settings]="virtualScrollSettings">
    </ng-multiselect-dropdown>
  `
})
export class VirtualScrollComponent {
  largeDataset: any[] = [];
  selectedItems = [];
  
  virtualScrollSettings = {
    idField: 'id',
    textField: 'name',
    allowSearchFilter: true,
    maxHeight: 300,
    lazyLoading: true,
    itemHeight: 35 // Height of each option item
  };

  ngOnInit() {
    // Generate large dataset
    for (let i = 1; i <= 10000; i++) {
      this.largeDataset.push({
        id: i,
        name: `Item ${i}`,
        description: `Description for item ${i}`
      });
    }
  }
}
```

### Custom Validation

```typescript
@Component({
  template: `
    <form [formGroup]="form">
      <ng-multiselect-dropdown
        formControlName="skills"
        [data]="skillsList"
        [settings]="settings">
      </ng-multiselect-dropdown>
      
      <div class="validation-messages">
        <div *ngIf="form.get('skills')?.hasError('required')" class="text-danger">
          Please select at least one skill
        </div>
        <div *ngIf="form.get('skills')?.hasError('maxSelected')" class="text-danger">
          You can select maximum {{ maxSkills }} skills
        </div>
        <div *ngIf="form.get('skills')?.hasError('requiredSkill')" class="text-danger">
          JavaScript is a required skill
        </div>
      </div>
    </form>
  `
})
export class ValidationComponent {
  maxSkills = 5;
  
  form = this.fb.group({
    skills: [[], [
      Validators.required,
      this.maxSelectedValidator(this.maxSkills),
      this.requiredSkillValidator(['JavaScript'])
    ]]
  });

  skillsList = [
    { id: 1, name: 'JavaScript' },
    { id: 2, name: 'TypeScript' },
    { id: 3, name: 'Angular' },
    { id: 4, name: 'React' },
    { id: 5, name: 'Vue.js' },
    { id: 6, name: 'Node.js' }
  ];

  settings = {
    idField: 'id',
    textField: 'name',
    limitSelection: this.maxSkills,
    allowSearchFilter: true
  };

  constructor(private fb: FormBuilder) {}

  maxSelectedValidator(max: number): ValidatorFn {
    return (control: AbstractControl): ValidationErrors | null => {
      if (control.value && control.value.length > max) {
        return { maxSelected: { max, actual: control.value.length } };
      }
      return null;
    };
  }

  requiredSkillValidator(requiredSkills: string[]): ValidatorFn {
    return (control: AbstractControl): ValidationErrors | null => {
      if (control.value && control.value.length > 0) {
        const selectedNames = control.value.map((item: any) => item.name);
        const hasRequired = requiredSkills.some(skill => selectedNames.includes(skill));
        return hasRequired ? null : { requiredSkill: { required: requiredSkills } };
      }
      return null;
    };
  }
}
```

## 🎨 Styling & Theming

### CSS Classes

```scss
// Custom styling
.multiselect-dropdown {
  .dropdown-btn {
    border: 2px solid #007bff;
    border-radius: 8px;
    
    &:focus {
      box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);
    }
  }
  
  .dropdown-list {
    border: 1px solid #dee2e6;
    border-radius: 8px;
    box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
    
    ul {
      max-height: 250px;
      
      li {
        padding: 8px 12px;
        
        &:hover {
          background-color: #f8f9fa;
        }
        
        &.selected {
          background-color: #e3f2fd;
          color: #1976d2;
        }
      }
    }
  }
  
  .selected-item {
    background-color: #007bff;
    color: white;
    border-radius: 16px;
    padding: 4px 8px;
    margin: 2px;
    font-size: 0.875rem;
    
    .remove-selected {
      margin-left: 4px;
      cursor: pointer;
      
      &:hover {
        opacity: 0.7;
      }
    }
  }
}
```

### Theming with CSS Custom Properties

```scss
.multiselect-dropdown {
  --primary-color: #6f42c1;
  --primary-light: #e7d9f7;
  --border-color: #ced4da;
  --text-color: #495057;
  --background-color: #fff;
  --hover-color: #f8f9fa;
  --focus-shadow: rgba(111, 66, 193, 0.25);
  
  .dropdown-btn {
    border-color: var(--border-color);
    background-color: var(--background-color);
    color: var(--text-color);
    
    &:focus {
      border-color: var(--primary-color);
      box-shadow: 0 0 0 0.2rem var(--focus-shadow);
    }
  }
  
  .selected-item {
    background-color: var(--primary-color);
  }
}

// Dark theme
.dark-theme .multiselect-dropdown {
  --primary-color: #bb86fc;
  --primary-light: #3c3c54;
  --border-color: #6c757d;
  --text-color: #e9ecef;
  --background-color: #343a40;
  --hover-color: #495057;
  --focus-shadow: rgba(187, 134, 252, 0.25);
}
```

## 🔌 Integration Examples

### With Angular Material

```typescript
@Component({
  template: `
    <mat-form-field appearance="outline">
      <mat-label>Select Technologies</mat-label>
      <ng-multiselect-dropdown
        formControlName="technologies"
        [data]="techList"
        [settings]="settings">
      </ng-multiselect-dropdown>
      <mat-error *ngIf="form.get('technologies')?.hasError('required')">
        Please select at least one technology
      </mat-error>
    </mat-form-field>
  `
})
export class MaterialIntegrationComponent {
  form = this.fb.group({
    technologies: [[], Validators.required]
  });

  techList = [
    { id: 1, name: 'Angular' },
    { id: 2, name: 'React' },
    { id: 3, name: 'Vue.js' }
  ];

  settings = {
    idField: 'id',
    textField: 'name',
    allowSearchFilter: true
  };
}
```

### With Bootstrap

```html
<div class="form-group">
  <label for="categories" class="form-label">Categories</label>
  <ng-multiselect-dropdown
    id="categories"
    class="form-control"
    [data]="categories"
    [(ngModel)]="selectedCategories"
    [settings]="bootstrapSettings">
  </ng-multiselect-dropdown>
  <div class="form-text">Select one or more categories</div>
</div>
```

## 🧪 Testing

```typescript
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { MultiselectComponent } from '@ertpl-ui/multi-select';
import { FormsModule } from '@angular/forms';

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

  beforeEach(() => {
    TestBed.configureTestingModule({
      declarations: [TestComponent],
      imports: [MultiselectComponent, FormsModule]
    });
    
    fixture = TestBed.createComponent(TestComponent);
    component = fixture.componentInstance;
  });

  it('should render dropdown options', () => {
    component.data = [
      { id: 1, name: 'Option 1' },
      { id: 2, name: 'Option 2' }
    ];
    
    fixture.detectChanges();
    
    const dropdown = fixture.nativeElement.querySelector('.dropdown-btn');
    dropdown.click();
    
    fixture.detectChanges();
    
    const options = fixture.nativeElement.querySelectorAll('.dropdown-list li');
    expect(options.length).toBe(2);
    expect(options[0].textContent.trim()).toBe('Option 1');
  });

  it('should select items', () => {
    spyOn(component, 'onItemSelect');
    
    // Simulate item selection
    const multiselect = fixture.nativeElement.querySelector('ng-multiselect-dropdown');
    multiselect.dispatchEvent(new CustomEvent('onSelect', { detail: { id: 1, name: 'Option 1' } }));
    
    expect(component.onItemSelect).toHaveBeenCalled();
  });
});

@Component({
  template: `
    <ng-multiselect-dropdown
      [data]="data"
      [(ngModel)]="selectedItems"
      [settings]="settings"
      (onSelect)="onItemSelect($event)">
    </ng-multiselect-dropdown>
  `
})
class TestComponent {
  data = [];
  selectedItems = [];
  settings = { idField: 'id', textField: 'name' };
  
  onItemSelect(item: any) {
    console.log('Selected:', item);
  }
}
```

## 🚀 Performance Tips

1. **Use trackBy functions** for better change detection:
```typescript
trackByFn = (index: number, item: any) => item.id;
```

2. **Enable lazy loading** for large datasets:
```typescript
settings = {
  lazyLoading: true,
  itemHeight: 35
};
```

3. **Limit search results**:
```typescript
onSearch(term: string) {
  if (term.length >= 2) {
    this.filteredData = this.allData
      .filter(item => item.name.toLowerCase().includes(term.toLowerCase()))
      .slice(0, 100); // Limit to 100 results
  }
}
```

## 🤝 Contributing

1. Fork the repository
2. Create your feature branch: `git checkout -b feature/my-feature`  
3. Commit your changes: `git commit -am 'Add my feature'`
4. Push to the branch: `git push origin feature/my-feature`
5. Submit a pull request

## 📄 License

MIT © [VUI](https://github.com/ultimatevarun/ng-vui-examples)

## 🔗 Related Packages

- `ng-vui-select-input` - Single select dropdown
- `ng-vui-grid` - Data grid component
- `ng-vui-date-picker` - Date picker component
- `ng-vui-text-input` - Text input component
- `ng-vui-textarea` - Textarea component
- `ng-vui-auto-complete` - Auto complete component