Skip to content

Commit

Permalink
fix(select): allow select to work with empty values in nb-option (#1282)
Browse files Browse the repository at this point in the history
BREAKING CHANGE:

Only 'null' and 'undefined' option values now considered as reset. false and falsy values such as 0, '', NaN don't reset select value anymore.
  • Loading branch information
aefox authored and yggg committed May 27, 2019
1 parent e5a7e82 commit ca4a1ff
Show file tree
Hide file tree
Showing 6 changed files with 253 additions and 17 deletions.
2 changes: 1 addition & 1 deletion src/framework/theme/components/select/option.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ export class NbOptionComponent<T> implements OnDestroy {
* Determines should we render checkbox.
* */
get withCheckbox(): boolean {
return this.multiple && !!this.value;
return this.multiple && this.value != null;
}

get content() {
Expand Down
10 changes: 5 additions & 5 deletions src/framework/theme/components/select/select.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ export class NbSelectLabelComponent {
*
* @stacked-example(Multiple, select/select-multiple.component)
*
* Items without values will clean selection.
* Items without values will clean the selection. Both `null` and `undefined` values will also clean the selection.
*
* @stacked-example(Clean selection, select/select-clean.component)
*
Expand Down Expand Up @@ -384,7 +384,7 @@ export class NbSelectComponent<T> implements OnInit, AfterViewInit, AfterContent
}

writeValue(value: T | T[]): void {
if (!value || !this.alive) {
if (!this.alive) {
return;
}

Expand All @@ -399,10 +399,10 @@ export class NbSelectComponent<T> implements OnInit, AfterViewInit, AfterContent
* Selects option or clear all selected options if value is null.
* */
protected handleOptionClick(option: NbOptionComponent<T>) {
if (option.value) {
this.selectOption(option);
} else {
if (option.value == null) {
this.reset();
} else {
this.selectOption(option);
}

this.cd.markForCheck();
Expand Down
239 changes: 236 additions & 3 deletions src/framework/theme/components/select/select.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*/

import { Component, EventEmitter, Input, Output, QueryList, ViewChild, ViewChildren } from '@angular/core';
import { Component, ElementRef, EventEmitter, Input, Output, QueryList, ViewChild, ViewChildren } from '@angular/core';
import { ComponentFixture, fakeAsync, flush, TestBed } from '@angular/core/testing';
import { FormControl, FormsModule, ReactiveFormsModule } from '@angular/forms';
import { RouterTestingModule } from '@angular/router/testing';
Expand Down Expand Up @@ -45,6 +45,15 @@ const TEST_GROUPS = [
{ title: 'Option 33', value: 'Option 33' },
],
},
{
title: 'Group 4',
options: [
{ title: 'Option 41', value: '' },
{ title: 'Option 42', value: '0' },
{ title: 'Option 43', value: 0 },
{ title: 'Option 44'},
],
},
];

@Component({
Expand Down Expand Up @@ -139,6 +148,105 @@ export class NbNgModelSelectComponent {
@ViewChild(NbOptionComponent) optionComponent: NbOptionComponent<number>;
}

@Component({
template: `
<nb-layout>
<nb-layout-column>
<nb-select>
<nb-option>No value option</nb-option>
<nb-option [value]="null">undefined value</nb-option>
<nb-option [value]="undefined">undefined value</nb-option>
<nb-option [value]="false">false value</nb-option>
<nb-option [value]="0">0 value</nb-option>
<nb-option [value]="''">empty string value</nb-option>
<nb-option [value]="nanValue">NaN value</nb-option>
<nb-option value="1">truthy value</nb-option>
</nb-select>
</nb-layout-column>
</nb-layout>
`,
})
export class NbSelectWithFalsyOptionValuesComponent {
nanValue = NaN;

@ViewChild(NbSelectComponent) select: NbSelectComponent<any>;
@ViewChildren(NbOptionComponent) options: QueryList<NbOptionComponent<any>>;
@ViewChildren(NbOptionComponent, { read: ElementRef }) optionElements: QueryList<ElementRef<HTMLElement>>;

get noValueOption(): NbOptionComponent<any> {
return this.options.toArray()[0];
}
get noValueOptionElement(): ElementRef<HTMLElement> {
return this.optionElements.toArray()[0];
}
get nullOption(): NbOptionComponent<any> {
return this.options.toArray()[1];
}
get nullOptionElement(): ElementRef<HTMLElement> {
return this.optionElements.toArray()[1];
}
get undefinedOption(): NbOptionComponent<any> {
return this.options.toArray()[2];
}
get undefinedOptionElement(): ElementRef<HTMLElement> {
return this.optionElements.toArray()[2];
}
get falseOption(): NbOptionComponent<any> {
return this.options.toArray()[3];
}
get falseOptionElement(): ElementRef<HTMLElement> {
return this.optionElements.toArray()[3];
}
get zeroOption(): NbOptionComponent<any> {
return this.options.toArray()[4];
}
get zeroOptionElement(): ElementRef<HTMLElement> {
return this.optionElements.toArray()[4];
}
get emptyStringOption(): NbOptionComponent<any> {
return this.options.toArray()[5];
}
get emptyStringOptionElement(): ElementRef<HTMLElement> {
return this.optionElements.toArray()[5];
}
get nanOption(): NbOptionComponent<any> {
return this.options.toArray()[6];
}
get nanOptionElement(): ElementRef<HTMLElement> {
return this.optionElements.toArray()[6];
}
get truthyOption(): NbOptionComponent<any> {
return this.options.toArray()[7];
}
get truthyOptionElement(): ElementRef<HTMLElement> {
return this.optionElements.toArray()[7];
}
}

@Component({
template: `
<nb-layout>
<nb-layout-column>
<nb-select multiple>
<nb-option>No value option</nb-option>
<nb-option [value]="null">undefined value</nb-option>
<nb-option [value]="undefined">undefined value</nb-option>
<nb-option [value]="false">false value</nb-option>
<nb-option [value]="0">0 value</nb-option>
<nb-option [value]="''">empty string value</nb-option>
<nb-option [value]="nanValue">NaN value</nb-option>
<nb-option value="1">truthy value</nb-option>
</nb-select>
</nb-layout-column>
</nb-layout>
`,
})
export class NbMultipleSelectWithFalsyOptionValuesComponent extends NbSelectWithFalsyOptionValuesComponent {}

describe('Component: NbSelectComponent', () => {
let fixture: ComponentFixture<NbSelectTestComponent>;
let overlayContainerService: NbOverlayContainerAdapter;
Expand Down Expand Up @@ -290,7 +398,7 @@ describe('Component: NbSelectComponent', () => {

const button = fixture.nativeElement.querySelector('button');
expect(button.textContent).toContain('1 noitpO');
})
});
});

it('should select initially specified value without errors', fakeAsync(() => {
Expand Down Expand Up @@ -342,7 +450,7 @@ describe('Component: NbSelectComponent', () => {
selectFixture.detectChanges();
flush();

const optionToSelect = testComponent.options.last;
const optionToSelect = testComponent.options.find(o => o.value != null);
const optionSelectSpy = spyOn(optionToSelect, 'select').and.callThrough();

expect(optionToSelect.selected).toEqual(false);
Expand Down Expand Up @@ -417,6 +525,131 @@ describe('Component: NbSelectComponent', () => {
});
});

describe('NbSelectComponent - falsy values', () => {
let fixture: ComponentFixture<NbSelectWithFalsyOptionValuesComponent>;
let testComponent: NbSelectWithFalsyOptionValuesComponent;
let select: NbSelectComponent<any>;

beforeEach(fakeAsync(() => {
TestBed.configureTestingModule({
imports: [
RouterTestingModule.withRoutes([]),
NbThemeModule.forRoot(),
NbLayoutModule,
NbSelectModule,
],
declarations: [
NbSelectWithFalsyOptionValuesComponent,
NbMultipleSelectWithFalsyOptionValuesComponent,
],
});

fixture = TestBed.createComponent(NbSelectWithFalsyOptionValuesComponent);
testComponent = fixture.componentInstance;
select = testComponent.select;

fixture.detectChanges();
flush();
}));

it('should clean selection when selected option does not have a value', fakeAsync(() => {
select.setSelected = testComponent.truthyOption.value;
fixture.detectChanges();

testComponent.noValueOption.onClick();
fixture.detectChanges();

expect(select.selectionModel.length).toEqual(0);
}));

it('should clean selection when selected option has null value', fakeAsync(() => {
select.setSelected = testComponent.truthyOption.value;
fixture.detectChanges();

testComponent.nullOption.onClick();
fixture.detectChanges();

expect(select.selectionModel.length).toEqual(0);
}));

it('should clean selection when selected option has undefined value', fakeAsync(() => {
select.setSelected = testComponent.truthyOption.value;
fixture.detectChanges();

testComponent.undefinedOption.onClick();
fixture.detectChanges();

expect(select.selectionModel.length).toEqual(0);
}));

it('should not reset selection when selected option has false value', fakeAsync(() => {
select.setSelected = testComponent.truthyOption.value;
fixture.detectChanges();

testComponent.falseOption.onClick();
fixture.detectChanges();

expect(select.selectionModel.length).toEqual(1);
}));

it('should not reset selection when selected option has zero value', fakeAsync(() => {
select.setSelected = testComponent.truthyOption.value;
fixture.detectChanges();

testComponent.zeroOption.onClick();
fixture.detectChanges();

expect(select.selectionModel.length).toEqual(1);
}));

it('should not reset selection when selected option has empty string value', fakeAsync(() => {
select.setSelected = testComponent.truthyOption.value;
fixture.detectChanges();

testComponent.emptyStringOption.onClick();
fixture.detectChanges();

expect(select.selectionModel.length).toEqual(1);
}));

it('should not reset selection when selected option has NaN value', fakeAsync(() => {
select.setSelected = testComponent.truthyOption.value;
fixture.detectChanges();

testComponent.nanOption.onClick();
fixture.detectChanges();

expect(select.selectionModel.length).toEqual(1);
}));

describe('multiple', () => {
beforeEach(fakeAsync(() => {
fixture = TestBed.createComponent(NbMultipleSelectWithFalsyOptionValuesComponent);
testComponent = fixture.componentInstance;
select = testComponent.select;

fixture.detectChanges();
flush();
select.show();
fixture.detectChanges();
}));

it('should not render checkbox on options with reset values', () => {
expect(testComponent.noValueOptionElement.nativeElement.querySelector('nb-checkbox')).toEqual(null);
expect(testComponent.nullOptionElement.nativeElement.querySelector('nb-checkbox')).toEqual(null);
expect(testComponent.undefinedOptionElement.nativeElement.querySelector('nb-checkbox')).toEqual(null);
});

it('should render checkbox on options with falsy non-reset values', () => {
expect(testComponent.falseOptionElement.nativeElement.querySelector('nb-checkbox')).not.toEqual(null);
expect(testComponent.zeroOptionElement.nativeElement.querySelector('nb-checkbox')).not.toEqual(null);
expect(testComponent.emptyStringOptionElement.nativeElement.querySelector('nb-checkbox')).not.toEqual(null);
expect(testComponent.nanOptionElement.nativeElement.querySelector('nb-checkbox')).not.toEqual(null);
expect(testComponent.truthyOptionElement.nativeElement.querySelector('nb-checkbox')).not.toEqual(null);
});
});
});

describe('NbOptionComponent', () => {
let fixture: ComponentFixture<NbReactiveFormSelectComponent>;
let testSelectComponent: NbReactiveFormSelectComponent;
Expand Down
13 changes: 8 additions & 5 deletions src/playground/with-layout/select/select-clean.component.html
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
<nb-select placeholder="Cleanable">
<nb-option>Clean</nb-option>
<nb-option value="1">Option 1</nb-option>
<nb-option value="2">Option 2</nb-option>
<nb-option value="3">Option 3</nb-option>
<nb-option value="4">Option 4</nb-option>
<nb-option>Clean with option without value</nb-option>
<nb-option [value]="null">Clean with null value</nb-option>
<nb-option [value]="undefined">Clean with null value</nb-option>
<nb-option [value]="0">Option 0</nb-option>
<nb-option [value]="1">Option 1</nb-option>
<nb-option [value]="2">Option 2</nb-option>
<nb-option [value]="3">Option 3</nb-option>
<nb-option [value]="4">Option 4</nb-option>
</nb-select>
3 changes: 1 addition & 2 deletions src/playground/with-layout/select/select-clean.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,4 @@ import { Component } from '@angular/core';
}
`],
})
export class SelectCleanComponent {
}
export class SelectCleanComponent {}
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
<nb-select placeholder="Select Showcase" [(selected)]="selectedItem">
<nb-option value="">Option empty</nb-option>
<nb-option value="0">Option 0</nb-option>
<nb-option value="1">Option 1</nb-option>
<nb-option value="2">Option 2</nb-option>
<nb-option value="3">Option 3</nb-option>
<nb-option value="4">Option 4</nb-option>
</nb-select>

0 comments on commit ca4a1ff

Please sign in to comment.