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

IgniteUI / igniteui-angular / 6787096818

07 Nov 2023 04:02PM UTC coverage: 92.1% (+0.002%) from 92.098%
6787096818

push

github

web-flow
fix(combos): selection event consistent data items, value args (#13619)

Rename existing `newSelection` and `oldSelection` to `newValue` and
`oldValue` respectively. Add new args in their place that emit data
items consistently regardless of `valueKey`. For `IgxCombo` also
update `added` and `removed` collections to always contain data items

15267 of 17958 branches covered (0.0%)

6 of 6 new or added lines in 2 files covered. (100.0%)

16 existing lines in 2 files now uncovered.

26417 of 28683 relevant lines covered (92.1%)

30202.33 hits per line

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

94.5
/projects/igniteui-angular/src/lib/combo/combo.component.ts
1
import { NgClass, NgIf, NgTemplateOutlet } from '@angular/common';
2
import {
3
    AfterViewInit, ChangeDetectorRef, Component, ElementRef, OnInit, OnDestroy,
4
    Optional, Inject, Injector, ViewChild, Input, Output, EventEmitter, HostListener, DoCheck, booleanAttribute
5
} from '@angular/core';
6

7
import { ControlValueAccessor, FormsModule, NG_VALUE_ACCESSOR } from '@angular/forms';
8

9
import { IgxSelectionAPIService } from '../core/selection';
10
import { IBaseEventArgs, IBaseCancelableEventArgs, CancelableEventArgs } from '../core/utils';
11
import { IgxStringFilteringOperand, IgxBooleanFilteringOperand } from '../data-operations/filtering-condition';
12
import { FilteringLogic } from '../data-operations/filtering-expression.interface';
13
import { IgxForOfDirective } from '../directives/for-of/for_of.directive';
14
import { IgxIconService } from '../icon/icon.service';
15
import { IgxRippleDirective } from '../directives/ripple/ripple.directive';
16
import { IgxButtonDirective } from '../directives/button/button.directive';
17
import { IgxInputGroupComponent } from '../input-group/input-group.component';
18
import { IgxComboItemComponent } from './combo-item.component';
19
import { IgxComboDropDownComponent } from './combo-dropdown.component';
20
import { IgxComboFilteringPipe, IgxComboGroupingPipe } from './combo.pipes';
21
import { DisplayDensityToken, IDisplayDensityOptions } from '../core/density';
22
import { IGX_COMBO_COMPONENT, IgxComboBaseDirective } from './combo.common';
23
import { IgxComboAddItemComponent } from './combo-add-item.component';
24
import { IgxComboAPIService } from './combo.api';
25
import { EditorProvider } from '../core/edit-provider';
26
import { IgxInputGroupType, IGX_INPUT_GROUP_TYPE } from '../input-group/public_api';
27
import { IgxDropDownItemNavigationDirective } from '../drop-down/drop-down-navigation.directive';
28
import { IgxIconComponent } from '../icon/icon.component';
29
import { IgxSuffixDirective } from '../directives/suffix/suffix.directive';
30
import { IgxInputDirective } from '../directives/input/input.directive';
31

32
/** Event emitted when an igx-combo's selection is changing */
2✔
33
export interface IComboSelectionChangingEventArgs extends IBaseCancelableEventArgs {
209✔
34
    /** An array containing the values that are currently selected */
209✔
35
    oldValue: any[];
593✔
36
    /** An array containing the values that will be selected after this event */
357✔
37
    newValue: any[];
38
    /** An array containing the items that are currently selected */
39
    oldSelection: any[];
209✔
40
    /** An array containing the items that will be selected after this event */
41
    newSelection: any[];
42
    /** An array containing the items that will be added to the selection (if any) */
43
    added: any[];
44
    /** An array containing the items that will be removed from the selection (if any) */
45
    removed: any[];
46
    /** The text that will be displayed in the combo text box */
47
    displayText: string;
48
    /** The user interaction that triggered the selection change */
49
    event?: Event;
50
}
51

52
/** Event emitted when the igx-combo's search input changes */
53
export interface IComboSearchInputEventArgs extends IBaseCancelableEventArgs {
54
    /** The text that has been typed into the search input */
55
    searchText: string;
56
}
57

58
export interface IComboItemAdditionEvent extends IBaseEventArgs, CancelableEventArgs {
59
    oldCollection: any[];
60
    addedItem: any;
2✔
61
    newCollection: any[];
UNCOV
62
}
×
63

64
/**
65
 * When called with sets A & B, returns A - B (as array);
90✔
66
 *
67
 * @hidden
68
 */
69
const diffInSets = (set1: Set<any>, set2: Set<any>): any[] => {
70
    const results = [];
UNCOV
71
    set1.forEach(entry => {
×
72
        if (!set2.has(entry)) {
73
            results.push(entry);
74
        }
75
    });
1,610✔
76
    return results;
77
};
78

79
/**
9,162!
80
 *  Represents a drop-down list that provides editable functionalities, allowing users to choose an option from a predefined list.
290✔
81
 *
82
 * @igxModule IgxComboModule
83
 * @igxTheme igx-combo-theme
125✔
84
 * @igxKeywords combobox, combo selection
125✔
85
 * @igxGroup Grids & Lists
125✔
86
 *
125✔
87
 * @remarks
88
 * It provides the ability to filter items as well as perform selection with the provided data.
89
 * Additionally, it exposes keyboard navigation and custom styling capabilities.
90
 * @example
125✔
91
 * ```html
125✔
92
 * <igx-combo [itemsMaxHeight]="250" [data]="locationData"
125✔
93
 *  [displayKey]="'field'" [valueKey]="'field'"
125✔
94
 *  placeholder="Location(s)" searchPlaceholder="Search...">
125✔
95
 * </igx-combo>
96
 * ```
97
 */
2✔
98
@Component({
2✔
99
    selector: 'igx-combo',
2✔
100
    templateUrl: 'combo.component.html',
101
    providers: [
102
        IgxComboAPIService,
103
        { provide: IGX_COMBO_COMPONENT, useExisting: IgxComboComponent },
1,215✔
104
        { provide: NG_VALUE_ACCESSOR, useExisting: IgxComboComponent, multi: true }
105
    ],
106
    standalone: true,
107
    imports: [
108
        NgIf,
109
        NgTemplateOutlet,
110
        NgClass,
12✔
111
        FormsModule,
6✔
112
        IgxInputGroupComponent,
6✔
113
        IgxInputDirective,
114
        IgxSuffixDirective,
6✔
115
        IgxIconComponent,
2✔
116
        IgxComboDropDownComponent,
117
        IgxDropDownItemNavigationDirective,
118
        IgxForOfDirective,
119
        IgxComboItemComponent,
120
        IgxComboAddItemComponent,
121
        IgxButtonDirective,
122
        IgxRippleDirective,
2✔
123
        IgxComboFilteringPipe,
1✔
124
        IgxComboGroupingPipe
125
    ]
126
})
1✔
127
export class IgxComboComponent extends IgxComboBaseDirective implements AfterViewInit, ControlValueAccessor, OnInit,
128
    OnDestroy, DoCheck, EditorProvider {
129
    /**
130
     * An @Input property that controls whether the combo's search box
131
     * should be focused after the `opened` event is called
132
     * When `false`, the combo's list item container will be focused instead
133
     */
50✔
134
    @Input({ transform: booleanAttribute })
50✔
135
    public autoFocusSearch = true;
50✔
136

50✔
137
    /**
50✔
138
     * @deprecated in version 14.0.0. Use the IComboFilteringOptions.filterable
50✔
139
     *
140
     * An @Input property that enabled/disables filtering in the list. The default is `true`.
141
     * ```html
142
     * <igx-combo [filterable]="false">
458✔
143
     * ```
101✔
144
     */
297✔
145
    @Input({ transform: booleanAttribute })
146
    public get filterable(): boolean {
458✔
147
        return this.filteringOptions.filterable;
148
    }
149
    public set filterable(value: boolean) {
150
        this.filteringOptions = Object.assign({}, this.filteringOptions, { filterable: value });
151
    }
152

4✔
153
    /**
154
     * Defines the placeholder value for the combo dropdown search field
155
     *
156
     * ```typescript
157
     * // get
UNCOV
158
     * let myComboSearchPlaceholder = this.combo.searchPlaceholder;
×
159
     * ```
160
     *
161
     * ```html
162
     * <!--set-->
163
     * <igx-combo [searchPlaceholder]='newPlaceHolder'></igx-combo>
164
     * ```
165
     */
166
    @Input()
9✔
167
    public searchPlaceholder = 'Enter a Search Term';
1✔
168

169
    /**
8✔
170
     * Emitted when item selection is changing, before the selection completes
8✔
171
     *
4✔
172
     * ```html
173
     * <igx-combo (selectionChanging)='handleSelection()'></igx-combo>
174
     * ```
4✔
175
     */
176
    @Output()
8✔
177
    public selectionChanging = new EventEmitter<IComboSelectionChangingEventArgs>();
178

179
    /** @hidden @internal */
180
    @ViewChild(IgxComboDropDownComponent, { static: true })
181
    public dropdown: IgxComboDropDownComponent;
182

183
    /**
184
     * @hidden @internal
185
     */
186
    public get inputEmpty(): boolean {
187
        return this.displayValue.length === 0 && !this.placeholder;
188
    }
72!
189

72✔
190
    /** @hidden @internal */
72✔
191
    public get filteredData(): any[] | null {
192
        return this.filteringOptions.filterable ? this._filteredData : this.data;
193
    }
194
    /** @hidden @internal */
195
    public set filteredData(val: any[] | null) {
196
        this._filteredData = this.groupKey ? (val || []).filter((e) => e.isHeader !== true) : val;
197
        this.checkMatch();
198
    }
199

200
    /**
201
     * @hidden @internal
202
     */
17!
203
    public filteringLogic = FilteringLogic.Or;
17✔
204

17✔
205
    protected stringFilters = IgxStringFilteringOperand;
206
    protected booleanFilters = IgxBooleanFilteringOperand;
207
    protected _prevInputValue = '';
208

209
    private _displayText: string;
210

211
    constructor(
212
        elementRef: ElementRef,
213
        cdr: ChangeDetectorRef,
214
        selectionService: IgxSelectionAPIService,
215
        comboAPI: IgxComboAPIService,
216
        _iconService: IgxIconService,
4✔
217
        @Optional() @Inject(DisplayDensityToken) _displayDensityOptions: IDisplayDensityOptions,
4✔
218
        @Optional() @Inject(IGX_INPUT_GROUP_TYPE) _inputGroupType: IgxInputGroupType,
4✔
219
        @Optional() _injector: Injector) {
220
        super(elementRef, cdr, selectionService, comboAPI, _iconService, _displayDensityOptions, _inputGroupType, _injector);
221
        this.comboAPI.register(this);
222
    }
223

224
    @HostListener('keydown.ArrowDown', ['$event'])
225
    @HostListener('keydown.Alt.ArrowDown', ['$event'])
226
    public onArrowDown(event: Event) {
227
        event.preventDefault();
228
        event.stopPropagation();
229
        this.open();
11✔
230
    }
11✔
231

1✔
232
    /** @hidden @internal */
233
    public get displaySearchInput(): boolean {
11✔
234
        return this.filteringOptions.filterable || this.allowCustomValues;
235
    }
236

237
    /**
238
     * @hidden @internal
239
     */
240
    public handleKeyUp(event: KeyboardEvent): void {
241
        // TODO: use PlatformUtil for keyboard navigation
242
        if (event.key === 'ArrowDown' || event.key === 'Down') {
243
            this.dropdown.focusedItem = this.dropdown.items[0];
244
            this.dropdownContainer.nativeElement.focus();
245
        } else if (event.key === 'Escape' || event.key === 'Esc') {
246
            this.toggle();
247
        }
248
    }
249

250
    /**
251
     * @hidden @internal
252
     */
253
    public handleSelectAll(evt) {
254
        if (evt.checked) {
×
255
            this.selectAllItems();
7!
UNCOV
256
        } else {
×
257
            this.deselectAllItems();
258
        }
7✔
259
    }
4✔
260

261
    /**
262
     * @hidden @internal
3✔
263
     */
264
    public writeValue(value: any[]): void {
265
        const selection = Array.isArray(value) ? value.filter(x => x !== undefined) : [];
266
        const oldSelection = this.selection;
267
        this.selectionService.select_items(this.id, selection, true);
44✔
268
        this.cdr.markForCheck();
269
        this._displayValue = this.createDisplayText(this.selection, oldSelection);
270
        this._value = this.valueKey ? this.selection.map(item => item[this.valueKey]) : this.selection;
271
    }
44✔
272

41✔
273
    /** @hidden @internal */
274
    public override ngDoCheck(): void {
275
        if (this.data?.length && this.selection.length) {
3✔
276
            this._displayValue = this._displayText || this.createDisplayText(this.selection, []);
277
            this._value = this.valueKey ? this.selection.map(item => item[this.valueKey]) : this.selection;
44✔
278
        }
279
        super.ngDoCheck();
280
    }
281

46✔
282
    /**
45✔
283
     * @hidden
284
     */
285
    public getEditElement(): HTMLElement {
1!
286
        return this.comboInput.nativeElement;
1✔
287
    }
288

UNCOV
289
    /**
×
UNCOV
290
     * @hidden @internal
×
291
     */
292
    public get context(): any {
293
        return {
294
            $implicit: this
295
        };
104✔
296
    }
104✔
297

104✔
298
    /**
104✔
299
     * @hidden @internal
104!
300
     */
104✔
301
    public handleClearItems(event: Event): void {
104✔
302
        if (this.disabled) {
104✔
303
            return;
104✔
304
        }
305
        this.deselectAllItems(true, event);
306
        if (this.collapsed) {
307
            this.getEditElement().focus();
308
        } else {
309
            this.focusSearchInput(true);
310
        }
311
        event.stopPropagation();
312
    }
313

314
    /**
315
     * Select defined items
104✔
316
     *
104✔
317
     * @param newItems new items to be selected
102✔
318
     * @param clearCurrentSelection if true clear previous selected items
102✔
319
     * ```typescript
102✔
320
     * this.combo.select(["New York", "New Jersey"]);
2✔
321
     * ```
322
     */
323
    public select(newItems: Array<any>, clearCurrentSelection?: boolean, event?: Event) {
100✔
324
        if (newItems) {
325
            const newSelection = this.selectionService.add_items(this.id, newItems, clearCurrentSelection);
102✔
326
            this.setSelection(newSelection, event);
327
        }
2✔
328
    }
1✔
329

330
    /**
331
     * Deselect defined items
332
     *
832✔
333
     * @param items items to deselected
354✔
334
     * ```typescript
335
     * this.combo.deselect(["New York", "New Jersey"]);
336
     * ```
337
     */
338
    public deselect(items: Array<any>, event?: Event) {
339
        if (items) {
313✔
340
            const newSelection = this.selectionService.delete_items(this.id, items);
825✔
341
            this.setSelection(newSelection, event);
342
        }
313✔
343
    }
344

2✔
345
    /**
346
     * Select all (filtered) items
347
     *
348
     * @param ignoreFilter if set to true, selects all items, otherwise selects only the filtered ones.
349
     * ```typescript
350
     * this.combo.selectAllItems();
351
     * ```
352
     */
353
    public selectAllItems(ignoreFilter?: boolean, event?: Event) {
354
        const allVisible = this.selectionService.get_all_ids(ignoreFilter ? this.data : this.filteredData, this.valueKey);
2✔
355
        const newSelection = this.selectionService.add_items(this.id, allVisible);
356
        this.setSelection(newSelection, event);
357
    }
358

359
    /**
360
     * Deselect all (filtered) items
361
     *
362
     * @param ignoreFilter if set to true, deselects all items, otherwise deselects only the filtered ones.
363
     * ```typescript
2✔
364
     * this.combo.deselectAllItems();
365
     * ```
366
     */
367
    public deselectAllItems(ignoreFilter?: boolean, event?: Event): void {
368
        let newSelection = this.selectionService.get_empty();
369
        if (this.filteredData.length !== this.data.length && !ignoreFilter) {
370
            newSelection = this.selectionService.delete_items(this.id, this.selectionService.get_all_ids(this.filteredData, this.valueKey));
371
        }
372
        this.setSelection(newSelection, event);
373
    }
374

375
    /**
376
     * Selects/Deselects a single item
377
     *
378
     * @param itemID the itemID of the specific item
379
     * @param select If the item should be selected (true) or deselected (false)
380
     *
381
     * Without specified valueKey;
382
     * ```typescript
383
     * this.combo.valueKey = null;
384
     * const items: { field: string, region: string}[] = data;
385
     * this.combo.setSelectedItem(items[0], true);
386
     * ```
387
     * With specified valueKey;
388
     * ```typescript
389
     * this.combo.valueKey = 'field';
390
     * const items: { field: string, region: string}[] = data;
391
     * this.combo.setSelectedItem('Connecticut', true);
392
     * ```
393
     */
394
    public setSelectedItem(itemID: any, select = true, event?: Event): void {
395
        if (itemID === undefined) {
396
            return;
397
        }
398
        if (select) {
399
            this.select([itemID], false, event);
400
        } else {
401
            this.deselect([itemID], event);
402
        }
403
    }
404

405
    /** @hidden @internal */
406
    public handleOpened() {
407
        this.triggerCheck();
408

409
        // Disabling focus of the search input should happen only when drop down opens.
410
        // During keyboard navigation input should receive focus, even the autoFocusSearch is disabled.
411
        // That is why in such cases focusing of the dropdownContainer happens outside focusSearchInput method.
412
        if (this.autoFocusSearch) {
413
            this.focusSearchInput(true);
414
        } else {
415
            this.dropdownContainer.nativeElement.focus();
416
        }
417
        this.opened.emit({ owner: this });
418
    }
419

420
    /** @hidden @internal */
421
    public focusSearchInput(opening?: boolean): void {
422
        if (this.displaySearchInput && this.searchInput) {
423
            this.searchInput.nativeElement.focus();
424
        } else {
425
            if (opening) {
426
                this.dropdownContainer.nativeElement.focus();
427
            } else {
428
                this.comboInput.nativeElement.focus();
429
                this.toggle();
430
            }
431
        }
432
    }
433

434
    protected setSelection(selection: Set<any>, event?: Event): void {
435
        const currentSelection = this.selectionService.get(this.id);
436
        const removed = this.convertKeysToItems(diffInSets(currentSelection, selection));
437
        const added = this.convertKeysToItems(diffInSets(selection, currentSelection));
438
        const newValue = Array.from(selection);
439
        const oldValue = Array.from(currentSelection || []);
440
        const newSelection = this.convertKeysToItems(newValue);
441
        const oldSelection = this.convertKeysToItems(oldValue);
442
        const displayText = this.createDisplayText(this.convertKeysToItems(newValue), oldValue);
443
        const args: IComboSelectionChangingEventArgs = {
444
            newValue,
445
            oldValue,
446
            newSelection,
447
            oldSelection,
448
            added,
449
            removed,
450
            event,
451
            owner: this,
452
            displayText,
453
            cancel: false
454
        };
455
        this.selectionChanging.emit(args);
456
        if (!args.cancel) {
457
            this.selectionService.select_items(this.id, args.newValue, true);
458
            this._value = args.newValue;
459
            if (displayText !== args.displayText) {
460
                this._displayValue = this._displayText = args.displayText;
461
            } else {
462
                this._displayValue = this.createDisplayText(this.selection, args.oldSelection);
463
            }
464
            this._onChangeCallback(args.newValue);
465
        } else if (this.isRemote) {
466
            this.registerRemoteEntries(diffInSets(selection, currentSelection), false);
467
        }
468
    }
469

470
    protected createDisplayText(newSelection: any[], oldSelection: any[]) {
471
        const selection = this.valueKey ? newSelection.map(item => item[this.valueKey]) : newSelection;
472
        return this.isRemote
473
            ? this.getRemoteSelection(selection, oldSelection)
474
            : this.concatDisplayText(newSelection);
475
    }
476

477
    /** Returns a string that should be populated in the combo's text box */
478
    private concatDisplayText(selection: any[]): string {
479
        const value = this.displayKey !== null && this.displayKey !== undefined ?
480
            selection.map(entry => entry[this.displayKey]).join(', ') :
481
            selection.join(', ');
482
        return value;
483
    }
484
}
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