Skip to content

Commit 0e9383a

Browse files
devversionkara
authored andcommitted
fix(checkbox, slide-toggle): forward required attribute to input. (#1137)
* 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
1 parent a732e88 commit 0e9383a

File tree

10 files changed

+147
-6
lines changed

10 files changed

+147
-6
lines changed

src/demo-app/slide-toggle/slide-toggle-demo.html

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,19 @@
1111
<md-slide-toggle [disabled]="firstToggle">
1212
Disable Bound
1313
</md-slide-toggle>
14+
15+
<p>Example where the slide toggle is required inside of a form.</p>
16+
17+
<form #form="ngForm" (ngSubmit)="onFormSubmit()">
18+
19+
<md-slide-toggle name="slideToggle" required ngModel>
20+
Slide Toggle
21+
</md-slide-toggle>
22+
23+
<p>
24+
<button md-raised-button type="submit">Submit Form</button>
25+
</p>
26+
27+
</form>
28+
1429
</div>

src/demo-app/slide-toggle/slide-toggle-demo.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,9 @@ import {Component} from '@angular/core';
99
})
1010
export class SlideToggleDemo {
1111
firstToggle: boolean;
12+
13+
onFormSubmit() {
14+
alert(`You submitted the form.`);
15+
}
16+
1217
}

src/lib/checkbox/checkbox.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
<div class="md-checkbox-inner-container">
33
<input class="md-checkbox-input md-visually-hidden" type="checkbox"
44
[id]="inputId"
5+
[required]="required"
56
[checked]="checked"
67
[disabled]="disabled"
78
[name]="name"

src/lib/checkbox/checkbox.scss

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -402,4 +402,11 @@ md-checkbox {
402402
}
403403
}
404404

405+
.md-checkbox-input {
406+
// Move the input to the bottom and in the middle.
407+
// Visual improvement to properly show browser popups when being required.
408+
bottom: 0;
409+
left: 50%;
410+
}
411+
405412
@include md-temporary-ink-ripple(checkbox);

src/lib/checkbox/checkbox.spec.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,18 @@ describe('MdCheckbox', () => {
255255

256256
}));
257257

258+
it('should forward the required attribute', () => {
259+
testComponent.isRequired = true;
260+
fixture.detectChanges();
261+
262+
expect(inputElement.required).toBe(true);
263+
264+
testComponent.isRequired = false;
265+
fixture.detectChanges();
266+
267+
expect(inputElement.required).toBe(false);
268+
});
269+
258270
describe('state transition css classes', () => {
259271
it('should transition unchecked -> checked -> unchecked', () => {
260272
testComponent.isChecked = true;
@@ -502,6 +514,7 @@ describe('MdCheckbox', () => {
502514
<div (click)="parentElementClicked = true" (keyup)="parentElementKeyedUp = true">
503515
<md-checkbox
504516
id="simple-check"
517+
[required]="isRequired"
505518
[align]="alignment"
506519
[checked]="isChecked"
507520
[indeterminate]="isIndeterminate"
@@ -516,6 +529,7 @@ describe('MdCheckbox', () => {
516529
class SingleCheckbox {
517530
alignment: string = 'start';
518531
isChecked: boolean = false;
532+
isRequired: boolean = false;
519533
isIndeterminate: boolean = false;
520534
isDisabled: boolean = false;
521535
parentElementClicked: boolean = false;

src/lib/checkbox/checkbox.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
ModuleWithProviders,
1313
} from '@angular/core';
1414
import {NG_VALUE_ACCESSOR, ControlValueAccessor} from '@angular/forms';
15+
import {BooleanFieldValue} from '@angular2-material/core';
1516

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

96+
/** Whether the checkbox is required or not. */
97+
@Input() @BooleanFieldValue() required: boolean = false;
98+
9599
/** Whether or not the checkbox should come before or after the label. */
96100
@Input() align: 'start' | 'end' = 'start';
97101

src/lib/slide-toggle/slide-toggle.html

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,9 @@
1313
</div>
1414
</div>
1515

16-
<input #input class="md-slide-toggle-checkbox md-visually-hidden" type="checkbox"
16+
<input #input class="md-slide-toggle-input md-visually-hidden" type="checkbox"
1717
[id]="getInputId()"
18+
[required]="required"
1819
[tabIndex]="tabIndex"
1920
[checked]="checked"
2021
[disabled]="disabled"

src/lib/slide-toggle/slide-toggle.scss

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,15 @@ $md-slide-toggle-margin: 16px !default;
123123
border-radius: 8px;
124124
}
125125

126+
// The slide toggle shows a visually hidden input inside of the component, which is used
127+
// to take advantage of the native browser functionality.
128+
.md-slide-toggle-input {
129+
// Move the input to the bottom and in the middle of the thumb.
130+
// Visual improvement to properly show browser popups when being required.
131+
bottom: 0;
132+
left: $md-slide-toggle-thumb-size / 2;
133+
}
134+
126135
.md-slide-toggle-bar,
127136
.md-slide-toggle-thumb {
128137
transition: $swift-linear;

src/lib/slide-toggle/slide-toggle.spec.ts

Lines changed: 89 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ describe('MdSlideToggle', () => {
99
beforeEach(async(() => {
1010
TestBed.configureTestingModule({
1111
imports: [MdSlideToggleModule.forRoot(), FormsModule],
12-
declarations: [SlideToggleTestApp],
12+
declarations: [SlideToggleTestApp, SlideToggleFormsTestApp],
1313
});
1414

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

321+
it('should forward the required attribute', () => {
322+
testComponent.isRequired = true;
323+
fixture.detectChanges();
324+
325+
expect(inputElement.required).toBe(true);
326+
327+
testComponent.isRequired = false;
328+
fixture.detectChanges();
329+
330+
expect(inputElement.required).toBe(false);
331+
});
332+
321333
});
322334

323335
describe('custom template', () => {
@@ -331,6 +343,55 @@ describe('MdSlideToggle', () => {
331343
}));
332344
});
333345

346+
describe('with forms', () => {
347+
348+
let fixture: ComponentFixture<any>;
349+
let testComponent: SlideToggleFormsTestApp;
350+
let buttonElement: HTMLButtonElement;
351+
let labelElement: HTMLLabelElement;
352+
let inputElement: HTMLInputElement;
353+
354+
// This initialization is async() because it needs to wait for ngModel to set the initial value.
355+
beforeEach(async(() => {
356+
fixture = TestBed.createComponent(SlideToggleFormsTestApp);
357+
358+
testComponent = fixture.debugElement.componentInstance;
359+
360+
fixture.detectChanges();
361+
362+
buttonElement = fixture.debugElement.query(By.css('button')).nativeElement;
363+
labelElement = fixture.debugElement.query(By.css('label')).nativeElement;
364+
inputElement = fixture.debugElement.query(By.css('input')).nativeElement;
365+
}));
366+
367+
it('should prevent the form from submit when being required', () => {
368+
369+
if ('reportValidity' in inputElement === false) {
370+
// If the browser does not report the validity then the tests will break.
371+
// e.g Safari 8 on Mobile.
372+
return;
373+
}
374+
375+
testComponent.isRequired = true;
376+
377+
fixture.detectChanges();
378+
379+
buttonElement.click();
380+
fixture.detectChanges();
381+
382+
expect(testComponent.isSubmitted).toBe(false);
383+
384+
testComponent.isRequired = false;
385+
fixture.detectChanges();
386+
387+
buttonElement.click();
388+
fixture.detectChanges();
389+
390+
expect(testComponent.isSubmitted).toBe(true);
391+
});
392+
393+
});
394+
334395
});
335396

336397
/**
@@ -347,16 +408,25 @@ function dispatchFocusChangeEvent(eventName: string, element: HTMLElement): void
347408
@Component({
348409
selector: 'slide-toggle-test-app',
349410
template: `
350-
<md-slide-toggle [(ngModel)]="slideModel" [disabled]="isDisabled" [color]="slideColor"
351-
[id]="slideId" [checked]="slideChecked" [name]="slideName"
352-
[ariaLabel]="slideLabel" [ariaLabelledby]="slideLabelledBy"
353-
(change)="onSlideChange($event)"
411+
<md-slide-toggle [(ngModel)]="slideModel"
412+
[required]="isRequired"
413+
[disabled]="isDisabled"
414+
[color]="slideColor"
415+
[id]="slideId"
416+
[checked]="slideChecked"
417+
[name]="slideName"
418+
[ariaLabel]="slideLabel"
419+
[ariaLabelledby]="slideLabelledBy"
420+
(change)="onSlideChange($event)"
354421
(click)="onSlideClick($event)">
422+
355423
<span>Test Slide Toggle</span>
424+
356425
</md-slide-toggle>`,
357426
})
358427
class SlideToggleTestApp {
359428
isDisabled: boolean = false;
429+
isRequired: boolean = false;
360430
slideModel: boolean = false;
361431
slideChecked: boolean = false;
362432
slideColor: string;
@@ -371,3 +441,17 @@ class SlideToggleTestApp {
371441
this.lastEvent = event;
372442
}
373443
}
444+
445+
446+
@Component({
447+
selector: 'slide-toggle-forms-test-app',
448+
template: `
449+
<form (ngSubmit)="isSubmitted = true">
450+
<md-slide-toggle name="slide" ngModel [required]="isRequired">Required</md-slide-toggle>
451+
<button type="submit"></button>
452+
</form>`
453+
})
454+
class SlideToggleFormsTestApp {
455+
isSubmitted: boolean = false;
456+
isRequired: boolean = false;
457+
}

src/lib/slide-toggle/slide-toggle.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ export class MdSlideToggle implements AfterContentInit, ControlValueAccessor {
6666
private _slideRenderer: SlideToggleRenderer = null;
6767

6868
@Input() @BooleanFieldValue() disabled: boolean = false;
69+
@Input() @BooleanFieldValue() required: boolean = false;
6970
@Input() name: string = null;
7071
@Input() id: string = this._uniqueId;
7172
@Input() tabIndex: number = 0;

0 commit comments

Comments
 (0)