-
Notifications
You must be signed in to change notification settings - Fork 6.9k
feat(stepper): Add support for linear stepper #6116
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
74b24ce
5d78514
5e57f17
1acfef4
71cb69f
e648c0e
d8b53ef
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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<any>; | ||
|
|
||
| /** 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); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we only want to emit a change event if the selected index changes due to user interaction, therefore this should be moved to one of the user event handlers
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I thought this is commonly called by all user interactions and that's why we decided to delegate emitting a change event here. Is this not what you meant? |
||
| 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. | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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"] | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You should be able to delete the entire
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. possibly... depends where her stepper branch is synced to
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| } | ||
| }, | ||
| "files": [ | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,52 @@ | ||
| <h2>Linear Vertical Stepper Demo</h2> | ||
| <form [formGroup]="formGroup" novalidate> | ||
|
||
| <div formArrayName="formArray"> | ||
|
||
| <md-vertical-stepper> | ||
| <md-step> | ||
| <div [formGroupName]="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>Next</button> | ||
| </div> | ||
| </div> | ||
| </md-step> | ||
|
|
||
| <md-step [disabled]="!formGroup.controls.formArray.controls[0].valid"> | ||
|
||
| <div [formGroupName]="1"> | ||
| <ng-template mdStepLabel> | ||
| <div>Fill out your phone number</div> | ||
| </ng-template> | ||
| <md-input-container> | ||
| <input mdInput placeholder="Phone number" formControlName="phoneFormCtrl" required> | ||
| <md-error>This field is required</md-error> | ||
| </md-input-container> | ||
| <div> | ||
| <button md-button mdStepperPrevious>Back</button> | ||
| <button md-button mdStepperNext>Next</button> | ||
| </div> | ||
| </div> | ||
| </md-step> | ||
|
|
||
| <md-step [disabled]="!formGroup.controls.formArray.controls[1].valid"> | ||
|
||
| <ng-template mdStepLabel>Confirm your information</ng-template> | ||
| Everything seems correct. | ||
| <div> | ||
| <button md-button>Done</button> | ||
| </div> | ||
| </md-step> | ||
| </md-vertical-stepper> | ||
| </div> | ||
| </form> | ||
|
|
||
| <h2>Horizontal Stepper Demo</h2> | ||
| <md-horizontal-stepper> | ||
| <md-step *ngFor="let step of steps" [label]="step.label"> | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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], | ||
| }) | ||
| ]) | ||
| }); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 <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 = | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. FYI, #6147 should make this easier
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I saw the PR, but should this change be made after that PR is merged into master and 'stepper' branch has synced to upstream master?
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yep, just an fyi |
||
| errorOptions ? errorOptions.errorStateMatcher || defaultErrorStateMatcher | ||
| : defaultErrorStateMatcher; | ||
| } | ||
|
|
||
| /** Custom error state matcher that additionally checks for validity of interacted form. */ | ||
| errorStateMatcher = (control: FormControl, form: FormGroupDirective | NgForm) => { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The class should
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Does it work if this is a prototype method rather than a property?
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No because the |
||
| let originalErrorState = this._originalErrorStateMatcher(control, form); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you add a comment that explains the background for why we are doing this custom error matcher stuff? (i.e., everything we talked about in the meeting) |
||
| 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>; | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure about this, depending on how they've set up their forms they might want this button to trigger submission
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe we could add guidance to the docs instead?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I removed
'type': 'button'. I'll add it to the docs instead as @kara suggested.