Skip to content

Commit 9cce98a

Browse files
authored
feat: allow to configure marking first item (#145)
* feat: allow to configure marking first item closes #103 * fix: don't crash if default bindlabel doesn't exist on item
1 parent 1b7d88d commit 9cce98a

File tree

4 files changed

+56
-19
lines changed

4 files changed

+56
-19
lines changed

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,9 +97,10 @@ map: {
9797
| bindLabel | string | `label` | no | Object property to use for label. Default `label` |
9898
| bindValue | string | `-` | no | Object property to use for selected model. By default binds to whole object. |
9999
| [clearable] | boolean | `true` | no | Allow to clear selected value. Default `true`|
100+
| [markFirst] | boolean | `true` | no | Marks first item as focused when opening/filtering. Default `true`|
100101
| [searchable] | boolean | `true` | no | Allow to search for value. Default `true`|
101102
| multiple | boolean | `false` | no | Allows to select multiple items. |
102-
| [addTag] | Function or boolean | `false` | no | Using boolean simply adds tag with value as bindLabel. If you want custom properties add function which returns object. |
103+
| [addTag] | Function or boolean | `false` | no | Allows to create custom options. Using boolean simply adds tag with value as bindLabel. If you want custom properties add function which returns object. |
103104
| placeholder | string | `-` | no | Placeholder text. |
104105
| notFoundText | string | `No items found` | no | Set custom text when filter returns empty result |
105106
| typeToSearchText | string | `Type to search` | no | Set custom text when using Typeahead |

src/ng-select/items-list.ts

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ export class ItemsList {
5858
item.selected = false;
5959
}
6060

61-
unselectLastItem() {
61+
unselectLast() {
6262
if (this._selected.length === 0) {
6363
return;
6464
}
@@ -84,7 +84,6 @@ export class ItemsList {
8484
filter(term: string, bindLabel: string) {
8585
const filterFuncVal = this.getDefaultFilterFunc(term, bindLabel);
8686
this.filteredItems = term ? this.items.filter(val => filterFuncVal(val)) : this.items;
87-
this._markedIndex = 0;
8887
}
8988

9089
clearFilter() {
@@ -99,25 +98,27 @@ export class ItemsList {
9998
this.stepToItem(-1);
10099
}
101100

102-
markItem(item: NgOption = null) {
101+
markItem(item: NgOption) {
102+
this._markedIndex = this.filteredItems.indexOf(item);
103+
}
104+
105+
markSelectedOrDefault(markDefault) {
103106
if (this.filteredItems.length === 0) {
104107
return;
105108
}
106109

107-
item = item || this.lastSelectedItem;
108-
if (item) {
109-
this._markedIndex = this.filteredItems.indexOf(item);
110+
if (this.lastSelectedItem) {
111+
this._markedIndex = this.filteredItems.indexOf(this.lastSelectedItem);
110112
} else {
111-
this._markedIndex = 0;
113+
this._markedIndex = markDefault ? 0 : -1;
112114
}
113115
}
114116

115117
private getNextItemIndex(steps: number) {
116118
if (steps > 0) {
117119
return (this._markedIndex === this.filteredItems.length - 1) ? 0 : (this._markedIndex + 1);
118-
} else {
119-
return (this._markedIndex === 0) ? (this.filteredItems.length - 1) : (this._markedIndex - 1);
120120
}
121+
return (this._markedIndex === 0) ? (this.filteredItems.length - 1) : (this._markedIndex - 1);
121122
}
122123

123124
private stepToItem(steps: number) {
@@ -133,7 +134,7 @@ export class ItemsList {
133134

134135
private getDefaultFilterFunc(term, bindLabel: string) {
135136
return (val: NgOption) => {
136-
return searchHelper.stripSpecialChars(val[bindLabel])
137+
return searchHelper.stripSpecialChars(val[bindLabel] || '')
137138
.toUpperCase()
138139
.indexOf(searchHelper.stripSpecialChars(term).toUpperCase()) > -1;
139140
};
@@ -147,8 +148,9 @@ export class ItemsList {
147148
return items.map((item, index) => {
148149
let option = item;
149150
if (this._simple) {
150-
option = {};
151-
option['label'] = item as any;
151+
option = {
152+
label: item as any
153+
};
152154
}
153155

154156
return {

src/ng-select/ng-select.component.spec.ts

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -362,6 +362,12 @@ describe('NgSelectComponent', function () {
362362
triggerKeyDownEvent(getNgSelectElement(fixture), KeyCode.Space);
363363
expect(fixture.componentInstance.select.itemsList.markedItem).toEqual(jasmine.objectContaining(result));
364364
});
365+
366+
it('should open dropdown without marking first item', () => {
367+
fixture.componentInstance.select.markFirst = false;
368+
triggerKeyDownEvent(getNgSelectElement(fixture), KeyCode.Space);
369+
expect(fixture.componentInstance.select.itemsList.markedItem).toEqual(undefined);
370+
});
365371
});
366372

367373
describe('arrows', () => {
@@ -488,7 +494,7 @@ describe('NgSelectComponent', function () {
488494
});
489495
});
490496

491-
describe('document:click', () => {
497+
describe('Document:click', () => {
492498
let fixture: ComponentFixture<NgSelectBasicTestCmp>;
493499

494500
beforeEach(() => {
@@ -610,7 +616,7 @@ describe('NgSelectComponent', function () {
610616
});
611617
});
612618

613-
describe('tagging', () => {
619+
describe('Tagging', () => {
614620
it('should select default tag', fakeAsync(() => {
615621
let fixture = createTestingModule(
616622
NgSelectBasicTestCmp,
@@ -750,6 +756,21 @@ describe('NgSelectComponent', function () {
750756
expect(fixture.componentInstance.select.selectedItems).toEqual([result]);
751757
}));
752758

759+
it('should not mark first item on filter when markFirst disabled', fakeAsync(() => {
760+
fixture = createTestingModule(
761+
NgSelectFilterTestCmp,
762+
`<ng-select [items]="cities"
763+
bindLabel="name"
764+
[markFirst]="false"
765+
[(ngModel)]="selectedCity">
766+
</ng-select>`);
767+
768+
tick(200);
769+
fixture.componentInstance.select.onFilter({ target: { value: 'pab' } });
770+
tick(200);
771+
expect(fixture.componentInstance.select.itemsList.markedItem).toEqual(undefined)
772+
}));
773+
753774
it('should clear filterValue on selected item', fakeAsync(() => {
754775
fixture = createTestingModule(
755776
NgSelectFilterTestCmp,
@@ -794,6 +815,16 @@ describe('NgSelectComponent', function () {
794815
expect(fixture.componentInstance.select.selectedItems).toEqual([jasmine.objectContaining({ id: 4, name: 'Bukiskes' })])
795816
}));
796817

818+
it('should not mark first item when typeahead results are loaded', fakeAsync(() => {
819+
fixture.componentInstance.select.markFirst = false;
820+
fixture.componentInstance.customFilter.subscribe();
821+
fixture.componentInstance.select.onFilter({ target: { value: 'buk' } });
822+
fixture.componentInstance.cities = [{ id: 4, name: 'Bukiskes' }];
823+
tickAndDetectChanges(fixture);
824+
triggerKeyDownEvent(getNgSelectElement(fixture), KeyCode.Enter);
825+
expect(fixture.componentInstance.select.selectedItems).toEqual([])
826+
}));
827+
797828
it('should start and stop loading indicator', fakeAsync(() => {
798829
fixture.componentInstance.customFilter.subscribe();
799830
fixture.componentInstance.select.onFilter({ target: { value: 'buk' } });

src/ng-select/ng-select.component.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ export class NgSelectComponent implements OnInit, OnDestroy, OnChanges, AfterVie
6262
@Input() bindLabel: string;
6363
@Input() bindValue: string;
6464
@Input() clearable = true;
65+
@Input() markFirst = true;
6566
@Input() disableVirtualScroll = false;
6667
@Input() placeholder: string;
6768
@Input() notFoundText;
@@ -205,6 +206,7 @@ export class NgSelectComponent implements OnInit, OnDestroy, OnChanges, AfterVie
205206
}
206207
}
207208

209+
// TODO: make private
208210
clearModel() {
209211
if (!this.clearable) {
210212
return;
@@ -246,7 +248,7 @@ export class NgSelectComponent implements OnInit, OnDestroy, OnChanges, AfterVie
246248
return;
247249
}
248250
this.isOpen = true;
249-
this.itemsList.markItem();
251+
this.itemsList.markSelectedOrDefault(this.markFirst);
250252
this.scrollToMarked();
251253
this.focusSearchInput();
252254
this.openEvent.emit();
@@ -348,6 +350,7 @@ export class NgSelectComponent implements OnInit, OnDestroy, OnChanges, AfterVie
348350
this.typeahead.next(this.filterValue);
349351
} else {
350352
this.itemsList.filter(this.filterValue, this.bindLabel);
353+
this.itemsList.markSelectedOrDefault(this.markFirst);
351354
}
352355
}
353356

@@ -382,13 +385,13 @@ export class NgSelectComponent implements OnInit, OnDestroy, OnChanges, AfterVie
382385

383386
if (this.isTypeahead) {
384387
this.isLoading = false;
385-
this.itemsList.markItem(this.itemsList.filteredItems[0]);
388+
this.itemsList.markSelectedOrDefault(this.markFirst);
386389
}
387390
}
388391

389392
private setItemsFromNgOptions() {
390393
if (!this.bindValue) {
391-
this.bindValue = 'value';
394+
this.bindValue = this._defaultValue;
392395
}
393396

394397
const handleNgOptions = (options) => {
@@ -550,7 +553,7 @@ export class NgSelectComponent implements OnInit, OnDestroy, OnChanges, AfterVie
550553
}
551554

552555
if (this.multiple) {
553-
this.itemsList.unselectLastItem();
556+
this.itemsList.unselectLast();
554557
this.updateModel();
555558
} else {
556559
this.clearModel();

0 commit comments

Comments
 (0)