Skip to content

Commit c06604f

Browse files
committed
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,
1 parent 9ff6196 commit c06604f

File tree

10 files changed

+130
-6
lines changed

10 files changed

+130
-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: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,10 @@ import {Component} from '@angular/core';
77
templateUrl: 'slide-toggle-demo.html',
88
styleUrls: ['slide-toggle-demo.css'],
99
})
10-
export class SlideToggleDemo {}
10+
export class SlideToggleDemo {
11+
12+
onFormSubmit() {
13+
alert(`You submitted the form.`)
14+
}
15+
16+
}

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" type="checkbox"
44
[id]="inputId"
5+
[required]="required"
56
[checked]="checked"
67
[disabled]="disabled"
78
[name]="name"

src/lib/checkbox/checkbox.scss

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -470,6 +470,11 @@ md-checkbox {
470470
// Visually hidden but still able to respond to focus.
471471
.md-checkbox-input {
472472
@include md-visually-hidden;
473+
474+
// Move the input to the bottom and in the middle.
475+
// Visual improvement to properly show browser popups when being required.
476+
bottom: 0;
477+
left: 50%;
473478
}
474479

475480
@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
@@ -11,6 +11,7 @@ import {
1111
NgModule,
1212
} from '@angular/core';
1313
import {NG_VALUE_ACCESSOR, ControlValueAccessor} from '@angular/forms';
14+
import {BooleanFieldValue} from '@angular2-material/core/annotations/field-value';
1415

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

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

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515

1616
<input #input class="md-slide-toggle-checkbox" 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: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,11 @@ $md-slide-toggle-margin: 16px !default;
176176
// Like accessibility and keyboard interaction.
177177
.md-slide-toggle-checkbox {
178178
@include md-visually-hidden();
179+
180+
// Move the input to the bottom and in the middle of the thumb.
181+
// Visual improvement to properly show browser popups when being required.
182+
bottom: 0;
183+
left: $md-slide-toggle-thumb-size / 2;
179184
}
180185

181186
.md-slide-toggle-bar,

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

Lines changed: 77 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, FormsModule],
12-
declarations: [SlideToggleTestApp],
12+
declarations: [SlideToggleTestApp, SlideToggleFormsTestApp],
1313
});
1414

1515
TestBed.compileComponents();
@@ -332,6 +332,18 @@ describe('MdSlideToggle', () => {
332332
expect(slideToggleElement.classList).toContain('md-slide-toggle-focused');
333333
});
334334

335+
it('should forward the required attribute', () => {
336+
testComponent.isRequired = true;
337+
fixture.detectChanges();
338+
339+
expect(inputElement.required).toBe(true);
340+
341+
testComponent.isRequired = false;
342+
fixture.detectChanges();
343+
344+
expect(inputElement.required).toBe(false);
345+
});
346+
335347
});
336348

337349
describe('custom template', () => {
@@ -345,6 +357,44 @@ describe('MdSlideToggle', () => {
345357
}));
346358
});
347359

360+
describe('with forms', () => {
361+
362+
let fixture: ComponentFixture<any>;
363+
let testComponent: SlideToggleFormsTestApp;
364+
let buttonElement: HTMLButtonElement;
365+
let labelElement: HTMLLabelElement;
366+
367+
// This initialization is async() because it needs to wait for ngModel to set the initial value.
368+
beforeEach(async(() => {
369+
fixture = TestBed.createComponent(SlideToggleFormsTestApp);
370+
371+
testComponent = fixture.debugElement.componentInstance;
372+
373+
fixture.detectChanges();
374+
375+
buttonElement = fixture.debugElement.query(By.css('button')).nativeElement;
376+
labelElement = fixture.debugElement.query(By.css('label')).nativeElement;
377+
}));
378+
379+
it('should prevent the form from submit when being required', async(() => {
380+
381+
let fixture = TestBed.createComponent(SlideToggleFormsTestApp);
382+
383+
fixture.detectChanges();
384+
385+
buttonElement.click();
386+
expect(testComponent.isSubmitted).toBe(false);
387+
388+
// Make the form valid by setting the slide-toggle to true.
389+
labelElement.click();
390+
fixture.detectChanges();
391+
392+
buttonElement.click();
393+
expect(testComponent.isSubmitted).toBe(true);
394+
}));
395+
396+
})
397+
348398
});
349399

350400
/**
@@ -361,16 +411,25 @@ function dispatchFocusChangeEvent(eventName: string, element: HTMLElement): void
361411
@Component({
362412
selector: 'slide-toggle-test-app',
363413
template: `
364-
<md-slide-toggle [(ngModel)]="slideModel" [disabled]="isDisabled" [color]="slideColor"
365-
[id]="slideId" [checked]="slideChecked" [name]="slideName"
366-
[ariaLabel]="slideLabel" [ariaLabelledby]="slideLabelledBy"
367-
(change)="onSlideChange($event)"
414+
<md-slide-toggle [(ngModel)]="slideModel"
415+
[required]="isRequired"
416+
[disabled]="isDisabled"
417+
[color]="slideColor"
418+
[id]="slideId"
419+
[checked]="slideChecked"
420+
[name]="slideName"
421+
[ariaLabel]="slideLabel"
422+
[ariaLabelledby]="slideLabelledBy"
423+
(change)="onSlideChange($event)"
368424
(click)="onSlideClick($event)">
425+
369426
<span>Test Slide Toggle</span>
427+
370428
</md-slide-toggle>`,
371429
})
372430
class SlideToggleTestApp {
373431
isDisabled: boolean = false;
432+
isRequired: boolean = false;
374433
slideModel: boolean = false;
375434
slideChecked: boolean = false;
376435
slideColor: string;
@@ -385,3 +444,16 @@ class SlideToggleTestApp {
385444
this.lastEvent = event;
386445
}
387446
}
447+
448+
449+
@Component({
450+
selector: 'slide-toggle-forms-test-app',
451+
template: `
452+
<form (ngSubmit)="isSubmitted = true">
453+
<md-slide-toggle name="slideToggle" ngModel required>Required</md-slide-toggle>
454+
<button type="submit"></button>
455+
</form>`
456+
})
457+
class SlideToggleFormsTestApp {
458+
isSubmitted: boolean = false;
459+
}

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

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

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

0 commit comments

Comments
 (0)