# Tag autocomplete

- Entry point: `@koobiq/components/tags`
- NgModule: `KbqTagsModule`
- Page: https://koobiq.io/en/components/tag-autocomplete/overview

## Overview

Tags Autocomplete is used to select one or multiple values from a list and to input custom values.

When focusing on the field, the dictionary menu opens. Autocomplete changes as you type. Selecting an option from the list adds a tag, clears the text in the field, and closes the menu.

**Example: Tag autocomplete overview** (`tag-autocomplete-overview`)

```ts tag-autocomplete-overview-example.ts
import { moveItemInArray } from '@angular/cdk/drag-drop';
import { COMMA, ENTER } from '@angular/cdk/keycodes';
import { ChangeDetectionStrategy, Component, computed, ElementRef, model, viewChild } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { KbqAutocompleteModule, KbqAutocompleteSelectedEvent } from '@koobiq/components/autocomplete';
import { KbqHighlightBackgroundPipe } from '@koobiq/components/core';
import { KbqIconModule } from '@koobiq/components/icon';
import { KbqInputModule } from '@koobiq/components/input';
import {
    KbqTagEvent,
    KbqTagInput,
    KbqTagInputEvent,
    KbqTagListDroppedEvent,
    KbqTagsModule
} from '@koobiq/components/tags';

const getAutocompleteOptions = () => [
    'BruteForce',
    'Complex Attack',
    'DDoS',
    'HIPS alert',
    'IDS/IPS Alert',
    'Zero-Day Exploit',
    'XSS',
    'Malware',
    'Ransomware',
    'Phishing'
];

@Component({
    selector: 'tag-autocomplete-overview-example',
    imports: [
        FormsModule,
        KbqTagsModule,
        KbqAutocompleteModule,
        KbqIconModule,
        KbqInputModule,
        KbqHighlightBackgroundPipe
    ],
    template: `
        <kbq-form-field>
            <kbq-tag-list #tagList="kbqTagList" draggable (dropped)="dropped($event)">
                @for (tag of tags(); track tag) {
                    <kbq-tag [value]="tag" (removed)="removed($event)">
                        {{ tag }}
                        <i kbq-icon="kbq-xmark-s_16" kbqTagRemove (click)="afterRemove()"></i>
                    </kbq-tag>
                }

                <input
                    #input
                    kbqInput
                    placeholder="New tag"
                    [kbqTagInputFor]="tagList"
                    [kbqAutocomplete]="autocomplete"
                    [kbqTagInputSeparatorKeyCodes]="separatorKeysCodes"
                    [kbqTagInputAddOnBlur]="true"
                    [(ngModel)]="tagInputModel"
                    (kbqTagInputTokenEnd)="create($event)"
                />

                <kbq-cleaner />
            </kbq-tag-list>

            <kbq-autocomplete #autocomplete="kbqAutocomplete" (optionSelected)="selected($event, input)">
                @for (option of filteredOptions(); track option) {
                    <kbq-option [value]="option">
                        <span [innerHTML]="option | kbqHighlightBackground: tagInputModel().trim()"></span>
                    </kbq-option>
                }
            </kbq-autocomplete>
        </kbq-form-field>
    `,
    styles: `
        :host {
            display: flex;
            flex-direction: column;
            align-items: center;
            gap: var(--kbq-size-m);
            margin: var(--kbq-size-5xl);
        }
    `,
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class TagAutocompleteOverviewExample {
    private readonly options = getAutocompleteOptions();
    protected readonly separatorKeysCodes = [ENTER, COMMA];
    protected readonly tagInputModel = model('');
    protected readonly tags = model(this.options.slice(0, 3));
    protected readonly filteredOptions = computed(() => {
        const current = this.tagInputModel().trim().toLowerCase();
        const options = this.options.filter((option) => !this.tags().includes(option));

        return current ? options.filter((option) => option.toLowerCase().includes(current)) : options;
    });
    private readonly input = viewChild.required(KbqTagInput, { read: ElementRef });

    protected create({ input, value = '' }: KbqTagInputEvent): void {
        if (value) {
            this.tags.update((tags) => [...tags, value]);
            input.value = '';
            this.tagInputModel.set('');
        }
    }

    protected removed({ tag }: KbqTagEvent): void {
        this.tags.update((tags) => {
            const index = tags.indexOf(tag.value);

            tags.splice(index, 1);

            return [...tags];
        });
    }

    protected selected({ option }: KbqAutocompleteSelectedEvent, input: HTMLInputElement): void {
        this.tags.update((tags) => [...tags, option.value]);
        input.value = '';
        this.tagInputModel.set('');
        option.deselect();
    }

    protected dropped({ previousIndex, currentIndex }: KbqTagListDroppedEvent): void {
        moveItemInArray(this.tags(), previousIndex, currentIndex);
        this.focusInput();
    }

    protected afterRemove(): void {
        this.focusInput();
    }

    private focusInput(): void {
        this.input().nativeElement.focus();
    }
}
```

The menu belongs to [autocomplete](https://koobiq.io/en/components/autocomplete.md) — its width is configured there, through the `panelWidth` and `panelMinWidth` attributes of `<kbq-autocomplete>`, and is measured against the whole field rather than the input.

### Menu next to the caret

Suggestions can be shown next to the text caret. Set `kbqAutocompleteRelativeToCaret` on the input field to enable this.

**Example: Tag autocomplete relative to caret** (`tag-autocomplete-relative-to-caret`)

```ts tag-autocomplete-relative-to-caret-example.ts
import { COMMA, ENTER } from '@angular/cdk/keycodes';
import { ChangeDetectionStrategy, Component, computed, model } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { KbqAutocompleteModule, KbqAutocompleteSelectedEvent } from '@koobiq/components/autocomplete';
import { KbqHighlightBackgroundPipe } from '@koobiq/components/core';
import { KbqIconModule } from '@koobiq/components/icon';
import { KbqInputModule } from '@koobiq/components/input';
import { KbqTagEvent, KbqTagInputEvent, KbqTagsModule } from '@koobiq/components/tags';

@Component({
    selector: 'tag-autocomplete-relative-to-caret-example',
    imports: [
        FormsModule,
        KbqTagsModule,
        KbqAutocompleteModule,
        KbqIconModule,
        KbqInputModule,
        KbqHighlightBackgroundPipe
    ],
    template: `
        <kbq-form-field>
            <kbq-tag-list #tagList="kbqTagList">
                @for (tag of tags(); track tag) {
                    <kbq-tag [value]="tag" (removed)="removed($event)">
                        {{ tag }}
                        <i kbq-icon="kbq-xmark-s_16" kbqTagRemove></i>
                    </kbq-tag>
                }

                <input
                    #input
                    kbqInput
                    placeholder="New tag"
                    [kbqAutocomplete]="autocomplete"
                    [kbqAutocompleteRelativeToCaret]="true"
                    [kbqTagInputAddOnBlur]="true"
                    [kbqTagInputFor]="tagList"
                    [kbqTagInputSeparatorKeyCodes]="separatorKeysCodes"
                    [(ngModel)]="tagInputModel"
                    (kbqTagInputTokenEnd)="create($event)"
                />
            </kbq-tag-list>

            <kbq-autocomplete #autocomplete="kbqAutocomplete" (optionSelected)="selected($event, input)">
                @for (option of filteredOptions(); track option) {
                    <kbq-option [value]="option">
                        <span [innerHTML]="option | kbqHighlightBackground: tagInputModel().trim()"></span>
                    </kbq-option>
                }
            </kbq-autocomplete>
        </kbq-form-field>
    `,
    styles: `
        kbq-form-field {
            width: 480px;
        }
    `,
    changeDetection: ChangeDetectionStrategy.OnPush,
    host: {
        class: 'layout-margin-5xl layout-row layout-align-center-center'
    }
})
export class TagAutocompleteRelativeToCaretExample {
    private readonly options = [
        'Acronis',
        'AegisLab',
        'AhnLab-V3',
        'Avast',
        'Babable',
        'ClamAV',
        'CrowdStrike Falcon',
        'NANO-Antivirus',
        'Palo Alto Networks',
        'TheHacker'
    ];

    protected readonly separatorKeysCodes = [ENTER, COMMA];
    protected readonly tagInputModel = model('');
    protected readonly tags = model(['TheHacker', 'ClamAV', 'NANO-Antivirus', 'Babable', 'Palo Alto Networks']);
    protected readonly filteredOptions = computed(() => {
        const current = this.tagInputModel().trim().toLowerCase();
        const options = this.options.filter((option) => !this.tags().includes(option));

        return current ? options.filter((option) => option.toLowerCase().includes(current)) : options;
    });

    protected create({ input, value = '' }: KbqTagInputEvent): void {
        if (value) {
            this.tags.update((tags) => [...tags, value]);
            input.value = '';
            this.tagInputModel.set('');
        }
    }

    protected removed({ tag }: KbqTagEvent): void {
        this.tags.update((tags) => tags.filter((value) => value !== tag.value));
    }

    protected selected({ option }: KbqAutocompleteSelectedEvent, input: HTMLInputElement): void {
        this.tags.update((tags) => [...tags, option.value]);
        input.value = '';
        this.tagInputModel.set('');
        option.deselect();
    }
}
```

### Search

Search splits a multi-word query into parts and searches for them independently, trims leading and trailing spaces, is case-insensitive, and folds diacritics. The algorithm is described in the [Smart search guide](https://koobiq.io/en/other/search-smart.md).

**Example: Tag autocomplete search** (`tag-autocomplete-search`)

```ts tag-autocomplete-search-example.ts
import { COMMA, ENTER } from '@angular/cdk/keycodes';
import { ChangeDetectionStrategy, Component, computed, model } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { KbqAutocompleteModule, KbqAutocompleteSelectedEvent } from '@koobiq/components/autocomplete';
import { createSearchPredicate, KbqHighlightBackgroundPipe, tokenizeSearchQuery } from '@koobiq/components/core';
import { KbqIconModule } from '@koobiq/components/icon';
import { KbqInputModule } from '@koobiq/components/input';
import { KbqTagEvent, KbqTagInputEvent, KbqTagsModule } from '@koobiq/components/tags';

const getAutocompleteOptions = () => [
    'BruteForce',
    'Complex Attack',
    'DDoS',
    'HIPS alert',
    'IDS/IPS Alert',
    'Zero-Day Exploit',
    'XSS',
    'Malware',
    'Ransomware',
    'Phishing'
];

@Component({
    selector: 'tag-autocomplete-search-example',
    imports: [
        FormsModule,
        KbqTagsModule,
        KbqAutocompleteModule,
        KbqIconModule,
        KbqInputModule,
        KbqHighlightBackgroundPipe
    ],
    template: `
        <kbq-form-field>
            <kbq-tag-list #tagList="kbqTagList">
                @for (tag of tags(); track tag) {
                    <kbq-tag [value]="tag" (removed)="removed($event)">
                        {{ tag }}
                        <i kbq-icon="kbq-xmark-s_16" kbqTagRemove></i>
                    </kbq-tag>
                }

                <input
                    #input
                    kbqInput
                    placeholder='Try "exploit zero"'
                    [kbqTagInputFor]="tagList"
                    [kbqAutocomplete]="autocomplete"
                    [kbqTagInputSeparatorKeyCodes]="separatorKeysCodes"
                    [kbqTagInputAddOnBlur]="true"
                    [(ngModel)]="tagInputModel"
                    (kbqTagInputTokenEnd)="create($event)"
                />

                <kbq-cleaner />
            </kbq-tag-list>

            <kbq-autocomplete #autocomplete="kbqAutocomplete" (optionSelected)="selected($event, input)">
                @for (option of filteredOptions(); track option) {
                    <kbq-option [value]="option">
                        <span [innerHTML]="option | kbqHighlightBackground: searchTokens()"></span>
                    </kbq-option>
                }
            </kbq-autocomplete>
        </kbq-form-field>
    `,
    styles: `
        :host {
            display: flex;
            flex-direction: column;
            align-items: center;
            gap: var(--kbq-size-m);
            margin: var(--kbq-size-5xl);
        }
    `,
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class TagAutocompleteSearchExample {
    private readonly options = getAutocompleteOptions();
    protected readonly separatorKeysCodes = [ENTER, COMMA];
    protected readonly tagInputModel = model('');
    protected readonly tags = model(this.options.slice(0, 2));
    protected readonly searchTokens = computed(() => tokenizeSearchQuery(this.tagInputModel().trim()));
    protected readonly filteredOptions = computed(() => {
        const options = this.options.filter((option) => !this.tags().includes(option));
        const predicate = createSearchPredicate(this.tagInputModel().trim());

        return options.filter(predicate);
    });

    protected create({ input, value = '' }: KbqTagInputEvent): void {
        if (value) {
            this.tags.update((tags) => [...tags, value]);
            input.value = '';
            this.tagInputModel.set('');
        }
    }

    protected removed({ tag }: KbqTagEvent): void {
        this.tags.update((tags) => {
            const index = tags.indexOf(tag.value);

            tags.splice(index, 1);

            return [...tags];
        });
    }

    protected selected({ option }: KbqAutocompleteSelectedEvent, input: HTMLInputElement): void {
        this.tags.update((tags) => [...tags, option.value]);
        input.value = '';
        this.tagInputModel.set('');
        option.deselect();
    }
}
```

### Adding to dictionary

The option to create a new tag is placed first in the autocomplete menu. It creates a new token from the entered text if it doesn't exist in the dictionary.

Adding a tag also works with the `Tab` key.

Creating a duplicate of a selected tag is not allowed: the system will show the message `Nothing found`.

**Example: Tag autocomplete option operations** (`tag-autocomplete-option-operations`)

```ts tag-autocomplete-option-operations-example.ts
import { AsyncPipe } from '@angular/common';
import { AfterViewInit, ChangeDetectionStrategy, Component, ElementRef, viewChild } from '@angular/core';
import { FormControl, FormsModule, ReactiveFormsModule } from '@angular/forms';
import { KbqAutocompleteModule, KbqAutocompleteSelectedEvent } from '@koobiq/components/autocomplete';
import { KbqHighlightBackgroundPipe } from '@koobiq/components/core';
import { KbqIconModule } from '@koobiq/components/icon';
import { KbqInputModule } from '@koobiq/components/input';
import { KbqTagInput, KbqTagInputEvent, KbqTagsModule } from '@koobiq/components/tags';
import { Observable } from 'rxjs';
import { map, startWith } from 'rxjs/operators';

const autocompleteValueCoercion = (value): string => (value?.new ? value.value : value) || '';
const getAutocompleteOptions = () => [
    'BruteForce',
    'Complex Attack',
    'DDoS',
    'HIPS alert',
    'IDS/IPS Alert',
    'Zero-Day Exploit',
    'XSS',
    'Malware',
    'Ransomware',
    'Phishing'
];

@Component({
    selector: 'tag-autocomplete-option-operations-example',
    imports: [
        FormsModule,
        KbqTagsModule,
        ReactiveFormsModule,
        KbqAutocompleteModule,
        KbqIconModule,
        AsyncPipe,
        KbqInputModule,
        KbqHighlightBackgroundPipe
    ],
    template: `
        <kbq-form-field>
            <kbq-tag-list #tagList="kbqTagList">
                @for (tag of selectedTags; track $index) {
                    <kbq-tag [value]="tag" (removed)="onRemove(tag)">
                        {{ tag }}
                        <i kbq-icon="kbq-xmark-s_16" kbqTagRemove (click)="afterRemove()"></i>
                    </kbq-tag>
                }
                <input
                    #tagInput
                    autocomplete="off"
                    placeholder="New tag"
                    kbqInput
                    [distinct]="true"
                    [formControl]="control"
                    [kbqAutocomplete]="autocomplete"
                    [kbqTagInputAddOnBlur]="false"
                    [kbqTagInputFor]="tagList"
                    [kbqTagInputSeparatorKeyCodes]="separatorKeysCodes"
                    (blur)="addOnBlurFunc($event)"
                    (kbqTagInputTokenEnd)="onCreate($event)"
                />

                <kbq-cleaner />
            </kbq-tag-list>
            <kbq-autocomplete #autocomplete (optionSelected)="onSelect($event)">
                @if (canCreate) {
                    <kbq-option [value]="{ new: true, value: tagInput.value }">Create: {{ tagInput.value }}</kbq-option>
                }
                @if (hasDuplicates) {
                    <kbq-option [disabled]="true">Nothing found</kbq-option>
                }
                @for (tag of filteredTags | async; track tag) {
                    <kbq-option [value]="tag">
                        <span [innerHTML]="tag | kbqHighlightBackground: tagInput.value"></span>
                    </kbq-option>
                }
            </kbq-autocomplete>
        </kbq-form-field>
    `,
    styles: `
        :host {
            display: flex;
            flex-direction: column;
            align-items: center;
            gap: var(--kbq-size-m);
            margin: var(--kbq-size-5xl);
        }
    `,
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class TagAutocompleteOptionOperationsExample implements AfterViewInit {
    protected readonly allTags = getAutocompleteOptions();
    protected readonly selectedTags = [...this.allTags.slice(0, 2)];
    protected readonly separatorKeysCodes = [];

    readonly tagInput = viewChild.required<ElementRef<HTMLInputElement>>('tagInput');
    readonly tagInputDirective = viewChild.required('tagInput', { read: KbqTagInput });

    control = new FormControl();

    filteredTags: Observable<string[]>;
    hasDuplicates: boolean = false;

    readonly kbqOptionTagName = 'KBQ-OPTION';

    get canCreate(): boolean {
        const cleanedValue: string = (this.control.value || '').trim();

        return (
            !!cleanedValue && [...new Set(this.allTags.concat(this.selectedTags))].every((tag) => tag !== cleanedValue)
        );
    }

    ngAfterViewInit(): void {
        this.filteredTags = this.control.valueChanges
            .pipe(map((e) => this.onControlValueChanges(e)))
            .pipe(startWith(this.allTags.filter((tag) => !this.selectedTags.includes(tag))));
    }

    onCreate({ input, value }: KbqTagInputEvent): void {
        this.control.setValue(value);
        const cleanedValue = (value || '').trim();

        if (cleanedValue && this.canCreate) {
            this.selectedTags.push(cleanedValue);

            // Reset the input value
            if (input) input.value = '';

            this.control.setValue(null);

            return;
        }

        // save input value on paste event to continue editing
        input.value = cleanedValue;
        this.control.setValue(cleanedValue);
    }

    onSelect({ option }: KbqAutocompleteSelectedEvent): void {
        option.deselect();

        this.selectedTags.push(autocompleteValueCoercion(option.value));
        this.control.setValue(null);
        this.tagInput().nativeElement.value = '';
    }

    onRemove(tag: any): void {
        const index = this.selectedTags.indexOf(tag);

        if (index >= 0) {
            this.selectedTags.splice(index, 1);
        }
    }

    addOnBlurFunc(event: FocusEvent) {
        const target: HTMLElement = event.relatedTarget as HTMLElement;

        if (!target || target.tagName !== this.kbqOptionTagName) {
            const kbqTagEvent: KbqTagInputEvent = {
                input: this.tagInput().nativeElement,
                value: this.tagInput().nativeElement.value
            };

            this.onCreate(kbqTagEvent);
        }
    }

    private filter(value: string): string[] {
        // Convert the input value to lowercase for case-insensitive comparison
        const filterValue = value ? value.toLowerCase() : '';

        // Combine all tags and selected tags into a single array, removing duplicates
        const uniqueTags = [...new Set(this.allTags.concat(this.selectedTags))];

        return uniqueTags.filter((tag) => tag.toLowerCase().indexOf(filterValue) === 0);
    }

    private onControlValueChanges = (value: string | null) => {
        const typedText: string | undefined = autocompleteValueCoercion(value)?.trim();
        const filteredTagsByInput = typedText ? this.filter(typedText) : this.allTags.slice();

        const inputAndSelectionTagsDiff = filteredTagsByInput.filter((tag) => !this.selectedTags.includes(tag));

        // check for scenario where duplicate exists but also can create/select other tags
        this.hasDuplicates = !inputAndSelectionTagsDiff.length && this.tagInputDirective().hasDuplicates;

        return inputAndSelectionTagsDiff;
    };

    protected afterRemove(): void {
        this.tagInputDirective().focus();
    }
}
```

### Tag removal

Tag removal order:

- If there are selected tags, all of them will be removed.
- If there are no selected tags, the focused tag will be removed.

Tags can be removed in several ways:

- Click on the remove icon (`kbqTagRemove` directive) inside the tag.
- Press `Delete` or `Backspace` key.
- Click on the reset control, which removes every tag except the disabled ones.
- Programmatic removal through the component [API](https://koobiq.io/en/components/tag.md).

The removal option is configured using the `removable` attribute (enabled by default). A disabled tag does not show its remove icon, and a list with `removable="false"` has no reset control either.

**Example: Tag autocomplete removable** (`tag-autocomplete-removable`)

```ts tag-autocomplete-removable-example.ts
import { COMMA, ENTER } from '@angular/cdk/keycodes';
import { ChangeDetectionStrategy, Component, computed, ElementRef, model, viewChild } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { KbqAutocompleteModule, KbqAutocompleteSelectedEvent } from '@koobiq/components/autocomplete';
import { KbqHighlightBackgroundPipe } from '@koobiq/components/core';
import { KbqIconModule } from '@koobiq/components/icon';
import { KbqInputModule } from '@koobiq/components/input';
import { KbqTagEvent, KbqTagInput, KbqTagInputEvent, KbqTagsModule } from '@koobiq/components/tags';

const getAutocompleteOptions = () => [
    'BruteForce',
    'Complex Attack',
    'DDoS',
    'HIPS alert',
    'IDS/IPS Alert',
    'Zero-Day Exploit',
    'XSS',
    'Malware',
    'Ransomware',
    'Phishing'
];

@Component({
    selector: 'tag-autocomplete-removable-example',
    imports: [
        FormsModule,
        KbqTagsModule,
        KbqAutocompleteModule,
        KbqIconModule,
        KbqInputModule,
        KbqHighlightBackgroundPipe
    ],
    template: `
        <kbq-form-field>
            <kbq-tag-list #tagList="kbqTagList" removable>
                @for (tag of tags(); track tag) {
                    <kbq-tag [value]="tag" (removed)="removed($event)">
                        {{ tag }}
                        <i kbq-icon="kbq-xmark-s_16" kbqTagRemove (click)="afterRemove()"></i>
                    </kbq-tag>
                }

                <input
                    #input
                    kbqInput
                    placeholder="New tag"
                    [kbqTagInputFor]="tagList"
                    [kbqAutocomplete]="autocomplete"
                    [kbqTagInputSeparatorKeyCodes]="separatorKeysCodes"
                    [kbqTagInputAddOnBlur]="false"
                    [(ngModel)]="tagInputModel"
                    (kbqTagInputTokenEnd)="create($event)"
                />

                <kbq-cleaner />
            </kbq-tag-list>

            <kbq-autocomplete #autocomplete="kbqAutocomplete" (optionSelected)="selected($event, input)">
                @for (option of filteredOptions(); track option) {
                    <kbq-option [value]="option">
                        <span [innerHTML]="option | kbqHighlightBackground: tagInputModel().trim()"></span>
                    </kbq-option>
                }
            </kbq-autocomplete>
        </kbq-form-field>
    `,
    styles: `
        :host {
            display: flex;
            flex-direction: column;
            align-items: center;
            gap: var(--kbq-size-m);
            margin: var(--kbq-size-5xl);
        }
    `,
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class TagAutocompleteRemovableExample {
    private readonly options = getAutocompleteOptions();
    protected readonly separatorKeysCodes = [ENTER, COMMA];
    protected readonly tagInputModel = model('');
    protected readonly tags = model(this.options.slice(0, 3));
    protected readonly filteredOptions = computed(() => {
        const current = this.tagInputModel().trim().toLowerCase();
        const options = this.options.filter((option) => !this.tags().includes(option));

        return current ? options.filter((option) => option.toLowerCase().includes(current)) : options;
    });
    private readonly input = viewChild.required(KbqTagInput, { read: ElementRef });

    protected create({ input, value = '' }: KbqTagInputEvent): void {
        if (value) {
            this.tags.update((tags) => [...tags, value]);
            input.value = '';
            this.tagInputModel.set('');
        }
    }

    protected removed({ tag }: KbqTagEvent): void {
        this.tags.update((tags) => {
            const index = tags.indexOf(tag.value);

            tags.splice(index, 1);

            return [...tags];
        });
    }

    protected selected({ option }: KbqAutocompleteSelectedEvent, input: HTMLInputElement): void {
        this.tags.update((tags) => [...tags, option.value]);
        input.value = '';
        this.tagInputModel.set('');
        option.deselect();
    }

    protected afterRemove(): void {
        this.input().nativeElement.focus();
    }
}
```

The reset control needs no handler of its own — it removes the tags through the same `removed` output as the remove icon inside a tag. Which tags it removes is set by the `clearPredicate` attribute: `[clearPredicate]="clearEverything"` and `clearEverything = () => true` clear the disabled ones too.

**Example: Tag autocomplete cleaner with disabled tags** (`tag-autocomplete-cleaner-with-disabled`)

```ts tag-autocomplete-cleaner-with-disabled-example.ts
import { COMMA, ENTER } from '@angular/cdk/keycodes';
import { ChangeDetectionStrategy, Component, computed, model } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { KbqAutocompleteModule, KbqAutocompleteSelectedEvent } from '@koobiq/components/autocomplete';
import { KbqIconModule } from '@koobiq/components/icon';
import { KbqInputModule } from '@koobiq/components/input';
import { KbqTagEvent, KbqTagInputEvent, KbqTagsModule } from '@koobiq/components/tags';

const getAutocompleteOptions = () => [
    'BruteForce',
    'Complex Attack',
    'DDoS',
    'HIPS alert',
    'Malware',
    'Phishing'
];

/** Tags the user is not allowed to take off. */
const lockedTags = ['DDoS'];

@Component({
    selector: 'tag-autocomplete-cleaner-with-disabled-example',
    imports: [
        FormsModule,
        KbqAutocompleteModule,
        KbqIconModule,
        KbqInputModule,
        KbqTagsModule
    ],
    template: `
        <kbq-form-field>
            <kbq-tag-list #tagList="kbqTagList">
                @for (tag of tags(); track tag) {
                    <kbq-tag [value]="tag" [disabled]="isLocked(tag)" (removed)="removed($event)">
                        {{ tag }}
                        <i kbq-icon="kbq-xmark-s_16" kbqTagRemove></i>
                    </kbq-tag>
                }

                <input
                    #input
                    kbqInput
                    placeholder="New tag"
                    [kbqTagInputFor]="tagList"
                    [kbqAutocomplete]="autocomplete"
                    [kbqTagInputSeparatorKeyCodes]="separatorKeysCodes"
                    [(ngModel)]="tagInputModel"
                    (kbqTagInputTokenEnd)="create($event)"
                />

                <kbq-cleaner />
            </kbq-tag-list>

            <kbq-autocomplete #autocomplete="kbqAutocomplete" (optionSelected)="selected($event, input)">
                @for (option of filteredOptions(); track option) {
                    <kbq-option [value]="option">{{ option }}</kbq-option>
                }
            </kbq-autocomplete>
        </kbq-form-field>
    `,
    styles: `
        :host {
            display: flex;
            justify-content: center;
            margin: var(--kbq-size-5xl);
        }
    `,
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class TagAutocompleteCleanerWithDisabledExample {
    private readonly options = getAutocompleteOptions();
    protected readonly separatorKeysCodes = [ENTER, COMMA];
    protected readonly tagInputModel = model('');
    protected readonly tags = model(this.options.slice(0, 4));
    protected readonly filteredOptions = computed(() => {
        const current = this.tagInputModel().trim().toLowerCase();
        const options = this.options.filter((option) => !this.tags().includes(option));

        return current ? options.filter((option) => option.toLowerCase().includes(current)) : options;
    });

    protected isLocked(tag: string): boolean {
        return lockedTags.includes(tag);
    }

    protected create({ input, value = '' }: KbqTagInputEvent): void {
        if (value) {
            this.tags.update((tags) => [...tags, value]);
            input.value = '';
            this.tagInputModel.set('');
        }
    }

    protected removed({ tag }: KbqTagEvent): void {
        this.tags.update((tags) => tags.filter((value) => value !== tag.value));
    }

    protected selected({ option }: KbqAutocompleteSelectedEvent, input: HTMLInputElement): void {
        this.tags.update((tags) => [...tags, option.value]);
        input.value = '';
        this.tagInputModel.set('');
        option.deselect();
    }
}
```

### Tag selection

Tags can be selected in several ways:

- Click on a tag while holding `Ctrl`.
- Click on a tag while holding `Shift` to change the selection range from the anchor tag to the clicked tag.
- Press `Shift` + `←` / `→` to expand or shrink the range.
- Press `Ctrl+A` key combination.
- `Space` when the tag is focused.
- Programmatic selection through the component [API](https://koobiq.io/en/components/tag.md).

Pointer and keyboard interactions change the same range relative to its initial tag. When the range shrinks, tags outside it are deselected. Disabled tags remain unchanged.

The selection option is configured using the `selectable` attribute (enabled by default).

### Editing

To enable editing mode, you need to set the `editable` property for `kbq-tag-list` or individual `kbq-tag`.

Enter editing mode:

- On double click on the tag.
- On pressing `Enter` or `F2` key (when tag is focused).

Save changes:

- On pressing `Enter` key.
- On clicking the confirmation button (`kbqTagEditSubmit` directive).

Cancel changes:

- On pressing `Escape` key.
- On focus loss.

In editing mode, the tag transforms into an input field and remains at the same position within the control.

**Example: Tag autocomplete editable** (`tag-autocomplete-editable`)

```ts tag-autocomplete-editable-example.ts
import { COMMA, ENTER } from '@angular/cdk/keycodes';
import { ChangeDetectionStrategy, Component, computed, ElementRef, model, viewChild } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { KbqAutocompleteModule, KbqAutocompleteSelectedEvent } from '@koobiq/components/autocomplete';
import { KbqComponentColors, KbqHighlightBackgroundPipe } from '@koobiq/components/core';
import { KbqIconModule } from '@koobiq/components/icon';
import { KbqInputModule } from '@koobiq/components/input';
import { KbqTagEditChange, KbqTagEvent, KbqTagInput, KbqTagInputEvent, KbqTagsModule } from '@koobiq/components/tags';
import { KbqTitleModule } from '@koobiq/components/title';

const getAutocompleteOptions = () => [
    'BruteForce',
    'Complex Attack',
    'DDoS',
    'HIPS alert',
    'IDS/IPS Alert',
    'Zero-Day Exploit',
    'XSS',
    'Malware',
    'Ransomware',
    'Phishing'
];

@Component({
    selector: 'tag-autocomplete-editable-example',
    imports: [
        FormsModule,
        KbqTagsModule,
        KbqAutocompleteModule,
        KbqIconModule,
        KbqInputModule,
        KbqTitleModule,
        KbqHighlightBackgroundPipe
    ],
    template: `
        <kbq-form-field>
            <kbq-tag-list #tagList="kbqTagList" editable>
                @for (tag of tags(); track tag) {
                    <kbq-tag
                        kbq-title
                        [value]="tag"
                        (editChange)="editChange($event, $index, input)"
                        (removed)="removed($event)"
                    >
                        {{ tag }}
                        <input kbqInput kbqTagEditInput [(ngModel)]="editInputModel" />
                        @if (editInputModel().length === 0) {
                            <i kbq-icon-button="kbq-xmark-s_16" kbqTagEditSubmit [color]="color.Theme"></i>
                        } @else {
                            <i kbq-icon-button="kbq-check-s_16" kbqTagEditSubmit [color]="color.Theme"></i>
                        }
                        <i kbq-icon-button="kbq-xmark-s_16" kbqTagRemove (click)="afterRemove()"></i>
                    </kbq-tag>
                }

                <input
                    #input
                    kbqInput
                    placeholder="New tag"
                    [kbqTagInputFor]="tagList"
                    [kbqAutocomplete]="autocomplete"
                    [kbqTagInputSeparatorKeyCodes]="separatorKeysCodes"
                    [kbqTagInputAddOnBlur]="false"
                    [(ngModel)]="tagInputModel"
                    (kbqTagInputTokenEnd)="create($event)"
                />

                <kbq-cleaner />
            </kbq-tag-list>

            <kbq-autocomplete #autocomplete="kbqAutocomplete" (optionSelected)="selected($event, input)">
                @for (option of filteredOptions(); track option) {
                    <kbq-option [value]="option">
                        <span [innerHTML]="option | kbqHighlightBackground: tagInputModel().trim()"></span>
                    </kbq-option>
                }
            </kbq-autocomplete>
        </kbq-form-field>
    `,
    styles: `
        :host {
            display: flex;
            flex-direction: column;
            align-items: center;
            gap: var(--kbq-size-m);
            margin: var(--kbq-size-5xl);
        }
    `,
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class TagAutocompleteEditableExample {
    private readonly options = getAutocompleteOptions();
    protected readonly color = KbqComponentColors;
    protected readonly separatorKeysCodes = [ENTER, COMMA];
    protected readonly tagInputModel = model('');
    protected readonly tags = model(this.options.slice(0, 2));
    protected readonly filteredOptions = computed(() => {
        const current = this.tagInputModel().trim().toLowerCase();
        const options = this.options.filter((option) => !this.tags().includes(option));

        return current ? options.filter((option) => option.toLowerCase().includes(current)) : options;
    });
    protected readonly editInputModel = model<string>('');
    private readonly input = viewChild.required(KbqTagInput, { read: ElementRef });

    protected create({ input, value = '' }: KbqTagInputEvent): void {
        if (value) {
            this.tags.update((tags) => [...tags, value]);
            input.value = '';
            this.tagInputModel.set('');
        }
    }

    protected removed({ tag }: KbqTagEvent): void {
        this.tags.update((tags) => {
            const index = tags.indexOf(tag.value);

            tags.splice(index, 1);

            return [...tags];
        });
    }

    protected selected({ option }: KbqAutocompleteSelectedEvent, input: HTMLInputElement): void {
        this.tags.update((tags) => [...tags, option.value]);
        input.value = '';
        this.tagInputModel.set('');
        option.deselect();
    }

    protected editChange({ type, tag }: KbqTagEditChange, index: number, input: HTMLInputElement): void {
        switch (type) {
            case 'start': {
                this.editInputModel.set(tag.value);
                break;
            }
            case 'cancel': {
                this.editInputModel.set('');
                input.focus();
                break;
            }
            case 'submit': {
                if (!this.editInputModel()) {
                    tag.remove();
                } else {
                    this.tags.update((tags) => {
                        tags[index] = this.editInputModel();

                        return [...tags];
                    });
                    this.editInputModel.set('');
                }

                input.focus();
                break;
            }
            default:
        }
    }

    protected afterRemove(): void {
        this.input().nativeElement.focus();
    }
}
```

### Drag and drop

To enable tag reordering, you need to set the `draggable` property for `kbq-tag-list`.

**Example: Tag autocomplete draggable** (`tag-autocomplete-draggable`)

```ts tag-autocomplete-draggable-example.ts
import { moveItemInArray } from '@angular/cdk/drag-drop';
import { COMMA, ENTER } from '@angular/cdk/keycodes';
import { ChangeDetectionStrategy, Component, computed, ElementRef, model, viewChild } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { KbqAutocompleteModule, KbqAutocompleteSelectedEvent } from '@koobiq/components/autocomplete';
import { KbqHighlightBackgroundPipe } from '@koobiq/components/core';
import { KbqIconModule } from '@koobiq/components/icon';
import { KbqInputModule } from '@koobiq/components/input';
import {
    KbqTagEvent,
    KbqTagInput,
    KbqTagInputEvent,
    KbqTagListDroppedEvent,
    KbqTagsModule
} from '@koobiq/components/tags';

const getAutocompleteOptions = () => [
    'BruteForce',
    'Complex Attack',
    'DDoS',
    'HIPS alert',
    'IDS/IPS Alert',
    'Zero-Day Exploit',
    'XSS',
    'Malware',
    'Ransomware',
    'Phishing'
];

@Component({
    selector: 'tag-autocomplete-draggable-example',
    imports: [
        FormsModule,
        KbqTagsModule,
        KbqAutocompleteModule,
        KbqIconModule,
        KbqInputModule,
        KbqHighlightBackgroundPipe
    ],
    template: `
        <kbq-form-field>
            <kbq-tag-list #tagList="kbqTagList" draggable (dropped)="dropped($event)">
                @for (tag of tags(); track tag) {
                    <kbq-tag [value]="tag" (removed)="removed($event)">
                        {{ tag }}
                        <i kbq-icon="kbq-xmark-s_16" kbqTagRemove (click)="afterRemove()"></i>
                    </kbq-tag>
                }

                <input
                    #input
                    kbqInput
                    placeholder="New tag"
                    [kbqTagInputFor]="tagList"
                    [kbqAutocomplete]="autocomplete"
                    [kbqTagInputSeparatorKeyCodes]="separatorKeysCodes"
                    [kbqTagInputAddOnBlur]="false"
                    [(ngModel)]="tagInputModel"
                    (kbqTagInputTokenEnd)="create($event)"
                />

                <kbq-cleaner />
            </kbq-tag-list>

            <kbq-autocomplete #autocomplete="kbqAutocomplete" (optionSelected)="selected($event, input)">
                @for (option of filteredOptions(); track option) {
                    <kbq-option [value]="option">
                        <span [innerHTML]="option | kbqHighlightBackground: tagInputModel().trim()"></span>
                    </kbq-option>
                }
            </kbq-autocomplete>
        </kbq-form-field>
    `,
    styles: `
        :host {
            display: flex;
            flex-direction: column;
            align-items: center;
            gap: var(--kbq-size-m);
            margin: var(--kbq-size-5xl);
        }
    `,
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class TagAutocompleteDraggableExample {
    private readonly options = getAutocompleteOptions();
    protected readonly separatorKeysCodes = [ENTER, COMMA];
    protected readonly tagInputModel = model('');
    protected readonly tags = model(this.options.slice(0, 6));
    protected readonly filteredOptions = computed(() => {
        const current = this.tagInputModel().trim().toLowerCase();
        const options = this.options.filter((option) => !this.tags().includes(option));

        return current ? options.filter((option) => option.toLowerCase().includes(current)) : options;
    });
    private readonly input = viewChild.required(KbqTagInput, { read: ElementRef });

    protected create({ input, value = '' }: KbqTagInputEvent): void {
        if (value) {
            this.tags.update((tags) => [...tags, value]);
            input.value = '';
            this.tagInputModel.set('');
        }
    }

    protected afterRemove(): void {
        this.focusInput();
    }

    protected removed({ tag }: KbqTagEvent): void {
        this.tags.update((tags) => {
            const index = tags.indexOf(tag.value);

            tags.splice(index, 1);

            return [...tags];
        });
    }

    protected selected({ option }: KbqAutocompleteSelectedEvent, input: HTMLInputElement): void {
        this.tags.update((tags) => [...tags, option.value]);
        input.value = '';
        this.tagInputModel.set('');
        option.deselect();
    }

    protected dropped({ previousIndex, currentIndex }: KbqTagListDroppedEvent): void {
        moveItemInArray(this.tags(), previousIndex, currentIndex);
        this.focusInput();
    }

    private focusInput(): void {
        this.input().nativeElement.focus();
    }
}
```

### Validation

Validators belong on `<kbq-tag-list>`: the tag list is the form control for the whole set of tags, while the input is only a text entry helper.

A `[formControl]` or `[ngModel]` of its own on `<input kbqTagInputFor>` is still required here: `kbqAutocomplete` drives the input through it, and without it panel filtering and typed-text tracking break. The library simply does not consider validators attached to that control.

The control value is the array of tags, so a single set of validators covers both the number of tags and the content of each one. Validation applies equally to tags picked from the autocomplete panel, typed by hand and pasted from the clipboard.

Validators do not prevent a tag from being added: the tag is added and the field goes into the error state. To keep an invalid value out of the list, filter it in the `(kbqTagInputTokenEnd)` and `(optionSelected)` handlers.

When the error becomes visible is controlled by [ErrorStateMatcher](https://koobiq.io/en/other/validation.md), and by default happens once the control is invalid and either touched or the form has been submitted.

**Example: Tag autocomplete with form control validators** (`tag-autocomplete-with-form-control-validators`)

```ts tag-autocomplete-with-form-control-validators-example.ts
import { ChangeDetectionStrategy, Component, computed, ElementRef, signal, viewChild } from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import {
    AbstractControl,
    FormControl,
    ReactiveFormsModule,
    ValidationErrors,
    ValidatorFn,
    Validators
} from '@angular/forms';
import { KbqAutocompleteModule, KbqAutocompleteSelectedEvent } from '@koobiq/components/autocomplete';
import { KbqHighlightBackgroundPipe } from '@koobiq/components/core';
import { KbqIconModule } from '@koobiq/components/icon';
import { KbqInputModule } from '@koobiq/components/input';
import { KbqTagInput, KbqTagInputEvent, KbqTagsModule } from '@koobiq/components/tags';

const MAX_TAG_COUNT = 5;
const LATIN_PATTERN = /^[a-zA-Z]+$/;
const OPTIONS = ['Phishing', 'Ransomware', 'Spyware', 'Вирус', 'Trojan', 'Botnet', 'Rootkit', 'Keylogger'];

/** Validates every tag separately, reporting the offending ones back to the template. */
const latinValidator = (): ValidatorFn => {
    return ({ value }: AbstractControl<string[] | null>): ValidationErrors | null => {
        const invalidTags = (value || []).filter((tag) => !LATIN_PATTERN.test(tag));

        return invalidTags.length ? { latin: { invalidTags } } : null;
    };
};

/** @title Tag autocomplete with form control validators */
@Component({
    selector: 'tag-autocomplete-with-form-control-validators-example',
    imports: [
        ReactiveFormsModule,
        KbqTagsModule,
        KbqAutocompleteModule,
        KbqIconModule,
        KbqInputModule,
        KbqHighlightBackgroundPipe
    ],
    template: `
        <kbq-form-field>
            <kbq-tag-list #tagList="kbqTagList" [formControl]="formControl">
                @for (tag of formControl.value; track $index) {
                    <kbq-tag [value]="tag" (removed)="removeTag(tag)">
                        {{ tag }}
                        <i kbq-icon-button="kbq-xmark-s_16" kbqTagRemove (click)="afterRemove()"></i>
                    </kbq-tag>
                }

                <input
                    #input
                    autocomplete="off"
                    kbqInput
                    placeholder="New tag"
                    [kbqAutocomplete]="autocomplete"
                    [kbqTagInputFor]="tagList"
                    (input)="query.set(input.value)"
                    (kbqTagInputTokenEnd)="createTag($event)"
                />

                <kbq-cleaner />
            </kbq-tag-list>

            <kbq-autocomplete #autocomplete="kbqAutocomplete" (optionSelected)="selected($event, input)">
                @for (option of filteredOptions(); track option) {
                    <kbq-option [value]="option">
                        <span [innerHTML]="option | kbqHighlightBackground: query().trim()"></span>
                    </kbq-option>
                }
            </kbq-autocomplete>

            <kbq-hint>Only latin letters, up to {{ maxTagCount }} tags</kbq-hint>

            @if (formControl.hasError('required')) {
                <kbq-error>Field is required</kbq-error>
            }

            @if (formControl.getError('maxlength'); as error) {
                <kbq-error>No more than {{ error.requiredLength }} tags</kbq-error>
            }

            @if (formControl.getError('latin'); as error) {
                <kbq-error>Invalid tags: {{ error.invalidTags.join(', ') }}</kbq-error>
            }
        </kbq-form-field>
    `,
    styles: `
        :host {
            display: flex;
            flex-direction: column;
            align-items: center;
            gap: var(--kbq-size-m);
            margin: var(--kbq-size-5xl);
        }
    `,
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class TagAutocompleteWithFormControlValidatorsExample {
    private readonly input = viewChild.required(KbqTagInput, { read: ElementRef });

    protected readonly maxTagCount = MAX_TAG_COUNT;
    protected readonly query = signal('');
    protected readonly formControl = new FormControl<string[]>(OPTIONS.slice(0, 3), [
        Validators.required,
        Validators.maxLength(MAX_TAG_COUNT),
        latinValidator()
    ]);

    /** The control value as a signal, so the option list can be derived from it. */
    private readonly tags = toSignal(this.formControl.valueChanges, { initialValue: this.formControl.value });

    protected readonly filteredOptions = computed(() => {
        const current = this.query().trim().toLowerCase();
        const options = OPTIONS.filter((option) => !this.tags()?.includes(option));

        return current ? options.filter((option) => option.toLowerCase().includes(current)) : options;
    });

    protected createTag({ value, input }: KbqTagInputEvent): void {
        if (value) {
            this.addTag(value);
        }

        this.resetInput(input);
    }

    protected selected({ option }: KbqAutocompleteSelectedEvent, input: HTMLInputElement): void {
        this.addTag(option.value);
        this.resetInput(input);
        option.deselect();
    }

    protected removeTag(tag: string): void {
        const tags = [...(this.formControl.value || [])];
        const index = tags.indexOf(tag);

        if (index >= 0) {
            tags.splice(index, 1);
            this.formControl.setValue(tags);
        }
    }

    protected afterRemove(): void {
        this.input().nativeElement.focus();
    }

    private addTag(tag: string): void {
        this.formControl.setValue([...(this.formControl.value || []), tag]);
    }

    private resetInput(input: HTMLInputElement): void {
        input.value = '';
        this.query.set('');
    }
}
```

### Keyboard navigation

#### Focus in empty input area

| Key                         | Action                                         |
| --------------------------- | ---------------------------------------------- |
| Backspace / ← / Shift + Tab | Move focus to the last tag.                    |
| Ctrl + A                    | Select all tags and set focus to the last one. |

#### Focus on tag

| Key                | Action                           |
| ------------------ | -------------------------------- |
| ← / →              | Move focus to previous/next tag. |
| Shift + ← / →      | Change the range and move focus. |
| Space              | Select/deselect tag.             |
| Delete / Backspace | Remove tag.                      |
| F2 / Enter         | Start editing.                   |
| Ctrl + A           | Select all tags.                 |
| Home / End         | Move focus to first/last tag.    |

#### Tag editing mode

| Key   | Action          |
| ----- | --------------- |
| Enter | Save changes.   |
| Esc   | Cancel changes. |

## API

### Components

#### KbqTag (component)

```ts
@Component({
    selector: 'kbq-tag, [kbq-tag], kbq-basic-tag, [kbq-basic-tag]',
    exportAs: 'kbqTag'
})
class KbqTag extends KbqColorDirective implements IFocusableOption, KbqTitleTextRef {
    readonly editable = input<boolean | undefined>();
    readonly preventEditSubmit = input<boolean>(false);
    readonly selected = input(false);
    value = input<NonNullable<unknown> | null | undefined>();
    readonly selectable = input(false);
    readonly removable = input(true);
    tabindex = input<number | null | undefined>();
    disabled = input<boolean | undefined>();
    readonly color = input<T | null | undefined>();
    readonly editChange = output<KbqTagEditChange>();
    readonly selectionChange = output<KbqTagSelectionChange>();
    readonly destroyed = output<KbqTagEvent>();
    readonly removed = output<KbqTagEvent>();
    changeDetectorRef: ChangeDetectorRef;
    readonly contentChildren: Signal<readonly KbqIcon[]>;
    readonly avatar: Signal<KbqTagAvatar | undefined>;
    readonly trailingIcon: Signal<KbqTagTrailingIcon | undefined>;
    readonly removeIcon: Signal<any>;
    get canRemove(): boolean;
    get colorClassName(): string;
    select(): void;
    deselect(): void;
    toggleSelected(isUserInput?: boolean): boolean;
    focus(): void;
    remove(): void;
    setDefaultColor(color: T): void;
}
```

- `[editable]`: `boolean | undefined` — Whether the tag is editable.
- `[preventEditSubmit]`: `boolean` — Whether the tag edits can't be submitted.
- `[selected]`: `Signal<boolean>` — Whether the tag is selected.
- `[value]`: `NonNullable<unknown> | null | undefined` — The value of the tag. Defaults to the content inside `<kbq-tag>` tags.
- `[selectable]`: `Signal<boolean>` — Whether the tag is selectable.
- `[removable]`: `Signal<boolean>` — Determines whether the tag is removable.
- `[tabindex]`: `number | null | undefined` — Tab order of the tag.
- `[disabled]`: `boolean | undefined` — Whether the tag is disabled.
- `[localeOverrides]`: `KbqPartialLocaleData | undefined` — Localized strings for this element and its subtree, keyed by locale section. Every key is optional at
  every depth.
- `[color]`: `T | null | undefined` — Color of the component. A value set in code holds until the input changes, and the default color is used
  while both are unset or falsy.
- `(editChange)`: `KbqTagEditChange` — Emits event when the tag is edited.
- `(selectionChange)`: `KbqTagSelectionChange` — Emitted when the tag is selected or deselected.
- `(destroyed)`: `KbqTagEvent` — Emitted when the tag is destroyed and leaving the DOM.
- `(removed)`: `KbqTagEvent` — Emitted when a tag is to be removed.
  Fires on programmatic and UI removal (click or keyboard).
- `avatar`: `Signal<KbqTagAvatar | undefined>` — The tag avatar
- `trailingIcon`: `Signal<KbqTagTrailingIcon | undefined>` — The tag's trailing icon.
- `removeIcon`: `Signal<any>` — The tag's remove toggler.
- `canRemove`: `boolean` — Whether the user may remove this tag through the controls the tag itself offers — the remove
  control and the `Delete` key. A disabled tag offers neither.

  This is the affordance, not the channel: `remove()` stays open so the list can still clear a
  disabled tag when `clearPredicate` asks for it.
- `colorClassName`: `string` — current class name of color
- `select()`: `void` — Selects the tag.
- `deselect()`: `void` — Deselects the tag.
- `toggleSelected()`: `boolean` — Toggles the current selected state of the tag.
- `focus()`: `void` — Focuses the tag.
- `remove()`: `void` — Allows for programmatic removal of the tag. Called by the KbqTagList when the DELETE or
  BACKSPACE keys are pressed.

  Informs any listeners of the removal request. Does not remove the tag from the DOM.
- `setDefaultColor()`: `void` — this color will be used as a default value. For example [color]="'' | false | undefined | null".

#### KbqTagList (component)

```ts
@Component({ selector: 'kbq-tag-list', exportAs: 'kbqTagList' })
class KbqTagList implements KbqFormFieldControl<any>, ControlValueAccessor, CanUpdateErrorState {
    draggable = input<boolean | undefined>();
    readonly selectable = input<boolean>(true);
    readonly editable = input<boolean>(false);
    readonly clearPredicate = input<(tag: KbqTag) => boolean>(
        (tag) => !tag.disabled
    );
    readonly removable = input<boolean>(true);
    readonly errorStateMatcher = input<ErrorStateMatcher>();
    readonly dropped = output<KbqTagListDroppedEvent>();
    readonly change = output<KbqTagListChange>();
    defaultErrorStateMatcher: ErrorStateMatcher;
    parentForm: NgForm | null;
    parentFormGroup: FormGroupDirective | null;
    ngControl: NgControl;
    readonly focused: Signal<boolean>;
    notifyPendingTagChange(): void;
    focus(): void;
    blur(): void;
    markAsTouched(): void;
}
```

- `[draggable]`: `boolean | undefined` — Whether the tags in the list can be reordered by dragging.
- `[selectable]`: `boolean` — Whether or not this tag list is selectable. When a tag list is not selectable,
  the selected states for all the tags inside the tag list are always ignored.
- `[editable]`: `boolean` — Whether the tags in the list are editable.
- `[clearPredicate]`: `(tag: KbqTag) => boolean` — Decides which tags the projected `KbqCleaner` removes: return `true` to clear the tag, `false` to
  keep it. Disabled tags are kept by default. Bind a stable reference — a new function on every change
  detection re-runs the predicate over every tag.

  A tag the list does not allow to be removed is never offered, whatever the predicate answers.
- `[removable]`: `boolean` — Whether the tags in the list are removable.
- `[errorStateMatcher]`: `ErrorStateMatcher` — An object used to control when error messages are shown.
- `(dropped)`: `KbqTagListDroppedEvent` — Emits when the user drops tag inside tag list container.
- `(change)`: `KbqTagListChange` — Event emitted when the selected tag list value has been changed by the user.
- `focused`: `Signal<boolean>` — Whether any tags or the kbqTagInput inside of this tag-list has focus.
- `notifyPendingTagChange()`: `void` — Notifies that the next `tags.changes` emission is UI-initiated.
- `focus()`: `void` — Focuses the tag list. If there is a tag input, focuses that instead.
- `blur()`: `void` — When blurred, mark the field as touched when focus moved outside the tag list.
- `markAsTouched()`: `void` — Mark the field as touched

### Directives

#### KbqTagEditInput (directive)

Directive to add input behavior for the editable tag.

```ts
@Directive({ selector: '[kbqTagEditInput]', exportAs: 'kbqTagEditInput' })
class KbqTagEditInput {}
```

#### KbqTagEditSubmit (directive)

Directive to add submit behavior for the editable tag.

```ts
@Directive({ selector: '[kbqTagEditSubmit]', exportAs: 'kbqTagEditSubmit' })
class KbqTagEditSubmit {}
```

#### KbqTagInput (directive)

Directive that adds tag-specific behaviors to an input element inside `<kbq-form-field>`.
May be placed inside or outside of an `<kbq-tag-list>`.

```ts
@Directive({
    selector: 'input[kbqTagInputFor]',
    exportAs: 'kbqTagInput, kbqTagInputFor'
})
class KbqTagInput implements KbqTagTextControl {
    readonly separatorKeyCodes = input<number[]>(
        this.defaultOptions.separatorKeyCodes,
        { alias: 'kbqTagInputSeparatorKeyCodes' }
    );
    readonly distinct = input<boolean>(false);
    readonly placeholder = input<string>('');
    readonly id = input<string>(
        inject(_IdGenerator).getId('kbq-tag-list-input-')
    );
    readonly tagList = input<KbqTagList | undefined>(undefined, {
        alias: 'kbqTagInputFor'
    });
    readonly addOnBlur = input<boolean>(true, {
        alias: 'kbqTagInputAddOnBlur'
    });
    readonly addOnPaste = input<boolean>(
        this.defaultOptions.addOnPaste ?? true,
        { alias: 'kbqTagInputAddOnPaste' }
    );
    disabled = input<boolean | undefined>();
    readonly tagEnd = output<KbqTagInputEvent>({
        alias: 'kbqTagInputTokenEnd'
    });
    readonly autofilled: Signal<boolean>;
    readonly empty: Signal<boolean>;
    focus(): void;
}
```

- `[kbqTagInputSeparatorKeyCodes]`: `number[]` — The list of key codes that will trigger a tagEnd event.

  Defaults to `[ENTER]`.
- `[distinct]`: `boolean` — A value indicating whether allow/prevent tags duplication
- `[placeholder]`: `string` — The input's placeholder text.
- `[id]`: `string` — Unique id for the input.
- `[kbqTagInputFor]`: `KbqTagList | undefined` — Register input for tag list.
- `[kbqTagInputAddOnBlur]`: `boolean` — Whether or not the tagEnd event will be emitted when the input is blurred.
- `[kbqTagInputAddOnPaste]`: `boolean` — Whether the tagEnd event will be emitted when the text pasted. Defaults to `addOnPaste` of
  `KBQ_TAGS_DEFAULT_OPTIONS`, or `true`.
- `[disabled]`: `boolean | undefined` — Whether the input is disabled.
- `(kbqTagInputTokenEnd)`: `KbqTagInputEvent` — Emitted when a tag is to be added.
- `autofilled`: `Signal<boolean>` — Whether the control's value was filled in by the browser.
- `empty`: `Signal<boolean>` — Whether the input is empty.
- `focus()`: `void` — Focuses the input.

#### KbqTagPrefix (directive)

Marks content to be projected into the prefix (leading) slot of a tag.

```ts
@Directive({ selector: '[kbqTagPrefix]' })
class KbqTagPrefix {}
```

#### KbqTagRemove (directive)

Example:

    `<kbq-tag>
      <kbq-icon kbqTagRemove>cancel</kbq-icon>
    </kbq-tag>`

You *may* use a custom icon, but you may need to override the `kbq-tag-remove` positioning
styles to properly center the icon within the tag.

```ts
@Directive({ selector: '[kbqTagRemove]' })
class KbqTagRemove {
    readonly ariaLabel = input<string | undefined>(undefined, {
        alias: 'aria-label'
    });
}
```

- `[aria-label]`: `string | undefined` — Accessible name of the button. Defaults to the localized "Remove".

#### KbqTagSuffix (directive)

Marks content to be projected into the suffix (trailing) slot of a tag.

```ts
@Directive({ selector: '[kbqTagSuffix]' })
class KbqTagSuffix {}
```

### Classes

#### KbqTagEditChange (class)

Event object emitted by KbqTag when the tag is edited.

```ts
class KbqTagEditChange {
    readonly tag: KbqTag;
    readonly type: "start" | "cancel" | "submit";
    readonly reason: string;
    constructor(
        tag: KbqTag,
        type: "start" | "cancel" | "submit",
        reason: string
    );
}
```

#### KbqTagListChange (class)

Change event object that is emitted when the tag list value has changed.

```ts
class KbqTagListChange {
    source: KbqTagList;
    value: any;
    constructor(source: KbqTagList, value: any);
}
```

#### KbqTagSelectionChange (class)

Event object emitted by KbqTag when selected or deselected.

```ts
class KbqTagSelectionChange {
    source: KbqTag;
    selected: boolean;
    isUserInput: boolean;
    constructor(
        source: KbqTag,
        selected: boolean,
        isUserInput?: boolean
    );
}
```

### Types

#### KbqTagEvent (type)

Event object emitted by KbqTag.

```ts
type KbqTagEvent = {
    tag: KbqTag;
};
```

#### KbqTagFocusEvent (type)

Event object emitted when the KbqTag is focused.

```ts
type KbqTagFocusEvent = KbqTagEvent & {
    origin: FocusOrigin;
};
```

#### KbqTagInputEvent (interface)

Represents an input event on a `kbqTagInput`.

```ts
interface KbqTagInputEvent {
    input: HTMLInputElement;
    value: string;
}
```

- `input`: `HTMLInputElement` — The native `<input>` element that the event is being fired for.
- `value`: `string` — The value of the input.

#### KbqTagListDroppedEvent (type)

```ts
type KbqTagListDroppedEvent = Pick<CdkDragDrop<unknown>, 'event' | 'previousIndex' | 'currentIndex'> & {
    tag: KbqTag;
};
```

#### KbqTagsDefaultOptions (interface)

Default options for the tags module that can be overridden.

```ts
interface KbqTagsDefaultOptions {
    separatorKeyCodes: number[];
    separators?: KbqTagSeparator[];
    addOnPaste?: boolean;
}
```

- `separatorKeyCodes`: `number[]` — The list of key codes that will trigger a tagEnd event.
- `separators?`: `KbqTagSeparator[]` — Custom separator definitions to use instead of the built-in defaults.
- `addOnPaste?`: `boolean` — Whether the tagEnd event will be emitted when text is pasted.

#### KbqTagSeparator (interface)

Defines a character or pattern that ends a tag, and the contexts in which it applies.

```ts
interface KbqTagSeparator {
    symbol: RegExp;
    key?: string;
    keyCode?: number;
    appliesTo?: KbqTagSeparatorContext[];
}
```

- `symbol`: `RegExp` — Regular expression used to match/split this separator in text.
- `key?`: `string` — `KeyboardEvent.key` that triggers this separator while typing.
  Omit for separators with no single-keystroke equivalent (e.g. a run of whitespace) —
  those are implicitly paste-only, since they can never match a `keydown`.
- `keyCode?`: `number` — Numeric key code gating whether this separator is enabled, set via `kbqTagInputSeparatorKeyCodes`.
  Omit together with `key` for separators that should always be active regardless of that input.
- `appliesTo?`: `KbqTagSeparatorContext[]` — Contexts this separator applies to. Defaults to `['input', 'paste']`.

#### KbqTagSeparatorContext (type)

Contexts a `KbqTagSeparator` can be active in.

```ts
type KbqTagSeparatorContext = 'input' | 'paste';
```

### Constants

#### KBQ_TAGS_DEFAULT_OPTIONS (const)

Injection token to be used to override the default options.

```ts
const KBQ_TAGS_DEFAULT_OPTIONS: InjectionToken<KbqTagsDefaultOptions>;
```

#### kbqTagsDefaultOptionsProvider (const)

Utility provider for `KBQ_TAGS_DEFAULT_OPTIONS`.

```ts
const kbqTagsDefaultOptionsProvider: (
    options: Partial<KbqTagsDefaultOptions>
) => Provider;
```

## Examples

### Disable tag creation on the "paste" event

**Example: Tag autocomplete onpaste off** (`tag-autocomplete-onpaste-off`)

```ts tag-autocomplete-onpaste-off-example.ts
﻿import { AsyncPipe } from '@angular/common';
import { AfterViewInit, ChangeDetectionStrategy, Component, ElementRef, viewChild } from '@angular/core';
import { FormControl, ReactiveFormsModule } from '@angular/forms';
import { KbqAutocompleteModule, KbqAutocompleteSelectedEvent } from '@koobiq/components/autocomplete';
import { ENTER, KbqHighlightBackgroundPipe } from '@koobiq/components/core';
import { KbqFormFieldModule } from '@koobiq/components/form-field';
import { KbqIconModule } from '@koobiq/components/icon';
import { KbqTagInput, KbqTagInputEvent, KbqTagsModule, kbqTagsDefaultOptionsProvider } from '@koobiq/components/tags';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';

const autocompleteValueCoercion = (value): string => (value?.new ? value.value : value) || '';

@Component({
    selector: 'tag-autocomplete-onpaste-off-example',
    imports: [
        KbqTagsModule,
        KbqFormFieldModule,
        KbqIconModule,
        ReactiveFormsModule,
        KbqAutocompleteModule,
        AsyncPipe,
        KbqHighlightBackgroundPipe
    ],
    templateUrl: 'tag-autocomplete-onpaste-off-example.html',
    styles: `
        :host {
            display: flex;
            flex-direction: column;
            align-items: center;
            gap: var(--kbq-size-m);
            margin: var(--kbq-size-5xl);
        }
    `,
    // turn off tag add on paste with InjectionToken
    providers: [kbqTagsDefaultOptionsProvider({ separatorKeyCodes: [ENTER], addOnPaste: false })],
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class TagAutocompleteOnpasteOffExample implements AfterViewInit {
    readonly tagInput = viewChild.required<ElementRef<HTMLInputElement>>('tagInput');
    readonly tagInputDirective = viewChild.required('tagInput', { read: KbqTagInput });

    control = new FormControl();

    suggestions: string[] = ['tag1', 'tag2', 'tag3', 'tag4', 'tag5', 'tag6', 'tag7', 'tag8', 'tag9', 'tag10'];
    selectedTags: string[] = ['tag1'];

    filteredTags: Observable<string[]>;
    hasDuplicates: boolean;

    get canCreate(): boolean {
        const cleanedValue: string = (this.control.value || '').trim();

        return (
            !!cleanedValue &&
            [...new Set(this.suggestions.concat(this.selectedTags))].every((tag) => tag !== cleanedValue)
        );
    }

    ngAfterViewInit(): void {
        this.filteredTags = this.control.valueChanges.pipe(map((e: string | null) => this.onControlValueChanges(e)));
    }

    addOnBlurFunc(event: FocusEvent) {
        const target: HTMLElement = event.relatedTarget as HTMLElement;

        if (!target || target.tagName !== 'KBQ-OPTION') {
            const mcTagEvent: KbqTagInputEvent = {
                input: this.tagInput().nativeElement,
                value: this.tagInput().nativeElement.value
            };

            this.onCreate(mcTagEvent);
        }
    }

    onCreate({ input, value }: KbqTagInputEvent): void {
        this.control.setValue(value);
        const cleanedValue = (value || '').trim();

        if (cleanedValue && this.canCreate) {
            this.selectedTags.push(cleanedValue);

            // Reset the input value
            if (input) input.value = '';

            this.control.setValue(null);

            return;
        }

        // save input value on paste event to continue editing
        input.value = cleanedValue;
        this.control.setValue(cleanedValue);
    }

    onSelect({ option }: KbqAutocompleteSelectedEvent): void {
        option.deselect();

        this.selectedTags.push(autocompleteValueCoercion(option.value));
        this.tagInput().nativeElement.value = '';
        this.control.setValue(null);
    }

    onRemove(fruit: any): void {
        const index = this.selectedTags.indexOf(fruit);

        if (index >= 0) {
            this.selectedTags.splice(index, 1);
        }
    }

    private filter(value: string): string[] {
        // Convert the input value to lowercase for case-insensitive comparison
        const filterValue = value ? value.toLowerCase() : '';

        // Combine all tags and selected tags into a single array, removing duplicates
        const uniqueTags = [...new Set(this.suggestions.concat(this.selectedTags))];

        return uniqueTags.filter((tag) => tag.toLowerCase().indexOf(filterValue) === 0);
    }

    private onControlValueChanges = (value: string | null) => {
        const typedText: string | undefined = autocompleteValueCoercion(value)?.trim();
        const filteredTagsByInput = typedText ? this.filter(typedText) : this.suggestions.slice();

        const inputAndSelectionTagsDiff = filteredTagsByInput.filter((tag) => !this.selectedTags.includes(tag));

        // check for scenario where duplicate exists but also can create/select other tags
        this.hasDuplicates = !inputAndSelectionTagsDiff.length && this.tagInputDirective().hasDuplicates;

        // Return the difference between filtered tags and selected tags
        return inputAndSelectionTagsDiff;
    };

    protected afterRemove(): void {
        this.tagInput().nativeElement.focus();
    }
}
```

```html tag-autocomplete-onpaste-off-example.html
<kbq-form-field>
    <kbq-tag-list #tagList="kbqTagList">
        @for (tag of selectedTags; track $index) {
            <kbq-tag [value]="tag" (removed)="onRemove(tag)">
                {{ tag }}
                <i kbq-icon="kbq-xmark-s_16" kbqTagRemove (click)="afterRemove()"></i>
            </kbq-tag>
        }

        <!-- turn off tag add on paste with Input property -->
        <input
            #tagInput
            autocomplete="off"
            placeholder="New tag"
            [distinct]="true"
            [formControl]="control"
            [kbqAutocomplete]="autocomplete"
            [kbqTagInputAddOnBlur]="false"
            [kbqTagInputAddOnPaste]="false"
            [kbqTagInputFor]="tagList"
            [kbqTagInputSeparatorKeyCodes]="[]"
            (blur)="addOnBlurFunc($event)"
            (kbqTagInputTokenEnd)="onCreate($event)"
        />

        <kbq-cleaner />
    </kbq-tag-list>
    <kbq-autocomplete #autocomplete (optionSelected)="onSelect($event)">
        @if (canCreate) {
            <kbq-option [value]="{ new: true, value: tagInput.value }">Создать: {{ tagInput.value }}</kbq-option>
        }
        @if (hasDuplicates) {
            <kbq-option [disabled]="true">Ничего не найдено</kbq-option>
        }
        @for (tag of filteredTags | async; track $index) {
            <kbq-option [value]="tag">
                <span [innerHTML]="tag | kbqHighlightBackground: tagInput.value"></span>
            </kbq-option>
        }
    </kbq-autocomplete>
</kbq-form-field>
```
