• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

IgniteUI / igniteui-angular / 4373580873

pending completion
4373580873

push

github

GitHub
fix(simple-combo): display all list items when clearing the input by Space (#12693)

14978 of 17621 branches covered (85.0%)

26699 of 28892 relevant lines covered (92.41%)

29812.21 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

90.36
/projects/igniteui-angular/src/lib/simple-combo/simple-combo.component.ts
1
import { CommonModule } from '@angular/common';
2
import {
3
    AfterViewInit, ChangeDetectorRef, Component, ElementRef, EventEmitter, HostListener, Inject, Injector,
4
    NgModule, Optional, Output, ViewChild
5
} from '@angular/core';
6
import { ControlValueAccessor, FormsModule, NG_VALUE_ACCESSOR, ReactiveFormsModule } from '@angular/forms';
7
import { takeUntil } from 'rxjs/operators';
8
import { IgxCheckboxModule } from '../checkbox/checkbox.component';
9
import { IgxComboAddItemComponent } from '../combo/combo-add-item.component';
10
import { IgxComboDropDownComponent } from '../combo/combo-dropdown.component';
11
import { IgxComboItemComponent } from '../combo/combo-item.component';
12
import { IgxComboAPIService } from '../combo/combo.api';
13
import { IgxComboBaseDirective, IGX_COMBO_COMPONENT } from '../combo/combo.common';
14
import { IgxComboModule } from '../combo/combo.component';
15
import { DisplayDensityToken, IDisplayDensityOptions } from '../core/displayDensity';
16
import { IgxSelectionAPIService } from '../core/selection';
17
import { CancelableEventArgs, IBaseCancelableBrowserEventArgs, IBaseEventArgs, PlatformUtil } from '../core/utils';
18
import { IgxButtonModule } from '../directives/button/button.directive';
19
import { IgxForOfModule } from '../directives/for-of/for_of.directive';
20
import { IgxRippleModule } from '../directives/ripple/ripple.directive';
21
import { IgxTextSelectionDirective, IgxTextSelectionModule } from '../directives/text-selection/text-selection.directive';
22
import { IgxToggleModule } from '../directives/toggle/toggle.directive';
23
import { IgxDropDownModule } from '../drop-down/public_api';
24
import { IgxIconModule, IgxIconService } from '../icon/public_api';
2✔
25
import { IgxInputGroupModule, IgxInputGroupType, IGX_INPUT_GROUP_TYPE } from '../input-group/public_api';
26

73✔
27
/** Emitted when an igx-simple-combo's selection is changing.  */
73✔
28
export interface ISimpleComboSelectionChangingEventArgs extends CancelableEventArgs, IBaseEventArgs {
73✔
29
    /** An object which represents the value that is currently selected */
73✔
30
    oldSelection: any;
73✔
31
    /** An object which represents the value that will be selected after this event */
73✔
32
    newSelection: any;
73✔
33
    /** The text that will be displayed in the combo text box */
73✔
34
    displayText: string;
73✔
35
}
73✔
36

73✔
37
/**
38
 * Represents a drop-down list that provides filtering functionality, allowing users to choose a single option from a predefined list.
73✔
39
 *
73✔
40
 * @igxModule IgxSimpleComboModule
41
 * @igxTheme igx-combo-theme
73✔
42
 * @igxKeywords combobox, single combo selection
73✔
43
 * @igxGroup Grids & Lists
73✔
44
 *
1,158✔
45
 * @remarks
1,158✔
46
 * It provides the ability to filter items as well as perform single selection on the provided data.
47
 * Additionally, it exposes keyboard navigation and custom styling capabilities.
15✔
48
 * @example
49
 * ```html
1,143✔
50
 * <igx-simple-combo [itemsMaxHeight]="250" [data]="locationData"
1,143✔
51
 *  [displayKey]="'field'" [valueKey]="'field'"
52
 *  placeholder="Location" searchPlaceholder="Search...">
73✔
53
 * </igx-simple-combo>
54
 * ```
55
 */
56
@Component({
806✔
57
    selector: 'igx-simple-combo',
58
    templateUrl: 'simple-combo.component.html',
59
    providers: [
60
        IgxComboAPIService,
4,208!
61
        { provide: IGX_COMBO_COMPONENT, useExisting: IgxSimpleComboComponent },
153✔
62
        { provide: NG_VALUE_ACCESSOR, useExisting: IgxSimpleComboComponent, multi: true }
63
    ]
64
})
65
export class IgxSimpleComboComponent extends IgxComboBaseDirective implements ControlValueAccessor, AfterViewInit {
7,359✔
66
    /** @hidden @internal */
67
    @ViewChild(IgxComboDropDownComponent, { static: true })
68
    public dropdown: IgxComboDropDownComponent;
150✔
69

70
    /** @hidden @internal */
71
    @ViewChild(IgxComboAddItemComponent)
69✔
72
    public addItem: IgxComboAddItemComponent;
73

74
    /**
6✔
75
     * Emitted when item selection is changing, before the selection completes
3✔
76
     *
3✔
77
     * ```html
3✔
78
     * <igx-simple-combo (selectionChanging)='handleSelection()'></igx-simple-combo>
79
     * ```
80
     */
3✔
81
    @Output()
2✔
82
    public selectionChanging = new EventEmitter<ISimpleComboSelectionChangingEventArgs>();
2✔
83

84
    @ViewChild(IgxTextSelectionDirective, { static: true })
1!
85
    private textSelection: IgxTextSelectionDirective;
1✔
86

87
    /** @hidden @internal */
88
    public composing = false;
89

90
    private _updateInput = true;
91

92
    // stores the last filtered value - move to common?
93
    private _internalFilter = '';
94

95
    private _collapsing = false;
96

97
    /** @hidden @internal */
98
    public get filteredData(): any[] | null {
48!
99
        return this._filteredData;
48✔
100
    }
48✔
101
    /** @hidden @internal */
102
    public set filteredData(val: any[] | null) {
103
        this._filteredData = this.groupKey ? (val || []).filter((e) => e.isHeader !== true) : val;
104
        this.checkMatch();
105
    }
106

107
    /** @hidden @internal */
108
    public get searchValue(): string {
109
        return this._searchValue;
110
    }
111
    public set searchValue(val: string) {
112
        this._searchValue = val;
2✔
113
    }
114

115
    private get selectedItem(): any {
116
        return this.selectionService.get(this.id).values().next().value;
50✔
117
    }
50✔
118

50✔
119
    constructor(protected elementRef: ElementRef,
50✔
120
        protected cdr: ChangeDetectorRef,
50✔
121
        protected selectionService: IgxSelectionAPIService,
122
        protected comboAPI: IgxComboAPIService,
123
        protected _iconService: IgxIconService,
124
        private platformUtil: PlatformUtil,
59✔
125
        @Optional() @Inject(DisplayDensityToken) protected _displayDensityOptions: IDisplayDensityOptions,
1!
126
        @Optional() @Inject(IGX_INPUT_GROUP_TYPE) protected _inputGroupType: IgxInputGroupType,
1✔
127
        @Optional() protected _injector: Injector) {
10!
128
        super(elementRef, cdr, selectionService, comboAPI,
10!
129
            _iconService, _displayDensityOptions, _inputGroupType, _injector);
×
130
        this.comboAPI.register(this);
131
    }
10✔
132

133
    /** @hidden @internal */
1!
134
    @HostListener('keydown.ArrowDown', ['$event'])
135
    @HostListener('keydown.Alt.ArrowDown', ['$event'])
136
    public onArrowDown(event: Event): void {
137
        if (this.collapsed) {
×
138
            event.preventDefault();
139
            event.stopPropagation();
140
            this.open();
141
        } else {
59✔
142
            if (this.virtDir.igxForOf.length > 0 && !this.selectedItem) {
50!
143
                this.dropdown.navigateFirst();
×
144
                this.dropdownContainer.nativeElement.focus();
145
            } else if (this.allowCustomValues) {
50✔
146
                this.addItem?.element.nativeElement.focus();
50✔
147
            }
50✔
148
        }
29✔
149
    }
29✔
150

151
    /**
21✔
152
     * Select a defined item
153
     *
59✔
154
     * @param item the item to be selected
11✔
155
     * ```typescript
2✔
156
     * this.combo.select("New York");
157
     * ```
11✔
158
     */
159
    public select(item: any): void {
59✔
160
        if (item !== undefined) {
18!
161
            const newSelection = this.selectionService.add_items(this.id, item instanceof Array ? item : [item], true);
×
162
            this.setSelection(newSelection);
163
        }
18!
164
    }
18✔
165

166
    /**
167
     * Deselect the currently selected item
×
168
     *
×
169
     * @param item the items to be deselected
170
     * ```typescript
18✔
171
     * this.combo.deselect("New York");
172
     * ```
59✔
173
     */
4✔
174
    public deselect(): void {
175
        this.clearSelection();
176
    }
177

59✔
178
    /** @hidden @internal */
52✔
179
    public writeValue(value: any): void {
52✔
180
        const oldSelection = this.selection;
181
        this.selectionService.select_items(this.id, this.isValid(value) ? [value] : [], true);
59✔
182
        this.cdr.markForCheck();
183
        this._value = this.createDisplayText(this.selection, oldSelection);
184
        this.filterValue = this._internalFilter = this._value?.toString();
185
    }
22✔
186

20✔
187
    /** @hidden @internal */
188
    public ngAfterViewInit(): void {
22✔
189
        this.virtDir.contentSizeChange.pipe(takeUntil(this.destroy$)).subscribe(() => {
22✔
190
            if (this.selection.length > 0) {
4✔
191
                const index = this.virtDir.igxForOf.findIndex(e => {
192
                    let current = e? e[this.valueKey] : undefined;
22✔
193
                    if (this.valueKey === null || this.valueKey === undefined) {
194
                        current = e;
1✔
195
                    }
1✔
196
                    return current === this.selection[0];
1✔
197
                });
198
                if (!this.isRemote) {
22✔
199
                    // navigate to item only if we have local data
2✔
200
                    // as with remote data this will fiddle with igxFor's scroll handler
201
                    // and will trigger another chunk load which will break the visualization
202
                    this.dropdown.navigateItem(index);
22✔
203
                }
14✔
204
            }
205
        });
22✔
206
        this.dropdown.opening.pipe(takeUntil(this.destroy$)).subscribe((args) => {
22✔
207
            if (args.cancel) {
208
                return;
209
            }
210
            this._collapsing = false;
32✔
211
            const filtered = this.filteredData.find(this.findAllMatches);
2✔
212
            if (filtered === undefined || filtered === null) {
2!
213
                this.filterValue = this.searchValue = this.comboInput.value;
×
214
                return;
215
            }
2✔
216
            this.filterValue = this.searchValue = '';
2✔
217
        });
2✔
218
        this.dropdown.opened.pipe(takeUntil(this.destroy$)).subscribe(() => {
2✔
219
            if (this.composing) {
220
                this.comboInput.focus();
2✔
221
            }
2✔
222
            this._internalFilter = this.comboInput.value;
2✔
223
        });
224
        this.dropdown.closing.pipe(takeUntil(this.destroy$)).subscribe((args) => {
30✔
225
            if (args.cancel) {
226
                return;
2✔
227
            }
2✔
228
            if (this.getEditElement() && !args.event) {
229
                this._collapsing = true;
30✔
230
            } else {
7✔
231
                this.clearOnBlur();
7✔
232
                this._onTouchedCallback();
233
            }
30✔
234
            this.comboInput.focus();
30✔
235
        });
236
        this.dropdown.closed.pipe(takeUntil(this.destroy$)).subscribe(() => {
237
            this.filterValue = this._internalFilter = this.comboInput.value;
238
        });
16✔
239

1✔
240
        // in reactive form the control is not present initially
1!
241
        // and sets the selection to an invalid value in writeValue method
×
242
        if (!this.isValid(this.selectedItem)) {
243
            this.selectionService.clear(this.id);
1✔
244
            this._value = '';
245
        }
246

247
        super.ngAfterViewInit();
248
    }
2!
249

×
250
    /** @hidden @internal */
×
251
    public handleInputChange(event?: any): void {
×
252
        if (event !== undefined) {
253
            this.filterValue = this._internalFilter = this.searchValue = typeof event === 'string' ? event : event.target.value;
2!
254
        }
×
255
        this._onChangeCallback(this.searchValue);
256
        if (this.collapsed && this.comboInput.focused) {
257
            this.open();
258
        }
259
        if (!this.comboInput.value.trim() && this.selection.length) {
23✔
260
            // handle clearing of input by space
23✔
261
            this.clearSelection();
262
            this._onChangeCallback(null);
263
            this.filterValue = '';
264
        }
265
        if (this.selection.length) {
266
            this.selectionService.clear(this.id);
10✔
267
        }
7✔
268
        // when filtering the focused item should be the first item or the currently selected item
269
        if (!this.dropdown.focusedItem || this.dropdown.focusedItem.id !== this.dropdown.items[0].id) {
10✔
270
            this.dropdown.navigateFirst();
271
        }
272
        super.handleInputChange(event);
273
        this.composing = true;
31✔
274
    }
275

276
    /** @hidden @internal */
277
    public handleKeyDown(event: KeyboardEvent): void {
18✔
278
        if (event.key === this.platformUtil.KEYMAP.ENTER) {
279
            const filtered = this.filteredData.find(this.findAllMatches);
280
            if (filtered === null || filtered === undefined) {
281
                return;
5✔
282
            }
1✔
283
            this.select(this.dropdown.focusedItem.itemID);
284
            event.preventDefault();
4✔
285
            event.stopPropagation();
4✔
286
            this.close();
2✔
287
            // manually trigger text selection as it will not be triggered during editing
2✔
288
            this.textSelection.trigger();
289
            this.filterValue = this.getElementVal(filtered);
290
            return;
2✔
291
        }
292
        if (event.key === this.platformUtil.KEYMAP.BACKSPACE
4✔
293
            || event.key === this.platformUtil.KEYMAP.DELETE) {
4✔
294
            this._updateInput = false;
4✔
295
            this.clearSelection(true);
4✔
296
        }
4✔
297
        if (!this.collapsed && event.key === this.platformUtil.KEYMAP.TAB) {
298
            this.clearOnBlur();
299
            this.close();
300
        }
11✔
301
        this.composing = false;
11✔
302
        super.handleKeyDown(event);
11✔
303
    }
304

305
    /** @hidden @internal */
306
    public handleKeyUp(event: KeyboardEvent): void {
20✔
307
        if (event.key === this.platformUtil.KEYMAP.ARROW_DOWN) {
20✔
308
            const firstItem = this.selectionService.first_item(this.id);
20✔
309
            this.dropdown.focusedItem = firstItem && this.filteredData.length > 0
20✔
310
                ? this.dropdown.items.find(i => i.itemID === firstItem)
1✔
311
                : this.dropdown.items[0];
312
            this.dropdownContainer.nativeElement.focus();
19✔
313
        }
314
    }
19✔
315

19✔
316
    /** @hidden @internal */
317
    public handleItemKeyDown(event: KeyboardEvent): void {
318
        if (event.key === this.platformUtil.KEYMAP.ARROW_UP && event.altKey) {
319
            this.close();
2!
320
            this.comboInput.focus();
2✔
321
            return;
322
        }
323
        if (event.key === this.platformUtil.KEYMAP.ENTER) {
×
324
            this.comboInput.focus();
×
325
        }
326
    }
327

328
    /** @hidden @internal */
329
    public handleItemClick(): void {
5✔
330
        this.close();
5✔
331
        this.comboInput.focus();
3✔
332
    }
333

334
    /** @hidden @internal */
335
    public onBlur(): void {
68✔
336
        // when clicking the toggle button to close the combo and immediately clicking outside of it
68!
337
        // the collapsed state is not modified as the dropdown is still not closed
68✔
338
        if (this.collapsed || this._collapsing) {
68✔
339
            this.clearOnBlur();
340
        }
341
        super.onBlur();
342
    }
343

344
    /** @hidden @internal */
345
    public onFocus(): void {
68✔
346
        this._internalFilter = this.comboInput.value || '';
56✔
347
    }
348

349
    /** @hidden @internal */
68✔
350
    public getEditElement(): HTMLElement {
67✔
351
        return this.comboInput.nativeElement;
352
    }
353

67✔
354
    /** @hidden @internal */
67✔
355
    public handleClear(event: Event): void {
67✔
356
        if (this.disabled) {
65!
357
            return;
358
        }
359
        this.clearSelection(true);
360
        if (this.collapsed) {
67✔
361
            this.open();
67✔
362
            this.dropdown.navigateFirst();
363
        } else {
1!
364
            this.focusSearchInput(true);
1✔
365
        }
366
        event.stopPropagation();
367

368
        this.comboInput.value = this.filterValue = this.searchValue = '';
192✔
369
        this.dropdown.focusedItem = null;
21✔
370
        this.composing = false;
371
        this.comboInput.focus();
171✔
372
    }
373

374
    /** @hidden @internal */
68✔
375
    public handleOpened(): void {
376
        this.triggerCheck();
103✔
377
        this.dropdownContainer.nativeElement.focus();
378
        this.opened.emit({ owner: this });
379
    }
21✔
380

6✔
381
    /** @hidden @internal */
6✔
382
    public handleClosing(e: IBaseCancelableBrowserEventArgs): void {
383
        const args: IBaseCancelableBrowserEventArgs = { owner: this, event: e.event, cancel: e.cancel };
15✔
384
        this.closing.emit(args);
15✔
385
        e.cancel = args.cancel;
15✔
386
        if (e.cancel) {
387
            return;
388
        }
15✔
389

37✔
390
        this.composing = false;
37✔
391
        // explicitly update selection and trigger text selection so that we don't have to force CD
15✔
392
        this.textSelection.selected = true;
393
        this.textSelection.trigger();
394
    }
22✔
395

396
    /** @hidden @internal */
397
    public focusSearchInput(opening?: boolean): void {
398
        if (opening) {
20✔
399
            this.dropdownContainer.nativeElement.focus();
20✔
400
        } else {
1✔
401
            this.comboInput.nativeElement.focus();
402
            this.toggle();
20✔
403
        }
404
    }
405

14✔
406
    /** @hidden @internal */
1!
407
    public onClick(event: Event): void {
1✔
408
        super.onClick(event);
1!
409
        if (this.comboInput.value.length === 0) {
×
410
            this.virtDir.scrollTo(0);
411
        }
1✔
412
    }
413

13✔
414
    protected findAllMatches = (element: any): boolean => {
415
        const value = this.displayKey ? element[this.displayKey] : element;
13✔
416
        if (value === null || value === undefined || value === '') {
11✔
417
            // we can accept null, undefined and empty strings as empty display values
418
            return true;
419
        }
420
        const searchValue = this.searchValue || this.comboInput.value;
2!
421
        return !!searchValue && value.toString().toLowerCase().includes(searchValue.toLowerCase());
2✔
422
    };
423

424
    protected setSelection(newSelection: any): void {
11✔
425
        const newSelectionAsArray = newSelection ? Array.from(newSelection) as IgxComboItemComponent[] : [];
11✔
426
        const oldSelectionAsArray = Array.from(this.selectionService.get(this.id) || []);
427
        const displayText = this.createDisplayText(newSelectionAsArray, oldSelectionAsArray);
428
        const args: ISimpleComboSelectionChangingEventArgs = {
176✔
429
            newSelection: newSelectionAsArray[0],
85✔
430
            oldSelection: oldSelectionAsArray[0],
431
            displayText,
432
            owner: this,
2✔
433
            cancel: false
434
        };
435
        if (args.newSelection !== args.oldSelection) {
436
            this.selectionChanging.emit(args);
437
        }
438
        // TODO: refactor below code as it sets the selection and the display text
439
        if (!args.cancel) {
440
            let argsSelection = this.isValid(args.newSelection)
441
                ? args.newSelection
442
                : [];
443
            argsSelection = Array.isArray(argsSelection) ? argsSelection : [argsSelection];
2✔
444
            this.selectionService.select_items(this.id, argsSelection, true);
445
            if (this._updateInput) {
446
                this.comboInput.value = this._internalFilter = this._value = this.searchValue = displayText !== args.displayText
447
                    ? args.displayText
448
                    : this.createDisplayText(argsSelection, [args.oldSelection]);
449
            }
450
            this._onChangeCallback(args.newSelection);
451
            this._updateInput = true;
2✔
452
        } else if (this.isRemote) {
453
            this.registerRemoteEntries(newSelectionAsArray, false);
454
        }
455
    }
456

457
    protected createDisplayText(newSelection: any[], oldSelection: any[]): string {
458
        if (this.isRemote) {
459
            return this.getRemoteSelection(newSelection, oldSelection);
460
        }
461

462
        if (this.displayKey !== null
463
            && this.displayKey !== undefined
2✔
464
            && newSelection.length > 0) {
465
            return this.convertKeysToItems(newSelection).filter(e => e).map(e => e[this.displayKey])[0]?.toString() || '';
2✔
466
        }
467

468
        return newSelection[0]?.toString() || '';
469
    }
470

471
    protected getRemoteSelection(newSelection: any[], oldSelection: any[]): string {
472
        if (!newSelection.length) {
473
            this.registerRemoteEntries(oldSelection, false);
474
            return '';
475
        }
476

477
        this.registerRemoteEntries(oldSelection, false);
478
        this.registerRemoteEntries(newSelection);
479
        return Object.keys(this._remoteSelection).map(e => this._remoteSelection[e])[0];
480
    }
481

482
    /** Contains key-value pairs of the selected valueKeys and their resp. displayKeys */
483
    protected registerRemoteEntries(ids: any[], add = true) {
484
        const selection = this.getValueDisplayPairs(ids)[0];
485

486
        if (add && selection) {
487
            this._remoteSelection[selection[this.valueKey]] = selection[this.displayKey].toString();
488
        } else {
489
            delete this._remoteSelection[ids[0]];
490
        }
491
    }
492

493
    private clearSelection(ignoreFilter?: boolean): void {
494
        let newSelection = this.selectionService.get_empty();
495
        if (this.filteredData.length !== this.data.length && !ignoreFilter) {
496
            newSelection = this.selectionService.delete_items(this.id, this.selectionService.get_all_ids(this.filteredData, this.valueKey));
497
        }
498
        this.setSelection(newSelection);
499
    }
500

501
    private clearOnBlur(): void {
502
        if (this.isRemote) {
503
            const searchValue = this.searchValue || this.comboInput.value;
504
            const remoteValue = Object.keys(this._remoteSelection).map(e => this._remoteSelection[e])[0];
505
            if (remoteValue && searchValue !== remoteValue) {
506
                this.clear();
507
            }
508
            return;
509
        }
510

511
        const filtered = this.filteredData.find(this.findMatch);
512
        // selecting null in primitive data returns undefined as the search text is '', but the item is null
513
        if (filtered === undefined && this.selectedItem !== null || !this.selection.length) {
514
            this.clear();
515
        }
516
    }
517

518
    private getElementVal(element: any): string {
519
        const elementVal = this.displayKey ? element[this.displayKey] : element;
520
        return String(elementVal);
521
    }
522

523
    private clear(): void {
524
        this.clearSelection(true);
525
        this.comboInput.value = this._internalFilter = this._value = this.searchValue = '';
526
    }
527

528
    private isValid(value: any): boolean {
529
        return this.required
530
        ? value !== null && value !== '' && value !== undefined
531
        : value !== undefined;
532
    }
533
}
534

535
@NgModule({
536
    declarations: [IgxSimpleComboComponent],
537
    imports: [
538
        IgxComboModule, IgxRippleModule, CommonModule,
539
        IgxInputGroupModule, FormsModule, ReactiveFormsModule,
540
        IgxForOfModule, IgxToggleModule, IgxCheckboxModule,
541
        IgxDropDownModule, IgxButtonModule, IgxIconModule,
542
        IgxTextSelectionModule
543
    ],
544
    exports: [IgxSimpleComboComponent, IgxComboModule]
545
})
546
export class IgxSimpleComboModule { }
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc