4
votes

As the title says, I am using https://material.angular.io/components/bottom-sheet/overview. When the bottom sheet is open and the user clicks on the back button of his browser he is being navigated away from the page that opened the Bottom Sheet. Instead I would simply like to to close the Bottom Sheet on 'browser back'. What would be the best way (if any) to achieve this in Angular 5/6?

5

5 Answers

2
votes

I worked around this by using a "fake route" via "#" link:

this.location.go("#");
let sheet = this.bottomSheet.open(MyBottomSheetComponent, {
  data: { someData: someData },
});
let subscription = this.location.subscribe(x => {
  if (x.url === 'this_is_the_URL_you_are_coming_from') {
    sheet.dismiss(true);
  } else {
    subscription.unsubscribe();
  }
});
sheet.afterDismissed().subscribe(x => {
  if (!x) {
    this.location.back();
  }
});

This "technique" can also be used for angular material's dialog component. It doesn't look very idiomatic to me, so if someone has an easier way of doing this, please post an answer!

2
votes

Here i am using angular 6.

To close bottom sheet :-

  1. import { MatBottomSheet } from '@angular/material';

  2. Add bottom sheet in your constructor

constructor(private bottomSheet: MatBottomSheet) { } 3.

associateAthlete(participant: any) {
    this.bottomSheet._openedBottomSheetRef = 
           this.bottomSheet.open(AssociateAthleteComponent,
             { data: { member: participant, organizations: 
              this.ogranizationsWithoutZeroIndex }, disableClose: true });


          //after closing the bottom sheet afterDismissed function will be fired.

                this.bottomSheet._openedBottomSheetRef.afterDismissed().subscribe((data) 
              => {
                  alert('dismissed');
             })
}

That's it. Once bottom sheet opens and you close it afterDismissed() will be fired.

1
votes

Using angular 10, you can use the dismiss() method of MatBottomSheetRef. It is documented here: https://material.angular.io/components/bottom-sheet/api

import { MatBottomSheet } '@angular/material/bottom-sheet';

class MyClass{
  constructor(private bottomSheet: MatBottomSheet){
  }

  context(){
    // opening bottomSheet
    let sheetRef = this.bottomSheet.open(MyComponent);
    // closing bottomSheet
    sheetRef.dismiss();
  }
}
0
votes

There is another way to handle back key in and dismiss the BottomSheet on the back press, It has been tested in Angular 10, browser, and Android device, I think it will work for all Angular versions.

export class BottomSheetWidgetComponent implements OnInit {

  constructor(
    @Inject(MAT_BOTTOM_SHEET_DATA) private _data: any,
    private sheet: MatBottomSheetRef<BottomSheetWidgetComponent>

  ) {
    super(injector);
  }

  ngOnInit() {
    this.handleBackKey();
  }

  handleBackKey() {
    window.history.pushState(null, "Back", window.location.href);

    this.sheet.afterDismissed().subscribe((res) => {
      window.onpopstate = null;
      window.history.go(-1);
    });

    window.onpopstate = () => {
      this.sheet.dismiss();
      window.history.pushState(null, "Back", window.location.href);
    };
  }
 
}
0
votes

This is a much, much more complicated problem than I first thought. To get the exact behaviour, changes were need in multiple places. Please follow along carefully.

Problem Statement:

  1. Detect browser back/forward button press, including programmatic usage of history.back/forward/go, etc.
  2. When detected, gracefully dismiss the bottom sheet
  3. Undo changes to the browser history made by the button press without affecting the router/components, or refetching data, or triggering other side-effects, etc, etc

Assumptions

  1. app.module.ts
    @NgModule({
      declarations: [
        MyPageComponent,
        MyBottomSheetComponent
      ],
      imports: [
        RouterModule.forRoot([
          {
             path: 'my-page',
             component: MyPageComponent
          }
        ], {
          // this is the default for this setting, so you don't need to type it out
          // I have only mentioned it to indicate that this is very important.
          // Setting 'reload' instead of 'ignore' will not work with the solution I have made
          onSameUrlNavigation: 'ignore'
        }),
    
        MatBottomSheetModule,
        //... other imports
      ]
    })
    export class AppModule {};
    
  2. The MyPageComponent is responsible for opening the bottom sheet.
  3. The bottom sheet is going to be an instance of MyBottomSheetComponent.
  4. The router config

Step 1: Detect browser back/forward

This step is useful in any use case where you want to keep track of the history changes, not just for bottom sheet.

First, there is no way to detect the browser button presses that I know of, Nor is there a way to preventDefault on them. Once pressed, they will compulsory change your position in the history stack. Additionally, the window.onpopstate event will be fired not only for user interactions but also for programmatically invoking the history API calls.

Second, the Angular router assigns a navigationId to each navigation, starting from 1, and incementing on each navigation. Even if you go back, the navigationId increases by 1. Also, when you refresh twice successively, this navigationId resets to 1.

Given the above information, we can use a similar approach to angular - we use a custom constNavId to track the changes to the history stack, except that it stays unchanged for any given history entry and is resilient to browser refreshes.

app.component.ts
import { Router } from '@angular/router';

@Component(
  //...
)
export class AppComponent {
  constructor(
    router: Router
  ){}

  ngOnInit() {
    this.router.events.pipe(
      map(event => {
        switch(true) {
          // hook into the NavigationStart router event
          case event instanceof NavigationStart:
            const nav = this.router.getCurrentNavigation();
            // get the state from previous navigation
            // if null, check history.state to cover cover cases of browser refresh
            const prevState = nav.previousNavigation?.extras.state ?? history.state;
            // if angular's 'navigationId' is still there
            // this means angular did not initiate this navigation
            // must be a page refresh, or coming from browser history page
            const isPageRefresh = prevState && 'navigationId' in prevState;

            // if state exists, make no changes
            // if state doesn't exist increment by one,
            // but don't increment if it is a page refresh
            nav.extras.state = nav.extras.state ?? {
              constNavId: (prevState?.constNavId ?? 0) + !isPageRefresh
            }

            // we'll also store the direction and quantity of history jumps 
            // for example, if the navigation was triggered by history.go(3),
            // or the user long-pressed the browser back button and chose an item 3 places past in the history
            // we'll use a custom property 'popDirection' for this purpose
            if (nav.trigger === 'popstate') {
              nav.extras.state.popDirection = nav.extras.state.constNavId - prevState.constNavId;
            }
            // BUT delete the 'popDirection' if it came pre-recorded from a non-popstate navigation
            // like if user is coming from the full browser history page
            else
              delete nav.extras.state.popDirection;

            // if needed, update history.state so that double-refresh does not cause loss of state
            if (isPageRefresh) {
              // I change the page title elsewhere in my code, so I pass null
              // make changes according to how you process page title
              history.replaceState(nav.extras.state, null)
            }

            // log the nav state to be able to conveniently verify that it works
            console.log(nav.extras.state);
          
            break;
        
          // extra tip: hook other router events inside this switch case 
          // if you need to hook into them, instead of multiple subscriptions

          default: break;
        }
      })
    )
    .subscribe()
  }
}

After this, start from a new tab, and navigate along your website - you should see the log reflecting the index of the history stack you are on, and the popDirection if any. Remember, negative popDirection means you went backward, and positive popDirection means you went forwards.

Step 2: Gracefully dismiss the bottom sheet

Whenever any navigation occurs, the bottom sheet closes without any animation. It's very sudden and does not match the smooth entry animation. To remedy this, we need to:

my-page.component.ts
@Component(
  //...
)
export class MyPageComponent {
  constructor(
    private bSheet: MatBottomSheet
  ) {}

  // called from template or your component code,
  // as per your requirement
  openBottomSheet() {
    this.bSheet.open(MyBottomSheetComponent, {
      //setting this property will enable us to take 
      //complete manual control of the dismissal of the bottom sheet
      closeOnNavigation: false
    });
  }
}

Step 3: Override the angular router navigation, and reverse changes made to the history stack

We are going to use a CanDeactivate route guard for this. Since the behaviour I want is specific to MyPageComponet, I am going to put it in the same file (also because CanDeactivate is generic).

my-page.component.ts
//IMPORTANT: don't confuse this with the browser Location API
import { Location } from '@angular/common';
import { Router } from '@angular/router';

export class MyPageComponent {
  //...
}

// explicit naming to prevent confusion with other guards I may use for other Bottom sheets
export class Disable_Navigation_While_MyBottomSheet_Is_Open implements CanDeactivate<MyPageComponent> {
  constructor(
    private bSheet: MatBottomSheet,
    private router: Router
    private location: Location, 
  ) {}

  async canDeactivate(
    component: MenuListPageComponent,
    currentRoute: ActivatedRouteSnapshot,
    currentState: RouterStateSnapshot,
    nextState: RouterStateSnapshot
  ) {
    const nav = this.router.getCurrentNavigation();

    // only process further if this was triggered by popstate,
    // otherwise just let the navigation go ahead normally
    if (nav.trigger === 'popstate') {
      const sheet = this.bSheet._openedBottomSheetRef?.instance;
      // check if a bottom sheet is open, and if open
      // check if it is the bottom sheet we are interested in
      if (!!sheet && sheet instanceof MyBottomSheetComponent) {
        // since this was a popstate event, 
        // the history change has already been done by the browser
        // we should reverse that - we can use 'popDirection' from Step 1 to help
        // simply add a negative sign to reverse the popDirection and pass it to the API
        this.location.historyGo(-nav.extras.state.popDirection);

        // dismiss the sheet
        this.bSheet.dismiss();

        // deliberately wait for the 'location.historyGo' to trigger NavigationCancel
        // this is to break a race condition which may cause the resolvers
        // of the cancelled navigation to execute
        await new Promise<void>(resolve => setTimeout(resolve, 250))
      }
    }
    return true;
  }
}

Finally, do not forget to provide this in the module, and update the path configuration:

app.module.ts
@NgModule({
  imports: [
    RouterModule.forRoot([
      {
        path: 'my-page',
        component: MyPageComponent,
        canDeactivate: [Disable_Navigation_While_MyBottomSheet_Is_Open]
      }
    ])
  ]
  providers: [
    { provide: Disable_Navigation_While_MyBottomSheet_Is_Open }
  ]
})
export class AppComponent {}