@anglr/select 17.3.0 → 17.4.0-beta.20261111130212
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/package.json +1 -1
- package/readme.md +752 -222
- package/version.bak +1 -1
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
|
-
**
|
|
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 `
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
393
|
+
protected field = form(signal<string|null>(null));
|
|
394
|
+
|
|
395
|
+
protected options: RecursivePartial<SelectOptions> =
|
|
347
396
|
{
|
|
348
|
-
|
|
349
|
-
{
|
|
350
|
-
type: FilterLiveSearch,
|
|
351
|
-
},
|
|
352
|
-
popup:
|
|
397
|
+
plugins:
|
|
353
398
|
{
|
|
354
|
-
|
|
399
|
+
liveSearch:
|
|
400
|
+
{
|
|
401
|
+
type: FilterLiveSearch,
|
|
402
|
+
},
|
|
403
|
+
popup:
|
|
355
404
|
{
|
|
356
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
|
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
|
|
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
|
|
506
|
+
export class CustomReadonlyStateComponent implements ReadonlyState
|
|
416
507
|
{
|
|
417
|
-
public readonly selectPlugins
|
|
418
|
-
public readonly pluginElement
|
|
419
|
-
public
|
|
420
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
561
|
+
plugins:
|
|
439
562
|
{
|
|
440
|
-
|
|
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
|
-
|
|
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: '
|
|
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
|
|
646
|
+
export class CustomOptionsHandlerSampleComponent
|
|
466
647
|
{
|
|
467
|
-
|
|
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
|
-
| `
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
495
|
-
|
|
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
|
-
|
|
498
|
-
effect(() =>
|
|
709
|
+
@Component(
|
|
499
710
|
{
|
|
500
|
-
|
|
501
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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()` —
|
|
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
|
-
|
|
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
|
|
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
|
|
639
|
-
@use '@anglr/select/styles/core/mixins' as selectMixins;
|
|
940
|
+
@use '@anglr/select/styles' as select;
|
|
640
941
|
|
|
641
|
-
$customTheme:
|
|
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
|
-
|
|
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="
|
|
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
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
1234
|
+
SelectFormControl,
|
|
912
1235
|
],
|
|
1236
|
+
changeDetection: ChangeDetectionStrategy.OnPush
|
|
913
1237
|
})
|
|
914
|
-
export class
|
|
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
|
|
1343
|
+
interface Item
|
|
998
1344
|
{
|
|
999
|
-
|
|
1000
|
-
|
|
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<
|
|
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
|
-
|
|
1029
|
-
{
|
|
1030
|
-
{
|
|
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.
|
|
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,
|
|
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(
|
|
1458
|
+
constructor()
|
|
1098
1459
|
{
|
|
1099
1460
|
effect(async () =>
|
|
1100
1461
|
{
|
|
1101
1462
|
const search = this.select().executeAndReturn(getSearch());
|
|
1102
|
-
const result = await this.
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
1132
|
-
|
|
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
|
-
|
|
1529
|
+
valueExtractor: option => option.value()?.id ?? '',
|
|
1530
|
+
plugins:
|
|
1135
1531
|
{
|
|
1136
|
-
|
|
1137
|
-
|
|
1532
|
+
liveSearch:
|
|
1533
|
+
{
|
|
1534
|
+
type: FilterLiveSearch,
|
|
1535
|
+
},
|
|
1536
|
+
popup:
|
|
1138
1537
|
{
|
|
1139
|
-
|
|
1538
|
+
options:
|
|
1140
1539
|
{
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
1164
|
-
|
|
1165
|
-
<ng-
|
|
1166
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1636
|
+
protected field = form(signal<string|null>(null));
|
|
1637
|
+
|
|
1638
|
+
protected selectOptions: RecursivePartial<SelectOptions<string>> =
|
|
1180
1639
|
{
|
|
1181
|
-
|
|
1640
|
+
plugins:
|
|
1182
1641
|
{
|
|
1183
|
-
|
|
1642
|
+
interactions:
|
|
1184
1643
|
{
|
|
1185
|
-
|
|
1644
|
+
options:
|
|
1645
|
+
{
|
|
1646
|
+
handleClickOutside: false,
|
|
1647
|
+
},
|
|
1186
1648
|
},
|
|
1187
|
-
|
|
1188
|
-
positioner:
|
|
1189
|
-
{
|
|
1190
|
-
type: PopoverPositioner,
|
|
1191
|
-
options:
|
|
1649
|
+
positioner:
|
|
1192
1650
|
{
|
|
1193
|
-
|
|
1194
|
-
|
|
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
|
|
1672
|
+
Show a cancel button in the normal state that clears the selected value:
|
|
1209
1673
|
|
|
1210
1674
|
```typescript
|
|
1211
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1700
|
+
plugins:
|
|
1216
1701
|
{
|
|
1217
|
-
|
|
1702
|
+
normalState:
|
|
1218
1703
|
{
|
|
1219
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1733
|
+
@Component(
|
|
1241
1734
|
{
|
|
1242
|
-
|
|
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
|
-
|
|
1755
|
+
plugins:
|
|
1245
1756
|
{
|
|
1246
|
-
|
|
1757
|
+
normalState:
|
|
1247
1758
|
{
|
|
1248
|
-
|
|
1249
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1813
|
+
plugins:
|
|
1279
1814
|
{
|
|
1280
|
-
|
|
1815
|
+
optionsHandler:
|
|
1281
1816
|
{
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
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
|
-
|
|
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
|
|
1868
|
+
protected isReadonly = signal(false);
|
|
1312
1869
|
protected selectField = form(
|
|
1313
1870
|
signal<string|null>(null),
|
|
1314
|
-
path => readonly(path, () => this.
|
|
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
|
-
|
|
1321
|
-
|
|
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
|
-
|
|
1894
|
+
<button type="button" (click)="setSelectReadonly()">set readonly via extension</button>
|
|
1331
1895
|
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|