Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
Ben Nadel at cf.Objective() 2011 (Minneapolis, MN) with: Karsten Pearce
Ben Nadel at cf.Objective() 2011 (Minneapolis, MN) with: Karsten Pearce@vexeddeveloper )

Prevent Body Scrolling With A WindowScrolling Service When Showing A Modal Window In Angular 5.0.2

By Ben Nadel on

The other day, I came across an interesting post on David Walsh's blog about preventing body scrolling. In his article, he was using the CSS property, "overflow," to disable and re-enable scrollbars on the Body element. I had never seen this done before and it got me thinking about how I might implement this approach in an Angular 5 application. This is an especially interesting problem considering that the Body element is outside of the Angular 5 application boundary. To help keep my Angular components decoupled from the parent page, I've encapsulated the scrolling functionality inside a WindowScrolling service that can, itself, be consumed within the Angular component tree.


 
 
 

 
 
 
 
 

Run this demo in my JavaScript Demos project on GitHub.

A few weeks ago, I created an Angular attribute directly called [trapScroll] that would trap mouse wheel events and prevent scrolling in a parent element. This directive is intended for use in an "overflow: scroll" context; but, such an approach could also be used to prevent scrolling on the body element. However, an attribute directive isn't always so easy to apply. For example, in a routable view, there is no component element on which to bind the [trapScroll] directive.

As such, there are cases when it makes sense to provide an Angular service that specifically deals with the window / body scrolling functionality. For example, if I route to a Modal window, it would be nice to disable and then re-enable body scrolling when the modal window is opened and closed, respectively. This way, as the user is interacting with the modal window, they are not accidentally causing a scroll in the hidden (or mostly hidden) Body element.

To experiment with this idea, I created a WindowScrolling service that exposes two public methods:

  • enable()
  • disable()

The .disable() method prevents scrolling on the Body element by injecting a Style tag that assigns "overflow:hidden" to the Body. The .enable() method then re-enables scrolling on the Body by removing the aforementioned Style tag:

  • export class WindowScrolling {
  •  
  • private styleTag: HTMLStyleElement;
  •  
  • // I initialize the window-scrolling service.
  • // --
  • // CAUTION: This service makes direct references to the global DOCUMENT object.
  • // Theoretically, the Renderer2 service should be able to provide an API that would
  • // allow me to side-step direct DOM-references. However, the Renderer2 service cannot
  • // be injected directly into another Service - only into a Directive. As such, I'm
  • // just dropping all the pretenses and I'm using the document directly.
  • constructor() {
  •  
  • // Rather than directly overwriting the style of the BODY tag (which is dicey),
  • // we're going to inject a STYLE element that overrides the scroll behavior. This
  • // way we can add and remove the style in order to toggle the behavior.
  • this.styleTag = this.buildStyleElement();
  •  
  • }
  •  
  • // ---
  • // PUBLIC METHODS.
  • // ---
  •  
  • // I disable the scrolling feature on the main viewport.
  • public disable() : void {
  •  
  • document.body.appendChild( this.styleTag );
  •  
  • }
  •  
  •  
  • // I re-enable the scrolling feature on the main viewport.
  • public enable() : void {
  •  
  • document.body.removeChild( this.styleTag );
  •  
  • }
  •  
  • // ---
  • // PRIVATE METHODS.
  • // ---
  •  
  • // I build and return a Style element that will prevent scrolling on the body.
  • private buildStyleElement() : HTMLStyleElement {
  •  
  • var style = document.createElement( "style" );
  •  
  • style.type = "text/css";
  • style.setAttribute( "data-debug", "Injected by WindowScrolling service." );
  • style.textContent = `
  • body {
  • overflow: hidden !important ;
  • }
  • `;
  •  
  • return( style );
  •  
  • }
  •  
  • }

In this service, I'm making direct calls to the global document object. Generally speaking, this is frowned upon in the Angular world because it couples the application to the Browser environment (as opposed to the Server environment). I tried to use the Renderer2 service to maintain some degree of indirection; but, it seems that the Renderer2 service cannot be injected into a "Service" - it can only be injected into a Directive. Because of this, combined with the fact that I am completely befuddled by the idea of running an Angular app in any other context than the Browser, I decided to just reference the document directly and let my WindowService be the point of abstraction.

I can then inject this service into any one of my Angular components. In this demo, I've created a super simple modal window in my root component. When the root component opens the modal window, it's going to disable Body scrolling. And, when the root component closes the modal window, it's going to re-enable Body scrolling:

  • // Import the core angular services.
  • import { Component } from "@angular/core";
  •  
  • // Import the application components and services.
  • import { WindowScrolling } from "./window-scrolling";
  •  
  • // ----------------------------------------------------------------------------------- //
  • // ----------------------------------------------------------------------------------- //
  •  
  • @Component({
  • selector: "my-app",
  • styleUrls: [ "./app.component.less" ],
  • template:
  • `
  • <p>
  • <a (click)="toggleModal()" class="open">Open Modal Window</a>
  • </p>
  •  
  • <div *ngIf="isShowingModal" class="modal">
  • <div class="modal__content">
  •  
  • This is a modal &mdash; <a (click)="toggleModal()">Close the modal</a>
  •  
  • </div>
  • </div>
  •  
  • <p *ngFor="let i of offsets">
  • This is paragraph 1-{{ i }}.
  • </p>
  •  
  • <p>
  • <a (click)="toggleModal()" class="open">Open Modal Window</a>
  • </p>
  •  
  • <p *ngFor="let i of offsets">
  • This is paragraph 2-{{ i }}.
  • </p>
  •  
  • <p>
  • <a (click)="toggleModal()" class="open">Open Modal Window</a>
  • </p>
  • `
  • })
  • export class AppComponent {
  •  
  • public isShowingModal: boolean;
  • public offsets: number[];
  •  
  • private windowScrolling: WindowScrolling;
  •  
  • // I initialize the app component.
  • constructor( windowScrolling: WindowScrolling ) {
  •  
  • this.windowScrolling = windowScrolling;
  • this.offsets = this.range( 1, 20 );
  •  
  • }
  •  
  • // ---
  • // PUBLIC METHODS.
  • // ---
  •  
  • // I open or close the modal window.
  • public toggleModal() : void {
  •  
  • // When we open the modal window, we want to prevent scrolling on the main
  • // document. This way, if the user can scroll within the modal window, the
  • // scroll will never bleed into the body context.
  • if ( this.isShowingModal = ! this.isShowingModal ) {
  •  
  • this.windowScrolling.disable();
  •  
  • } else {
  •  
  • this.windowScrolling.enable();
  •  
  • }
  •  
  • }
  •  
  • // ---
  • // PRIVATE METHODS.
  • // ---
  •  
  • // I build an increasing range for the given numbers, inclusive.
  • private range( from: number, to: number ) : number[] {
  •  
  • var values = [];
  •  
  • for ( var i = from ; i <= to ; i++ ) {
  •  
  • values.push( i );
  •  
  • }
  •  
  • return( values );
  •  
  • }
  •  
  • }

As you can see, when I toggle the modal window, I'm either calling windowScrolling.enable() or windowScrolling.disable() to make sure that modal-window interactions don't bleed out into the body context. And, when we run this code and open the modal window, we can see that scrolling has been disabled:


 
 
 

 
 Using a WindowScrolling service to disable scrolling on the body element when a modal window is open in Angular 5.0.2. 
 
 
 

As you can see, when the modal window is opened, the WindowScrolling service is appending a Style tag to the Body element that forcibly hides the overflow of the body. This causes the scrollbars on the Body element to disappear (while maintaining the appropriate scrollTop), thereby preventing wheel events in the modal window from affecting the body offset.

And, when the modal window is closed, the WindowScrolling service will remove the Style tag, effectively re-enabling scrolling on the Body element:


 
 
 

 
 Using a WindowScrolling service to disable scrolling on the body element when a modal window is open in Angular 5.0.2. 
 
 
 

Apparently, this "overflow: hidden" trick has been around for a while; but, it's the first time that I've seen it. Ironically, after reading Walsh's blog, I found out that we actually use something similar in our production app. So, it seems like I'm fairly behind the curve on this matter. That said, I love the fact that we can easily prevent Body scrolling with a simple style change. Apparently this doesn't work as well on mobile devices; but, I don't think that such platform differences detract from this approach in the least.



Looking For A New Job?

Ooops, there are no jobs. Post one now for only $29 and own this real estate!

100% of job board revenue is donated to Kiva. Loans that change livesFind out more »

Reader Comments

overflow: hidden; doesnt work with mobile safari, so it will require extra js for (touchmove) event

Reply to this Comment

@Sergey,

Very true. That said, I consider this feature a "nice to have", not necessarily something that is absolutely required. So, if it doesn't work on a mobile device, I'm not overly concerned. I am not even sure that a modal window, in general, is a great UI to have on a mobile version of an app. Of course, I don't really have all that much mobile-dev experience, so please take that with a grain of salt.

Reply to this Comment

Post A Comment

You — Get Out Of My Dreams, Get Into My Comments
Live in the Now
Oops!
NEW: Some basic markdown formatting is now supported: bold, italic, blockquotes, lists, fenced code-blocks. Read more about markdown syntax »
Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.