Moving Elements Around the Snackbar with Angular

While looking through StackOverflow (SO), I came across a post where a user wanted to move some tabs down when a snackbar notice popped up, or in this case down. While this type of UI trick is frowned upon by the folks at Google (snackbars should be at the bottom, don’t move other elements around snackbars) it seemed like a simple but interesting problem to solve.

The solution turned out to be very simple. What we came up with is to add some css to the element we want to move. We even used Angular’s example code for the snackbar, mostly out of the box, from here. Getting to the solution took some time, as my mind immediately went to all kinds of fancy code, and interacting with the cdk-overlay. Let’s look at how I approached this.

The solution

The CSS is really where the magic is. And yes, all we are doing is adding a margin-top to the element we are moving. We add the amount of height of the snackbar we are popping up. This is it. We’ll look at the code that does this in a moment.

.moved {
    margin-top: 5em;
}

The HTML for our parent component shows a form field where you can enter the duration the SnackBar appears. We would typically hard code that into our component, but wanted to show the length of time is of no consequence. We then have a plain old button that calls our openSnackBar() method, which we’ll look at later. There’s nothing special about the parent page, or the popup which we’ll see in a moment. One thing you will probably want to do is add an id or class to the element(s) you want to move to find it in your HTML easier.

<div>
	<mat-form-field>
		<mat-label>Snack bar duration (seconds)</mat-label>
		<input type="number" [(ngModel)]="durationInSeconds" matInput>
</mat-form-field>

		<button mat-button (click)="openSnackBar()" aria-label="Show an example snack-bar">
  Show Snack
</button>
</div>

The HTML for the popup is very simple here. It shows the text “Pizza party!!!” with an icon.

<span class="example-pizza-party">
  Pizza party!!! 🍕
</span>

Now we’ll look at our component.ts file. We’ll start with the PizzaPartyComponent. The component is there to show the content of our SnackBar, and nothing else. We could do a bit more interactive bits there, including injecting data, but in this case it just shows a static message. For more details on building up and testing the SnackBar, see our post here.

@Component({
  selector: 'snack-bar-component-example-snack',
  templateUrl: 'snack-bar-component-example-snack.html',
  styles: [`
    .example-pizza-party {
      color: hotpink;
    }
  `],
})
export class PizzaPartyComponent {}

Now on to the first part of the code magic of the solution. Here we use standard code to show our snackbar, with our duration taken from our input. We then subscribe to the snackbar’s afterDismissed action, and call our own afterDismissed() method, which we’ll see in a moment.

The code that handles moving the element, finds the element we want to move and adds our ‘moved’ css class to it. Really? That’s it? Yep, it is that simple.

  openSnackBar() {
    const ourSnackbar = this._snackBar.openFromComponent(PizzaPartyComponent, {
      duration: this.durationInSeconds * 1000,
      verticalPosition: 'top'
    });

    ourSnackbar.afterDismissed().subscribe(() => this.afterDismissed());

    let elements = document.getElementsByTagName("div");
    Array.from(elements).forEach(element => {
      element.classList.add('moved');
    });
  }

Here is the code that’s called after the snackbar is dismissed, either through a click or through a timeout. The code goes in, finds our elements again and removes the ‘moved’ css class.

  afterDismissed() {
    let elements = document.getElementsByTagName("div");

    Array.from(elements).forEach(element => {
      element.classList.remove('moved');
    });
  }

Here is the full component.ts code.

import {Component} from '@angular/core';
import {MatSnackBar} from '@angular/material/snack-bar';

@Component({
  selector: 'snack-bar-component-example',
  templateUrl: 'snack-bar-component-example.html',
  styleUrls: ['snack-bar-component-example.css'],
})
export class SnackBarComponentExample {
  durationInSeconds = 5;

  constructor(private _snackBar: MatSnackBar) {}

  openSnackBar() {
    const ourSnackbar = this._snackBar.openFromComponent(PizzaPartyComponent, {
      duration: this.durationInSeconds * 1000,
      verticalPosition: 'top'
    });

    ourSnackbar.afterDismissed().subscribe(() => this.afterDismissed());

    let elements = document.getElementsByTagName("div");
    Array.from(elements).forEach(element => {
      element.classList.add('moved');
    });
  }

  afterDismissed() {
    let elements = document.getElementsByTagName("div");

    Array.from(elements).forEach(element => {
      element.classList.remove('moved');
    });
  }
}


@Component({
  selector: 'snack-bar-component-example-snack',
  templateUrl: 'snack-bar-component-example-snack.html',
  styles: [`
    .example-pizza-party {
      color: hotpink;
    }
  `],
})
export class PizzaPartyComponent {}

Wrapup

The user was ultimately looking for an example in pure javascript, and while I wrote this Angular-centric, the idea behind it, and even some of the functionality that drives it, can be used anywhere javascript is used.

I enjoyed finding a solution to this problem, as it forced me to think simply, which I don’t always do.

Find the stackblitz here and the SO post here.