8000 feat(stepper): allow for orientation to be changed dynamically by crisbeto · Pull Request #9173 · angular/components · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

feat(stepper): allow for orientation to be changed dynamically #9173

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

Closed
wants to merge 1 commit into from

Conversation

crisbeto
Copy link
Member
  • Turns the MatStepper into a proper component that allows consumers to switch between horizontal and vertical dynamically, allowing for use cases like having a different layout depending on the screen size.
  • Combines the mat-vertical-stepper and mat-horizontal-stepper templates into a single file to avoid all the code duplication.

Relates to #7700.

@crisbeto crisbeto requested a review from mmalerba as a code owner December 30, 2017 19:41
@googlebot googlebot added the cla: yes PR author has agreed to Google's Contributor License Agreement label Dec 30, 2017
@mmalerba
Copy link
Contributor

I prefer this approach, but @jelbourn is the one who pushed for separate horizontal and vertical components, so I'd like to hear what he thinks

* Turns the `MatStepper` into a proper component that allows consumers to switch between `horizontal` and `vertical` dynamically, allowing for use cases like having a different layout depending on the screen size.
* Combines the `mat-vertical-stepper` and `mat-horizontal-stepper` templates into a single file to avoid all the code duplication.

Relates to angular#7700.
@crisbeto crisbeto force-pushed the 7700/stepper-orientation branch from acdd568 to 3655323 Compare December 31, 2017 06:06
@@ -40,7 +42,7 @@ <h3>Linear Vertical Stepper Demo using a single form</h3>
<button mat-button>Done</button>
</div>
</mat-step>
</mat-vertical-stepper>
</mat-stepper>
</form>

<h3>Linear Horizontal Stepper Demo using a different form for each step</h3>
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The rest of this file should be updated with the new stepper.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not necessarily. The separate horizontal/vertical steppers are still valid use cases.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got it, that makes sense.

@jelbourn
Copy link
Member
jelbourn commented Jan 3, 2018

So, my reason for wanting to make separate steppers was that there can be more than just the two vertical and horizontal variants; there are a couple other layouts some Google applications use. Baking them all into one would balloon the complexity.

What if we did some kind of dynamic stepper that delegates between them for you while still maintaining the separate implementations as standalone components?

@crisbeto
Copy link
Member Author
crisbeto commented Jan 3, 2018

That would probably end up being an ngSwitch between the different variants anyway (which is what it does here) @jelbourn. I went with this approach, because at least it allows us to re-use some of the templating for the individual steps.

@mmalerba
Copy link
Contributor

needs rebase

@jelbourn
Copy link
Member

Thinking about this more, there are upcoming Angular features that would make this easier / better optimizable. We might want to wait for that (probably v7) before committing to this.

@crisbeto crisbeto added blocked This issue is blocked by some external factor, such as a prerequisite PR target: major This PR is targeted for the next major release labels Jan 26, 2018
@danielpiedra
Copy link

@jelbourn is still the idea to wait until angular v7?

@jelbourn
Copy link
Member
jelbourn commented Feb 5, 2018

It's not set in stone, but there are potential features on the Angular roadmap that would make this a lot easier to do (and in such a way that it can be optimized better).

@danielpiedra
Copy link

reasonable enough, we wait. thanks.

@paulogr
Copy link
paulogr commented Mar 5, 2018

What about change the horizontal to work like in guidelines on mobile?

https://material.io/guidelines/components/steppers.html#steppers-types-of-steps

Is that the right approach?

@GuerricPhalippou
Copy link

v7 is out! Are there news about this issue?

@GuerricPhalippou

This comment has been minimized.

@nomanbiniqbal

This comment has been minimized.

@eduardoschonhofen
Copy link

Hi! Any news? This would be really useful,since using *ngIf to switch between horizontal and vertical make duplicates of the components in the steps.

@ManuelGraf
Copy link
ManuelGraf commented Apr 4, 2019

come on... This has been a problem for a super long time and is still not adressed? its like you dont want people to use angular material.... will this be addressed in 8.0 beta at least?
having this as seperate components makes no sense. If there are other steppers you want to offer, make components for them as soon as thyre supported. the regular one-direction stepper shoul have two orientations (RESPONSIVENESS!). I can't think of any reason not to do that.
If it was at least possible to just use templateoutet and templateref - those would suffice to fix the issue that if i want to change orientation of the stepper during runtime i have to duplicate the whole content. this is BS!

@mmalerba mmalerba added aaa and removed aaa labels Apr 25, 2019
@grant77
Copy link
grant77 commented Apr 26, 2019

I literally just finished writing this and, y 8000 es, it is a bit hacky. This has not been tested at all. Use at own risk.

import { Directionality } from '@angular/cdk/bidi';
import { CdkStep, StepperSelectionEvent } from '@angular/cdk/stepper';
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ContentChildren, ElementRef, EventEmitter, forwardRef, Inject, Input, Optional, Output, QueryList, ViewChildren } from '@angular/core';
import { MatStep, MatStepper } from '@angular/material';
import { DOCUMENT } from '@angular/platform-browser';

const MAT_STEPPER_PROXY_FACTORY_PROVIDER = {
    provide: MatStepper,
    deps: [forwardRef(() => StepperComponent), [new Optional(), Directionality], ChangeDetectorRef, [new Inject(DOCUMENT)]],
    useFactory: MAT_STEPPER_PROXY_FACTORY
};

export function MAT_STEPPER_PROXY_FACTORY(component: StepperComponent, directionality: Directionality,
    changeDetectorRef: ChangeDetectorRef, docuement: Document) {
    // We create a fake stepper primarily so we can generate a proxy from it.  The fake one, however, is used until 
    // our view is initialized.  The reason we need a proxy is so we can toggle between our 2 steppers 
    // (vertical and horizontal) depending on  our "orientation" property.  Probably a good idea to include a polyfill 
    // for the Proxy class: https://github.com/GoogleChrome/proxy-polyfill.

    const elementRef = new ElementRef(document.createElement('mat-horizontal-stepper'));
    const stepper = new MatStepper(directionality, changeDetectorRef, elementRef, document);
    return new Proxy(stepper, {
        get: (target, property) => Reflect.get(component.stepper || target, property),
        set: (target, property, value) => Reflect.set(component.stepper || target, property, value)
    });
}

@Component({
    selector: 'app-stepper',
    // templateUrl: './stepper.component.html',
    // styleUrls: ['./stepper.component.scss'],
    changeDetection: ChangeDetectionStrategy.OnPush,
    providers: [MAT_STEPPER_PROXY_FACTORY_PROVIDER],
    template: `
<ng-container [ngSwitch]="orientation">
    <mat-horizontal-stepper *ngSwitchCase="'horizontal'"
                            [labelPosition]="labelPosition"
                            [linear]="linear"
                            [selected]="selected"
                            [selectedIndex]="selectedIndex"
                            (animationDone)="animationDone.emit($event)"
                            (selectionChange)="selectionChange.emit($event)">
    </mat-horizontal-stepper>


    <mat-vertical-stepper *ngSwitchDefault
                            [linear]="linear"
                            [selected]="selected"
                            [selectedIndex]="selectedIndex"
                            (animationDone)="animationDone.emit($event)"
                            (selectionChange)="selectionChange.emit($event)">
    </mat-vertical-stepper>
</ng-container>
`
})
export class StepperComponent {
    // public properties
    @Input() labelPosition?: 'bottom' | 'end';
    @Input() linear?: boolean;
    @Input() orientation?: 'horizontal' | 'vertical';
    @Input() selected?: CdkStep;
    @Input() selectedIndex?: number;

    // public events
    @Output() animationDone = new EventEmitter<void>();
    @Output() selectionChange = new EventEmitter<StepperSelectionEvent>();

    // internal properties
    @ViewChildren(MatStepper) stepperList!: QueryList<MatStepper>;
    @ContentChildren(MatStep) steps!: QueryList<MatStep>;
    get stepper(): MatStepper { return this.stepperList && this.stepperList.first; }

    // private properties
    private lastSelectedIndex?: number;
    private needsFocus = false;
    
    // public methods
    constructor(private changeDetectorRef: ChangeDetectorRef) { }
    ngAfterViewInit() {
        this.reset();
        this.stepperList.changes.subscribe(() => this.reset());
        this.selectionChange.subscribe((e: StepperSelectionEvent) => this.lastSelectedIndex = e.selectedIndex);
    }
    ngAfterViewChecked() {
        if (this.needsFocus) {
            this.needsFocus = false;
            const { _elementRef, _keyManager, selectedIndex } = <any>this.stepper;
            _elementRef.nativeElement.focus();
            _keyManager.setActiveItem(selectedIndex);
        }
    }

    // private properties
    private reset() {
        const { stepper, steps, changeDetectorRef, lastSelectedIndex } = this;
        stepper.steps.reset(steps.toArray());
        stepper.steps.notifyOnChanges();
        if (lastSelectedIndex) {
            stepper.selectedIndex = lastSelectedIndex;
        }

        Promise.resolve().then(() => {
            this.needsFocus = true;
            changeDetectorRef.markForCheck();
        });
    }
}

@jrcasso
Copy link
jrcasso commented Jul 14, 2019

We're on Angular 8 now. I think it's time to revisit this functionality.

Right now, people are wanting different stepper layouts depending on device orientation or screen size. But the child component data gets lost during the transition with a simple switch between the two.

Frankly, I think it's odd that you went this direction in the first place. It may have been a complex component, but to think that this wouldn't be a desired behavior for the component on mobile devices is shortsighted. Optimization should have been secondary to functionality expectations.

@davideas
Copy link

@grant77, currently we receive this error when using it:
No provider for CdkStepper!.
Do we need some provider declaration?

And now DOCUMENT is now: import { DOCUMENT } from '@angular/common';

@grant77
Copy link
grant77 commented Nov 1, 2019

@davideas, Here is an updated version:

import { Directionality } from '@angular/cdk/bidi';
import { CdkStep, StepperSelectionEvent, CdkStepper } from '@angular/cdk/stepper';
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ContentChildren, ElementRef, EventEmitter, forwardRef, Inject, Input, Optional, Output, QueryList, ViewChildren, TypeProvider, Type } from '@angular/core';
import { MatStep, MatStepper } from '@angular/material';
import { DOCUMENT } from '@angular/common';

const MAT_STEPPER_PROXY_FACTORY_PROVIDER = {
  provide: MatStepper,
  deps: [forwardRef(() => StepperComponent), [new Optional(), Directionality], ChangeDetectorRef, [new Inject(DOCUMENT)]],
  useFactory: MAT_STEPPER_PROXY_FACTORY
};

const CDK_STEPPER_PROXY_FACTORY_PROVIDER = { ...MAT_STEPPER_PROXY_FACTORY_PROVIDER, provide: CdkStepper }

export function MAT_STEPPER_PROXY_FACTORY(component: StepperComponent, directionality: Directionality,
  changeDetectorRef: ChangeDetectorRef, docuement: Document) {
  // We create a fake stepper primarily so we can generate a proxy from it.  The fake one, however, is used until 
  // our view is initialized.  The reason we need a proxy is so we can toggle between our 2 steppers 
  // (vertical and horizontal) depending on  our "orientation" property.  Probably a good idea to include a polyfill 
  // for the Proxy class: https://github.com/GoogleChrome/proxy-polyfill.

  const elementRef = new ElementRef(document.createElement('mat-horizontal-stepper'));
  const stepper = new MatStepper(directionality, changeDetectorRef, elementRef, document);
  return new Proxy(stepper, {
    get: (target, property) => Reflect.get(component.stepper || target, property),
    set: (target, property, value) => Reflect.set(component.stepper || target, property, value)
  });
}

@Component({
  selector: 'app-stepper',
  // templateUrl: './stepper.component.html',
  // styleUrls: ['./stepper.component.scss'],
  changeDetection: ChangeDetectionStrategy.OnPush,
  providers: [
    MAT_STEPPER_PROXY_FACTORY_PROVIDER,
    CDK_STEPPER_PROXY_FACTORY_PROVIDER
  ],
  template: `
<ng-container [ngSwitch]="orientation">
    <mat-horizontal-stepper *ngSwitchCase="'horizontal'"
                            [labelPosition]="labelPosition"
                            [linear]="linear"
                            [selected]="selected"
                            [selectedIndex]="selectedIndex"
                            (animationDone)="animationDone.emit($event)"
                            (selectionChange)="selectionChange.emit($event)">
    </mat-horizontal-stepper>


    <mat-vertical-stepper *ngSwitchDefault
                            [linear]="linear"
                            [selected]="selected"
                            [selectedIndex]="selectedIndex"
                            (animationDone)="animationDone.emit($event)"
                            (selectionChange)="selectionChange.emit($event)">
    </mat-vertical-stepper>
</ng-container>
`
})
export class StepperComponent {
  // public properties
  @Input() labelPosition?: 'bottom' | 'end';
  @Input() linear?: boolean;
  @Input() orientation?: 'horizontal' | 'vertical';
  @Input() selected?: CdkStep;
  @Input() selectedIndex?: number;

  // public events
  @Output() animationDone = new EventEmitter<void>();
  @Output() selectionChange = new EventEmitter<StepperSelectionEvent>();

  // internal properties
  @ViewChildren(MatStepper) stepperList!: QueryList<MatStepper>;
  @ContentChildren(MatStep) steps!: QueryList<MatStep>;
  get stepper(): MatStepper { return this.stepperList && this.stepperList.first; }

  // private properties
  private lastSelectedIndex?: number;
  private needsFocus = false;

  // public methods
  constructor(private changeDetectorRef: ChangeDetectorRef) { }
  ngAfterViewInit() {
    this.reset();
    this.stepperList.changes.subscribe(() => this.reset());
    this.selectionChange.subscribe((e: StepperSelectionEvent) => this.lastSelectedIndex = e.selectedIndex);
  }
  ngAfterViewChecked() {
    if (this.needsFocus) {
      this.needsFocus = false;
      const { _elementRef, _keyManager, selectedIndex } = <any>this.stepper;
      _elementRef.nativeElement.focus();
      _keyManager.setActiveItem(selectedIndex);
    }
  }

  // private properties
  private reset() {
    const { stepper, steps, changeDetectorRef, lastSelectedIndex } = this;
    stepper.steps.reset(steps.toArray());
    stepper.steps.notifyOnChanges();
    if (lastSelectedIndex) {
      stepper.selectedIndex = lastSelectedIndex;
    }

    Promise.resolve().then(() => {
      this.needsFocus = true;
      changeDetectorRef.markForCheck();
    });
  }
}

@davideas
Copy link
davideas commented Nov 2, 2019

@grant77 Thank you, it works really great! I created a gist with some more fixes and a nice feature:

Angular 8.x Responsive Stepper with headers disable feature

responsive-stepper.component.ts

@hanktrizz
Copy link

Hi, it's v10 now. I'm just wondering what the status of this feature is in the pipeline? It'll be nice to know if it will be supported or not at all as it'll provide some closure rather than waiting for updates to this issue endlessly.

@maykon-oliveira

This comment has been minimized.

@ChristianHardy

This comment has been minimized.

crisbeto added a commit to crisbeto/material2 that referenced this pull request Mar 7, 2021
Combines `mat-vertical-stepper` and `mat-horizontal-stepper` into a single `mat-stepper`
class in order to allow for the orientation to be changed dynamically. Also deprecates
`MatVerticalStepper` and `MatHorizontalStepper`.

This is a reimplementation of angular#9173, however this time I took a different approach which should
make it easier to maintain and eventually remove the two separate steppers. It should result in a
smaller bundle as well. The main differences are:

1. Rather than have 3 components (`MatStepper`, `MatVerticalStepper` and `MatHorizontalStepper`),
these changes combine everything into `MatStepper` while `MatVerticalStepper` and
`MatHorizontalStepper` are only used as injection tokens for backwards compatibility. The `selector`
and `exportAs` of `MatStepper` is changed to match the two individual steppers and the orientation
is inferred from the tag name. This will make it much easier to remove the deprecated directives.
Furthermore, it should result in a smaller bundle since the template and styles only need to be
inlined in one place.
2. `MatVerticalStepper` and `MatHorizontalStepper` are turned into very basic directives that have
the same public API as `MatStepper` and they proxy everything to it. This is primarily so that if
somebody managed to get a hold of a `MatVerticalStepper` or `MatHorizontalStepper` instance, or
they used the old classes to type their own code, it wouldn't result in a breaking change.

Relates to angular#7700.
@crisbeto
Copy link
Member Author
crisbeto commented Mar 7, 2021

We've decided to revisit the feature, however rebasing this PR is going to take a while and there are better ways of achieving the same result now. I'm closing it in favor of #22139.

@crisbeto crisbeto closed this Mar 7, 2021
crisbeto added a commit to crisbeto/material2 that referenced this pull request Mar 7, 2021
Combines `mat-vertical-stepper` and `mat-horizontal-stepper` into a single `mat-stepper`
class in order to allow for the orientation to be changed dynamically. Also deprecates
`MatVerticalStepper` and `MatHorizontalStepper`.

This is a reimplementation of angular#9173, however this time I took a different approach which should
make it easier to maintain and eventually remove the two separate steppers. It should result in a
smaller bundle as well. The main differences are:

1. Rather than have 3 components (`MatStepper`, `MatVerticalStepper` and `MatHorizontalStepper`),
these changes combine everything into `MatStepper` while `MatVerticalStepper` and
`MatHorizontalStepper` are only used as injection tokens for backwards compatibility. The `selector`
and `exportAs` of `MatStepper` is changed to match the two individual steppers and the orientation
is inferred from the tag name. This will make it much easier to remove the deprecated directives.
Furthermore, it should result in a smaller bundle since the template and styles only need to be
inlined in one place.
2. `MatVerticalStepper` and `MatHorizontalStepper` are turned into very basic directives that have
the same public API as `MatStepper` and they proxy everything to it. This is primarily so that if
somebody managed to get a hold of a `MatVerticalStepper` or `MatHorizontalStepper` instance, or
they used the old classes to type their own code, it wouldn't result in a breaking change.

Relates to angular#7700.
crisbeto added a commit to crisbeto/material2 that referenced this pull request Mar 7, 2021
Combines `mat-vertical-stepper` and `mat-horizontal-stepper` into a single `mat-stepper`
class in order to allow for the orientation to be changed dynamically. Also deprecates
`MatVerticalStepper` and `MatHorizontalStepper`.

This is a reimplementation of angular#9173, however this time I took a different approach which should
make it easier to maintain and eventually remove the two separate steppers. It should result in a
smaller bundle as well. The main differences are:

1. Rather than have 3 components (`MatStepper`, `MatVerticalStepper` and `MatHorizontalStepper`),
these changes combine everything into `MatStepper` while `MatVerticalStepper` and
`MatHorizontalStepper` are only used as injection tokens for backwards compatibility. The `selector`
and `exportAs` of `MatStepper` is changed to match the two individual steppers and the orientation
is inferred from the tag name. This will make it much easier to remove the deprecated directives.
Furthermore, it should result in a smaller bundle since the template and styles only need to be
inlined in one place.
2. `MatVerticalStepper` and `MatHorizontalStepper` are turned into very basic directives that have
the same public API as `MatStepper` and they proxy everything to it. This is primarily so that if
somebody managed to get a hold of a `MatVerticalStepper` or `MatHorizontalStepper` instance, or
they used the old classes to type their own code, it wouldn't result in a breaking change.

Relates to angular#7700.
crisbeto added a commit to crisbeto/material2 that referenced this pull request Mar 12, 2021
Combines `mat-vertical-stepper` and `mat-horizontal-stepper` into a single `mat-stepper`
class in order to allow for the orientation to be changed dynamically. Also deprecates
`MatVerticalStepper` and `MatHorizontalStepper`.

This is a reimplementation of angular#9173, however this time I took a different approach which should
make it easier to maintain and eventually remove the two separate steppers. It should result in a
smaller bundle as well. The main differences are:

1. Rather than have 3 components (`MatStepper`, `MatVerticalStepper` and `MatHorizontalStepper`),
these changes combine everything into `MatStepper` while `MatVerticalStepper` and
`MatHorizontalStepper` are only used as injection tokens for backwards compatibility. The `selector`
and `exportAs` of `MatStepper` is changed to match the two individual steppers and the orientation
is inferred from the tag name. This will make it much easier to remove the deprecated directives.
Furthermore, it should result in a smaller bundle since the template and styles only need to be
inlined in one place.
2. `MatVerticalStepper` and `MatHorizontalStepper` are turned into very basic directives that have
the same public API as `MatStepper` and they proxy everything to it. This is primarily so that if
somebody managed to get a hold of a `MatVerticalStepper` or `MatHorizontalStepper` instance, or
they used the old classes to type their own code, it wouldn't result in a breaking change.

Relates to angular#7700.
crisbeto added a commit to crisbeto/material2 that referenced this pull request Mar 12, 2021
Combines `mat-vertical-stepper` and `mat-horizontal-stepper` into a single `mat-stepper`
class in order to allow for the orientation to be changed dynamically. Also deprecates
`MatVerticalStepper` and `MatHorizontalStepper`.

This is a reimplementation of angular#9173, however this time I took a different approach which should
make it easier to maintain and eventually remove the two separate steppers. It should result in a
smaller bundle as well. The main differences are:

1. Rather than have 3 components (`MatStepper`, `MatVerticalStepper` and `MatHorizontalStepper`),
these changes combine everything into `MatStepper` while `MatVerticalStepper` and
`MatHorizontalStepper` are only used as injection tokens for backwards compatibility. The `selector`
and `exportAs` of `MatStepper` is changed to match the two individual steppers and the orientation
is inferred from the tag name. This will make it much easier to remove the deprecated directives.
Furthermore, it should result in a smaller bundle since the template and styles only need to be
inlined in one place.
2. `MatVerticalStepper` and `MatHorizontalStepper` are turned into very basic directives that have
the same public API as `MatStepper` and they proxy everything to it. This is primarily so that if
somebody managed to get a hold of a `MatVerticalStepper` or `MatHorizontalStepper` instance, or
they used the old classes to type their own code, it wouldn't result in a breaking change.

Relates to angular#7700.
annieyw pushed a commit that referenced this pull request Apr 2, 2021
…ly (#22139)

Combines `mat-vertical-stepper` and `mat-horizontal-stepper` into a single `mat-stepper`
class in order to allow for the orientation to be changed dynamically. Also deprecates
`MatVerticalStepper` and `MatHorizontalStepper`.

This is a reimplementation of #9173, however this time I took a different approach which should
make it easier to maintain and eventually remove the two separate steppers. It should result in a
smaller bundle as well. The main differences are:

1. Rather than have 3 components (`MatStepper`, `MatVerticalStepper` and `MatHorizontalStepper`),
these changes combine everything into `MatStepper` while `MatVerticalStepper` and
`MatHorizontalStepper` are only used as injection tokens for backwards compatibility. The `selector`
and `exportAs` of `MatStepper` is changed to match the two individual steppers and the orientation
is inferred from the tag name. This will make it much easier to remove the deprecated directives.
Furthermore, it should result in a smaller bundle since the template and styles only need to be
inlined in one place.
2. `MatVerticalStepper` and `MatHorizontalStepper` are turned into very basic directives that have
the same public API as `MatStepper` and they proxy everything to it. This is primarily so that if
somebody managed to get a hold of a `MatVerticalStepper` or `MatHorizontalStepper` instance, or
they used the old classes to type their own code, it wouldn't result in a breaking change.

Relates to #7700.
@angular-automatic-lock-bot
Copy link

This issue has been automatically locked due to inactivity.
Please file a new issue if you are encountering a similar or related problem.

Read more about our automatic conversation locking policy.

This action has been performed automatically by a bot.

@angular-automatic-lock-bot angular-automatic-lock-bot bot locked and limited conversation to collaborators Apr 7, 2021
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
blocked This issue is blocked by some external factor, such as a prerequisite PR cla: yes PR author has agreed to Google's Contributor License Agreement target: major This PR is targeted for the next major release
Projects
None yet
Development

Successfully merging this pull request may close these issues.

0