Skip to content

Commit

Permalink
fix(checkbox, slide-toggle): forward required attribute to input. (#1137
Browse files Browse the repository at this point in the history
)

* fix(checkbox, slide-toggle): forward required attribute to input.

* Now forwards the required attribute to the input.
* This allows us to take advantage of the native browser behavior to prevent a form submission.

Fixes #1133,

* Fix linters

* Fix Browserstack test for super old Safari browser.

* Safari 8 does not report input validity in forms, so the tests fail for it..

* No longer use deep import of core
  • Loading branch information
devversion authored and kara committed Sep 23, 2016
1 parent a732e88 commit 0e9383a
Show file tree
Hide file tree
Showing 10 changed files with 147 additions and 6 deletions.
15 changes: 15 additions & 0 deletions src/demo-app/slide-toggle/slide-toggle-demo.html
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,19 @@
<md-slide-toggle [disabled]="firstToggle">
Disable Bound
</md-slide-toggle>

<p>Example where the slide toggle is required inside of a form.</p>

<form #form="ngForm" (ngSubmit)="onFormSubmit()">

<md-slide-toggle name="slideToggle" required ngModel>
Slide Toggle
</md-slide-toggle>

<p>
<button md-raised-button type="submit">Submit Form</button>
</p>

</form>

</div>
5 changes: 5 additions & 0 deletions src/demo-app/slide-toggle/slide-toggle-demo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,9 @@ import {Component} from '@angular/core';
})
export class SlideToggleDemo {
firstToggle: boolean;

onFormSubmit() {
alert(`You submitted the form.`);
}

}
1 change: 1 addition & 0 deletions src/lib/checkbox/checkbox.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
<div class="md-checkbox-inner-container">
<input class="md-checkbox-input md-visually-hidden" type="checkbox"
[id]="inputId"
[required]="required"
[checked]="checked"
[disabled]="disabled"
[name]="name"
Expand Down
7 changes: 7 additions & 0 deletions src/lib/checkbox/checkbox.scss
Original file line number Diff line number Diff line change
Expand Up @@ -402,4 +402,11 @@ md-checkbox {
}
}

.md-checkbox-input {
// Move the input to the bottom and in the middle.
// Visual improvement to properly show browser popups when being required.
bottom: 0;
left: 50%;
}

@include md-temporary-ink-ripple(checkbox);
14 changes: 14 additions & 0 deletions src/lib/checkbox/checkbox.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,18 @@ describe('MdCheckbox', () => {

}));

it('should forward the required attribute', () => {
testComponent.isRequired = true;
fixture.detectChanges();

expect(inputElement.required).toBe(true);

testComponent.isRequired = false;
fixture.detectChanges();

expect(inputElement.required).toBe(false);
});

describe('state transition css classes', () => {
it('should transition unchecked -> checked -> unchecked', () => {
testComponent.isChecked = true;
Expand Down Expand Up @@ -502,6 +514,7 @@ describe('MdCheckbox', () => {
<div (click)="parentElementClicked = true" (keyup)="parentElementKeyedUp = true">
<md-checkbox
id="simple-check"
[required]="isRequired"
[align]="alignment"
[checked]="isChecked"
[indeterminate]="isIndeterminate"
Expand All @@ -516,6 +529,7 @@ describe('MdCheckbox', () => {
class SingleCheckbox {
alignment: string = 'start';
isChecked: boolean = false;
isRequired: boolean = false;
isIndeterminate: boolean = false;
isDisabled: boolean = false;
parentElementClicked: boolean = false;
Expand Down
4 changes: 4 additions & 0 deletions src/lib/checkbox/checkbox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
ModuleWithProviders,
} from '@angular/core';
import {NG_VALUE_ACCESSOR, ControlValueAccessor} from '@angular/forms';
import {BooleanFieldValue} from '@angular2-material/core';

/**
* Monotonically increasing integer used to auto-generate unique ids for checkbox components.
Expand Down Expand Up @@ -92,6 +93,9 @@ export class MdCheckbox implements ControlValueAccessor {
return `input-${this.id}`;
}

/** Whether the checkbox is required or not. */
@Input() @BooleanFieldValue() required: boolean = false;

/** Whether or not the checkbox should come before or after the label. */
@Input() align: 'start' | 'end' = 'start';

Expand Down
3 changes: 2 additions & 1 deletion src/lib/slide-toggle/slide-toggle.html
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,9 @@
</div>
</div>

<input #input class="md-slide-toggle-checkbox md-visually-hidden" type="checkbox"
<input #input class="md-slide-toggle-input md-visually-hidden" type="checkbox"
[id]="getInputId()"
[required]="required"
[tabIndex]="tabIndex"
[checked]="checked"
[disabled]="disabled"
Expand Down
9 changes: 9 additions & 0 deletions src/lib/slide-toggle/slide-toggle.scss
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,15 @@ $md-slide-toggle-margin: 16px !default;
border-radius: 8px;
}

// The slide toggle shows a visually hidden input inside of the component, which is used
// to take advantage of the native browser functionality.
.md-slide-toggle-input {
// Move the input to the bottom and in the middle of the thumb.
// Visual improvement to properly show browser popups when being required.
bottom: 0;
left: $md-slide-toggle-thumb-size / 2;
}

.md-slide-toggle-bar,
.md-slide-toggle-thumb {
transition: $swift-linear;
Expand Down
94 changes: 89 additions & 5 deletions src/lib/slide-toggle/slide-toggle.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ describe('MdSlideToggle', () => {
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [MdSlideToggleModule.forRoot(), FormsModule],
declarations: [SlideToggleTestApp],
declarations: [SlideToggleTestApp, SlideToggleFormsTestApp],
});

TestBed.compileComponents();
Expand Down Expand Up @@ -318,6 +318,18 @@ describe('MdSlideToggle', () => {
expect(slideToggleElement.classList).toContain('md-slide-toggle-focused');
});

it('should forward the required attribute', () => {
testComponent.isRequired = true;
fixture.detectChanges();

expect(inputElement.required).toBe(true);

testComponent.isRequired = false;
fixture.detectChanges();

expect(inputElement.required).toBe(false);
});

});

describe('custom template', () => {
Expand All @@ -331,6 +343,55 @@ describe('MdSlideToggle', () => {
}));
});

describe('with forms', () => {

let fixture: ComponentFixture<any>;
let testComponent: SlideToggleFormsTestApp;
let buttonElement: HTMLButtonElement;
let labelElement: HTMLLabelElement;
let inputElement: HTMLInputElement;

// This initialization is async() because it needs to wait for ngModel to set the initial value.
beforeEach(async(() => {
fixture = TestBed.createComponent(SlideToggleFormsTestApp);

testComponent = fixture.debugElement.componentInstance;

fixture.detectChanges();

buttonElement = fixture.debugElement.query(By.css('button')).nativeElement;
labelElement = fixture.debugElement.query(By.css('label')).nativeElement;
inputElement = fixture.debugElement.query(By.css('input')).nativeElement;
}));

it('should prevent the form from submit when being required', () => {

if ('reportValidity' in inputElement === false) {
// If the browser does not report the validity then the tests will break.
// e.g Safari 8 on Mobile.
return;
}

testComponent.isRequired = true;

fixture.detectChanges();

buttonElement.click();
fixture.detectChanges();

expect(testComponent.isSubmitted).toBe(false);

testComponent.isRequired = false;
fixture.detectChanges();

buttonElement.click();
fixture.detectChanges();

expect(testComponent.isSubmitted).toBe(true);
});

});

});

/**
Expand All @@ -347,16 +408,25 @@ function dispatchFocusChangeEvent(eventName: string, element: HTMLElement): void
@Component({
selector: 'slide-toggle-test-app',
template: `
<md-slide-toggle [(ngModel)]="slideModel" [disabled]="isDisabled" [color]="slideColor"
[id]="slideId" [checked]="slideChecked" [name]="slideName"
[ariaLabel]="slideLabel" [ariaLabelledby]="slideLabelledBy"
(change)="onSlideChange($event)"
<md-slide-toggle [(ngModel)]="slideModel"
[required]="isRequired"
[disabled]="isDisabled"
[color]="slideColor"
[id]="slideId"
[checked]="slideChecked"
[name]="slideName"
[ariaLabel]="slideLabel"
[ariaLabelledby]="slideLabelledBy"
(change)="onSlideChange($event)"
(click)="onSlideClick($event)">
<span>Test Slide Toggle</span>
</md-slide-toggle>`,
})
class SlideToggleTestApp {
isDisabled: boolean = false;
isRequired: boolean = false;
slideModel: boolean = false;
slideChecked: boolean = false;
slideColor: string;
Expand All @@ -371,3 +441,17 @@ class SlideToggleTestApp {
this.lastEvent = event;
}
}


@Component({
selector: 'slide-toggle-forms-test-app',
template: `
<form (ngSubmit)="isSubmitted = true">
<md-slide-toggle name="slide" ngModel [required]="isRequired">Required</md-slide-toggle>
<button type="submit"></button>
</form>`
})
class SlideToggleFormsTestApp {
isSubmitted: boolean = false;
isRequired: boolean = false;
}
1 change: 1 addition & 0 deletions src/lib/slide-toggle/slide-toggle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ export class MdSlideToggle implements AfterContentInit, ControlValueAccessor {
private _slideRenderer: SlideToggleRenderer = null;

@Input() @BooleanFieldValue() disabled: boolean = false;
@Input() @BooleanFieldValue() required: boolean = false;
@Input() name: string = null;
@Input() id: string = this._uniqueId;
@Input() tabIndex: number = 0;
Expand Down

0 comments on commit 0e9383a

Please sign in to comment.