Skip to content

Commit 74b24ce

Browse files
committed
Add form controls and custom error state matcher
1 parent 9c41f1d commit 74b24ce

File tree

9 files changed

+148
-12
lines changed

9 files changed

+148
-12
lines changed

src/cdk/stepper/stepper-button.ts

+8-2
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,10 @@ import {CdkStepper} from './stepper';
1212
/** Button that moves to the next step in a stepper workflow. */
1313
@Directive({
1414
selector: 'button[cdkStepperNext]',
15-
host: {'(click)': '_stepper.next()'}
15+
host: {
16+
'(click)': '_stepper.next()',
17+
'type': 'button'
18+
}
1619
})
1720
export class CdkStepperNext {
1821
constructor(public _stepper: CdkStepper) { }
@@ -21,7 +24,10 @@ export class CdkStepperNext {
2124
/** Button that moves to the previous step in a stepper workflow. */
2225
@Directive({
2326
selector: 'button[cdkStepperPrevious]',
24-
host: {'(click)': '_stepper.previous()'}
27+
host: {
28+
'(click)': '_stepper.previous()',
29+
'type': 'button'
30+
}
2531
})
2632
export class CdkStepperPrevious {
2733
constructor(public _stepper: CdkStepper) { }

src/cdk/stepper/stepper.ts

+20-3
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import {
2424
} from '@angular/core';
2525
import {LEFT_ARROW, RIGHT_ARROW, ENTER, SPACE} from '@angular/cdk/keyboard';
2626
import {CdkStepLabel} from './step-label';
27+
import {coerceBooleanProperty} from '@angular/cdk/coercion';
2728

2829
/** Used to generate unique ID for each stepper component. */
2930
let nextId = 0;
@@ -45,7 +46,7 @@ export class CdkStepperSelectionEvent {
4546

4647
@Component({
4748
selector: 'cdk-step',
48-
templateUrl: 'step.html',
49+
templateUrl: 'step.html'
4950
})
5051
export class CdkStep {
5152
/** Template for step label if it exists. */
@@ -54,6 +55,21 @@ export class CdkStep {
5455
/** Template for step content. */
5556
@ViewChild(TemplateRef) content: TemplateRef<any>;
5657

58+
/** Whether step is disabled or not. */
59+
@Input()
60+
get disabled() { return this._disabled; }
61+
set disabled(value: any) {
62+
this._disabled = coerceBooleanProperty(value);
63+
}
64+
private _disabled = false;
65+
66+
/** Whether the user has interacted with step or not. */
67+
get interacted() { return this._interacted; }
68+
set interacted(value: any) {
69+
this._interacted = coerceBooleanProperty(value);
70+
}
71+
private _interacted = false;
72+
5773
/** Label of the step. */
5874
@Input()
5975
label: string;
@@ -84,7 +100,8 @@ export class CdkStepper {
84100
@Input()
85101
get selectedIndex() { return this._selectedIndex; }
86102
set selectedIndex(index: number) {
87-
if (this._selectedIndex != index) {
103+
this._steps.toArray()[this._selectedIndex].interacted = true;
104+
if (this._selectedIndex != index && !this._steps.toArray()[index].disabled) {
88105
this._emitStepperSelectionEvent(index);
89106
this._focusStep(this._selectedIndex);
90107
}
@@ -153,7 +170,7 @@ export class CdkStepper {
153170
break;
154171
case SPACE:
155172
case ENTER:
156-
this._emitStepperSelectionEvent(this._focusIndex);
173+
this.selectedIndex = this._focusIndex;
157174
break;
158175
default:
159176
// Return to avoid calling preventDefault on keys that are not explicitly handled.

src/cdk/stepper/tsconfig-build.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44
"outDir": "../../../dist/packages/cdk",
55
"baseUrl": ".",
66
"paths": {
7-
"@angular/cdk/keyboard": ["../../../dist/packages/cdk/keyboard/public_api"]
7+
"@angular/cdk/keyboard": ["../../../dist/packages/cdk/keyboard/public_api"],
8+
"@angular/cdk/coercion": ["../../../dist/packages/cdk/coercion/public_api"]
89
}
910
},
1011
"files": [

src/demo-app/stepper/stepper-demo.html

+49
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,52 @@
1+
<h2>Linear Vertical Stepper Demo</h2>
2+
<form [formGroup]="formGroup" novalidate>
3+
<div formArrayName="formArray">
4+
<md-vertical-stepper>
5+
<md-step>
6+
<div [formGroupName]="0">
7+
<ng-template mdStepLabel>Fill out your name</ng-template>
8+
<md-input-container>
9+
<input mdInput placeholder="First Name" formControlName="firstNameFormCtrl" required>
10+
<md-error>This field is required</md-error>
11+
</md-input-container>
12+
13+
<md-input-container>
14+
<input mdInput placeholder="Last Name" formControlName="lastNameFormCtrl" required>
15+
<md-error>This field is required</md-error>
16+
</md-input-container>
17+
<div>
18+
<button md-button mdStepperNext>Next</button>
19+
</div>
20+
</div>
21+
</md-step>
22+
23+
<md-step [disabled]="!formGroup.controls.formArray.controls[0].valid">
24+
<div [formGroupName]="1">
25+
<ng-template mdStepLabel>
26+
<div>Fill out your phone number</div>
27+
</ng-template>
28+
<md-input-container>
29+
<input mdInput placeholder="Phone number" formControlName="phoneFormCtrl" required>
30+
<md-error>This field is required</md-error>
31+
</md-input-container>
32+
<div>
33+
<button md-button mdStepperPrevious>Back</button>
34+
<button md-button mdStepperNext>Next</button>
35+
</div>
36+
</div>
37+
</md-step>
38+
39+
<md-step [disabled]="!formGroup.controls.formArray.controls[1].valid">
40+
<ng-template mdStepLabel>Confirm your information</ng-template>
41+
Everything seems correct.
42+
<div>
43+
<button md-button>Done</button>
44+
</div>
45+
</md-step>
46+
</md-vertical-stepper>
47+
</div>
48+
</form>
49+
150
<h2>Horizontal Stepper Demo</h2>
251
<md-horizontal-stepper>
352
<md-step *ngFor="let step of steps" [label]="step.label">

src/demo-app/stepper/stepper-demo.ts

+19
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import {Component} from '@angular/core';
2+
import {Validators, FormBuilder, FormGroup} from '@angular/forms';
23

34
@Component({
45
moduleId: module.id,
@@ -7,10 +8,28 @@ import {Component} from '@angular/core';
78
styleUrls: ['stepper-demo.scss'],
89
})
910
export class StepperDemo {
11+
formGroup: FormGroup;
12+
1013
steps = [
1114
{label: 'Confirm your name', content: 'Last name, First name.'},
1215
{label: 'Confirm your contact information', content: '123-456-7890'},
1316
{label: 'Confirm your address', content: '1600 Amphitheater Pkwy MTV'},
1417
{label: 'You are now done', content: 'Finished!'}
1518
];
19+
20+
constructor(private _fb: FormBuilder) { }
21+
22+
ngOnInit() {
23+
this.formGroup = this._fb.group({
24+
formArray: this._fb.array([
25+
this._fb.group({
26+
firstNameFormCtrl: ['', Validators.required],
27+
lastNameFormCtrl: ['', Validators.required],
28+
}),
29+
this._fb.group({
30+
phoneFormCtrl: ['', Validators.required],
31+
})
32+
])
33+
});
34+
}
1635
}

src/lib/stepper/stepper-button.ts

+8-2
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,21 @@ import {MdStepper} from './stepper';
1313
/** Button that moves to the next step in a stepper workflow. */
1414
@Directive({
1515
selector: 'button[mdStepperNext], button[matStepperNext]',
16-
host: {'(click)': '_stepper.next()'},
16+
host: {
17+
'(click)': '_stepper.next()',
18+
'type': 'button'
19+
},
1720
providers: [{provide: CdkStepper, useExisting: MdStepper}]
1821
})
1922
export class MdStepperNext extends CdkStepperNext { }
2023

2124
/** Button that moves to the previous step in a stepper workflow. */
2225
@Directive({
2326
selector: 'button[mdStepperPrevious], button[matStepperPrevious]',
24-
host: {'(click)': '_stepper.previous()'},
27+
host: {
28+
'(click)': '_stepper.previous()',
29+
'type': 'button'
30+
},
2531
providers: [{provide: CdkStepper, useExisting: MdStepper}]
2632
})
2733
export class MdStepperPrevious extends CdkStepperPrevious { }

src/lib/stepper/stepper.ts

+28-2
Original file line numberDiff line numberDiff line change
@@ -15,26 +15,52 @@ import {
1515
// considers such imports as unused (https://github.com/Microsoft/TypeScript/issues/14953)
1616
// tslint:disable-next-line:no-unused-variable
1717
ElementRef,
18+
Inject,
19+
Optional,
1820
QueryList,
21+
SkipSelf,
1922
ViewChildren
2023
}from '@angular/core';
2124
import {MdStepLabel} from './step-label';
25+
import {
26+
defaultErrorStateMatcher,
27+
ErrorOptions,
28+
MD_ERROR_GLOBAL_OPTIONS,
29+
ErrorStateMatcher
30+
} from '../core/error/error-options';
31+
import {FormControl, FormGroupDirective, NgForm} from '@angular/forms';
2232

2333
@Component({
2434
moduleId: module.id,
2535
selector: 'md-step, mat-step',
2636
templateUrl: 'step.html',
37+
providers: [{provide: MD_ERROR_GLOBAL_OPTIONS, useExisting: MdStep}]
2738
})
2839
export class MdStep extends CdkStep {
2940
/** Content for step label given by <ng-template matStepLabel> or <ng-template mdStepLabel>. */
3041
@ContentChild(MdStepLabel) stepLabel: MdStepLabel;
3142

32-
constructor(mdStepper: MdStepper) {
43+
/** Original ErrorStateMatcher that checks the validity of form control. */
44+
private _originalErrorStateMatcher: ErrorStateMatcher;
45+
46+
constructor(mdStepper: MdStepper,
47+
@Optional() @SkipSelf() @Inject(MD_ERROR_GLOBAL_OPTIONS) errorOptions: ErrorOptions) {
3348
super(mdStepper);
49+
this._originalErrorStateMatcher =
50+
errorOptions ? errorOptions.errorStateMatcher || defaultErrorStateMatcher
51+
: defaultErrorStateMatcher;
52+
}
53+
54+
/** Custom error state matcher that additionally checks for validity of interacted form. */
55+
errorStateMatcher = (control: FormControl, form: FormGroupDirective | NgForm) => {
56+
let originalErrorState = this._originalErrorStateMatcher(control, form);
57+
let customErrorState = control.invalid && this.interacted;
58+
59+
return originalErrorState || customErrorState;
3460
}
3561
}
3662

37-
export class MdStepper extends CdkStepper {
63+
export class MdStepper extends CdkStepper implements ErrorOptions {
3864
/** The list of step headers of the steps in the stepper. */
3965
@ViewChildren('stepHeader') _stepHeader: QueryList<ElementRef>;
4066

src/lib/table/cell.ts

+7-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,13 @@
77
*/
88

99
import {Directive, ElementRef, Input, Renderer2} from '@angular/core';
10-
import {CdkCell, CdkCellDef, CdkColumnDef, CdkHeaderCell, CdkHeaderCellDef} from '@angular/cdk/table';
10+
import {
11+
CdkCell,
12+
CdkCellDef,
13+
CdkColumnDef,
14+
CdkHeaderCell,
15+
CdkHeaderCellDef
16+
} from '@angular/cdk/table';
1117

1218
/** Workaround for https://github.com/angular/angular/issues/17849 */
1319
export const _MdCellDef = CdkCellDef;

src/lib/table/row.ts

+7-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,13 @@
77
*/
88

99
import {ChangeDetectionStrategy, Component, Directive} from '@angular/core';
10-
import {CdkHeaderRow, CdkRow, CDK_ROW_TEMPLATE, CdkRowDef, CdkHeaderRowDef} from '@angular/cdk/table';
10+
import {
11+
CdkHeaderRow,
12+
CdkRow,
13+
CDK_ROW_TEMPLATE,
14+
CdkRowDef,
15+
CdkHeaderRowDef
16+
} from '@angular/cdk/table';
1117

1218
/** Workaround for https://github.com/angular/angular/issues/17849 */
1319
export const _MdHeaderRowDef = CdkHeaderRowDef;

0 commit comments

Comments
 (0)