Skip to content

Commit

Permalink
feat(stepper): Add support for linear stepper (angular#6116)
Browse files Browse the repository at this point in the history
* Add form controls and custom error state matcher

* Modify form controls for stepper-demo and add custom validator

* Move custom step validation function so that users can simply import and use

* Implement @input() stepControl for each step

* Add linear attribute to stepper

* Add enabling/disabling linear state of demo
  • Loading branch information
jwshinjwshin committed Aug 22, 2017
1 parent 9958a88 commit 5b2522b
Show file tree
Hide file tree
Showing 5 changed files with 140 additions and 23 deletions.
40 changes: 36 additions & 4 deletions src/cdk/stepper/stepper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ 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';
import {AbstractControl} from '@angular/forms';

/** Used to generate unique ID for each stepper component. */
let nextId = 0;
Expand All @@ -45,7 +47,7 @@ export class CdkStepperSelectionEvent {

@Component({
selector: 'cdk-step',
templateUrl: 'step.html',
templateUrl: 'step.html'
})
export class CdkStep {
/** Template for step label if it exists. */
Expand All @@ -54,6 +56,17 @@ export class CdkStep {
/** Template for step content. */
@ViewChild(TemplateRef) content: TemplateRef<any>;

/** The top level abstract control of the step. */
@Input()
get stepControl() { return this._stepControl; }
set stepControl(control: AbstractControl) {
this._stepControl = control;
}
private _stepControl: AbstractControl;

/** Whether user has seen the expanded step content or not . */
interacted = false;

/** Label of the step. */
@Input()
label: string;
Expand All @@ -70,7 +83,7 @@ export class CdkStep {
selector: 'cdk-stepper',
host: {
'(focus)': '_focusStep()',
'(keydown)': '_onKeydown($event)',
'(keydown)': '_onKeydown($event)'
},
})
export class CdkStepper {
Expand All @@ -80,11 +93,17 @@ export class CdkStepper {
/** The list of step headers of the steps in the stepper. */
_stepHeader: QueryList<ElementRef>;

/** 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) {
if (this._selectedIndex != index) {
if (this._selectedIndex != index && !this._anyControlsInvalid(index)) {
this._emitStepperSelectionEvent(index);
this._focusStep(this._selectedIndex);
}
Expand Down Expand Up @@ -153,7 +172,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.
Expand All @@ -166,4 +185,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;
}
}
3 changes: 2 additions & 1 deletion src/cdk/stepper/tsconfig-build.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": [
Expand Down
57 changes: 44 additions & 13 deletions src/demo-app/stepper/stepper-demo.html
Original file line number Diff line number Diff line change
@@ -1,3 +1,47 @@
<h2>Linear Vertical Stepper Demo</h2>
<md-checkbox [(ngModel)]="isNonLinear">Disable linear mode</md-checkbox>
<form [formGroup]="formGroup">
<md-vertical-stepper formArrayName="formArray" [linear]="!isNonLinear">
<md-step formGroupName="0" [stepControl]="formArray.get([0])">
<ng-template mdStepLabel>Fill out your name</ng-template>
<md-input-container>
<input mdInput placeholder="First Name" formControlName="firstNameFormCtrl" required>
<md-error>This field is required</md-error>
</md-input-container>

<md-input-container>
<input mdInput placeholder="Last Name" formControlName="lastNameFormCtrl" required>
<md-error>This field is required</md-error>
</md-input-container>
<div>
<button md-button mdStepperNext type="button">Next</button>
</div>
</md-step>

<md-step formGroupName="1" [stepControl]="formArray.get([1])">
<ng-template mdStepLabel>
<div>Fill out your phone number</div>
</ng-template>
<md-input-container>
<input mdInput placeholder="Phone number" formControlName="phoneFormCtrl">
<md-error>This field is required</md-error>
</md-input-container>
<div>
<button md-button mdStepperPrevious type="button">Back</button>
<button md-button mdStepperNext type="button">Next</button>
</div>
</md-step>

<md-step>
<ng-template mdStepLabel>Confirm your information</ng-template>
Everything seems correct.
<div>
<button md-button>Done</button>
</div>
</md-step>
</md-vertical-stepper>
</form>

<h2>Vertical Stepper Demo</h2>
<md-vertical-stepper>
<md-step>
Expand Down Expand Up @@ -134,16 +178,3 @@ <h2>Horizontal Stepper Demo with Templated Label</h2>
</div>
</md-step>
</md-horizontal-stepper>

<h2>Vertical Stepper Demo</h2>
<md-vertical-stepper>
<md-step *ngFor="let step of steps" [label]="step.label">
<md-input-container>
<input mdInput placeholder="Answer" [(ngModel)]="step.content">
</md-input-container>
<div>
<button md-button mdStepperPrevious>Back</button>
<button md-button mdStepperNext>Next</button>
</div>
</md-step>
</md-vertical-stepper>
25 changes: 24 additions & 1 deletion src/demo-app/stepper/stepper-demo.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,39 @@
import {Component} from '@angular/core';
import {Validators, FormBuilder, FormGroup} from '@angular/forms';

@Component({
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.'},
{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!'}
];

/** Returns a FormArray with the name 'formArray'. */
get formArray() { return this.formGroup.get('formArray'); }

constructor(private _formBuilder: FormBuilder) { }

ngOnInit() {
this.formGroup = this._formBuilder.group({
formArray: this._formBuilder.array([
this._formBuilder.group({
firstNameFormCtrl: ['', Validators.required],
lastNameFormCtrl: ['', Validators.required],
}),
this._formBuilder.group({
phoneFormCtrl: [''],
})
])
});
}
}
38 changes: 34 additions & 4 deletions src/lib/stepper/stepper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,26 +15,56 @@ 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'
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 <ng-template matStepLabel> or <ng-template mdStepLabel>. */
@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);

// 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;
}
}

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<ElementRef>;

Expand Down

0 comments on commit 5b2522b

Please sign in to comment.