@anglr/select 17.2.0 → 17.3.1-beta.20261108092016

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/readme.md CHANGED
@@ -49,6 +49,7 @@ Angular component representing an HTML `<select>`. Fully plugin-based and signal
49
49
  - [Readonly](#readonly)
50
50
  - [Custom Readonly State](#custom-readonly-state)
51
51
  - [Styling Override](#styling-override)
52
+ - [License](#license)
52
53
 
53
54
  ## Resources
54
55
 
@@ -62,13 +63,32 @@ Angular component representing an HTML `<select>`. Fully plugin-based and signal
62
63
  npm install @anglr/select
63
64
  ```
64
65
 
65
- **Peer dependencies**: `@angular/core`, `@angular/common`, `@angular/forms`, `@anglr/common`, `@jscrpt/common`, `@css-styles/common`, `rxjs`, `lodash-es`.
66
+ **Dependencies**: `@angular/core`, `@angular/common`, `@angular/forms`, `@anglr/common`, `@jscrpt/common`, `@css-styles/common`, `rxjs`, `lodash-es`.
67
+
68
+ The default popup positioner (`CommonPositioner`) places the popup using the `Position` service from `@anglr/common`, so an implementation has to be registered once in the application, for example the Floating UI based one:
69
+
70
+ ```typescript
71
+ // app.config.ts
72
+ import {ApplicationConfig} from '@angular/core';
73
+ import {providePosition} from '@anglr/common';
74
+ import {FloatingUiDomPosition} from '@anglr/common/floating-ui';
75
+
76
+ export const appConfig: ApplicationConfig =
77
+ {
78
+ providers:
79
+ [
80
+ providePosition(FloatingUiDomPosition),
81
+ ],
82
+ };
83
+ ```
84
+
85
+ The styles have to be included as well, see [Styling](#styling).
66
86
 
67
87
  ---
68
88
 
69
89
  ## Quick Start
70
90
 
71
- Import the standalone components directly (no module required):
91
+ Import the standalone components directly (no module required). `SelectFormControl` connects the select to an Angular signal form field:
72
92
 
73
93
  ```typescript
74
94
  import {Component, ChangeDetectionStrategy, signal} from '@angular/core';
@@ -148,6 +168,8 @@ Groups `<ng-option>` children under a labelled header.
148
168
  |---|---|---|
149
169
  | `[text]` | `string` | **(required)** Group label text |
150
170
 
171
+ The popup renders a header for each group, followed by its options (`OptionGroup` has to be imported next to `Option`):
172
+
151
173
  ```html
152
174
  <ng-select [formField]="field">
153
175
  <ng-option-group text="Fruits">
@@ -197,12 +219,15 @@ export class MyComponent
197
219
  }
198
220
  ```
199
221
 
222
+ Values written to the control are applied to the select even when the value handler resolves them asynchronously (for example `DynamicValueHandler`). The control keeps the written value while it's being resolved.
223
+
200
224
  ### Signal Forms (FormField)
201
225
 
202
226
  Use the `SelectFormControl` directive with Angular's signal-based `form()` / `FormField`:
203
227
 
204
228
  ```typescript
205
229
  import {Component, ChangeDetectionStrategy, signal} from '@angular/core';
230
+ import {JsonPipe} from '@angular/common';
206
231
  import {form, FormField} from '@angular/forms/signals';
207
232
  import {Select, Option, SelectFormControl} from '@anglr/select';
208
233
 
@@ -219,6 +244,7 @@ import {Select, Option, SelectFormControl} from '@anglr/select';
219
244
  [
220
245
  Select,
221
246
  Option,
247
+ JsonPipe,
222
248
  FormField,
223
249
  SelectFormControl,
224
250
  ],
@@ -230,25 +256,30 @@ export class MyComponent
230
256
  }
231
257
  ```
232
258
 
259
+ The field's `disabled()` and `readonly()` states are reflected by the select (see [Readonly](#readonly)).
260
+
233
261
  ### Two-way Binding (withDirectAccess)
234
262
 
235
- Use the `withDirectAccess` directive for simple two-way model binding without a form layer:
263
+ Use the `WithDirectAccess` directive (`withDirectAccess` attribute) for simple two-way model binding without a form layer. Its `value` model works with any signal:
236
264
 
237
265
  ```typescript
238
266
  import {Component, ChangeDetectionStrategy, signal} from '@angular/core';
239
- import {Select, Option} from '@anglr/select';
267
+ import {Select, Option, WithDirectAccess} from '@anglr/select';
240
268
 
241
269
  @Component(
242
270
  {
243
271
  template: `
244
272
  <ng-select [(value)]="selectedValue" withDirectAccess>
245
273
  <ng-option value="a" text="A"/>
274
+ <ng-option value="b" text="B"/>
246
275
  </ng-select>
276
+ <div>Value: {{ selectedValue() }}</div>
247
277
  `,
248
278
  imports:
249
279
  [
250
280
  Select,
251
281
  Option,
282
+ WithDirectAccess,
252
283
  ],
253
284
  changeDetection: ChangeDetectionStrategy.OnPush
254
285
  })
@@ -266,7 +297,7 @@ export class MyComponent
266
297
  |---|---|---|
267
298
  | `SelectControlValueAccessor` | `ng-select[formControl]`, `ng-select[formControlName]`, `ng-select[ngModel]` | Bridges to Angular reactive/template-driven forms |
268
299
  | `SelectFormControl` | `ng-select[formField]` | Bridges to Angular signal-based forms |
269
- | `SelectEdit` | `ng-select[edit]` | Enables type-ahead / editable mode (swaps to `EditNormalState` + `EditLiveSearch`) |
300
+ | `SelectEdit` | `ng-select[edit]` | Enables type-ahead / editable mode (swaps to `EditNormalState` + `EditLiveSearch`, Space is typed into the input instead of opening the popup) |
270
301
  | `SelectAbsolute` | `ng-select[absolute]` | Appends popup to `document.body` for overflow-safe positioning |
271
302
  | `SelectMultipleKeepPopup` | `ng-select[multiple]` | Keeps popup open after each selection in multi-select mode |
272
303
  | `SelectPlaceholder` | `ng-select[placeholder]` | Sets placeholder text reactively |
@@ -324,7 +355,7 @@ Every plugin implements the `SelectPlugin` base interface and receives:
324
355
  | Slot | Default | Alternatives | Purpose |
325
356
  |---|---|---|---|
326
357
  | **Interactions** | `SimpleInteractions` | — | Click-to-toggle, option click, click-outside handling |
327
- | **KeyboardHandler** | `SimpleKeyboardHandler` | — | Arrow keys, Enter, Backspace, alphanumeric search |
358
+ | **KeyboardHandler** | `SimpleKeyboardHandler` | — | Arrow keys, Enter, Backspace, Space (opens popup), alphanumeric search |
328
359
  | **LiveSearch** | `NoLiveSearch` | `FilterLiveSearch`, `EditLiveSearch` | Text-based filtering of options |
329
360
  | **NormalState** | `SimpleNormalState` | `EditNormalState` | Closed-state appearance (value, caret, cancel button) |
330
361
  | **OptionsHandler** | `SimpleOptionsHandler` | `NoOptionsHandler` | Options list management, filtering, add-new-option |
@@ -335,61 +366,121 @@ Every plugin implements the `SelectPlugin` base interface and receives:
335
366
 
336
367
  ### Configuring Plugins
337
368
 
338
- Pass a `[selectOptions]` object to override plugin types and/or their options. Partial objects are deep-merged with defaults:
369
+ Pass a `[selectOptions]` object to override plugin types and/or their options. Partial objects are deep-merged with defaults. This select gets a filter input inside its popup:
339
370
 
340
371
  ```typescript
341
- import {FilterLiveSearch, SelectOptions} from '@anglr/select';
372
+ // configuringPlugins.component.ts
373
+ import {Component, ChangeDetectionStrategy, signal} from '@angular/core';
374
+ import {form, FormField} from '@angular/forms/signals';
375
+ import {FilterLiveSearch, Option, Select, SelectFormControl, SelectOptions} from '@anglr/select';
342
376
  import {RecursivePartial} from '@jscrpt/common';
343
377
 
344
- const options: RecursivePartial<SelectOptions> =
378
+ @Component(
379
+ {
380
+ selector: 'configuring-plugins-sample',
381
+ templateUrl: 'configuringPlugins.component.html',
382
+ imports:
383
+ [
384
+ Select,
385
+ Option,
386
+ FormField,
387
+ SelectFormControl,
388
+ ],
389
+ changeDetection: ChangeDetectionStrategy.OnPush
390
+ })
391
+ export class ConfiguringPluginsSampleComponent
345
392
  {
346
- plugins:
393
+ protected field = form(signal<string|null>(null));
394
+
395
+ protected options: RecursivePartial<SelectOptions> =
347
396
  {
348
- liveSearch:
349
- {
350
- type: FilterLiveSearch,
351
- },
352
- popup:
397
+ plugins:
353
398
  {
354
- options:
399
+ liveSearch:
400
+ {
401
+ type: FilterLiveSearch,
402
+ },
403
+ popup:
355
404
  {
356
- liveSearchEnabled: true,
405
+ options:
406
+ {
407
+ liveSearchEnabled: true,
408
+ },
357
409
  },
358
410
  },
359
- },
360
- };
411
+ };
412
+ }
361
413
  ```
362
414
 
363
415
  ```html
416
+ <!-- configuringPlugins.component.html -->
364
417
  <ng-select [selectOptions]="options" [formField]="field">
365
418
  <ng-option value="a" text="Alpha"/>
419
+ <ng-option value="b" text="Beta"/>
366
420
  </ng-select>
367
421
  ```
368
422
 
369
- You can also swap plugin types via dependency injection providers:
423
+ Plugin types and options can also be provided through dependency injection (`provide<Plugin>Type()` and `provide<Plugin>Options()` for every plugin slot). They apply to every select inside the providing component, or the whole application when registered in the application config. The following providers switch the selects to an editable mode, which is the same thing the `edit` directive does:
370
424
 
371
425
  ```typescript
372
- import {provideNormalStateType, EditNormalState} from '@anglr/select';
426
+ import {Component, ChangeDetectionStrategy, signal} from '@angular/core';
427
+ import {form, FormField} from '@angular/forms/signals';
428
+ import {EditLiveSearch, EditNormalState, Option, provideKeyboardHandlerOptions, provideLiveSearchType, provideNormalStateType, Select, SelectFormControl, SimpleKeyboardHandlerOptions} from '@anglr/select';
373
429
 
374
430
  @Component(
375
431
  {
432
+ selector: 'plugin-providers-sample',
433
+ template: `
434
+ <ng-select [formField]="field">
435
+ <ng-option value="a" text="Alpha"/>
436
+ <ng-option value="b" text="Beta"/>
437
+ </ng-select>
438
+ `,
439
+ imports:
440
+ [
441
+ Select,
442
+ Option,
443
+ FormField,
444
+ SelectFormControl,
445
+ ],
376
446
  providers:
377
447
  [
378
448
  provideNormalStateType(EditNormalState),
449
+ provideLiveSearchType(EditLiveSearch),
450
+ provideKeyboardHandlerOptions<SimpleKeyboardHandlerOptions>(
451
+ {
452
+ alphanumericSearch: false,
453
+ liveSearchEvents: true,
454
+ openPopupOnSpace: false,
455
+ }),
379
456
  ],
457
+ changeDetection: ChangeDetectionStrategy.OnPush
380
458
  })
459
+ export class PluginProvidersSampleComponent
460
+ {
461
+ protected field = form(signal<string|null>(null));
462
+ }
381
463
  ```
382
464
 
465
+ `SimpleKeyboardHandler` options (`SimpleKeyboardHandlerOptions`):
466
+
467
+ | Option | Type | Default | Description |
468
+ |---|---|---|---|
469
+ | `selectFirstDebounceTimeout` | `number` | `300` | Debounce timeout for selecting the first option while typing |
470
+ | `openPopupOnSpace` | `boolean` | `true` | Whether the Space key opens the popup |
471
+ | `alphanumericSearch` | `boolean` | `true` | Typing alphanumeric keys (outside of live search) selects the first option whose text starts with the typed text |
472
+ | `liveSearchEvents` | `boolean` | `false` | Whether keyboard events coming from the live search input are processed |
473
+
383
474
  ### Implementing a Custom Plugin
384
475
 
385
- To create a custom plugin, implement the corresponding plugin interface and register it via `selectOptions` or a provider.
476
+ To create a custom plugin, implement the corresponding plugin interface (or extend a provided base class) and register it via `selectOptions` or a provider. A plugin is a standalone component that the select creates dynamically. It gets the shared `SelectPluginInstances`, its own `ElementRef` and the `SelectBus` through `inject()`, and the select assigns `options` from the plugin description.
386
477
 
387
478
  **Example: Custom ReadonlyState Plugin**
388
479
 
389
- A custom readonly state that renders selected values differently.
480
+ A custom readonly state that renders the selected values as plain text, or "No value selected":
390
481
 
391
482
  ```typescript
392
- import {Component, ChangeDetectionStrategy, ElementRef} from '@angular/core';
483
+ import {Component, ChangeDetectionStrategy, ElementRef, inject} from '@angular/core';
393
484
  import {ReadonlyState, ReadonlyStateOptions, SelectBus, SelectPluginInstances} from '@anglr/select';
394
485
 
395
486
  @Component(
@@ -399,7 +490,7 @@ import {ReadonlyState, ReadonlyStateOptions, SelectBus, SelectPluginInstances} f
399
490
  <div class="custom-readonly">
400
491
  @if (selectBus.selectedOptions(); as selected) {
401
492
  @if (isArray(selected)) {
402
- @for (opt of selected; track opt.index) {
493
+ @for (opt of selected; track $index) {
403
494
  <span class="tag">{{ opt.text() }}</span>
404
495
  }
405
496
  } @else {
@@ -412,12 +503,21 @@ import {ReadonlyState, ReadonlyStateOptions, SelectBus, SelectPluginInstances} f
412
503
  `,
413
504
  changeDetection: ChangeDetectionStrategy.OnPush
414
505
  })
415
- export class CustomReadonlyStateComponent implements ReadonlyState<unknown, unknown>
506
+ export class CustomReadonlyStateComponent implements ReadonlyState
416
507
  {
417
- public readonly selectPlugins!: SelectPluginInstances<unknown, unknown>;
418
- public readonly pluginElement!: ElementRef<HTMLElement>;
419
- public options!: ReadonlyStateOptions;
420
- public readonly selectBus!: SelectBus<unknown, unknown>;
508
+ public readonly selectPlugins: SelectPluginInstances = inject(SelectPluginInstances);
509
+ public readonly pluginElement: ElementRef<HTMLElement> = inject(ElementRef);
510
+ public readonly selectBus: SelectBus = inject(SelectBus);
511
+
512
+ public options: ReadonlyStateOptions =
513
+ {
514
+ cancelValue: false,
515
+ texts:
516
+ {
517
+ cancelTitle: '',
518
+ },
519
+ cssClasses: {},
520
+ };
421
521
 
422
522
  protected isArray = Array.isArray;
423
523
 
@@ -428,46 +528,153 @@ export class CustomReadonlyStateComponent implements ReadonlyState<unknown, unkn
428
528
  }
429
529
  ```
430
530
 
431
- **Register the custom plugin:**
531
+ **Register the custom plugin.** The select below is readonly, so it renders the custom component instead of the normal state:
432
532
 
433
533
  ```typescript
434
- this.selectOptions =
534
+ // customReadonlyState.component.ts
535
+ import {Component, ChangeDetectionStrategy, signal} from '@angular/core';
536
+ import {form, FormField, readonly} from '@angular/forms/signals';
537
+ import {Option, Select, SelectFormControl, SelectOptions} from '@anglr/select';
538
+ import {RecursivePartial} from '@jscrpt/common';
539
+
540
+ import {CustomReadonlyStateComponent} from './customReadonlyStatePlugin.component';
541
+
542
+ @Component(
435
543
  {
436
- plugins:
544
+ selector: 'custom-readonly-state-sample',
545
+ templateUrl: 'customReadonlyState.component.html',
546
+ imports:
547
+ [
548
+ Select,
549
+ Option,
550
+ FormField,
551
+ SelectFormControl,
552
+ ],
553
+ changeDetection: ChangeDetectionStrategy.OnPush
554
+ })
555
+ export class CustomReadonlyStateSampleComponent
556
+ {
557
+ protected field = form(signal<string|null>('a'), path => readonly(path));
558
+
559
+ protected selectOptions: RecursivePartial<SelectOptions> =
437
560
  {
438
- readonlyState:
561
+ plugins:
439
562
  {
440
- type: CustomReadonlyStateComponent,
563
+ readonlyState:
564
+ {
565
+ type: CustomReadonlyStateComponent,
566
+ },
441
567
  },
442
- },
443
- };
568
+ };
569
+ }
444
570
  ```
445
571
 
446
572
  ```html
573
+ <!-- customReadonlyState.component.html -->
447
574
  <ng-select [selectOptions]="selectOptions" [formField]="field">
448
575
  <ng-option value="a" text="A"/>
576
+ <ng-option value="b" text="B"/>
449
577
  </ng-select>
450
578
  ```
451
579
 
452
580
  **Example: Custom OptionsHandler Plugin**
453
581
 
454
- You can extend `OptionsHandlerBase` to add custom filtering logic:
582
+ `OptionsHandlerBase` provides the injected plugin fields and the *add new option* support, so a custom options handler only needs `options`, `availableOptions` and `listOptions`. This one filters options by the live-search text and lists them sorted alphabetically, regardless of the order of the `<ng-option>` elements:
455
583
 
456
584
  ```typescript
457
- import {Component} from '@angular/core';
458
- import {OptionsHandlerBase} from '@anglr/select';
585
+ import {Component, ChangeDetectionStrategy, computed, Signal} from '@angular/core';
586
+ import {OptionsHandler, OptionsHandlerBase, OptionsHandlerOptions, SelectOptionState} from '@anglr/select';
459
587
 
588
+ /**
589
+ * Options handler that filters options by live search text and lists them sorted alphabetically by text
590
+ */
460
591
  @Component(
461
592
  {
462
- selector: 'custom-options-handler',
593
+ selector: 'sorted-options-handler',
463
594
  template: '',
595
+ changeDetection: ChangeDetectionStrategy.OnPush,
596
+ })
597
+ export class SortedOptionsHandler<TValue = unknown, TPublicValue = TValue> extends OptionsHandlerBase<TValue, TPublicValue> implements OptionsHandler<TValue, TPublicValue>
598
+ {
599
+ public options: OptionsHandlerOptions<TValue> =
600
+ {
601
+ newOptionGetter: null,
602
+ newOptionOnlyWhenNoOptions: false,
603
+ };
604
+
605
+ public readonly availableOptions: Signal<readonly SelectOptionState<TValue>[]|undefined|null> = computed(() => this.selectBus.selectOptions().optionsGatherer.availableOptions());
606
+
607
+ public readonly listOptions: Signal<readonly SelectOptionState<TValue>[]|undefined|null> = computed(() =>
608
+ {
609
+ const search = this.selectPlugins.LiveSearch.search();
610
+ const options = this.selectBus.selectOptions();
611
+
612
+ return this.availableOptions()
613
+ ?.filter(option => !search || options.textCompare(options.normalize(options.textExtractor(option)), options.normalize(search)))
614
+ .sort((first, second) => options.textExtractor(first).localeCompare(options.textExtractor(second)));
615
+ });
616
+ }
617
+ ```
618
+
619
+ ```typescript
620
+ import {Component, ChangeDetectionStrategy, signal} from '@angular/core';
621
+ import {form, FormField} from '@angular/forms/signals';
622
+ import {FilterLiveSearch, Option, Select, SelectFormControl, SelectOptions} from '@anglr/select';
623
+ import {RecursivePartial} from '@jscrpt/common';
624
+
625
+ import {SortedOptionsHandler} from './sortedOptionsHandler.component';
626
+
627
+ @Component(
628
+ {
629
+ selector: 'custom-options-handler-sample',
630
+ template: `
631
+ <ng-select [selectOptions]="selectOptions" [formField]="field">
632
+ <ng-option value="c" text="Cherry"/>
633
+ <ng-option value="a" text="Apple"/>
634
+ <ng-option value="b" text="Banana"/>
635
+ </ng-select>
636
+ `,
637
+ imports:
638
+ [
639
+ Select,
640
+ Option,
641
+ FormField,
642
+ SelectFormControl,
643
+ ],
644
+ changeDetection: ChangeDetectionStrategy.OnPush
464
645
  })
465
- export class CustomOptionsHandlerComponent extends OptionsHandlerBase<string>
646
+ export class CustomOptionsHandlerSampleComponent
466
647
  {
467
- // Override filtering, sorting, or add-new-option logic
648
+ protected field = form(signal<string|null>(null));
649
+
650
+ protected selectOptions: RecursivePartial<SelectOptions> =
651
+ {
652
+ plugins:
653
+ {
654
+ optionsHandler:
655
+ {
656
+ type: SortedOptionsHandler,
657
+ },
658
+ liveSearch:
659
+ {
660
+ type: FilterLiveSearch,
661
+ },
662
+ popup:
663
+ {
664
+ options:
665
+ {
666
+ liveSearchEnabled: true,
667
+ },
668
+ },
669
+ },
670
+ };
468
671
  }
469
672
  ```
470
673
 
674
+ **Custom ValueHandler**
675
+
676
+ A value handler implements `ValueHandler`: a `value` signal and a `setValue(value)` method. `setValue` returns `PromiseOr<void>`, so a value handler can resolve the written value asynchronously, as `DynamicValueHandler` does with its `optionGetter`. `SelectFormControl`, `SelectControlValueAccessor` and `WithDirectAccess` wait for the returned promise and keep the written value meanwhile.
677
+
471
678
  ---
472
679
 
473
680
  ## Extensions
@@ -475,7 +682,7 @@ export class CustomOptionsHandlerComponent extends OptionsHandlerBase<string>
475
682
  The `@anglr/select/extensions` entry point provides helper functions for programmatic interaction with the select via `execute()` / `executeAndReturn()`:
476
683
 
477
684
  ```typescript
478
- import {getSearch, getValue, setReadonly, setValue} from '@anglr/select/extensions';
685
+ import {getSearch, getSelectedOption, getSelectedOptions, getValue, setReadonly, setValue} from '@anglr/select/extensions';
479
686
  ```
480
687
 
481
688
  | Function | Type | Description |
@@ -483,23 +690,104 @@ import {getSearch, getValue, setReadonly, setValue} from '@anglr/select/extensio
483
690
  | `getValue()` | `SelectFunction` | Returns the current value (reactive) |
484
691
  | `setValue(value)` | `SelectAction` | Sets the select's value programmatically |
485
692
  | `getSearch()` | `SelectFunction` | Returns the current live-search text (reactive) |
486
- | `setReadonly(flag?)` | `SelectAction` | Sets the select to readonly mode |
693
+ | `getSelectedOption()` | `SelectFunction` | Returns the selected `SelectOption` of a single-value select (reactive); throws when the select is `multiple` |
694
+ | `getSelectedOptions()` | `SelectFunction` | Returns the selected `SelectOption` or `SelectOption[]` (reactive) |
695
+ | `setReadonly(flag?)` | `SelectAction` | Sets the select to readonly mode (`true` when the flag is omitted) |
487
696
 
488
- **Usage:**
697
+ The `get*` functions return `undefined` (`''` for `getSearch()`) until the select is initialized. They read signals, so used inside `computed()` or `effect()` they track changes of the select.
489
698
 
490
- ```typescript
491
- // Get the current value
492
- const value = this.select().executeAndReturn(getValue());
699
+ **Usage:** the page shows the value, live-search text and selected option text of the first select, and the texts of all options selected in the multiple select. The buttons change the value and toggle readonly:
493
700
 
494
- // Set a value programmatically
495
- this.select().execute(setValue('option-a'));
701
+ ```typescript
702
+ // extensions.component.ts
703
+ import {Component, ChangeDetectionStrategy, computed, Signal, viewChild} from '@angular/core';
704
+ import {JsonPipe} from '@angular/common';
705
+ import {FilterLiveSearch, Option, Select, SelectOption, SelectOptions} from '@anglr/select';
706
+ import {getSearch, getSelectedOption, getSelectedOptions, getValue, setReadonly, setValue} from '@anglr/select/extensions';
707
+ import {RecursivePartial} from '@jscrpt/common';
496
708
 
497
- // Get current search text (in an effect for reactivity)
498
- effect(() =>
709
+ @Component(
499
710
  {
500
- const search = this.select().executeAndReturn(getSearch());
501
- // fetch remote data based on search...
502
- });
711
+ selector: 'extensions-sample',
712
+ templateUrl: 'extensions.component.html',
713
+ imports:
714
+ [
715
+ Select,
716
+ Option,
717
+ JsonPipe,
718
+ ],
719
+ changeDetection: ChangeDetectionStrategy.OnPush
720
+ })
721
+ export class ExtensionsSampleComponent
722
+ {
723
+ protected select: Signal<Select<string>> = viewChild.required<Select<string>>('select');
724
+ protected multiSelect: Signal<Select<string>> = viewChild.required<Select<string>>('multiSelect');
725
+
726
+ protected selectOptions: RecursivePartial<SelectOptions<string>> =
727
+ {
728
+ plugins:
729
+ {
730
+ liveSearch:
731
+ {
732
+ type: FilterLiveSearch,
733
+ },
734
+ popup:
735
+ {
736
+ options:
737
+ {
738
+ liveSearchEnabled: true,
739
+ },
740
+ },
741
+ },
742
+ };
743
+
744
+ // reactive reads, recomputed whenever the select changes
745
+ protected value: Signal<string|string[]|undefined|null> = computed(() => this.select().executeAndReturn(getValue()));
746
+ protected search: Signal<string> = computed(() => this.select().executeAndReturn(getSearch()));
747
+ protected selectedOption: Signal<SelectOption<string>|undefined|null> = computed(() => this.select().executeAndReturn(getSelectedOption()));
748
+ protected selectedTexts: Signal<string[]> = computed(() =>
749
+ {
750
+ const options = this.multiSelect().executeAndReturn(getSelectedOptions());
751
+
752
+ return (Array.isArray(options) ? options : options ? [options] : []).map(option => option.text());
753
+ });
754
+
755
+ protected selectSecond(): void
756
+ {
757
+ this.select().execute(setValue('second'));
758
+ }
759
+
760
+ protected toggleReadonly(): void
761
+ {
762
+ const select = this.select();
763
+
764
+ select.execute(setReadonly(!select.selectOptions.readonly));
765
+ }
766
+ }
767
+ ```
768
+
769
+ ```html
770
+ <!-- extensions.component.html -->
771
+ <ng-select #select [selectOptions]="selectOptions">
772
+ <ng-option value="first" text="First value text"/>
773
+ <ng-option value="second" text="Second value text"/>
774
+ <ng-option value="third" text="Third value text"/>
775
+ </ng-select>
776
+
777
+ <button type="button" (click)="selectSecond()">select second</button>
778
+ <button type="button" (click)="toggleReadonly()">toggle readonly</button>
779
+
780
+ <div>Value: {{ value() | json }}</div>
781
+ <div>Search: {{ search() }}</div>
782
+ <div>Selected option text: {{ selectedOption()?.text() }}</div>
783
+
784
+ <ng-select #multiSelect multiple>
785
+ <ng-option value="first" text="First value text"/>
786
+ <ng-option value="second" text="Second value text"/>
787
+ <ng-option value="third" text="Third value text"/>
788
+ </ng-select>
789
+
790
+ <div>Selected texts: {{ selectedTexts().join(', ') }}</div>
503
791
  ```
504
792
 
505
793
  ---
@@ -522,14 +810,17 @@ effect(() =>
522
810
 
523
811
  ### Setup
524
812
 
525
- The library ships pre-built SCSS that uses CSS custom properties for theming. Import styles in your global stylesheet:
813
+ The library ships SCSS that uses CSS custom properties for theming. Import a theme in your global stylesheet and include it **inside a style rule** (the mixins output custom-property declarations and nested rules, so they can't be used at the stylesheet root):
526
814
 
527
815
  ```scss
528
816
  // Import the light theme (includes core CSS + variables)
529
817
  @use '@anglr/select/styles/themes/light' as selectLight;
530
818
 
531
819
  // Apply the theme
532
- @include selectLight.buildTheme();
820
+ :root
821
+ {
822
+ @include selectLight.buildTheme();
823
+ }
533
824
  ```
534
825
 
535
826
  Or for the dark theme:
@@ -537,7 +828,10 @@ Or for the dark theme:
537
828
  ```scss
538
829
  @use '@anglr/select/styles/themes/dark' as selectDark;
539
830
 
540
- @include selectDark.buildTheme();
831
+ :root
832
+ {
833
+ @include selectDark.buildTheme();
834
+ }
541
835
  ```
542
836
 
543
837
  ### Themes
@@ -551,7 +845,12 @@ Two pre-built themes are included:
551
845
 
552
846
  Each theme exposes:
553
847
  - `defineTheme($fontSize, $theme, $customization)` — returns a theme map
554
- - `buildTheme()` — applies the theme to the document
848
+ - `buildTheme()` — outputs the theme variables and the select CSS
849
+
850
+ The `@anglr/select/styles` entry point exposes the building blocks:
851
+ - `defineTheme($fontSize, $theme)` — builds a theme map from the default theme
852
+ - `buildTheme($theme)` — outputs the theme variables and the select CSS
853
+ - `buildThemeVars($theme)` / `buildThemeCss` — output only the variables or only the CSS, useful when switching between several themes (variables per theme class, CSS once)
555
854
 
556
855
  ### CSS Custom Properties
557
856
 
@@ -592,11 +891,11 @@ All visual aspects are driven by CSS custom properties. Here are the key ones:
592
891
 
593
892
  ### Customizing a Theme
594
893
 
595
- Override specific values when calling `defineTheme`:
894
+ Override specific values when calling `defineTheme` of a theme:
596
895
 
597
896
  ```scss
897
+ @use '@anglr/select/styles' as select;
598
898
  @use '@anglr/select/styles/themes/light' as selectLight;
599
- @use '@anglr/select/styles/core/mixins' as selectMixins;
600
899
 
601
900
  $myTheme: selectLight.defineTheme(
602
901
  $fontSize: 16px,
@@ -627,18 +926,20 @@ $myTheme: selectLight.defineTheme(
627
926
  ),
628
927
  );
629
928
 
630
- @include selectMixins.buildTheme($myTheme);
929
+ :root
930
+ {
931
+ @include select.buildTheme($myTheme);
932
+ }
631
933
  ```
632
934
 
633
935
  ### Building a Custom Theme
634
936
 
635
- Create a theme from scratch using the `defineTheme` function from the default theme:
937
+ Create a theme from scratch using `defineTheme` from `@anglr/select/styles`, which starts from the default theme values:
636
938
 
637
939
  ```scss
638
- @use '@anglr/select/styles/core/defaultTheme' as selectDefault;
639
- @use '@anglr/select/styles/core/mixins' as selectMixins;
940
+ @use '@anglr/select/styles' as select;
640
941
 
641
- $customTheme: selectDefault.defineTheme(
942
+ $customTheme: select.defineTheme(
642
943
  $fontSize: 14px,
643
944
  $theme: (
644
945
  select: (
@@ -662,27 +963,36 @@ $customTheme: selectDefault.defineTheme(
662
963
  ),
663
964
  );
664
965
 
665
- @include selectMixins.buildTheme($customTheme);
966
+ :root
967
+ {
968
+ @include select.buildTheme($customTheme);
969
+ }
666
970
  ```
667
971
 
668
972
  ### Overriding Styles in Components
669
973
 
670
- For component-scoped overrides, wrap the select in a container and override CSS custom properties:
974
+ For component-scoped overrides, wrap the select in a container and override CSS custom properties in the component's styles. Only selects inside the container change:
671
975
 
672
976
  ```html
977
+ <!-- stylingOverride.component.html -->
673
978
  <div class="select-override">
674
979
  <ng-select [formField]="field">
675
- <ng-option value="a" text="A"/>
980
+ <ng-option value="first" text="First value text"/>
981
+ <ng-option value="second" text="Second value text"/>
676
982
  </ng-select>
677
983
  </div>
678
984
  ```
679
985
 
680
986
  ```scss
987
+ // stylingOverride.component.scss
681
988
  .select-override {
682
989
  --select-normalState-borderColor: tomato;
683
990
  --select-normalState-background: lightyellow;
991
+ --select-normalState-foreground: #333;
684
992
  --select-normalState-focus-borderColor: red;
993
+ --select-normalState-focus-outlineColor: red;
685
994
  --select-popup-container-background: lightyellow;
995
+ --select-popup-container-borderColor: tomato;
686
996
  }
687
997
  ```
688
998
 
@@ -690,9 +1000,11 @@ For component-scoped overrides, wrap the select in a container and override CSS
690
1000
 
691
1001
  ## Samples
692
1002
 
1003
+ Each sample prints the current value next to the select (where it makes sense), so you can see what the select writes into the form.
1004
+
693
1005
  ### Basic Select
694
1006
 
695
- Minimal select with reactive forms:
1007
+ Minimal select with reactive forms. Picking an option writes its `value` into the `FormControl`:
696
1008
 
697
1009
  ```typescript
698
1010
  // basic.component.ts
@@ -736,7 +1048,7 @@ export class BasicSampleComponent
736
1048
 
737
1049
  ### Multiple Select
738
1050
 
739
- Multi-select with signal forms. The `multiple` attribute enables multi-selection, and `SelectMultipleKeepPopup` keeps the popup open after each pick:
1051
+ Multi-select with signal forms. The `multiple` attribute enables multi-selection: the value is an array and the selected options are shown as tags. `SelectMultipleKeepPopup` keeps the popup open after each pick:
740
1052
 
741
1053
  ```typescript
742
1054
  // multiple.component.ts
@@ -762,7 +1074,7 @@ import {Option, Select, SelectFormControl, SelectMultipleKeepPopup} from '@anglr
762
1074
  })
763
1075
  export class MultipleSampleComponent
764
1076
  {
765
- protected selectField = form(signal<string|null>(null));
1077
+ protected selectField = form(signal<string[]|null>(null));
766
1078
  }
767
1079
  ```
768
1080
 
@@ -781,7 +1093,7 @@ export class MultipleSampleComponent
781
1093
 
782
1094
  ### Edit / Type-ahead Mode
783
1095
 
784
- Add the `edit` directive for a type-ahead experience. Works for both single and multi-select:
1096
+ Add the `edit` directive for a type-ahead experience. The normal state becomes a text input that filters options as you type (Space is typed as text). Works for both single and multi-select:
785
1097
 
786
1098
  ```typescript
787
1099
  // edit.component.ts
@@ -815,6 +1127,8 @@ export class EditSampleComponent
815
1127
 
816
1128
  ```html
817
1129
  <!-- edit.component.html -->
1130
+ <div>Value: {{ selectField().value() | json }}</div>
1131
+
818
1132
  <!-- Single edit -->
819
1133
  <ng-select [formField]="selectField" edit>
820
1134
  <ng-option value="first" text="First value text"/>
@@ -822,6 +1136,8 @@ export class EditSampleComponent
822
1136
  <ng-option value="third" text="Third value text"/>
823
1137
  </ng-select>
824
1138
 
1139
+ <div>Values: {{ selectMultipleField().value() | json }}</div>
1140
+
825
1141
  <!-- Multiple edit -->
826
1142
  <ng-select multiple [formField]="selectMultipleField" edit>
827
1143
  <ng-option value="first" text="First value text"/>
@@ -832,7 +1148,7 @@ export class EditSampleComponent
832
1148
 
833
1149
  ### Live Search (Filter)
834
1150
 
835
- Enable a filter text input inside the popup using `FilterLiveSearch`:
1151
+ Enable a filter text input inside the popup using `FilterLiveSearch`. The popup only lists options whose text contains the typed text:
836
1152
 
837
1153
  ```typescript
838
1154
  // liveSearch.component.ts
@@ -895,26 +1211,56 @@ export class LiveSearchSampleComponent
895
1211
 
896
1212
  ### Search Highlighting
897
1213
 
898
- For custom option templates, wrap the highlightable spans with the `HighlightText` directive so nested text nodes are registered with the parent `HighlightSearch`:
1214
+ The popup highlights live-search matches in option texts using the CSS Highlight API. For custom option templates, mark the elements whose text should be highlighted with the `HighlightText` directive. Typing into the filter highlights matches only in the text part, not in the bold value:
899
1215
 
900
1216
  ```typescript
901
- import {HighlightText, NormalStateTemplate, Option, OptionTemplate, Select} from '@anglr/select';
1217
+ // searchHighlighting.component.ts
1218
+ import {Component, ChangeDetectionStrategy, signal} from '@angular/core';
1219
+ import {form, FormField} from '@angular/forms/signals';
1220
+ import {FilterLiveSearch, HighlightText, Option, OptionTemplate, Select, SelectFormControl, SelectOptions} from '@anglr/select';
1221
+ import {RecursivePartial} from '@jscrpt/common';
902
1222
 
903
1223
  @Component(
904
1224
  {
1225
+ selector: 'search-highlighting-sample',
1226
+ templateUrl: 'searchHighlighting.component.html',
905
1227
  imports:
906
1228
  [
907
1229
  Select,
908
1230
  Option,
1231
+ FormField,
909
1232
  HighlightText,
910
1233
  OptionTemplate,
911
- NormalStateTemplate,
1234
+ SelectFormControl,
912
1235
  ],
1236
+ changeDetection: ChangeDetectionStrategy.OnPush
913
1237
  })
914
- export class HighlightedTemplateComponent { /* ... */ }
1238
+ export class SearchHighlightingSampleComponent
1239
+ {
1240
+ protected field = form(signal<string|null>(null));
1241
+
1242
+ protected selectOptions: RecursivePartial<SelectOptions> =
1243
+ {
1244
+ plugins:
1245
+ {
1246
+ liveSearch:
1247
+ {
1248
+ type: FilterLiveSearch,
1249
+ },
1250
+ popup:
1251
+ {
1252
+ options:
1253
+ {
1254
+ liveSearchEnabled: true,
1255
+ },
1256
+ },
1257
+ },
1258
+ };
1259
+ }
915
1260
  ```
916
1261
 
917
1262
  ```html
1263
+ <!-- searchHighlighting.component.html -->
918
1264
  <ng-select [selectOptions]="selectOptions" [formField]="field">
919
1265
  <ng-option value="a" text="Apple"/>
920
1266
  <ng-option value="b" text="Banana"/>
@@ -928,7 +1274,7 @@ export class HighlightedTemplateComponent { /* ... */ }
928
1274
 
929
1275
  ### Custom Templates
930
1276
 
931
- Use `[normalStateTemplate]` and `[optionTemplate]` to completely customize rendering:
1277
+ Use `[normalStateTemplate]` and `[optionTemplate]` to completely customize rendering. The selected value is shown between arrows and every option shows its value and text:
932
1278
 
933
1279
  ```typescript
934
1280
  // customTemplate.component.ts
@@ -985,7 +1331,7 @@ export class CustomTemplateSampleComponent
985
1331
 
986
1332
  ### Lazy / Dynamic Options
987
1333
 
988
- Options can be loaded asynchronously. Use signals and `@for` to render them once loaded:
1334
+ Options can be loaded asynchronously. Use signals and `@for` to render them once loaded; here the options show up after 2.5 seconds:
989
1335
 
990
1336
  ```typescript
991
1337
  // basicLazy.component.ts
@@ -994,10 +1340,10 @@ import {JsonPipe} from '@angular/common';
994
1340
  import {form, FormField} from '@angular/forms/signals';
995
1341
  import {Option, Select, SelectFormControl} from '@anglr/select';
996
1342
 
997
- interface KodPopisValue
1343
+ interface Item
998
1344
  {
999
- kod: string;
1000
- popis: string;
1345
+ id: string;
1346
+ name: string;
1001
1347
  }
1002
1348
 
1003
1349
  @Component(
@@ -1017,17 +1363,18 @@ interface KodPopisValue
1017
1363
  export class BasicLazySampleComponent
1018
1364
  {
1019
1365
  protected selectField = form(signal<string|null>(null));
1020
- protected lazyOptions: WritableSignal<KodPopisValue[]> = signal([]);
1366
+ protected lazyOptions: WritableSignal<Item[]> = signal([]);
1021
1367
 
1022
1368
  constructor()
1023
1369
  {
1024
1370
  // Simulate async loading
1025
1371
  setTimeout(() =>
1026
1372
  {
1027
- this.lazyOptions.set([
1028
- {kod: 'first', popis: 'First value text'},
1029
- {kod: 'second', popis: 'Second value text'},
1030
- {kod: 'third', popis: 'Third value text'},
1373
+ this.lazyOptions.set(
1374
+ [
1375
+ {id: 'first', name: 'First value text'},
1376
+ {id: 'second', name: 'Second value text'},
1377
+ {id: 'third', name: 'Third value text'},
1031
1378
  ]);
1032
1379
  }, 2500);
1033
1380
  }
@@ -1039,25 +1386,27 @@ export class BasicLazySampleComponent
1039
1386
  <div>Value: {{ selectField().value() | json }}</div>
1040
1387
 
1041
1388
  <ng-select [formField]="selectField">
1042
- @for (option of lazyOptions(); track option) {
1043
- <ng-option [value]="option.kod" [text]="option.popis"/>
1389
+ @for (option of lazyOptions(); track option.id) {
1390
+ <ng-option [value]="option.id" [text]="option.name"/>
1044
1391
  }
1045
1392
  </ng-select>
1046
1393
  ```
1047
1394
 
1048
1395
  ### Dynamic Options with Remote Data
1049
1396
 
1050
- Use `CodeOptionsGatherer` + `DynamicValueHandler` + `FilterLiveSearch` to load options from a remote API:
1397
+ Use `CodeOptionsGatherer` + `DynamicValueHandler` + `FilterLiveSearch` to load options from a remote API. The options are fetched again whenever the live-search text changes. `ItemsService.search(query)` returns a `Promise<Item[]>`, where `Item` has an `id` and a `name`:
1051
1398
 
1052
1399
  ```typescript
1053
1400
  // dynamic.component.ts
1054
- import {Component, ChangeDetectionStrategy, effect, Signal, viewChild, signal} from '@angular/core';
1401
+ import {Component, ChangeDetectionStrategy, effect, inject, Signal, viewChild, signal} from '@angular/core';
1055
1402
  import {JsonPipe} from '@angular/common';
1056
1403
  import {form, FormField} from '@angular/forms/signals';
1057
- import {CodeOptionsGatherer, DynamicValueHandler, DynamicValueHandlerOptions, FilterLiveSearch, Select, SelectFormControl, SelectOption, SelectOptions} from '@anglr/select';
1404
+ import {CodeOptionsGatherer, DynamicValueHandler, FilterLiveSearch, Select, SelectFormControl, SelectOptions} from '@anglr/select';
1058
1405
  import {getSearch} from '@anglr/select/extensions';
1059
1406
  import {RecursivePartial} from '@jscrpt/common';
1060
1407
 
1408
+ import {ItemsService} from './items.service';
1409
+
1061
1410
  @Component(
1062
1411
  {
1063
1412
  selector: 'dynamic-sample',
@@ -1069,10 +1418,16 @@ import {RecursivePartial} from '@jscrpt/common';
1069
1418
  FormField,
1070
1419
  SelectFormControl,
1071
1420
  ],
1421
+ providers:
1422
+ [
1423
+ ItemsService,
1424
+ ],
1072
1425
  changeDetection: ChangeDetectionStrategy.OnPush
1073
1426
  })
1074
1427
  export class DynamicSampleComponent
1075
1428
  {
1429
+ private itemsSvc: ItemsService = inject(ItemsService);
1430
+
1076
1431
  protected optionsGatherer: CodeOptionsGatherer<string> = new CodeOptionsGatherer<string>();
1077
1432
  protected selectField = form(signal<string|null>(null));
1078
1433
  protected select: Signal<Select<string>> = viewChild.required<Select<string>>('select');
@@ -1085,21 +1440,27 @@ export class DynamicSampleComponent
1085
1440
  {
1086
1441
  type: FilterLiveSearch,
1087
1442
  },
1443
+ popup:
1444
+ {
1445
+ options:
1446
+ {
1447
+ liveSearchEnabled: true,
1448
+ },
1449
+ },
1088
1450
  valueHandler:
1089
1451
  {
1090
1452
  type: DynamicValueHandler,
1091
- options: {} as DynamicValueHandlerOptions<string>,
1092
1453
  },
1093
1454
  },
1094
1455
  optionsGatherer: this.optionsGatherer,
1095
1456
  };
1096
1457
 
1097
- constructor(private dataSvc: DataService)
1458
+ constructor()
1098
1459
  {
1099
1460
  effect(async () =>
1100
1461
  {
1101
1462
  const search = this.select().executeAndReturn(getSearch());
1102
- const result = await this.dataSvc.search(search);
1463
+ const result = await this.itemsSvc.search(search);
1103
1464
 
1104
1465
  this.optionsGatherer.setAvailableOptions(
1105
1466
  result.map(item =>
@@ -1123,107 +1484,235 @@ export class DynamicSampleComponent
1123
1484
 
1124
1485
  ### Dynamic Option Getter (Object Values)
1125
1486
 
1126
- When values are objects and you need to resolve them back from a server (e.g., when setting an initial value), use `DynamicValueHandlerOptions.optionGetter`:
1487
+ When option values are objects but the form stores only a key, use `valueExtractor` to produce the public value, and `DynamicValueHandlerOptions.optionGetter` to resolve the option for a value written from outside (such as the initial value). The form below starts with `'second'` and the select displays its text once `ItemsService.getDetail(id)` (returning `Promise<Item|null>`) resolves:
1127
1488
 
1128
1489
  ```typescript
1129
- this.selectOptions =
1490
+ // dynamicOptionGetter.component.ts
1491
+ import {Component, ChangeDetectionStrategy, effect, inject, Signal, viewChild, signal} from '@angular/core';
1492
+ import {JsonPipe} from '@angular/common';
1493
+ import {form, FormField} from '@angular/forms/signals';
1494
+ import {CodeOptionsGatherer, DynamicValueHandler, FilterLiveSearch, Select, SelectFormControl, SelectOptions} from '@anglr/select';
1495
+ import {getSearch} from '@anglr/select/extensions';
1496
+ import {RecursivePartial} from '@jscrpt/common';
1497
+
1498
+ import {Item, ItemsService} from './items.service';
1499
+
1500
+ @Component(
1130
1501
  {
1131
- valueExtractor: itm => itm.value()?.kod ?? '',
1132
- plugins:
1502
+ selector: 'dynamic-option-getter-sample',
1503
+ templateUrl: 'dynamicOptionGetter.component.html',
1504
+ imports:
1505
+ [
1506
+ Select,
1507
+ JsonPipe,
1508
+ FormField,
1509
+ SelectFormControl,
1510
+ ],
1511
+ providers:
1512
+ [
1513
+ ItemsService,
1514
+ ],
1515
+ changeDetection: ChangeDetectionStrategy.OnPush
1516
+ })
1517
+ export class DynamicOptionGetterSampleComponent
1518
+ {
1519
+ private itemsSvc: ItemsService = inject(ItemsService);
1520
+
1521
+ protected optionsGatherer: CodeOptionsGatherer<Item> = new CodeOptionsGatherer<Item>();
1522
+ protected select: Signal<Select<Item, string>> = viewChild.required<Select<Item, string>>('select');
1523
+
1524
+ // initial value is only an id, the option for it is resolved using optionGetter
1525
+ protected selectField = form(signal<string|null>('second'));
1526
+
1527
+ protected selectOptions: RecursivePartial<SelectOptions<Item, string>> =
1133
1528
  {
1134
- valueHandler:
1529
+ valueExtractor: option => option.value()?.id ?? '',
1530
+ plugins:
1135
1531
  {
1136
- type: DynamicValueHandler,
1137
- options:
1532
+ liveSearch:
1533
+ {
1534
+ type: FilterLiveSearch,
1535
+ },
1536
+ popup:
1138
1537
  {
1139
- optionGetter: async (value) =>
1538
+ options:
1140
1539
  {
1141
- const result = await lastValueFrom(dataSvc.getDetail(value));
1142
-
1143
- return result
1144
- ? {
1145
- group: signal(null),
1146
- text: signal(result.name),
1147
- value: signal({kod: result.kod, label: result.name}),
1148
- }
1149
- : null;
1540
+ liveSearchEnabled: true,
1541
+ },
1542
+ },
1543
+ valueHandler:
1544
+ {
1545
+ type: DynamicValueHandler,
1546
+ options:
1547
+ {
1548
+ optionGetter: async (value: string) =>
1549
+ {
1550
+ const item = await this.itemsSvc.getDetail(value);
1551
+
1552
+ if(!item)
1553
+ {
1554
+ return null;
1555
+ }
1556
+
1557
+ return {
1558
+ group: signal(null),
1559
+ text: signal(item.name),
1560
+ value: signal(item),
1561
+ };
1562
+ },
1150
1563
  },
1151
1564
  },
1152
1565
  },
1153
- },
1154
- optionsGatherer: this.optionsGatherer,
1155
- };
1566
+ optionsGatherer: this.optionsGatherer,
1567
+ };
1568
+
1569
+ constructor()
1570
+ {
1571
+ effect(async () =>
1572
+ {
1573
+ const search = this.select().executeAndReturn(getSearch());
1574
+ const result = await this.itemsSvc.search(search);
1575
+
1576
+ this.optionsGatherer.setAvailableOptions(
1577
+ result.map(item =>
1578
+ ({
1579
+ value: signal(item),
1580
+ text: signal(item.name),
1581
+ group: signal(null),
1582
+ })),
1583
+ );
1584
+ });
1585
+ }
1586
+ }
1587
+ ```
1588
+
1589
+ ```html
1590
+ <!-- dynamicOptionGetter.component.html -->
1591
+ <div>Value: {{ selectField().value() | json }}</div>
1592
+
1593
+ <ng-select #select [selectOptions]="selectOptions" [formField]="selectField"/>
1156
1594
  ```
1157
1595
 
1158
1596
  ### Absolute Popup
1159
1597
 
1160
- Use the `absolute` directive to render the popup as a direct child of `<body>`, preventing clipping by `overflow: hidden` containers:
1598
+ Use the `absolute` directive (`SelectAbsolute`) to render the popup as a direct child of `<body>`. The popup isn't clipped by the `overflow: hidden` container around the select:
1161
1599
 
1162
1600
  ```html
1163
- <ng-select [formField]="field" absolute>
1164
- <ng-option value="a" text="A"/>
1165
- <ng-option value="b" text="B"/>
1166
- </ng-select>
1601
+ <!-- absolute.component.html -->
1602
+ <div style="overflow: hidden; height: 40px;">
1603
+ <ng-select [formField]="field" absolute>
1604
+ <ng-option value="a" text="A"/>
1605
+ <ng-option value="b" text="B"/>
1606
+ </ng-select>
1607
+ </div>
1167
1608
  ```
1168
1609
 
1169
1610
  ### Popover Popup
1170
1611
 
1171
- Use the native Popover API for popup positioning:
1612
+ Use `PopoverPositioner` to display the popup using the native Popover API, so it's rendered in the top layer above any other content. With `popoverAuto` the browser closes the popup on outside click, so the click-outside handling of the interactions plugin is turned off:
1172
1613
 
1173
1614
  ```typescript
1174
- import {PopoverPositioner, PopoverPositionerOptions, SelectOptions} from '@anglr/select';
1615
+ // popover.component.ts
1616
+ import {Component, ChangeDetectionStrategy, signal} from '@angular/core';
1617
+ import {form, FormField} from '@angular/forms/signals';
1618
+ import {Option, PopoverPositioner, PopoverPositionerOptions, Select, SelectFormControl, SelectOptions} from '@anglr/select';
1175
1619
  import {RecursivePartial} from '@jscrpt/common';
1176
1620
 
1177
- const selectOptions: RecursivePartial<SelectOptions<string>> =
1621
+ @Component(
1622
+ {
1623
+ selector: 'popover-sample',
1624
+ templateUrl: 'popover.component.html',
1625
+ imports:
1626
+ [
1627
+ Select,
1628
+ Option,
1629
+ FormField,
1630
+ SelectFormControl,
1631
+ ],
1632
+ changeDetection: ChangeDetectionStrategy.OnPush
1633
+ })
1634
+ export class PopoverSampleComponent
1178
1635
  {
1179
- plugins:
1636
+ protected field = form(signal<string|null>(null));
1637
+
1638
+ protected selectOptions: RecursivePartial<SelectOptions<string>> =
1180
1639
  {
1181
- interactions:
1640
+ plugins:
1182
1641
  {
1183
- options:
1642
+ interactions:
1184
1643
  {
1185
- handleClickOutside: false,
1644
+ options:
1645
+ {
1646
+ handleClickOutside: false,
1647
+ },
1186
1648
  },
1187
- },
1188
- positioner:
1189
- {
1190
- type: PopoverPositioner,
1191
- options:
1649
+ positioner:
1192
1650
  {
1193
- popoverAuto: true,
1194
- } as PopoverPositionerOptions,
1651
+ type: PopoverPositioner,
1652
+ options:
1653
+ {
1654
+ popoverAuto: true,
1655
+ } as PopoverPositionerOptions,
1656
+ },
1195
1657
  },
1196
- },
1197
- };
1658
+ };
1659
+ }
1198
1660
  ```
1199
1661
 
1200
1662
  ```html
1663
+ <!-- popover.component.html -->
1201
1664
  <ng-select [formField]="field" [selectOptions]="selectOptions">
1202
1665
  <ng-option value="a" text="A"/>
1666
+ <ng-option value="b" text="B"/>
1203
1667
  </ng-select>
1204
1668
  ```
1205
1669
 
1206
1670
  ### Cancel Value
1207
1671
 
1208
- Show a cancel/clear button to reset the selected value:
1672
+ Show a cancel button in the normal state that clears the selected value:
1209
1673
 
1210
1674
  ```typescript
1211
- const selectOptions: RecursivePartial<SelectOptions<string>> =
1675
+ // cancelValue.component.ts
1676
+ import {Component, ChangeDetectionStrategy, signal} from '@angular/core';
1677
+ import {form, FormField} from '@angular/forms/signals';
1678
+ import {Option, Select, SelectFormControl, SelectOptions} from '@anglr/select';
1679
+ import {RecursivePartial} from '@jscrpt/common';
1680
+
1681
+ @Component(
1212
1682
  {
1213
- plugins:
1683
+ selector: 'cancel-value-sample',
1684
+ templateUrl: 'cancelValue.component.html',
1685
+ imports:
1686
+ [
1687
+ Select,
1688
+ Option,
1689
+ FormField,
1690
+ SelectFormControl,
1691
+ ],
1692
+ changeDetection: ChangeDetectionStrategy.OnPush
1693
+ })
1694
+ export class CancelValueSampleComponent
1695
+ {
1696
+ protected field = form(signal<string|null>(null));
1697
+
1698
+ protected selectOptions: RecursivePartial<SelectOptions<string>> =
1214
1699
  {
1215
- normalState:
1700
+ plugins:
1216
1701
  {
1217
- options:
1702
+ normalState:
1218
1703
  {
1219
- cancelValue: true,
1704
+ options:
1705
+ {
1706
+ cancelValue: true,
1707
+ },
1220
1708
  },
1221
1709
  },
1222
- },
1223
- };
1710
+ };
1711
+ }
1224
1712
  ```
1225
1713
 
1226
1714
  ```html
1715
+ <!-- cancelValue.component.html -->
1227
1716
  <ng-select [formField]="field" [selectOptions]="selectOptions">
1228
1717
  <ng-option value="a" text="A"/>
1229
1718
  <ng-option value="b" text="B"/>
@@ -1232,66 +1721,118 @@ const selectOptions: RecursivePartial<SelectOptions<string>> =
1232
1721
 
1233
1722
  ### Hide Caret
1234
1723
 
1235
- In edit mode, hide the dropdown caret:
1724
+ Hide the dropdown caret, either through the `carret` option of `EditNormalState` (in edit mode), or with the `noCarret` shorthand directive (`SelectNoCarret`):
1236
1725
 
1237
1726
  ```typescript
1238
- import {EditNormalStateOptions, SelectOptions} from '@anglr/select';
1727
+ // hideCaret.component.ts
1728
+ import {Component, ChangeDetectionStrategy, signal} from '@angular/core';
1729
+ import {form, FormField} from '@angular/forms/signals';
1730
+ import {EditNormalStateOptions, Option, Select, SelectEdit, SelectFormControl, SelectNoCarret, SelectOptions} from '@anglr/select';
1731
+ import {RecursivePartial} from '@jscrpt/common';
1239
1732
 
1240
- const selectOptions: RecursivePartial<SelectOptions<string>> =
1733
+ @Component(
1241
1734
  {
1242
- plugins:
1735
+ selector: 'hide-caret-sample',
1736
+ templateUrl: 'hideCaret.component.html',
1737
+ imports:
1738
+ [
1739
+ Select,
1740
+ Option,
1741
+ FormField,
1742
+ SelectEdit,
1743
+ SelectNoCarret,
1744
+ SelectFormControl,
1745
+ ],
1746
+ changeDetection: ChangeDetectionStrategy.OnPush
1747
+ })
1748
+ export class HideCaretSampleComponent
1749
+ {
1750
+ protected field = form(signal<string|null>(null));
1751
+ protected otherField = form(signal<string|null>(null));
1752
+
1753
+ protected selectOptions: RecursivePartial<SelectOptions<string>> =
1243
1754
  {
1244
- normalState:
1755
+ plugins:
1245
1756
  {
1246
- options:
1757
+ normalState:
1247
1758
  {
1248
- carret: false,
1249
- } as EditNormalStateOptions,
1759
+ options:
1760
+ {
1761
+ carret: false,
1762
+ } as EditNormalStateOptions,
1763
+ },
1250
1764
  },
1251
- },
1252
- };
1765
+ };
1766
+ }
1253
1767
  ```
1254
1768
 
1255
1769
  ```html
1770
+ <!-- hideCaret.component.html -->
1256
1771
  <ng-select [formField]="field" [selectOptions]="selectOptions" edit>
1257
1772
  <ng-option value="a" text="A"/>
1258
1773
  </ng-select>
1259
- ```
1260
-
1261
- Or use the shorthand directive:
1262
1774
 
1263
- ```html
1264
- <ng-select [formField]="field" noCarret>
1775
+ <ng-select [formField]="otherField" noCarret>
1265
1776
  <ng-option value="a" text="A"/>
1266
1777
  </ng-select>
1267
1778
  ```
1268
1779
 
1269
1780
  ### Add New Option
1270
1781
 
1271
- Allow users to create new options by typing. Configure `newOptionGetter` on the OptionsHandler:
1782
+ Allow users to create new options by typing. Configure `newOptionGetter` on the OptionsHandler: while typing, the popup offers the typed text as the first option, and picking it sets it as the value:
1272
1783
 
1273
1784
  ```typescript
1274
- const selectOptions: RecursivePartial<SelectOptions<string>> =
1785
+ // addNewOption.component.ts
1786
+ import {Component, ChangeDetectionStrategy, signal} from '@angular/core';
1787
+ import {JsonPipe} from '@angular/common';
1788
+ import {form, FormField} from '@angular/forms/signals';
1789
+ import {Option, Select, SelectEdit, SelectFormControl, SelectOptions} from '@anglr/select';
1790
+ import {RecursivePartial} from '@jscrpt/common';
1791
+
1792
+ @Component(
1275
1793
  {
1276
- plugins:
1794
+ selector: 'add-new-option-sample',
1795
+ templateUrl: 'addNewOption.component.html',
1796
+ imports:
1797
+ [
1798
+ Select,
1799
+ Option,
1800
+ JsonPipe,
1801
+ FormField,
1802
+ SelectEdit,
1803
+ SelectFormControl,
1804
+ ],
1805
+ changeDetection: ChangeDetectionStrategy.OnPush
1806
+ })
1807
+ export class AddNewOptionSampleComponent
1808
+ {
1809
+ protected field = form(signal<string|null>(null));
1810
+
1811
+ protected selectOptions: RecursivePartial<SelectOptions<string>> =
1277
1812
  {
1278
- optionsHandler:
1813
+ plugins:
1279
1814
  {
1280
- options:
1815
+ optionsHandler:
1281
1816
  {
1282
- newOptionGetter: (value: string) =>
1283
- ({
1284
- group: signal(null),
1285
- text: signal(value),
1286
- value: signal(value),
1287
- }),
1817
+ options:
1818
+ {
1819
+ newOptionGetter: (value: string) =>
1820
+ ({
1821
+ group: signal(null),
1822
+ text: signal(value),
1823
+ value: signal(value),
1824
+ }),
1825
+ },
1288
1826
  },
1289
1827
  },
1290
- },
1291
- };
1828
+ };
1829
+ }
1292
1830
  ```
1293
1831
 
1294
1832
  ```html
1833
+ <!-- addNewOption.component.html -->
1834
+ <div>Value: {{ field().value() | json }}</div>
1835
+
1295
1836
  <ng-select [formField]="field" [selectOptions]="selectOptions" edit>
1296
1837
  <ng-option value="first" text="First value text"/>
1297
1838
  <ng-option value="second" text="Second value text"/>
@@ -1300,82 +1841,71 @@ const selectOptions: RecursivePartial<SelectOptions<string>> =
1300
1841
 
1301
1842
  ### Readonly
1302
1843
 
1303
- Use Angular signal forms' `readonly()` to toggle readonly state:
1844
+ Use Angular signal forms' `readonly()` rule to toggle the readonly state of the first select. The second select is switched to readonly programmatically with the `setReadonly()` extension:
1304
1845
 
1305
1846
  ```typescript
1306
- import {signal} from '@angular/core';
1847
+ // readonly.component.ts
1848
+ import {Component, ChangeDetectionStrategy, Signal, signal, viewChild} from '@angular/core';
1307
1849
  import {form, FormField, readonly} from '@angular/forms/signals';
1850
+ import {Option, Select, SelectFormControl} from '@anglr/select';
1851
+ import {setReadonly} from '@anglr/select/extensions';
1308
1852
 
1853
+ @Component(
1854
+ {
1855
+ selector: 'readonly-sample',
1856
+ templateUrl: 'readonly.component.html',
1857
+ imports:
1858
+ [
1859
+ Select,
1860
+ Option,
1861
+ FormField,
1862
+ SelectFormControl,
1863
+ ],
1864
+ changeDetection: ChangeDetectionStrategy.OnPush
1865
+ })
1309
1866
  export class ReadonlySampleComponent
1310
1867
  {
1311
- protected readonly = signal(false);
1868
+ protected isReadonly = signal(false);
1312
1869
  protected selectField = form(
1313
1870
  signal<string|null>(null),
1314
- path => readonly(path, () => this.readonly()),
1871
+ path => readonly(path, () => this.isReadonly()),
1315
1872
  );
1873
+
1874
+ protected select: Signal<Select<string>> = viewChild.required<Select<string>>('select');
1875
+
1876
+ protected setSelectReadonly(): void
1877
+ {
1878
+ this.select().execute(setReadonly(true));
1879
+ }
1316
1880
  }
1317
1881
  ```
1318
1882
 
1319
1883
  ```html
1320
- <button (click)="readonly.set(!readonly())">
1321
- {{ readonly() ? 'to normal' : 'to readonly' }}
1884
+ <!-- readonly.component.html -->
1885
+ <button type="button" (click)="isReadonly.set(!isReadonly())">
1886
+ {{ isReadonly() ? 'to normal' : 'to readonly' }}
1322
1887
  </button>
1323
1888
 
1324
1889
  <ng-select [formField]="selectField">
1325
1890
  <ng-option value="a" text="A"/>
1326
1891
  <ng-option value="b" text="B"/>
1327
1892
  </ng-select>
1328
- ```
1329
1893
 
1330
- Or use the `setReadonly()` extension:
1894
+ <button type="button" (click)="setSelectReadonly()">set readonly via extension</button>
1331
1895
 
1332
- ```typescript
1333
- import {setReadonly} from '@anglr/select/extensions';
1334
-
1335
- this.select().execute(setReadonly(true));
1896
+ <ng-select #select>
1897
+ <ng-option value="a" text="A"/>
1898
+ <ng-option value="b" text="B"/>
1899
+ </ng-select>
1336
1900
  ```
1337
1901
 
1338
1902
  ### Custom Readonly State
1339
1903
 
1340
- Replace the readonly visual entirely with a custom plugin component:
1341
-
1342
- ```typescript
1343
- this.selectOptions =
1344
- {
1345
- plugins:
1346
- {
1347
- readonlyState:
1348
- {
1349
- type: CustomReadonlyStateComponent,
1350
- },
1351
- },
1352
- };
1353
- ```
1904
+ To replace the readonly visual entirely, register a custom `readonlyState` plugin, see [Example: Custom ReadonlyState Plugin](#implementing-a-custom-plugin).
1354
1905
 
1355
1906
  ### Styling Override
1356
1907
 
1357
- Override CSS custom properties in a scoped container:
1358
-
1359
- ```html
1360
- <div class="select-override">
1361
- <ng-select [formField]="field">
1362
- <ng-option value="first" text="First value text"/>
1363
- <ng-option value="second" text="Second value text"/>
1364
- </ng-select>
1365
- </div>
1366
- ```
1367
-
1368
- ```scss
1369
- .select-override {
1370
- --select-normalState-borderColor: tomato;
1371
- --select-normalState-background: lightyellow;
1372
- --select-normalState-foreground: #333;
1373
- --select-normalState-focus-borderColor: red;
1374
- --select-normalState-focus-outlineColor: red;
1375
- --select-popup-container-background: lightyellow;
1376
- --select-popup-container-borderColor: tomato;
1377
- }
1378
- ```
1908
+ To restyle selects in one place of an application only, override the CSS custom properties in a container, see [Overriding Styles in Components](#overriding-styles-in-components).
1379
1909
 
1380
1910
  ---
1381
1911