Skip to content

Commit

Permalink
feat(ng-option): support for disabled attribute (#280)
Browse files Browse the repository at this point in the history
closes #236
  • Loading branch information
varnastadeus committed Feb 22, 2018
1 parent 8a23b71 commit a5e2d16
Show file tree
Hide file tree
Showing 5 changed files with 101 additions and 28 deletions.
11 changes: 7 additions & 4 deletions demo/app/examples/data-source.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,12 +65,14 @@ import { Observable } from 'rxjs/Observable';
<p>
If you have simple use case, you can omit items array and bind options directly in html using <b>ng-option</b> component.
</p>
<button type="button" class="btn btn-secondary btn-sm" (click)="disable = !disable">Toggle disabled</button>
<hr/>
---html,true
<ng-select [searchable]="false" [(ngModel)]="staticValue">
<ng-option [value]="'Volvo'">Volvo</ng-option>
<ng-option [value]="'Saab'">Saab</ng-option>
<ng-option [value]="'Opel'">Opel</ng-option>
<ng-option [value]="'Audi'">Audi</ng-option>
<ng-option value="Volvo">Volvo</ng-option>
<ng-option [disabled]="disable" value="Saab">Saab</ng-option>
<ng-option value="Opel">Opel</ng-option>
<ng-option value="Audi">Audi</ng-option>
</ng-select>
---
<br />Selected: {{staticValue | json}}
Expand All @@ -84,6 +86,7 @@ export class DataSourceComponent {

selectedSimpleItem = 'Two';
simpleItems = [];
disable = true;

constructor(private dataService: DataService) { }

Expand Down
17 changes: 11 additions & 6 deletions src/ng-select/ng-option-highlight.directive.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import {Directive, ElementRef, Input, OnChanges, Renderer2} from '@angular/core';
import * as searchHelper from './search-helper';
import {
Directive,
ElementRef,
Input,
OnChanges,
Renderer2
} from '@angular/core';

@Directive({
selector: '[ngOptionHighlight]'
Expand All @@ -9,21 +15,20 @@ export class NgOptionHighlightDirective implements OnChanges {
@Input('ngOptionHighlight') term: string;
@Input('innerHTML') label: any;

constructor(private elementRef: ElementRef, private renderer: Renderer2) {
}
constructor(private elementRef: ElementRef, private renderer: Renderer2) { }

ngOnChanges(): void {
this._highlightLabelWithSearchTerm();
}

private _highlightLabelWithSearchTerm(): void {
let label: string = this.label ? this.label.toString() : '';
const label: string = this.label ? this.label.toString() : '';
if (!label || !this.term) {
this._setInnerHtml(label);
return;
}
let indexOfTerm: number;
indexOfTerm = searchHelper.stripSpecialChars(label)

const indexOfTerm = searchHelper.stripSpecialChars(label)
.toLowerCase()
.indexOf(searchHelper.stripSpecialChars(this.term).toLowerCase());
if (indexOfTerm > -1) {
Expand Down
34 changes: 31 additions & 3 deletions src/ng-select/ng-option.component.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,40 @@
import { Component, Input, ElementRef } from '@angular/core';
import {
ChangeDetectionStrategy,
Component,
ElementRef,
Input,
OnChanges,
SimpleChanges
} from '@angular/core';
import { Subject } from 'rxjs/Subject';

@Component({
selector: 'ng-option',
changeDetection: ChangeDetectionStrategy.OnPush,
template: `<ng-content></ng-content>`
})
export class NgOptionComponent {
export class NgOptionComponent implements OnChanges {

@Input() value: any;
@Input()
get disabled() { return this._disabled; }
set disabled(value: any) { this._disabled = this._isDisabled(value) }

readonly stateChange$ = new Subject<{ value: any, disabled: boolean }>();
private _disabled = false;

constructor(public elementRef: ElementRef) { }

ngOnChanges(changes: SimpleChanges) {
if (changes.disabled) {
this.stateChange$.next({
value: this.value,
disabled: this._disabled
});
}
}

constructor(public elementRef: ElementRef) {
private _isDisabled(value) {
return value != null && `${value}` !== 'false';
}
}
35 changes: 26 additions & 9 deletions src/ng-select/ng-select.component.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -293,7 +293,7 @@ describe('NgSelectComponent', function () {
selectOption(fixture, KeyCode.ArrowDown, 1);
tickAndDetectChanges(fixture);
expect(fixture.componentInstance.selectedCountry).toEqual('b');

fixture.componentInstance.selectedCountry = fixture.componentInstance.countries[2].description.id;
tickAndDetectChanges(fixture);
expect(fixture.componentInstance.select.selectedItems).toEqual([jasmine.objectContaining({
Expand All @@ -312,7 +312,7 @@ describe('NgSelectComponent', function () {
`<ng-select [items]="cities"
[(ngModel)]="selectedCity">
</ng-select>`);

selectOption(fixture, KeyCode.ArrowDown, 0);
tickAndDetectChanges(fixture);
expect(fixture.componentInstance.selectedCity).toBe('Vilnius');
Expand Down Expand Up @@ -378,7 +378,7 @@ describe('NgSelectComponent', function () {

fixture.componentInstance.cities = [{ id: 0, name: 'Vilnius' }];
fixture.componentInstance.selectedCity = 0;

tickAndDetectChanges(fixture);

const result = [jasmine.objectContaining({
Expand Down Expand Up @@ -736,7 +736,7 @@ describe('NgSelectComponent', function () {
it('should be set to `bottom` by default', () => {
const fixture = createTestingModule(
NgSelectBasicTestCmp,
`<ng-select id="select"></ng-select>`);
`<ng-select id="select"></ng-select>`);

const classes = fixture.debugElement.query(By.css('ng-select')).classes;
expect(classes.bottom).toBeTruthy();
Expand All @@ -746,7 +746,7 @@ describe('NgSelectComponent', function () {
it('should allow changing dropdown position', () => {
const fixture = createTestingModule(
NgSelectBasicTestCmp,
`<ng-select id="select" [dropdownPosition]="dropdownPosition"></ng-select>`);
`<ng-select id="select" [dropdownPosition]="dropdownPosition"></ng-select>`);

fixture.componentInstance.dropdownPosition = 'top';
fixture.detectChanges();
Expand Down Expand Up @@ -867,12 +867,28 @@ describe('NgSelectComponent', function () {
const items = fixture.componentInstance.select.itemsList.items;
expect(items.length).toBe(2);
expect(items[0]).toEqual(jasmine.objectContaining({
value: { label: 'Yes', value: true }
value: { label: 'Yes', value: true, disabled: false }
}));
expect(items[1]).toEqual(jasmine.objectContaining({
value: { label: 'No', value: false }
value: { label: 'No', value: false, disabled: false }
}));
}));

it('should update ng-option state', fakeAsync(() => {
const fixture = createTestingModule(
NgSelectBasicTestCmp,
`<ng-select [(ngModel)]="selectedCity">
<ng-option [disabled]="disabled" [value]="true">Yes</ng-option>
<ng-option [value]="false">No</ng-option>
</ng-select>`);

tickAndDetectChanges(fixture);
const items = fixture.componentInstance.select.itemsList.items;
expect(items[0].disabled).toBeFalsy();
fixture.componentInstance.disabled = true;
tickAndDetectChanges(fixture);
expect(items[0].disabled).toBeTruthy();
}));
});

describe('Multiple', () => {
Expand Down Expand Up @@ -945,7 +961,7 @@ describe('NgSelectComponent', function () {
expect((<NgOption[]>fixture.componentInstance.select.selectedItems).length).toBe(2);
}));

it('should click on arrow be disabled when maximum of items is reached', fakeAsync (() => {
it('should click on arrow be disabled when maximum of items is reached', fakeAsync(() => {
const clickArrow = () => arrowIcon.triggerEventHandler('click', { stopPropagation: () => { } });
selectOption(fixture, KeyCode.ArrowDown, 0);
selectOption(fixture, KeyCode.ArrowDown, 1);
Expand Down Expand Up @@ -1542,6 +1558,7 @@ class NgSelectBasicTestCmp {
@ViewChild(NgSelectComponent) select: NgSelectComponent;
selectedCity: { id: number; name: string };
multiple = false;
disabled = false;
dropdownPosition = 'bottom';
citiesLoading = false;
cities = [
Expand All @@ -1554,7 +1571,7 @@ class NgSelectBasicTestCmp {
}
tagFuncPromise(term) {
return Promise.resolve({
id: 5, name: term, valid: true
id: 5, name: term, valid: true
});
};
}
Expand Down
32 changes: 26 additions & 6 deletions src/ng-select/ng-select.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ import { VirtualScrollComponent } from './virtual-scroll.component';
import { NgOption, KeyCode, NgSelectConfig } from './ng-select.types';
import { ItemsList } from './items-list';
import { Subject } from 'rxjs/Subject';
import { merge } from 'rxjs/observable/merge';
import { takeUntil, startWith } from 'rxjs/operators';
import { NgOptionComponent } from './ng-option.component';

export const NG_SELECT_DEFAULT_CONFIG = new InjectionToken<NgSelectConfig>('ng-select-default-options');
Expand Down Expand Up @@ -122,6 +124,7 @@ export class NgSelectComponent implements OnInit, OnDestroy, OnChanges, AfterVie
private _defaultValue = 'value';
private _typeaheadLoading = false;

private readonly _destroy$ = new Subject<void>();
private _onChange = (_: NgOption) => { };
private _onTouched = () => { };
private _disposeDocumentClickListener = () => { };
Expand Down Expand Up @@ -179,12 +182,13 @@ export class NgSelectComponent implements OnInit, OnDestroy, OnChanges, AfterVie
}

ngOnDestroy() {
this.changeDetectorRef.detach();
this._disposeDocumentClickListener();
this._disposeDocumentResizeListener();
if (this.appendTo) {
this.elementRef.nativeElement.appendChild(this.dropdownPanel.nativeElement);
}
this._destroy$.next();
this._destroy$.complete();
}

@HostListener('keydown', ['$event'])
Expand Down Expand Up @@ -437,22 +441,38 @@ export class NgSelectComponent implements OnInit, OnDestroy, OnChanges, AfterVie
private _setItemsFromNgOptions() {
this.bindLabel = this.bindLabel || this._defaultLabel;
this.bindValue = this.bindValue || this._defaultValue;

const handleNgOptions = (options: QueryList<NgOptionComponent>) => {
this.items = options.map(option => ({
value: option.value,
label: option.elementRef.nativeElement.innerHTML
label: option.elementRef.nativeElement.innerHTML,
disabled: option.disabled
}));
this.itemsList.setItems(this.items, false);

if (this._isDefined(this._ngModel)) {
this.itemsList.clearSelected();
this._selectWriteValue(this._ngModel);
}
this.detectChanges();
};
}

this.ngOptions.changes.subscribe(options => handleNgOptions(options));
handleNgOptions(this.ngOptions);
const handleOptionChange = () => {
const changedOrDestroyed = merge(this.ngOptions.changes, this._destroy$);
merge(...this.ngOptions.map(option => option.stateChange$))
.pipe(takeUntil(changedOrDestroyed))
.subscribe(option => {
const item = this.itemsList.findItem(option.value);
item.disabled = option.disabled;
this.changeDetectorRef.markForCheck();
});
}

this.ngOptions.changes
.pipe(startWith(this.ngOptions), takeUntil(this._destroy$))
.subscribe(options => {
handleNgOptions(options);
handleOptionChange();
});
}

private _handleDocumentClick() {
Expand Down

0 comments on commit a5e2d16

Please sign in to comment.