npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

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

About

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

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

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

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

Open Software & Tools

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

© 2026 – Pkg Stats / Ryan Hefner

@anglr/select

v17.1.0

Published

Angular component representing html select

Readme

npm version Build status

@anglr/select

Angular component representing an HTML <select>. Fully plugin-based and signal-driven, with support for multi-select, live search, keyboard navigation, absolute positioning, and deep customisation via swappable plugins.

Resources

  • API reference: https://ressurectit.github.io/#/content/api/ng-select/select
  • API extensions reference: https://ressurectit.github.io/#/content/api/ng-select-extensions/select-extensions
  • Live samples: https://ressurectit.github.io/#/content/select#samples

Installation

npm install @anglr/select

Peer dependencies: @angular/core, @angular/common, @angular/forms, @anglr/common, @jscrpt/common, @css-styles/common, rxjs, lodash-es.


Quick Start

Import the standalone components directly (no module required):

import {Component, ChangeDetectionStrategy, signal} from '@angular/core';
import {form, FormField} from '@angular/forms/signals';
import {Select, Option, SelectFormControl} from '@anglr/select';

@Component(
{
    template: `
        <ng-select [formField]="myField">
            <ng-option value="a" text="Option A"/>
            <ng-option value="b" text="Option B"/>
            <ng-option value="c" text="Option C"/>
        </ng-select>
    `,
    imports:
    [
        Select,
        Option,
        FormField,
        SelectFormControl,
    ],
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class MyComponent
{
    protected myField = form(signal<string|null>(null));
}

Components

Select

Selector: ng-select

The root component. It manages the plugin lifecycle, template gathering, and options gathering.

| Input | Type | Description | |---|---|---| | [selectOptions] | RecursivePartial<SelectOptions> | Full configuration object (plugins, CSS classes, behaviour flags) | | [disabled] | boolean | Signal-based disabled state | | [readonly] | boolean | Signal-based readonly state |

| Property | Type | Description | |---|---|---| | initialized | Signal<boolean> | Emits true once every plugin is ready | | events | SelectEvents | Observable streams for focus, click, blur | | availableOptions | Signal<SelectOptionState[]> | Current list of <ng-option> children |

| Method | Returns | Description | |---|---|---| | getPlugin(type) | SelectPlugin | Retrieve a live plugin instance by SelectPluginType | | execute(...actions) | void | Run one or more SelectAction lambdas | | executeAndReturn(fn) | TResult | Run a SelectFunction and return its result |

Option

Selector: ng-option

Declares a single selectable option inside <ng-select>.

| Input | Type | Description | |---|---|---| | [value] | TValue | The value stored when selected | | [text] | string | Display text |

OptionGroup

Selector: ng-option-group

Groups <ng-option> children under a labelled header.

| Input | Type | Description | |---|---|---| | [text] | string | (required) Group label text |

<ng-select [formField]="field">
    <ng-option-group text="Fruits">
        <ng-option value="apple" text="Apple"/>
        <ng-option value="banana" text="Banana"/>
    </ng-option-group>
    <ng-option-group text="Vegetables">
        <ng-option value="carrot" text="Carrot"/>
        <ng-option value="pea" text="Pea"/>
    </ng-option-group>
</ng-select>

Form Integration

Reactive Forms (FormControl)

Use the SelectControlValueAccessor directive. It activates automatically on ng-select[formControl], ng-select[formControlName], or ng-select[ngModel].

import {Component, ChangeDetectionStrategy} from '@angular/core';
import {FormControl, ReactiveFormsModule} from '@angular/forms';
import {Select, Option, SelectControlValueAccessor} from '@anglr/select';

@Component(
{
    template: `
        <ng-select [formControl]="ctrl">
            <ng-option value="a" text="A"/>
            <ng-option value="b" text="B"/>
        </ng-select>
    `,
    imports:
    [
        Select,
        Option,
        ReactiveFormsModule,
        SelectControlValueAccessor,
    ],
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class MyComponent
{
    protected ctrl: FormControl<string|null> = new FormControl<string|null>(null);
}

Signal Forms (FormField)

Use the SelectFormControl directive with Angular's signal-based form() / FormField:

import {Component, ChangeDetectionStrategy, signal} from '@angular/core';
import {form, FormField} from '@angular/forms/signals';
import {Select, Option, SelectFormControl} from '@anglr/select';

@Component(
{
    template: `
        <ng-select [formField]="myField">
            <ng-option value="x" text="X"/>
            <ng-option value="y" text="Y"/>
        </ng-select>
        <div>Value: {{ myField().value() | json }}</div>
    `,
    imports:
    [
        Select,
        Option,
        FormField,
        SelectFormControl,
    ],
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class MyComponent
{
    protected myField = form(signal<string|null>(null));
}

Two-way Binding (withDirectAccess)

Use the withDirectAccess directive for simple two-way model binding without a form layer:

import {Component, ChangeDetectionStrategy, signal} from '@angular/core';
import {Select, Option} from '@anglr/select';

@Component(
{
    template: `
        <ng-select [(value)]="selectedValue" withDirectAccess>
            <ng-option value="a" text="A"/>
        </ng-select>
    `,
    imports:
    [
        Select,
        Option,
    ],
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class MyComponent
{
    protected selectedValue = signal<string|null>(null);
}

Directives

| Directive | Selector | Description | |---|---|---| | SelectControlValueAccessor | ng-select[formControl], ng-select[formControlName], ng-select[ngModel] | Bridges to Angular reactive/template-driven forms | | SelectFormControl | ng-select[formField] | Bridges to Angular signal-based forms | | SelectEdit | ng-select[edit] | Enables type-ahead / editable mode (swaps to EditNormalState + EditLiveSearch) | | SelectAbsolute | ng-select[absolute] | Appends popup to document.body for overflow-safe positioning | | SelectMultipleKeepPopup | ng-select[multiple] | Keeps popup open after each selection in multi-select mode | | SelectPlaceholder | ng-select[placeholder] | Sets placeholder text reactively | | SelectNoCarret | ng-select[noCarret] | Hides the dropdown caret | | NormalStateTemplate | [normalStateTemplate] | Custom template for the closed/normal state display | | NormalStateTagTemplate | [normalStateTagTemplate] | Custom template for individual tags in multi-select normal state | | OptionTemplate | [optionTemplate] | Custom template for each option in the popup | | OptionGroupTemplate | [optionGroupTemplate] | Custom template for option group headers | | HighlightInstance | [highlightInstance] | Manages ::highlight(search-query) CSS Highlight API ranges for the popup (bound to SimplePopup) | | HighlightSearch | [highlightSearch] | Attached to a popup option element; produces Range objects for every occurrence of the live-search query in its text nodes | | HighlightText | [highlightText] | Marks an inner element inside a custom optionTemplate whose text should participate in search highlighting (nested under HighlightSearch) | | WithDirectAccess | ng-select[withDirectAccess] | Two-way value binding without a form control |


Plugins

Plugin Architecture

The select component is composed of 9 independent plugin slots. Each slot has a default implementation that can be swapped for an alternative or custom one. Plugins communicate via a shared SelectBus — a centralized reactive bus with subjects/observables for events like option activation, popup show/hide, keyboard actions, etc.

┌─────────────────────────────────────────────┐
│                  ng-select                  │
│                                             │
│  ┌──────────────┐  ┌────────────────────┐   │
│  │  LiveSearch   │  │   Interactions     │   │
│  └──────────────┘  └────────────────────┘   │
│  ┌──────────────┐  ┌────────────────────┐   │
│  │OptionsHandler│  │  KeyboardHandler   │   │
│  └──────────────┘  └────────────────────┘   │
│  ┌──────────────┐  ┌────────────────────┐   │
│  │  Positioner   │  │   ValueHandler     │   │
│  └──────────────┘  └────────────────────┘   │
│                                             │
│  ┌──────────────────────────────────────┐   │
│  │          Visual Container            │   │
│  │  ┌──────────┐ ┌───────┐ ┌───────┐   │   │
│  │  │ReadonlyS.│ │Normal │ │ Popup │   │   │
│  │  └──────────┘ │ State │ │       │   │   │
│  │               └───────┘ └───────┘   │   │
│  └──────────────────────────────────────┘   │
└─────────────────────────────────────────────┘

Every plugin implements the SelectPlugin base interface and receives:

  • selectPlugins — references to all sibling plugin instances
  • pluginElement — its own host ElementRef
  • options — merged configuration object
  • selectBus — the shared reactive bus

Available Plugins

| Slot | Default | Alternatives | Purpose | |---|---|---|---| | Interactions | SimpleInteractions | — | Click-to-toggle, option click, click-outside handling | | KeyboardHandler | SimpleKeyboardHandler | — | Arrow keys, Enter, Backspace, alphanumeric search | | LiveSearch | NoLiveSearch | FilterLiveSearch, EditLiveSearch | Text-based filtering of options | | NormalState | SimpleNormalState | EditNormalState | Closed-state appearance (value, caret, cancel button) | | OptionsHandler | SimpleOptionsHandler | NoOptionsHandler | Options list management, filtering, add-new-option | | Popup | SimplePopup | — | Dropdown panel rendering (supports live-search text highlighting via the CSS Highlight API) | | Positioner | CommonPositioner | PopoverPositioner, NoPositioner | Popup positioning (z-index, absolute, popover API) | | ReadonlyState | (same as NormalState) | Custom component | Display when readonly | | ValueHandler | StaticValueHandler | DynamicValueHandler | Selected value storage, single/multi-select logic |

Configuring Plugins

Pass a [selectOptions] object to override plugin types and/or their options. Partial objects are deep-merged with defaults:

import {FilterLiveSearch, SelectOptions} from '@anglr/select';
import {RecursivePartial} from '@jscrpt/common';

const options: RecursivePartial<SelectOptions> =
{
    plugins:
    {
        liveSearch:
        {
            type: FilterLiveSearch,
        },
        popup:
        {
            options:
            {
                liveSearchEnabled: true,
            },
        },
    },
};
<ng-select [selectOptions]="options" [formField]="field">
    <ng-option value="a" text="Alpha"/>
</ng-select>

You can also swap plugin types via dependency injection providers:

import {provideNormalStateType, EditNormalState} from '@anglr/select';

@Component(
{
    providers:
    [
        provideNormalStateType(EditNormalState),
    ],
})

Implementing a Custom Plugin

To create a custom plugin, implement the corresponding plugin interface and register it via selectOptions or a provider.

Example: Custom ReadonlyState Plugin

A custom readonly state that renders selected values differently.

import {Component, ChangeDetectionStrategy, ElementRef} from '@angular/core';
import {ReadonlyState, ReadonlyStateOptions, SelectBus, SelectPluginInstances} from '@anglr/select';

@Component(
{
    selector: 'custom-readonly-state',
    template: `
        <div class="custom-readonly">
            @if (selectBus.selectedOptions(); as selected) {
                @if (isArray(selected)) {
                    @for (opt of selected; track opt.index) {
                        <span class="tag">{{ opt.text() }}</span>
                    }
                } @else {
                    <span>{{ selected.text() }}</span>
                }
            } @else {
                <em>No value selected</em>
            }
        </div>
    `,
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class CustomReadonlyStateComponent implements ReadonlyState<unknown, unknown>
{
    public readonly selectPlugins!: SelectPluginInstances<unknown, unknown>;
    public readonly pluginElement!: ElementRef<HTMLElement>;
    public options!: ReadonlyStateOptions;
    public readonly selectBus!: SelectBus<unknown, unknown>;

    protected isArray = Array.isArray;

    public focus(): void
    {
        // no-op for readonly
    }
}

Register the custom plugin:

this.selectOptions =
{
    plugins:
    {
        readonlyState:
        {
            type: CustomReadonlyStateComponent,
        },
    },
};
<ng-select [selectOptions]="selectOptions" [formField]="field">
    <ng-option value="a" text="A"/>
</ng-select>

Example: Custom OptionsHandler Plugin

You can extend OptionsHandlerBase to add custom filtering logic:

import {Component} from '@angular/core';
import {OptionsHandlerBase} from '@anglr/select';

@Component(
{
    selector: 'custom-options-handler',
    template: '',
})
export class CustomOptionsHandlerComponent extends OptionsHandlerBase<string>
{
    // Override filtering, sorting, or add-new-option logic
}

Extensions

The @anglr/select/extensions entry point provides helper functions for programmatic interaction with the select via execute() / executeAndReturn():

import {getSearch, getValue, setReadonly, setValue} from '@anglr/select/extensions';

| Function | Type | Description | |---|---|---| | getValue() | SelectFunction | Returns the current value (reactive) | | setValue(value) | SelectAction | Sets the select's value programmatically | | getSearch() | SelectFunction | Returns the current live-search text (reactive) | | setReadonly(flag?) | SelectAction | Sets the select to readonly mode |

Usage:

// Get the current value
const value = this.select().executeAndReturn(getValue());

// Set a value programmatically
this.select().execute(setValue('option-a'));

// Get current search text (in an effect for reactivity)
effect(() =>
{
    const search = this.select().executeAndReturn(getSearch());
    // fetch remote data based on search...
});

Pipes

| Pipe | Pure | Description | |---|---|---| | displayValue | No | Transforms SelectOption or SelectOption[] into a display string (for normal/readonly state) | | displayOptionValue | No | Transforms a single SelectOption shown in the popup into a display string; falls back to displaySelectedValue when displayOptionValue is not configured on SelectBusOptions | | hasValue | Yes | Returns true when the option(s) represent a selected value | | getPlugin | Yes | Retrieves a typed plugin instance from another plugin | | addNewOption | Yes | Produces the "add new option" label for synthetic options | | groupedListOptions | Yes | Converts a flat option list into grouped tuples for rendering | | optionCssClasses | No | Returns CSS class array for a popup option (selected, active, grouped) |


Styling

Setup

The library ships pre-built SCSS that uses CSS custom properties for theming. Import styles in your global stylesheet:

// Import the light theme (includes core CSS + variables)
@use '@anglr/select/styles/themes/light' as selectLight;

// Apply the theme
@include selectLight.buildTheme();

Or for the dark theme:

@use '@anglr/select/styles/themes/dark' as selectDark;

@include selectDark.buildTheme();

Themes

Two pre-built themes are included:

| Theme | Import Path | Description | |---|---|---| | Light | @anglr/select/styles/themes/light | Light grays, blue focus, black text | | Dark | @anglr/select/styles/themes/dark | Dark grays, blue focus, white text |

Each theme exposes:

  • defineTheme($fontSize, $theme, $customization) — returns a theme map
  • buildTheme() — applies the theme to the document

CSS Custom Properties

All visual aspects are driven by CSS custom properties. Here are the key ones:

Normal State

| Property | Default (Light) | Description | |---|---|---| | --select-normalState-borderColor | #666 | Border color | | --select-normalState-background | #eee | Background color | | --select-normalState-foreground | #000 | Text color | | --select-normalState-borderRadius | 4px | Corner rounding | | --select-normalState-padding | 1px 2px | Inner padding | | --select-normalState-focus-borderColor | #0066ff | Focus border | | --select-normalState-focus-outlineColor | #0066ff | Focus outline | | --select-normalState-tag-background | #ccc | Multi-select tag background | | --select-normalState-tag-foreground | #000 | Multi-select tag text | | --select-normalState-disabled-background | (derived) | Disabled background |

Popup

| Property | Default (Light) | Description | |---|---|---| | --select-popup-container-background | #F0F0F0 | Popup background | | --select-popup-container-foreground | #000 | Popup text color | | --select-popup-container-borderColor | #acacac | Popup border | | --select-popup-option-active-background | (derived from container) | Hovered option | | --select-popup-option-selected-background | (derived from container) | Selected option | | --select-popup-option-searchHighlight-textDecoration | underline | Decoration applied to ::highlight(search-query) matches inside the popup | | --select-popup-option-searchHighlight-textShadow | 0 0 2px hsl(from currentColor h s 45%) | Shadow applied to ::highlight(search-query) matches inside the popup |

Live Search

| Property | Default (Light) | Description | |---|---|---| | --select-liveSearch-placeholder-foreground | #000 | Placeholder color |

Customizing a Theme

Override specific values when calling defineTheme:

@use '@anglr/select/styles/themes/light' as selectLight;
@use '@anglr/select/styles/core/mixins' as selectMixins;

$myTheme: selectLight.defineTheme(
    $fontSize: 16px,
    $customization: (
        select: (
            normalState: (
                borderColor: #999,
                background: #fff,
                foreground: #333,
                borderRadius: 8px,
                focus: (
                    borderColor: #ff6600,
                    outlineColor: #ff6600,
                ),
                tag: (
                    background: #e0e0e0,
                    borderRadius: 12px,
                ),
            ),
            popup: (
                container: (
                    background: #fff,
                    borderColor: #ddd,
                    borderRadius: 8px,
                ),
            ),
        ),
    ),
);

@include selectMixins.buildTheme($myTheme);

Building a Custom Theme

Create a theme from scratch using the defineTheme function from the default theme:

@use '@anglr/select/styles/core/defaultTheme' as selectDefault;
@use '@anglr/select/styles/core/mixins' as selectMixins;

$customTheme: selectDefault.defineTheme(
    $fontSize: 14px,
    $theme: (
        select: (
            normalState: (
                borderColor: #3498db,
                background: #ecf0f1,
                foreground: #2c3e50,
                focus: (
                    borderColor: #2980b9,
                    outlineColor: #2980b9,
                ),
            ),
            popup: (
                container: (
                    background: #ecf0f1,
                    foreground: #2c3e50,
                    borderColor: #bdc3c7,
                ),
            ),
        ),
    ),
);

@include selectMixins.buildTheme($customTheme);

Overriding Styles in Components

For component-scoped overrides, wrap the select in a container and override CSS custom properties:

<div class="select-override">
    <ng-select [formField]="field">
        <ng-option value="a" text="A"/>
    </ng-select>
</div>
.select-override {
    --select-normalState-borderColor: tomato;
    --select-normalState-background: lightyellow;
    --select-normalState-focus-borderColor: red;
    --select-popup-container-background: lightyellow;
}

Samples

Basic Select

Minimal select with reactive forms:

// basic.component.ts
import {Component, ChangeDetectionStrategy} from '@angular/core';
import {JsonPipe} from '@angular/common';
import {FormControl, ReactiveFormsModule} from '@angular/forms';
import {Option, Select, SelectControlValueAccessor} from '@anglr/select';

@Component(
{
    selector: 'basic-sample',
    templateUrl: 'basic.component.html',
    imports:
    [
        Select,
        Option,
        JsonPipe,
        ReactiveFormsModule,
        SelectControlValueAccessor,
    ],
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class BasicSampleComponent
{
    protected selectControl: FormControl<string|null> = new FormControl<string|null>(null);
}
<!-- basic.component.html -->
<div>Value: {{ selectControl.value | json }}</div>

<ng-select [formControl]="selectControl">
    <ng-option value="first" text="First value text"/>
    <ng-option value="second" text="Second value text"/>
    <ng-option value="third" text="Third value text"/>
    <ng-option value="fourth" text="Fourth value text"/>
    <ng-option value="fifth" text="Fifth value text"/>
</ng-select>

Multiple Select

Multi-select with signal forms. The multiple attribute enables multi-selection, and SelectMultipleKeepPopup keeps the popup open after each pick:

// multiple.component.ts
import {Component, ChangeDetectionStrategy, signal} from '@angular/core';
import {JsonPipe} from '@angular/common';
import {form, FormField} from '@angular/forms/signals';
import {Option, Select, SelectFormControl, SelectMultipleKeepPopup} from '@anglr/select';

@Component(
{
    selector: 'multiple-sample',
    templateUrl: 'multiple.component.html',
    imports:
    [
        Select,
        Option,
        JsonPipe,
        FormField,
        SelectFormControl,
        SelectMultipleKeepPopup,
    ],
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class MultipleSampleComponent
{
    protected selectField = form(signal<string|null>(null));
}
<!-- multiple.component.html -->
<div>Value: {{ selectField().value() | json }}</div>

<ng-select multiple [formField]="selectField">
    <ng-option value="first" text="First value text"/>
    <ng-option value="second" text="Second value text"/>
    <ng-option value="third" text="Third value text"/>
    <ng-option value="fourth" text="Fourth value text"/>
    <ng-option value="fifth" text="Fifth value text"/>
</ng-select>

Edit / Type-ahead Mode

Add the edit directive for a type-ahead experience. Works for both single and multi-select:

// edit.component.ts
import {Component, ChangeDetectionStrategy, signal} from '@angular/core';
import {JsonPipe} from '@angular/common';
import {form, FormField} from '@angular/forms/signals';
import {Option, Select, SelectEdit, SelectFormControl, SelectMultipleKeepPopup} from '@anglr/select';

@Component(
{
    selector: 'edit-sample',
    templateUrl: 'edit.component.html',
    imports:
    [
        Select,
        Option,
        JsonPipe,
        FormField,
        SelectEdit,
        SelectFormControl,
        SelectMultipleKeepPopup,
    ],
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class EditSampleComponent
{
    protected selectField = form(signal<string|null>(null));
    protected selectMultipleField = form(signal<string[]|null>([]));
}
<!-- edit.component.html -->
<!-- Single edit -->
<ng-select [formField]="selectField" edit>
    <ng-option value="first" text="First value text"/>
    <ng-option value="second" text="Second value text"/>
    <ng-option value="third" text="Third value text"/>
</ng-select>

<!-- Multiple edit -->
<ng-select multiple [formField]="selectMultipleField" edit>
    <ng-option value="first" text="First value text"/>
    <ng-option value="second" text="Second value text"/>
    <ng-option value="third" text="Third value text"/>
</ng-select>

Live Search (Filter)

Enable a filter text input inside the popup using FilterLiveSearch:

// liveSearch.component.ts
import {Component, ChangeDetectionStrategy, signal} from '@angular/core';
import {JsonPipe} from '@angular/common';
import {form, FormField} from '@angular/forms/signals';
import {Option, Select, SelectOptions, FilterLiveSearch, SelectFormControl} from '@anglr/select';
import {RecursivePartial} from '@jscrpt/common';

@Component(
{
    selector: 'live-search-sample',
    templateUrl: 'liveSearch.component.html',
    imports:
    [
        Select,
        Option,
        JsonPipe,
        FormField,
        SelectFormControl,
    ],
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class LiveSearchSampleComponent
{
    protected selectField = form(signal<string|null>(null));

    protected selectOptions: RecursivePartial<SelectOptions> =
    {
        plugins:
        {
            popup:
            {
                options:
                {
                    liveSearchEnabled: true,
                },
            },
            liveSearch:
            {
                type: FilterLiveSearch,
            },
        },
    };
}
<!-- liveSearch.component.html -->
<div>Value: {{ selectField().value() | json }}</div>

<ng-select [selectOptions]="selectOptions" [formField]="selectField">
    <ng-option value="first" text="First value text"/>
    <ng-option value="second" text="Second value text"/>
    <ng-option value="third" text="Third value text"/>
    <ng-option value="fourth" text="Fourth value text"/>
    <ng-option value="fifth" text="Fifth value text"/>
</ng-select>

Search Highlighting

For custom option templates, wrap the highlightable spans with the HighlightText directive so nested text nodes are registered with the parent HighlightSearch:

import {HighlightText, NormalStateTemplate, Option, OptionTemplate, Select} from '@anglr/select';

@Component(
{
    imports:
    [
        Select,
        Option,
        HighlightText,
        OptionTemplate,
        NormalStateTemplate,
    ],
})
export class HighlightedTemplateComponent { /* ... */ }
<ng-select [selectOptions]="selectOptions" [formField]="field">
    <ng-option value="a" text="Apple"/>
    <ng-option value="b" text="Banana"/>

    <span *optionTemplate="let option">
        <strong>{{ option.value() }}</strong>
        <span highlightText> - {{ option.text() }}</span>
    </span>
</ng-select>

Custom Templates

Use [normalStateTemplate] and [optionTemplate] to completely customize rendering:

// customTemplate.component.ts
import {Component, ChangeDetectionStrategy, signal} from '@angular/core';
import {JsonPipe} from '@angular/common';
import {form, FormField} from '@angular/forms/signals';
import {DisplayValue, NormalStateTemplate, Option, OptionTemplate, Select, SelectFormControl} from '@anglr/select';

@Component(
{
    selector: 'custom-template-sample',
    templateUrl: 'customTemplate.component.html',
    imports:
    [
        Select,
        Option,
        JsonPipe,
        FormField,
        DisplayValue,
        OptionTemplate,
        SelectFormControl,
        NormalStateTemplate,
    ],
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class CustomTemplateSampleComponent
{
    protected selectField = form(signal<string|null>(null));
}
<!-- customTemplate.component.html -->
<div>Value: {{ selectField().value() | json }}</div>

<ng-select [formField]="selectField">
    <ng-option value="first" text="First value text"/>
    <ng-option value="second" text="Second value text"/>
    <ng-option value="third" text="Third value text"/>
    <ng-option value="fourth" text="Fourth value text"/>
    <ng-option value="fifth" text="Fifth value text"/>

    <!-- Custom normal state: shows arrows around the display value -->
    <span *normalStateTemplate="let normalState">
        =&gt; {{ normalState.selectBus.selectedOptions() | displayValue }} &lt;=
    </span>

    <!-- Custom option template: shows value and text -->
    <span *optionTemplate="let option">
        {{ option.value() }} - {{ option.text() }}
    </span>
</ng-select>

Lazy / Dynamic Options

Options can be loaded asynchronously. Use signals and @for to render them once loaded:

// basicLazy.component.ts
import {Component, ChangeDetectionStrategy, WritableSignal, signal} from '@angular/core';
import {JsonPipe} from '@angular/common';
import {form, FormField} from '@angular/forms/signals';
import {Option, Select, SelectFormControl} from '@anglr/select';

interface KodPopisValue
{
    kod: string;
    popis: string;
}

@Component(
{
    selector: 'basic-lazy-sample',
    templateUrl: 'basicLazy.component.html',
    imports:
    [
        Select,
        Option,
        JsonPipe,
        FormField,
        SelectFormControl,
    ],
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class BasicLazySampleComponent
{
    protected selectField = form(signal<string|null>(null));
    protected lazyOptions: WritableSignal<KodPopisValue[]> = signal([]);

    constructor()
    {
        // Simulate async loading
        setTimeout(() =>
        {
            this.lazyOptions.set([
                {kod: 'first', popis: 'First value text'},
                {kod: 'second', popis: 'Second value text'},
                {kod: 'third', popis: 'Third value text'},
            ]);
        }, 2500);
    }
}
<!-- basicLazy.component.html -->
<div>Value: {{ selectField().value() | json }}</div>

<ng-select [formField]="selectField">
    @for (option of lazyOptions(); track option) {
        <ng-option [value]="option.kod" [text]="option.popis"/>
    }
</ng-select>

Dynamic Options with Remote Data

Use CodeOptionsGatherer + DynamicValueHandler + FilterLiveSearch to load options from a remote API:

// dynamic.component.ts
import {Component, ChangeDetectionStrategy, effect, Signal, viewChild, signal} from '@angular/core';
import {JsonPipe} from '@angular/common';
import {form, FormField} from '@angular/forms/signals';
import {CodeOptionsGatherer, DynamicValueHandler, DynamicValueHandlerOptions, FilterLiveSearch, Select, SelectFormControl, SelectOption, SelectOptions} from '@anglr/select';
import {getSearch} from '@anglr/select/extensions';
import {RecursivePartial} from '@jscrpt/common';

@Component(
{
    selector: 'dynamic-sample',
    templateUrl: 'dynamic.component.html',
    imports:
    [
        Select,
        JsonPipe,
        FormField,
        SelectFormControl,
    ],
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class DynamicSampleComponent
{
    protected optionsGatherer: CodeOptionsGatherer<string> = new CodeOptionsGatherer<string>();
    protected selectField = form(signal<string|null>(null));
    protected select: Signal<Select<string>> = viewChild.required<Select<string>>('select');

    protected selectOptions: RecursivePartial<SelectOptions<string>> =
    {
        plugins:
        {
            liveSearch:
            {
                type: FilterLiveSearch,
            },
            valueHandler:
            {
                type: DynamicValueHandler,
                options: {} as DynamicValueHandlerOptions<string>,
            },
        },
        optionsGatherer: this.optionsGatherer,
    };

    constructor(private dataSvc: DataService)
    {
        effect(async () =>
        {
            const search = this.select().executeAndReturn(getSearch());
            const result = await this.dataSvc.search(search);

            this.optionsGatherer.setAvailableOptions(
                result.map(item =>
                ({
                    value: signal(item.id),
                    text: signal(item.name),
                    group: signal(null),
                })),
            );
        });
    }
}
<!-- dynamic.component.html -->
<div>Value: {{ selectField().value() | json }}</div>

<ng-select #select [selectOptions]="selectOptions" [formField]="selectField"/>

Dynamic Option Getter (Object Values)

When values are objects and you need to resolve them back from a server (e.g., when setting an initial value), use DynamicValueHandlerOptions.optionGetter:

this.selectOptions =
{
    valueExtractor: itm => itm.value()?.kod ?? '',
    plugins:
    {
        valueHandler:
        {
            type: DynamicValueHandler,
            options:
            {
                optionGetter: async (value) =>
                {
                    const result = await lastValueFrom(dataSvc.getDetail(value));

                    return result
                        ? {
                              group: signal(null),
                              text: signal(result.name),
                              value: signal({kod: result.kod, label: result.name}),
                          }
                        : null;
                },
            },
        },
    },
    optionsGatherer: this.optionsGatherer,
};

Absolute Popup

Use the absolute directive to render the popup as a direct child of <body>, preventing clipping by overflow: hidden containers:

<ng-select [formField]="field" absolute>
    <ng-option value="a" text="A"/>
    <ng-option value="b" text="B"/>
</ng-select>

Popover Popup

Use the native Popover API for popup positioning:

import {PopoverPositioner, PopoverPositionerOptions, SelectOptions} from '@anglr/select';
import {RecursivePartial} from '@jscrpt/common';

const selectOptions: RecursivePartial<SelectOptions<string>> =
{
    plugins:
    {
        interactions:
        {
            options:
            {
                handleClickOutside: false,
            },
        },
        positioner:
        {
            type: PopoverPositioner,
            options:
            {
                popoverAuto: true,
            } as PopoverPositionerOptions,
        },
    },
};
<ng-select [formField]="field" [selectOptions]="selectOptions">
    <ng-option value="a" text="A"/>
</ng-select>

Cancel Value

Show a cancel/clear button to reset the selected value:

const selectOptions: RecursivePartial<SelectOptions<string>> =
{
    plugins:
    {
        normalState:
        {
            options:
            {
                cancelValue: true,
            },
        },
    },
};
<ng-select [formField]="field" [selectOptions]="selectOptions">
    <ng-option value="a" text="A"/>
    <ng-option value="b" text="B"/>
</ng-select>

Hide Caret

In edit mode, hide the dropdown caret:

import {EditNormalStateOptions, SelectOptions} from '@anglr/select';

const selectOptions: RecursivePartial<SelectOptions<string>> =
{
    plugins:
    {
        normalState:
        {
            options:
            {
                carret: false,
            } as EditNormalStateOptions,
        },
    },
};
<ng-select [formField]="field" [selectOptions]="selectOptions" edit>
    <ng-option value="a" text="A"/>
</ng-select>

Or use the shorthand directive:

<ng-select [formField]="field" noCarret>
    <ng-option value="a" text="A"/>
</ng-select>

Add New Option

Allow users to create new options by typing. Configure newOptionGetter on the OptionsHandler:

const selectOptions: RecursivePartial<SelectOptions<string>> =
{
    plugins:
    {
        optionsHandler:
        {
            options:
            {
                newOptionGetter: (value: string) =>
                ({
                    group: signal(null),
                    text: signal(value),
                    value: signal(value),
                }),
            },
        },
    },
};
<ng-select [formField]="field" [selectOptions]="selectOptions" edit>
    <ng-option value="first" text="First value text"/>
    <ng-option value="second" text="Second value text"/>
</ng-select>

Readonly

Use Angular signal forms' readonly() to toggle readonly state:

import {signal} from '@angular/core';
import {form, FormField, readonly} from '@angular/forms/signals';

export class ReadonlySampleComponent
{
    protected readonly = signal(false);
    protected selectField = form(
        signal<string|null>(null),
        path => readonly(path, () => this.readonly()),
    );
}
<button (click)="readonly.set(!readonly())">
    {{ readonly() ? 'to normal' : 'to readonly' }}
</button>

<ng-select [formField]="selectField">
    <ng-option value="a" text="A"/>
    <ng-option value="b" text="B"/>
</ng-select>

Or use the setReadonly() extension:

import {setReadonly} from '@anglr/select/extensions';

this.select().execute(setReadonly(true));

Custom Readonly State

Replace the readonly visual entirely with a custom plugin component:

this.selectOptions =
{
    plugins:
    {
        readonlyState:
        {
            type: CustomReadonlyStateComponent,
        },
    },
};

Styling Override

Override CSS custom properties in a scoped container:

<div class="select-override">
    <ng-select [formField]="field">
        <ng-option value="first" text="First value text"/>
        <ng-option value="second" text="Second value text"/>
    </ng-select>
</div>
.select-override {
    --select-normalState-borderColor: tomato;
    --select-normalState-background: lightyellow;
    --select-normalState-foreground: #333;
    --select-normalState-focus-borderColor: red;
    --select-normalState-focus-outlineColor: red;
    --select-popup-container-background: lightyellow;
    --select-popup-container-borderColor: tomato;
}

License

MIT