Skip to content

Commit

Permalink
fix(checkbox, input, radio, slide-toggle): implement setDisabledState…
Browse files Browse the repository at this point in the history
… from ControlValueAccessor (#1750)

Implements the `setDisabledState` method from the `ControlValueAccessor` interface in all of the input-related components, in order to support disabling via reactive forms. Note that the `select` component is missing the implementation, however there's a pending PR for it already (#1667).

Fixes #1171.
  • Loading branch information
crisbeto authored and tinayuangao committed Nov 29, 2016
1 parent f1b9b2c commit 77a960c
Show file tree
Hide file tree
Showing 8 changed files with 207 additions and 30 deletions.
57 changes: 49 additions & 8 deletions src/lib/checkbox/checkbox.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import {
import {
NgControl,
FormsModule,
ReactiveFormsModule,
FormControl,
} from '@angular/forms';
import {Component, DebugElement} from '@angular/core';
import {By} from '@angular/platform-browser';
Expand All @@ -21,7 +23,7 @@ describe('MdCheckbox', () => {

beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [MdCheckboxModule.forRoot(), FormsModule],
imports: [MdCheckboxModule.forRoot(), FormsModule, ReactiveFormsModule],
declarations: [
SingleCheckbox,
CheckboxWithFormDirectives,
Expand All @@ -31,6 +33,7 @@ describe('MdCheckbox', () => {
CheckboxWithAriaLabelledby,
CheckboxWithNameAttribute,
CheckboxWithChangeEvent,
CheckboxWithFormControl,
],
});

Expand Down Expand Up @@ -561,18 +564,48 @@ describe('MdCheckbox', () => {
expect(inputElement.getAttribute('name')).toBe('test-name');
});
});


describe('with form control', () => {
let checkboxDebugElement: DebugElement;
let checkboxInstance: MdCheckbox;
let testComponent: CheckboxWithFormControl;

beforeEach(() => {
fixture = TestBed.createComponent(CheckboxWithFormControl);
fixture.detectChanges();

checkboxDebugElement = fixture.debugElement.query(By.directive(MdCheckbox));
checkboxInstance = checkboxDebugElement.componentInstance;
testComponent = fixture.debugElement.componentInstance;
});

it('should toggle the disabled state', () => {
expect(checkboxInstance.disabled).toBe(false);

testComponent.formControl.disable();
fixture.detectChanges();

expect(checkboxInstance.disabled).toBe(true);

testComponent.formControl.enable();
fixture.detectChanges();

expect(checkboxInstance.disabled).toBe(false);
});
});
});

/** Simple component for testing a single checkbox. */
@Component({
template: `
<div (click)="parentElementClicked = true" (keyup)="parentElementKeyedUp = true">
<md-checkbox
<div (click)="parentElementClicked = true" (keyup)="parentElementKeyedUp = true">
<md-checkbox
id="simple-check"
[required]="isRequired"
[align]="alignment"
[checked]="isChecked"
[indeterminate]="isIndeterminate"
[checked]="isChecked"
[indeterminate]="isIndeterminate"
[disabled]="isDisabled"
[color]="checkboxColor"
(change)="changeCount = changeCount + 1"
Expand Down Expand Up @@ -623,9 +656,9 @@ class MultipleCheckboxes { }
/** Simple test component with tabIndex */
@Component({
template: `
<md-checkbox
[tabindex]="customTabIndex"
[disabled]="isDisabled"
<md-checkbox
[tabindex]="customTabIndex"
[disabled]="isDisabled"
[disableRipple]="disableRipple">
</md-checkbox>`,
})
Expand Down Expand Up @@ -660,3 +693,11 @@ class CheckboxWithNameAttribute {}
class CheckboxWithChangeEvent {
lastEvent: MdCheckboxChange;
}

/** Test component with reactive forms */
@Component({
template: `<md-checkbox [formControl]="formControl"></md-checkbox>`
})
class CheckboxWithFormControl {
formControl = new FormControl();
}
9 changes: 8 additions & 1 deletion src/lib/checkbox/checkbox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ export class MdCheckbox implements ControlValueAccessor {
/** Whether or not the checkbox should come before or after the label. */
@Input() align: 'start' | 'end' = 'start';

private _disabled: boolean;
private _disabled: boolean = false;

/**
* Whether the checkbox is disabled. When the checkbox is disabled it cannot be interacted with.
Expand Down Expand Up @@ -245,6 +245,13 @@ export class MdCheckbox implements ControlValueAccessor {
this.onTouched = fn;
}

/**
* Implemented as a part of ControlValueAccessor.
*/
setDisabledState(isDisabled: boolean) {
this.disabled = isDisabled;
}

private _transitionCheckState(newState: TransitionCheckState) {
let oldState = this._currentCheckState;
let renderer = this._renderer;
Expand Down
31 changes: 29 additions & 2 deletions src/lib/input/input.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import {
TestBed,
} from '@angular/core/testing';
import {Component} from '@angular/core';
import {FormsModule} from '@angular/forms';
import {FormsModule, ReactiveFormsModule, FormControl} from '@angular/forms';
import {By} from '@angular/platform-browser';
import {MdInput, MdInputModule} from './input';

Expand All @@ -14,7 +14,7 @@ function isInternetExplorer11() {
describe('MdInput', function () {
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [MdInputModule.forRoot(), FormsModule],
imports: [MdInputModule.forRoot(), FormsModule, ReactiveFormsModule],
declarations: [
MdInputNumberTypeConservedTestComponent,
MdInputPlaceholderRequiredTestComponent,
Expand Down Expand Up @@ -58,6 +58,7 @@ describe('MdInput', function () {
MdInputPasswordTestController,
MdInputNumberTestController,
MdTextareaWithBindings,
MdInputWithFormControl,
],
});

Expand Down Expand Up @@ -621,6 +622,27 @@ describe('MdInput', function () {
expect(inputElement.name).toBe('some-name');
});

it('toggles the disabled state when used with a FormControl', () => {
let fixture = TestBed.createComponent(MdInputWithFormControl);

fixture.detectChanges();

let input: MdInput = fixture.debugElement.query(By.directive(MdInput)).componentInstance;
let testComponent: MdInputWithFormControl = fixture.debugElement.componentInstance;

expect(input.disabled).toBe(false);

testComponent.formControl.disable();
fixture.detectChanges();

expect(input.disabled).toBe(true);

testComponent.formControl.enable();
fixture.detectChanges();

expect(input.disabled).toBe(false);
});

describe('md-textarea', () => {
it('supports the rows, cols, and wrap attributes', () => {
let fixture = TestBed.createComponent(MdTextareaWithBindings);
Expand Down Expand Up @@ -807,6 +829,11 @@ class MdInputNumberTestController {
placeholder: string = '';
}

@Component({template: `<md-input [formControl]="formControl"></md-input>`})
class MdInputWithFormControl {
formControl = new FormControl();
}

@Component({template:
`<md-textarea [rows]="rows" [cols]="cols" [wrap]="wrap" placeholder="Snacks"></md-textarea>`})
class MdTextareaWithBindings {
Expand Down
7 changes: 7 additions & 0 deletions src/lib/input/input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,13 @@ export class MdInput implements ControlValueAccessor, AfterContentInit, OnChange
this._onTouchedCallback = fn;
}

/**
* Implemented as a part of ControlValueAccessor.
*/
setDisabledState(isDisabled: boolean) {
this.disabled = isDisabled;
}

/** TODO: internal */
ngAfterContentInit() {
this._validateConstraints();
Expand Down
54 changes: 48 additions & 6 deletions src/lib/radio/radio.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {async, ComponentFixture, TestBed, fakeAsync, tick} from '@angular/core/testing';
import {NgControl, FormsModule} from '@angular/forms';
import {NgControl, FormsModule, ReactiveFormsModule, FormControl} from '@angular/forms';
import {Component, DebugElement} from '@angular/core';
import {By} from '@angular/platform-browser';
import {MdRadioGroup, MdRadioButton, MdRadioChange, MdRadioModule} from './radio';
Expand All @@ -9,10 +9,11 @@ describe('MdRadio', () => {

beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [MdRadioModule.forRoot(), FormsModule],
imports: [MdRadioModule.forRoot(), FormsModule, ReactiveFormsModule],
declarations: [
RadiosInsideRadioGroup,
RadioGroupWithNgModel,
RadioGroupWithFormControl,
StandaloneRadioButtons,
],
});
Expand Down Expand Up @@ -152,7 +153,7 @@ describe('MdRadio', () => {
expect(spies[1]).toHaveBeenCalledTimes(1);
});

it(`should not emit a change event from the radio group when change group value
it(`should not emit a change event from the radio group when change group value
programmatically`, () => {
expect(groupInstance.value).toBeFalsy();

Expand Down Expand Up @@ -246,7 +247,7 @@ describe('MdRadio', () => {
}
}));

it(`should update the group's selected radio to null when unchecking that radio
it(`should update the group's selected radio to null when unchecking that radio
programmatically`, () => {
let changeSpy = jasmine.createSpy('radio-group change listener');
groupInstance.change.subscribe(changeSpy);
Expand Down Expand Up @@ -420,6 +421,36 @@ describe('MdRadio', () => {
});
});

describe('group with FormControl', () => {
let fixture: ComponentFixture<RadioGroupWithFormControl>;
let groupDebugElement: DebugElement;
let groupInstance: MdRadioGroup;
let testComponent: RadioGroupWithFormControl;

beforeEach(() => {
fixture = TestBed.createComponent(RadioGroupWithFormControl);
fixture.detectChanges();

testComponent = fixture.debugElement.componentInstance;
groupDebugElement = fixture.debugElement.query(By.directive(MdRadioGroup));
groupInstance = groupDebugElement.injector.get(MdRadioGroup);
});

it('should toggle the disabled state', () => {
expect(groupInstance.disabled).toBeFalsy();

testComponent.formControl.disable();
fixture.detectChanges();

expect(groupInstance.disabled).toBeTruthy();

testComponent.formControl.enable();
fixture.detectChanges();

expect(groupInstance.disabled).toBeFalsy();
});
});

describe('as standalone', () => {
let fixture: ComponentFixture<StandaloneRadioButtons>;
let radioDebugElements: DebugElement[];
Expand Down Expand Up @@ -548,11 +579,11 @@ class RadiosInsideRadioGroup {
<md-radio-button name="season" value="spring">Spring</md-radio-button>
<md-radio-button name="season" value="summer">Summer</md-radio-button>
<md-radio-button name="season" value="autum">Autumn</md-radio-button>
<md-radio-button name="weather" value="warm">Spring</md-radio-button>
<md-radio-button name="weather" value="hot">Summer</md-radio-button>
<md-radio-button name="weather" value="cool">Autumn</md-radio-button>
<span id="xyz">Baby Banana</span>
<md-radio-button name="fruit" value="banana" aria-label="Banana" aria-labelledby="xyz">
</md-radio-button>
Expand Down Expand Up @@ -581,6 +612,17 @@ class RadioGroupWithNgModel {
lastEvent: MdRadioChange;
}

@Component({
template: `
<md-radio-group [formControl]="formControl">
<md-radio-button value="1">One</md-radio-button>
</md-radio-group>
`
})
class RadioGroupWithFormControl {
formControl = new FormControl();
}

// TODO(jelbourn): remove everything below when Angular supports faking events.

/**
Expand Down
7 changes: 7 additions & 0 deletions src/lib/radio/radio.ts
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,13 @@ export class MdRadioGroup implements AfterContentInit, ControlValueAccessor {
registerOnTouched(fn: any) {
this.onTouched = fn;
}

/**
* Implemented as a part of ControlValueAccessor.
*/
setDisabledState(isDisabled: boolean) {
this.disabled = isDisabled;
}
}


Expand Down
Loading

0 comments on commit 77a960c

Please sign in to comment.