From 74b24ce5d47fa96ad78fb480746110f6cfc6a76b Mon Sep 17 00:00:00 2001 From: Ji Won Shin Date: Fri, 28 Jul 2017 09:30:32 -0700 Subject: [PATCH 1/6] Add form controls and custom error state matcher --- src/cdk/stepper/stepper-button.ts | 10 ++++-- src/cdk/stepper/stepper.ts | 23 ++++++++++-- src/cdk/stepper/tsconfig-build.json | 3 +- src/demo-app/stepper/stepper-demo.html | 49 ++++++++++++++++++++++++++ src/demo-app/stepper/stepper-demo.ts | 19 ++++++++++ src/lib/stepper/stepper-button.ts | 10 ++++-- src/lib/stepper/stepper.ts | 30 ++++++++++++++-- src/lib/table/cell.ts | 8 ++++- src/lib/table/row.ts | 8 ++++- 9 files changed, 148 insertions(+), 12 deletions(-) diff --git a/src/cdk/stepper/stepper-button.ts b/src/cdk/stepper/stepper-button.ts index c63d8c822eef..52f0fec3c6fa 100644 --- a/src/cdk/stepper/stepper-button.ts +++ b/src/cdk/stepper/stepper-button.ts @@ -12,7 +12,10 @@ import {CdkStepper} from './stepper'; /** Button that moves to the next step in a stepper workflow. */ @Directive({ selector: 'button[cdkStepperNext]', - host: {'(click)': '_stepper.next()'} + host: { + '(click)': '_stepper.next()', + 'type': 'button' + } }) export class CdkStepperNext { constructor(public _stepper: CdkStepper) { } @@ -21,7 +24,10 @@ export class CdkStepperNext { /** Button that moves to the previous step in a stepper workflow. */ @Directive({ selector: 'button[cdkStepperPrevious]', - host: {'(click)': '_stepper.previous()'} + host: { + '(click)': '_stepper.previous()', + 'type': 'button' + } }) export class CdkStepperPrevious { constructor(public _stepper: CdkStepper) { } diff --git a/src/cdk/stepper/stepper.ts b/src/cdk/stepper/stepper.ts index 428095a806a0..4569d066ae47 100644 --- a/src/cdk/stepper/stepper.ts +++ b/src/cdk/stepper/stepper.ts @@ -24,6 +24,7 @@ import { } from '@angular/core'; import {LEFT_ARROW, RIGHT_ARROW, ENTER, SPACE} from '@angular/cdk/keyboard'; import {CdkStepLabel} from './step-label'; +import {coerceBooleanProperty} from '@angular/cdk/coercion'; /** Used to generate unique ID for each stepper component. */ let nextId = 0; @@ -45,7 +46,7 @@ export class CdkStepperSelectionEvent { @Component({ selector: 'cdk-step', - templateUrl: 'step.html', + templateUrl: 'step.html' }) export class CdkStep { /** Template for step label if it exists. */ @@ -54,6 +55,21 @@ export class CdkStep { /** Template for step content. */ @ViewChild(TemplateRef) content: TemplateRef; + /** Whether step is disabled or not. */ + @Input() + get disabled() { return this._disabled; } + set disabled(value: any) { + this._disabled = coerceBooleanProperty(value); + } + private _disabled = false; + + /** Whether the user has interacted with step or not. */ + get interacted() { return this._interacted; } + set interacted(value: any) { + this._interacted = coerceBooleanProperty(value); + } + private _interacted = false; + /** Label of the step. */ @Input() label: string; @@ -84,7 +100,8 @@ export class CdkStepper { @Input() get selectedIndex() { return this._selectedIndex; } set selectedIndex(index: number) { - if (this._selectedIndex != index) { + this._steps.toArray()[this._selectedIndex].interacted = true; + if (this._selectedIndex != index && !this._steps.toArray()[index].disabled) { this._emitStepperSelectionEvent(index); this._focusStep(this._selectedIndex); } @@ -153,7 +170,7 @@ export class CdkStepper { break; case SPACE: case ENTER: - this._emitStepperSelectionEvent(this._focusIndex); + this.selectedIndex = this._focusIndex; break; default: // Return to avoid calling preventDefault on keys that are not explicitly handled. diff --git a/src/cdk/stepper/tsconfig-build.json b/src/cdk/stepper/tsconfig-build.json index d00cedda93ea..ea3a295b1e25 100644 --- a/src/cdk/stepper/tsconfig-build.json +++ b/src/cdk/stepper/tsconfig-build.json @@ -4,7 +4,8 @@ "outDir": "../../../dist/packages/cdk", "baseUrl": ".", "paths": { - "@angular/cdk/keyboard": ["../../../dist/packages/cdk/keyboard/public_api"] + "@angular/cdk/keyboard": ["../../../dist/packages/cdk/keyboard/public_api"], + "@angular/cdk/coercion": ["../../../dist/packages/cdk/coercion/public_api"] } }, "files": [ diff --git a/src/demo-app/stepper/stepper-demo.html b/src/demo-app/stepper/stepper-demo.html index 89d64a58574f..08e84afb8907 100644 --- a/src/demo-app/stepper/stepper-demo.html +++ b/src/demo-app/stepper/stepper-demo.html @@ -1,3 +1,52 @@ +

Linear Vertical Stepper Demo

+
+
+ + +
+ Fill out your name + + + This field is required + + + + + This field is required + +
+ +
+
+
+ + +
+ +
Fill out your phone number
+
+ + + This field is required + +
+ + +
+
+
+ + + Confirm your information + Everything seems correct. +
+ +
+
+
+
+
+

Horizontal Stepper Demo

diff --git a/src/demo-app/stepper/stepper-demo.ts b/src/demo-app/stepper/stepper-demo.ts index 7df83cde23f5..010a62f29ece 100644 --- a/src/demo-app/stepper/stepper-demo.ts +++ b/src/demo-app/stepper/stepper-demo.ts @@ -1,4 +1,5 @@ import {Component} from '@angular/core'; +import {Validators, FormBuilder, FormGroup} from '@angular/forms'; @Component({ moduleId: module.id, @@ -7,10 +8,28 @@ import {Component} from '@angular/core'; styleUrls: ['stepper-demo.scss'], }) export class StepperDemo { + formGroup: FormGroup; + steps = [ {label: 'Confirm your name', content: 'Last name, First name.'}, {label: 'Confirm your contact information', content: '123-456-7890'}, {label: 'Confirm your address', content: '1600 Amphitheater Pkwy MTV'}, {label: 'You are now done', content: 'Finished!'} ]; + + constructor(private _fb: FormBuilder) { } + + ngOnInit() { + this.formGroup = this._fb.group({ + formArray: this._fb.array([ + this._fb.group({ + firstNameFormCtrl: ['', Validators.required], + lastNameFormCtrl: ['', Validators.required], + }), + this._fb.group({ + phoneFormCtrl: ['', Validators.required], + }) + ]) + }); + } } diff --git a/src/lib/stepper/stepper-button.ts b/src/lib/stepper/stepper-button.ts index 7dee99256c29..e139539277a9 100644 --- a/src/lib/stepper/stepper-button.ts +++ b/src/lib/stepper/stepper-button.ts @@ -13,7 +13,10 @@ import {MdStepper} from './stepper'; /** Button that moves to the next step in a stepper workflow. */ @Directive({ selector: 'button[mdStepperNext], button[matStepperNext]', - host: {'(click)': '_stepper.next()'}, + host: { + '(click)': '_stepper.next()', + 'type': 'button' + }, providers: [{provide: CdkStepper, useExisting: MdStepper}] }) export class MdStepperNext extends CdkStepperNext { } @@ -21,7 +24,10 @@ export class MdStepperNext extends CdkStepperNext { } /** Button that moves to the previous step in a stepper workflow. */ @Directive({ selector: 'button[mdStepperPrevious], button[matStepperPrevious]', - host: {'(click)': '_stepper.previous()'}, + host: { + '(click)': '_stepper.previous()', + 'type': 'button' + }, providers: [{provide: CdkStepper, useExisting: MdStepper}] }) export class MdStepperPrevious extends CdkStepperPrevious { } diff --git a/src/lib/stepper/stepper.ts b/src/lib/stepper/stepper.ts index 29923d7857ee..15af9d1b531d 100644 --- a/src/lib/stepper/stepper.ts +++ b/src/lib/stepper/stepper.ts @@ -15,26 +15,52 @@ import { // considers such imports as unused (https://github.com/Microsoft/TypeScript/issues/14953) // tslint:disable-next-line:no-unused-variable ElementRef, + Inject, + Optional, QueryList, + SkipSelf, ViewChildren }from '@angular/core'; import {MdStepLabel} from './step-label'; +import { + defaultErrorStateMatcher, + ErrorOptions, + MD_ERROR_GLOBAL_OPTIONS, + ErrorStateMatcher +} from '../core/error/error-options'; +import {FormControl, FormGroupDirective, NgForm} from '@angular/forms'; @Component({ moduleId: module.id, selector: 'md-step, mat-step', templateUrl: 'step.html', + providers: [{provide: MD_ERROR_GLOBAL_OPTIONS, useExisting: MdStep}] }) export class MdStep extends CdkStep { /** Content for step label given by or . */ @ContentChild(MdStepLabel) stepLabel: MdStepLabel; - constructor(mdStepper: MdStepper) { + /** Original ErrorStateMatcher that checks the validity of form control. */ + private _originalErrorStateMatcher: ErrorStateMatcher; + + constructor(mdStepper: MdStepper, + @Optional() @SkipSelf() @Inject(MD_ERROR_GLOBAL_OPTIONS) errorOptions: ErrorOptions) { super(mdStepper); + this._originalErrorStateMatcher = + errorOptions ? errorOptions.errorStateMatcher || defaultErrorStateMatcher + : defaultErrorStateMatcher; + } + + /** Custom error state matcher that additionally checks for validity of interacted form. */ + errorStateMatcher = (control: FormControl, form: FormGroupDirective | NgForm) => { + let originalErrorState = this._originalErrorStateMatcher(control, form); + let customErrorState = control.invalid && this.interacted; + + return originalErrorState || customErrorState; } } -export class MdStepper extends CdkStepper { +export class MdStepper extends CdkStepper implements ErrorOptions { /** The list of step headers of the steps in the stepper. */ @ViewChildren('stepHeader') _stepHeader: QueryList; diff --git a/src/lib/table/cell.ts b/src/lib/table/cell.ts index 2ae636c97d50..3578ab6ccb7c 100644 --- a/src/lib/table/cell.ts +++ b/src/lib/table/cell.ts @@ -7,7 +7,13 @@ */ import {Directive, ElementRef, Input, Renderer2} from '@angular/core'; -import {CdkCell, CdkCellDef, CdkColumnDef, CdkHeaderCell, CdkHeaderCellDef} from '@angular/cdk/table'; +import { + CdkCell, + CdkCellDef, + CdkColumnDef, + CdkHeaderCell, + CdkHeaderCellDef +} from '@angular/cdk/table'; /** Workaround for https://github.com/angular/angular/issues/17849 */ export const _MdCellDef = CdkCellDef; diff --git a/src/lib/table/row.ts b/src/lib/table/row.ts index 9d9beff84fea..913a861a2eb6 100644 --- a/src/lib/table/row.ts +++ b/src/lib/table/row.ts @@ -7,7 +7,13 @@ */ import {ChangeDetectionStrategy, Component, Directive} from '@angular/core'; -import {CdkHeaderRow, CdkRow, CDK_ROW_TEMPLATE, CdkRowDef, CdkHeaderRowDef} from '@angular/cdk/table'; +import { + CdkHeaderRow, + CdkRow, + CDK_ROW_TEMPLATE, + CdkRowDef, + CdkHeaderRowDef +} from '@angular/cdk/table'; /** Workaround for https://github.com/angular/angular/issues/17849 */ export const _MdHeaderRowDef = CdkHeaderRowDef; From 5d7851426e1b1a8da1c1e2be47b127ff6ee0efe5 Mon Sep 17 00:00:00 2001 From: Ji Won Shin Date: Fri, 28 Jul 2017 15:11:46 -0700 Subject: [PATCH 2/6] Modify form controls for stepper-demo and add custom validator --- src/cdk/stepper/stepper-button.ts | 10 +-- src/cdk/stepper/stepper.ts | 7 ++- src/demo-app/stepper/stepper-demo.html | 84 +++++++++++++------------- src/demo-app/stepper/stepper-demo.ts | 34 ++++++++--- src/lib/stepper/stepper-button.ts | 10 +-- src/lib/stepper/stepper.ts | 8 ++- 6 files changed, 82 insertions(+), 71 deletions(-) diff --git a/src/cdk/stepper/stepper-button.ts b/src/cdk/stepper/stepper-button.ts index 52f0fec3c6fa..c63d8c822eef 100644 --- a/src/cdk/stepper/stepper-button.ts +++ b/src/cdk/stepper/stepper-button.ts @@ -12,10 +12,7 @@ import {CdkStepper} from './stepper'; /** Button that moves to the next step in a stepper workflow. */ @Directive({ selector: 'button[cdkStepperNext]', - host: { - '(click)': '_stepper.next()', - 'type': 'button' - } + host: {'(click)': '_stepper.next()'} }) export class CdkStepperNext { constructor(public _stepper: CdkStepper) { } @@ -24,10 +21,7 @@ export class CdkStepperNext { /** Button that moves to the previous step in a stepper workflow. */ @Directive({ selector: 'button[cdkStepperPrevious]', - host: { - '(click)': '_stepper.previous()', - 'type': 'button' - } + host: {'(click)': '_stepper.previous()'} }) export class CdkStepperPrevious { constructor(public _stepper: CdkStepper) { } diff --git a/src/cdk/stepper/stepper.ts b/src/cdk/stepper/stepper.ts index 4569d066ae47..878811d8deb0 100644 --- a/src/cdk/stepper/stepper.ts +++ b/src/cdk/stepper/stepper.ts @@ -55,6 +55,7 @@ export class CdkStep { /** Template for step content. */ @ViewChild(TemplateRef) content: TemplateRef; + // TODO(jwshin): use disabled mixin when moved to cdk. /** Whether step is disabled or not. */ @Input() get disabled() { return this._disabled; } @@ -63,10 +64,10 @@ export class CdkStep { } private _disabled = false; - /** Whether the user has interacted with step or not. */ + /** Whether user has seen the expanded step content or not . */ get interacted() { return this._interacted; } - set interacted(value: any) { - this._interacted = coerceBooleanProperty(value); + set interacted(value: boolean) { + this._interacted = value; } private _interacted = false; diff --git a/src/demo-app/stepper/stepper-demo.html b/src/demo-app/stepper/stepper-demo.html index 08e84afb8907..8e06d98eaa33 100644 --- a/src/demo-app/stepper/stepper-demo.html +++ b/src/demo-app/stepper/stepper-demo.html @@ -1,50 +1,48 @@

Linear Vertical Stepper Demo

-
-
- - -
- Fill out your name - - - This field is required - + + + + Fill out your name + + + This field is required + - - - This field is required - -
- -
-
-
+ + + This field is required + +
+ +
+ - -
- -
Fill out your phone number
-
- - - This field is required - -
- - -
-
-
+ + +
Fill out your phone number
+
+ + + This field is required + +
+ + +
+
- - Confirm your information - Everything seems correct. -
- -
-
-
-
+ + Confirm your information + Everything seems correct. +
+ +
+
+

Horizontal Stepper Demo

diff --git a/src/demo-app/stepper/stepper-demo.ts b/src/demo-app/stepper/stepper-demo.ts index 010a62f29ece..981ec0901bd0 100644 --- a/src/demo-app/stepper/stepper-demo.ts +++ b/src/demo-app/stepper/stepper-demo.ts @@ -1,5 +1,7 @@ import {Component} from '@angular/core'; -import {Validators, FormBuilder, FormGroup} from '@angular/forms'; +import { + Validators, FormBuilder, FormGroup, FormArray, ValidationErrors, ValidatorFn +} from '@angular/forms'; @Component({ moduleId: module.id, @@ -17,19 +19,35 @@ export class StepperDemo { {label: 'You are now done', content: 'Finished!'} ]; - constructor(private _fb: FormBuilder) { } + /** Returns a FormArray with the name 'formArray'. */ + get formArray() { return this.formGroup.get('formArray'); } + + constructor(private _formBuilder: FormBuilder) { } ngOnInit() { - this.formGroup = this._fb.group({ - formArray: this._fb.array([ - this._fb.group({ + this.formGroup = this._formBuilder.group({ + formArray: this._formBuilder.array([ + this._formBuilder.group({ firstNameFormCtrl: ['', Validators.required], lastNameFormCtrl: ['', Validators.required], }), - this._fb.group({ - phoneFormCtrl: ['', Validators.required], + this._formBuilder.group({ + phoneFormCtrl: [''], }) - ]) + ], this._stepValidator) }); } + + /** + * Form array validator to check if all form groups in form array are valid. + * If not, it will return the index of the first invalid form group. + */ + private _stepValidator: ValidatorFn = (formArray: FormArray): ValidationErrors | null => { + for (let i = 0; i < formArray.length; i++) { + if (formArray.at(i).invalid) { + return {'invalid step': {'index': i}}; + } + } + return null; + } } diff --git a/src/lib/stepper/stepper-button.ts b/src/lib/stepper/stepper-button.ts index e139539277a9..7dee99256c29 100644 --- a/src/lib/stepper/stepper-button.ts +++ b/src/lib/stepper/stepper-button.ts @@ -13,10 +13,7 @@ import {MdStepper} from './stepper'; /** Button that moves to the next step in a stepper workflow. */ @Directive({ selector: 'button[mdStepperNext], button[matStepperNext]', - host: { - '(click)': '_stepper.next()', - 'type': 'button' - }, + host: {'(click)': '_stepper.next()'}, providers: [{provide: CdkStepper, useExisting: MdStepper}] }) export class MdStepperNext extends CdkStepperNext { } @@ -24,10 +21,7 @@ export class MdStepperNext extends CdkStepperNext { } /** Button that moves to the previous step in a stepper workflow. */ @Directive({ selector: 'button[mdStepperPrevious], button[matStepperPrevious]', - host: { - '(click)': '_stepper.previous()', - 'type': 'button' - }, + host: {'(click)': '_stepper.previous()'}, providers: [{provide: CdkStepper, useExisting: MdStepper}] }) export class MdStepperPrevious extends CdkStepperPrevious { } diff --git a/src/lib/stepper/stepper.ts b/src/lib/stepper/stepper.ts index 15af9d1b531d..8f02222d5116 100644 --- a/src/lib/stepper/stepper.ts +++ b/src/lib/stepper/stepper.ts @@ -36,7 +36,7 @@ import {FormControl, FormGroupDirective, NgForm} from '@angular/forms'; templateUrl: 'step.html', providers: [{provide: MD_ERROR_GLOBAL_OPTIONS, useExisting: MdStep}] }) -export class MdStep extends CdkStep { +export class MdStep extends CdkStep implements ErrorOptions { /** Content for step label given by or . */ @ContentChild(MdStepLabel) stepLabel: MdStepLabel; @@ -54,6 +54,12 @@ export class MdStep extends CdkStep { /** Custom error state matcher that additionally checks for validity of interacted form. */ errorStateMatcher = (control: FormControl, form: FormGroupDirective | NgForm) => { let originalErrorState = this._originalErrorStateMatcher(control, form); + + /** + * Custom error state checks for the validity of form that is not submitted or touched + * since user can trigger a form change by calling for another step without directly + * interacting with the current form. + */ let customErrorState = control.invalid && this.interacted; return originalErrorState || customErrorState; From 5e57f17f8e51a436cebb6f9548d895f407c39c55 Mon Sep 17 00:00:00 2001 From: Ji Won Shin Date: Thu, 3 Aug 2017 09:51:11 -0700 Subject: [PATCH 3/6] Move custom step validation function so that users can simply import and use --- src/cdk/stepper/stepper.ts | 6 +----- src/demo-app/stepper/stepper-demo.ts | 20 +++----------------- src/lib/stepper/stepper.ts | 23 ++++++++++++++++++++++- 3 files changed, 26 insertions(+), 23 deletions(-) diff --git a/src/cdk/stepper/stepper.ts b/src/cdk/stepper/stepper.ts index 878811d8deb0..8647bfc99ba1 100644 --- a/src/cdk/stepper/stepper.ts +++ b/src/cdk/stepper/stepper.ts @@ -65,11 +65,7 @@ export class CdkStep { private _disabled = false; /** Whether user has seen the expanded step content or not . */ - get interacted() { return this._interacted; } - set interacted(value: boolean) { - this._interacted = value; - } - private _interacted = false; + interacted = false; /** Label of the step. */ @Input() diff --git a/src/demo-app/stepper/stepper-demo.ts b/src/demo-app/stepper/stepper-demo.ts index 981ec0901bd0..b51146b0ed28 100644 --- a/src/demo-app/stepper/stepper-demo.ts +++ b/src/demo-app/stepper/stepper-demo.ts @@ -1,7 +1,6 @@ import {Component} from '@angular/core'; -import { - Validators, FormBuilder, FormGroup, FormArray, ValidationErrors, ValidatorFn -} from '@angular/forms'; +import {Validators, FormBuilder, FormGroup} from '@angular/forms'; +import {stepValidator} from '@angular/material'; @Component({ moduleId: module.id, @@ -34,20 +33,7 @@ export class StepperDemo { this._formBuilder.group({ phoneFormCtrl: [''], }) - ], this._stepValidator) + ], stepValidator) }); } - - /** - * Form array validator to check if all form groups in form array are valid. - * If not, it will return the index of the first invalid form group. - */ - private _stepValidator: ValidatorFn = (formArray: FormArray): ValidationErrors | null => { - for (let i = 0; i < formArray.length; i++) { - if (formArray.at(i).invalid) { - return {'invalid step': {'index': i}}; - } - } - return null; - } } diff --git a/src/lib/stepper/stepper.ts b/src/lib/stepper/stepper.ts index 8f02222d5116..603a44efbfdb 100644 --- a/src/lib/stepper/stepper.ts +++ b/src/lib/stepper/stepper.ts @@ -28,7 +28,28 @@ import { MD_ERROR_GLOBAL_OPTIONS, ErrorStateMatcher } from '../core/error/error-options'; -import {FormControl, FormGroupDirective, NgForm} from '@angular/forms'; +import { + FormArray, FormControl, FormGroupDirective, NgForm, ValidationErrors, + ValidatorFn +} from '@angular/forms'; + +/** + * Form array validator to check if all form groups in form array are valid. + * If not, it will return the index of the first invalid form group. + */ +export const stepValidator: ValidatorFn = (formArray: FormArray): ValidationErrors | null => { + for (let i = 0; i < formArray.length; i++) { + if (formArray.at(i).invalid) { + return {'invalid step': {'index': i}}; + } + } + return null; +}; + +/** + * Form array validator to check if all form groups in form array are valid. + * If not, it will return the index of the first invalid form group. + */ @Component({ moduleId: module.id, From 1acfef4e581ea1d2222b9fc835e064ffea088274 Mon Sep 17 00:00:00 2001 From: Ji Won Shin Date: Fri, 4 Aug 2017 17:23:52 -0700 Subject: [PATCH 4/6] Implement @Input() stepControl for each step --- src/cdk/stepper/stepper.ts | 17 ++++++++++------- src/demo-app/stepper/stepper-demo.html | 10 +++------- src/demo-app/stepper/stepper-demo.ts | 3 +-- src/lib/stepper/stepper.ts | 13 ------------- 4 files changed, 14 insertions(+), 29 deletions(-) diff --git a/src/cdk/stepper/stepper.ts b/src/cdk/stepper/stepper.ts index 8647bfc99ba1..8127e4ba5685 100644 --- a/src/cdk/stepper/stepper.ts +++ b/src/cdk/stepper/stepper.ts @@ -25,6 +25,7 @@ import { import {LEFT_ARROW, RIGHT_ARROW, ENTER, SPACE} from '@angular/cdk/keyboard'; import {CdkStepLabel} from './step-label'; import {coerceBooleanProperty} from '@angular/cdk/coercion'; +import {AbstractControl} from '@angular/forms'; /** Used to generate unique ID for each stepper component. */ let nextId = 0; @@ -55,14 +56,13 @@ export class CdkStep { /** Template for step content. */ @ViewChild(TemplateRef) content: TemplateRef; - // TODO(jwshin): use disabled mixin when moved to cdk. - /** Whether step is disabled or not. */ + /** The top level abstract control of the step. */ @Input() - get disabled() { return this._disabled; } - set disabled(value: any) { - this._disabled = coerceBooleanProperty(value); + get stepControl() { return this._stepControl; } + set stepControl(control: AbstractControl) { + this._stepControl = control; } - private _disabled = false; + private _stepControl: AbstractControl; /** Whether user has seen the expanded step content or not . */ interacted = false; @@ -98,7 +98,10 @@ export class CdkStepper { get selectedIndex() { return this._selectedIndex; } set selectedIndex(index: number) { this._steps.toArray()[this._selectedIndex].interacted = true; - if (this._selectedIndex != index && !this._steps.toArray()[index].disabled) { + for (let i = 0; i < index; i++) { + if (!this._steps.toArray()[i].stepControl.valid) { return; } + } + if (this._selectedIndex != index) { this._emitStepperSelectionEvent(index); this._focusStep(this._selectedIndex); } diff --git a/src/demo-app/stepper/stepper-demo.html b/src/demo-app/stepper/stepper-demo.html index 8e06d98eaa33..39eff56bbb52 100644 --- a/src/demo-app/stepper/stepper-demo.html +++ b/src/demo-app/stepper/stepper-demo.html @@ -1,7 +1,7 @@

Linear Vertical Stepper Demo

- + Fill out your name @@ -17,9 +17,7 @@

Linear Vertical Stepper Demo

- +
Fill out your phone number
@@ -33,9 +31,7 @@

Linear Vertical Stepper Demo

- + Confirm your information Everything seems correct.
diff --git a/src/demo-app/stepper/stepper-demo.ts b/src/demo-app/stepper/stepper-demo.ts index b51146b0ed28..6d2074e26704 100644 --- a/src/demo-app/stepper/stepper-demo.ts +++ b/src/demo-app/stepper/stepper-demo.ts @@ -1,6 +1,5 @@ import {Component} from '@angular/core'; import {Validators, FormBuilder, FormGroup} from '@angular/forms'; -import {stepValidator} from '@angular/material'; @Component({ moduleId: module.id, @@ -33,7 +32,7 @@ export class StepperDemo { this._formBuilder.group({ phoneFormCtrl: [''], }) - ], stepValidator) + ]) }); } } diff --git a/src/lib/stepper/stepper.ts b/src/lib/stepper/stepper.ts index 603a44efbfdb..b57c734d5816 100644 --- a/src/lib/stepper/stepper.ts +++ b/src/lib/stepper/stepper.ts @@ -33,19 +33,6 @@ import { ValidatorFn } from '@angular/forms'; -/** - * Form array validator to check if all form groups in form array are valid. - * If not, it will return the index of the first invalid form group. - */ -export const stepValidator: ValidatorFn = (formArray: FormArray): ValidationErrors | null => { - for (let i = 0; i < formArray.length; i++) { - if (formArray.at(i).invalid) { - return {'invalid step': {'index': i}}; - } - } - return null; -}; - /** * Form array validator to check if all form groups in form array are valid. * If not, it will return the index of the first invalid form group. From e648c0ef0beef9b3c1534b20f15fae023ca10788 Mon Sep 17 00:00:00 2001 From: Ji Won Shin Date: Mon, 7 Aug 2017 18:24:37 -0700 Subject: [PATCH 5/6] Add linear attribute to stepper --- src/cdk/stepper/stepper.ts | 28 +++++++++++++++++++++----- src/demo-app/stepper/stepper-demo.html | 2 +- src/lib/stepper/stepper.ts | 10 +-------- 3 files changed, 25 insertions(+), 15 deletions(-) diff --git a/src/cdk/stepper/stepper.ts b/src/cdk/stepper/stepper.ts index 8127e4ba5685..3940fa97f7d1 100644 --- a/src/cdk/stepper/stepper.ts +++ b/src/cdk/stepper/stepper.ts @@ -84,6 +84,7 @@ export class CdkStep { host: { '(focus)': '_focusStep()', '(keydown)': '_onKeydown($event)', + '[linear]': 'linear', }, }) export class CdkStepper { @@ -93,15 +94,19 @@ export class CdkStepper { /** The list of step headers of the steps in the stepper. */ _stepHeader: QueryList; + /** Whether the validity of previous steps should be checked or not. */ + @Input() + get linear() { return this._linear; } + set linear(value: any) { + this._linear = coerceBooleanProperty(value); + } + private _linear = false; + /** The index of the selected step. */ @Input() get selectedIndex() { return this._selectedIndex; } set selectedIndex(index: number) { - this._steps.toArray()[this._selectedIndex].interacted = true; - for (let i = 0; i < index; i++) { - if (!this._steps.toArray()[i].stepControl.valid) { return; } - } - if (this._selectedIndex != index) { + if (this._selectedIndex != index && !this._anyControlsInvalid(index)) { this._emitStepperSelectionEvent(index); this._focusStep(this._selectedIndex); } @@ -183,4 +188,17 @@ export class CdkStepper { this._focusIndex = index; this._stepHeader.toArray()[this._focusIndex].nativeElement.focus(); } + + private _anyControlsInvalid(index: number): boolean { + const stepsArray = this._steps.toArray(); + stepsArray[this._selectedIndex].interacted = true; + if (this._linear) { + for (let i = 0; i < index; i++) { + if (!stepsArray[i].stepControl.valid) { + return true; + } + } + } + return false; + } } diff --git a/src/demo-app/stepper/stepper-demo.html b/src/demo-app/stepper/stepper-demo.html index 427d7e64c427..c9e8b9fae038 100644 --- a/src/demo-app/stepper/stepper-demo.html +++ b/src/demo-app/stepper/stepper-demo.html @@ -1,6 +1,6 @@

Linear Vertical Stepper Demo

- + Fill out your name diff --git a/src/lib/stepper/stepper.ts b/src/lib/stepper/stepper.ts index b57c734d5816..8f02222d5116 100644 --- a/src/lib/stepper/stepper.ts +++ b/src/lib/stepper/stepper.ts @@ -28,15 +28,7 @@ import { MD_ERROR_GLOBAL_OPTIONS, ErrorStateMatcher } from '../core/error/error-options'; -import { - FormArray, FormControl, FormGroupDirective, NgForm, ValidationErrors, - ValidatorFn -} from '@angular/forms'; - -/** - * Form array validator to check if all form groups in form array are valid. - * If not, it will return the index of the first invalid form group. - */ +import {FormControl, FormGroupDirective, NgForm} from '@angular/forms'; @Component({ moduleId: module.id, From d8b53ef1e5557c83203907567b37e37a42fc0dc7 Mon Sep 17 00:00:00 2001 From: Ji Won Shin Date: Tue, 8 Aug 2017 09:37:12 -0700 Subject: [PATCH 6/6] Add enabling/disabling linear state of demo --- src/cdk/stepper/stepper.ts | 7 ++----- src/demo-app/stepper/stepper-demo.html | 3 ++- src/demo-app/stepper/stepper-demo.ts | 3 ++- src/lib/stepper/stepper.ts | 8 +++----- 4 files changed, 9 insertions(+), 12 deletions(-) diff --git a/src/cdk/stepper/stepper.ts b/src/cdk/stepper/stepper.ts index 3940fa97f7d1..e80ff26fbf81 100644 --- a/src/cdk/stepper/stepper.ts +++ b/src/cdk/stepper/stepper.ts @@ -83,8 +83,7 @@ export class CdkStep { selector: 'cdk-stepper', host: { '(focus)': '_focusStep()', - '(keydown)': '_onKeydown($event)', - '[linear]': 'linear', + '(keydown)': '_onKeydown($event)' }, }) export class CdkStepper { @@ -97,9 +96,7 @@ export class CdkStepper { /** Whether the validity of previous steps should be checked or not. */ @Input() get linear() { return this._linear; } - set linear(value: any) { - this._linear = coerceBooleanProperty(value); - } + set linear(value: any) { this._linear = coerceBooleanProperty(value); } private _linear = false; /** The index of the selected step. */ diff --git a/src/demo-app/stepper/stepper-demo.html b/src/demo-app/stepper/stepper-demo.html index c9e8b9fae038..f394b0de8a24 100644 --- a/src/demo-app/stepper/stepper-demo.html +++ b/src/demo-app/stepper/stepper-demo.html @@ -1,6 +1,7 @@

Linear Vertical Stepper Demo

+Disable linear mode - + Fill out your name diff --git a/src/demo-app/stepper/stepper-demo.ts b/src/demo-app/stepper/stepper-demo.ts index 6d2074e26704..cc0bc6673215 100644 --- a/src/demo-app/stepper/stepper-demo.ts +++ b/src/demo-app/stepper/stepper-demo.ts @@ -5,10 +5,11 @@ import {Validators, FormBuilder, FormGroup} from '@angular/forms'; moduleId: module.id, selector: 'stepper-demo', templateUrl: 'stepper-demo.html', - styleUrls: ['stepper-demo.scss'], + styleUrls: ['stepper-demo.scss'] }) export class StepperDemo { formGroup: FormGroup; + isNonLinear = false; steps = [ {label: 'Confirm your name', content: 'Last name, First name.'}, diff --git a/src/lib/stepper/stepper.ts b/src/lib/stepper/stepper.ts index 8f02222d5116..5a0dd890fa89 100644 --- a/src/lib/stepper/stepper.ts +++ b/src/lib/stepper/stepper.ts @@ -55,11 +55,9 @@ export class MdStep extends CdkStep implements ErrorOptions { errorStateMatcher = (control: FormControl, form: FormGroupDirective | NgForm) => { let originalErrorState = this._originalErrorStateMatcher(control, form); - /** - * Custom error state checks for the validity of form that is not submitted or touched - * since user can trigger a form change by calling for another step without directly - * interacting with the current form. - */ + // Custom error state checks for the validity of form that is not submitted or touched + // since user can trigger a form change by calling for another step without directly + // interacting with the current form. let customErrorState = control.invalid && this.interacted; return originalErrorState || customErrorState;