Skip to content

Commit

Permalink
feat: allow to configure marking first item (#145)
Browse files Browse the repository at this point in the history
* feat: allow to configure marking first item
closes #103
* fix: don't crash if default bindlabel doesn't exist on item
  • Loading branch information
varnastadeus authored Nov 27, 2017
1 parent 1b7d88d commit 9cce98a
Show file tree
Hide file tree
Showing 4 changed files with 56 additions and 19 deletions.
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,9 +97,10 @@ map: {
| bindLabel | string | `label` | no | Object property to use for label. Default `label` |
| bindValue | string | `-` | no | Object property to use for selected model. By default binds to whole object. |
| [clearable] | boolean | `true` | no | Allow to clear selected value. Default `true`|
| [markFirst] | boolean | `true` | no | Marks first item as focused when opening/filtering. Default `true`|
| [searchable] | boolean | `true` | no | Allow to search for value. Default `true`|
| multiple | boolean | `false` | no | Allows to select multiple items. |
| [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. |
| [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. |
| placeholder | string | `-` | no | Placeholder text. |
| notFoundText | string | `No items found` | no | Set custom text when filter returns empty result |
| typeToSearchText | string | `Type to search` | no | Set custom text when using Typeahead |
Expand Down
26 changes: 14 additions & 12 deletions src/ng-select/items-list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ export class ItemsList {
item.selected = false;
}

unselectLastItem() {
unselectLast() {
if (this._selected.length === 0) {
return;
}
Expand All @@ -84,7 +84,6 @@ export class ItemsList {
filter(term: string, bindLabel: string) {
const filterFuncVal = this.getDefaultFilterFunc(term, bindLabel);
this.filteredItems = term ? this.items.filter(val => filterFuncVal(val)) : this.items;
this._markedIndex = 0;
}

clearFilter() {
Expand All @@ -99,25 +98,27 @@ export class ItemsList {
this.stepToItem(-1);
}

markItem(item: NgOption = null) {
markItem(item: NgOption) {
this._markedIndex = this.filteredItems.indexOf(item);
}

markSelectedOrDefault(markDefault) {
if (this.filteredItems.length === 0) {
return;
}

item = item || this.lastSelectedItem;
if (item) {
this._markedIndex = this.filteredItems.indexOf(item);
if (this.lastSelectedItem) {
this._markedIndex = this.filteredItems.indexOf(this.lastSelectedItem);
} else {
this._markedIndex = 0;
this._markedIndex = markDefault ? 0 : -1;
}
}

private getNextItemIndex(steps: number) {
if (steps > 0) {
return (this._markedIndex === this.filteredItems.length - 1) ? 0 : (this._markedIndex + 1);
} else {
return (this._markedIndex === 0) ? (this.filteredItems.length - 1) : (this._markedIndex - 1);
}
return (this._markedIndex === 0) ? (this.filteredItems.length - 1) : (this._markedIndex - 1);
}

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

private getDefaultFilterFunc(term, bindLabel: string) {
return (val: NgOption) => {
return searchHelper.stripSpecialChars(val[bindLabel])
return searchHelper.stripSpecialChars(val[bindLabel] || '')
.toUpperCase()
.indexOf(searchHelper.stripSpecialChars(term).toUpperCase()) > -1;
};
Expand All @@ -147,8 +148,9 @@ export class ItemsList {
return items.map((item, index) => {
let option = item;
if (this._simple) {
option = {};
option['label'] = item as any;
option = {
label: item as any
};
}

return {
Expand Down
35 changes: 33 additions & 2 deletions src/ng-select/ng-select.component.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -362,6 +362,12 @@ describe('NgSelectComponent', function () {
triggerKeyDownEvent(getNgSelectElement(fixture), KeyCode.Space);
expect(fixture.componentInstance.select.itemsList.markedItem).toEqual(jasmine.objectContaining(result));
});

it('should open dropdown without marking first item', () => {
fixture.componentInstance.select.markFirst = false;
triggerKeyDownEvent(getNgSelectElement(fixture), KeyCode.Space);
expect(fixture.componentInstance.select.itemsList.markedItem).toEqual(undefined);
});
});

describe('arrows', () => {
Expand Down Expand Up @@ -488,7 +494,7 @@ describe('NgSelectComponent', function () {
});
});

describe('document:click', () => {
describe('Document:click', () => {
let fixture: ComponentFixture<NgSelectBasicTestCmp>;

beforeEach(() => {
Expand Down Expand Up @@ -610,7 +616,7 @@ describe('NgSelectComponent', function () {
});
});

describe('tagging', () => {
describe('Tagging', () => {
it('should select default tag', fakeAsync(() => {
let fixture = createTestingModule(
NgSelectBasicTestCmp,
Expand Down Expand Up @@ -750,6 +756,21 @@ describe('NgSelectComponent', function () {
expect(fixture.componentInstance.select.selectedItems).toEqual([result]);
}));

it('should not mark first item on filter when markFirst disabled', fakeAsync(() => {
fixture = createTestingModule(
NgSelectFilterTestCmp,
`<ng-select [items]="cities"
bindLabel="name"
[markFirst]="false"
[(ngModel)]="selectedCity">
</ng-select>`);

tick(200);
fixture.componentInstance.select.onFilter({ target: { value: 'pab' } });
tick(200);
expect(fixture.componentInstance.select.itemsList.markedItem).toEqual(undefined)
}));

it('should clear filterValue on selected item', fakeAsync(() => {
fixture = createTestingModule(
NgSelectFilterTestCmp,
Expand Down Expand Up @@ -794,6 +815,16 @@ describe('NgSelectComponent', function () {
expect(fixture.componentInstance.select.selectedItems).toEqual([jasmine.objectContaining({ id: 4, name: 'Bukiskes' })])
}));

it('should not mark first item when typeahead results are loaded', fakeAsync(() => {
fixture.componentInstance.select.markFirst = false;
fixture.componentInstance.customFilter.subscribe();
fixture.componentInstance.select.onFilter({ target: { value: 'buk' } });
fixture.componentInstance.cities = [{ id: 4, name: 'Bukiskes' }];
tickAndDetectChanges(fixture);
triggerKeyDownEvent(getNgSelectElement(fixture), KeyCode.Enter);
expect(fixture.componentInstance.select.selectedItems).toEqual([])
}));

it('should start and stop loading indicator', fakeAsync(() => {
fixture.componentInstance.customFilter.subscribe();
fixture.componentInstance.select.onFilter({ target: { value: 'buk' } });
Expand Down
11 changes: 7 additions & 4 deletions src/ng-select/ng-select.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ export class NgSelectComponent implements OnInit, OnDestroy, OnChanges, AfterVie
@Input() bindLabel: string;
@Input() bindValue: string;
@Input() clearable = true;
@Input() markFirst = true;
@Input() disableVirtualScroll = false;
@Input() placeholder: string;
@Input() notFoundText;
Expand Down Expand Up @@ -205,6 +206,7 @@ export class NgSelectComponent implements OnInit, OnDestroy, OnChanges, AfterVie
}
}

// TODO: make private
clearModel() {
if (!this.clearable) {
return;
Expand Down Expand Up @@ -246,7 +248,7 @@ export class NgSelectComponent implements OnInit, OnDestroy, OnChanges, AfterVie
return;
}
this.isOpen = true;
this.itemsList.markItem();
this.itemsList.markSelectedOrDefault(this.markFirst);
this.scrollToMarked();
this.focusSearchInput();
this.openEvent.emit();
Expand Down Expand Up @@ -348,6 +350,7 @@ export class NgSelectComponent implements OnInit, OnDestroy, OnChanges, AfterVie
this.typeahead.next(this.filterValue);
} else {
this.itemsList.filter(this.filterValue, this.bindLabel);
this.itemsList.markSelectedOrDefault(this.markFirst);
}
}

Expand Down Expand Up @@ -382,13 +385,13 @@ export class NgSelectComponent implements OnInit, OnDestroy, OnChanges, AfterVie

if (this.isTypeahead) {
this.isLoading = false;
this.itemsList.markItem(this.itemsList.filteredItems[0]);
this.itemsList.markSelectedOrDefault(this.markFirst);
}
}

private setItemsFromNgOptions() {
if (!this.bindValue) {
this.bindValue = 'value';
this.bindValue = this._defaultValue;
}

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

if (this.multiple) {
this.itemsList.unselectLastItem();
this.itemsList.unselectLast();
this.updateModel();
} else {
this.clearModel();
Expand Down

0 comments on commit 9cce98a

Please sign in to comment.