Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Prevent scrolling of child element to propagate in Angular 2

This is a classic. You have a parent and a child element. Child element is absolutely positioned and you want the user to scroll through its content. However, when you reach the bottom of the child element, the parent element (which also is allowed to have scroll bars) begins to scroll. That's undesirable. The basic behavior I want to reproduce is that of the comment section of the New York Times. For example:

enter image description here

The body is allowed to scroll down, but when you are at the bottom of the comment section, scrolling down doesn't do anything. I think the main difference in this case, is that I want to let the user scroll down when the cursor is positioned over the body element. Other approaches require to add a class to the body to prevent any scroll event in the body. I thought I would be able to do this with a bit of Javascript in Angular 2, but this is my failed attempt so far:

enter image description here

I have a custom directive in my child component:

<child-element scroller class="child"></child-element>

and this directive is supposed to stop the propagation of the scroll event to the body element:

import {Component} from 'angular2/core'
import {Directive, ElementRef, Renderer} from 'angular2/core';

@Directive({
    selector: '[scroller]',
})
export class ScrollerDirective {
    constructor(private elRef: ElementRef, private renderer: Renderer) {
        renderer.listen(elRef.nativeElement, 'scroll', (event) => {
            console.log('event!');
            event.stopPropagation();
            event.preventDefault();
        })
    }

}

It actually listens to the event but it doesn't stop the propagation.

Demo: Scroll down through the list of numbers and when you reach the bottom, its parent element starts to scroll down. That's the problem.

If you have another approach to accomplish this, please let me know.

UPDATE: Based on the answer provided by Günter Zöchbauer, I'm trying to prevent the wheel event when the user reaches the bottom. This is basically what I have so far in this updated demo:

renderer.listen(elRef.nativeElement, 'wheel', (e) => {
    console.log('event', e);
    console.log('scrollTop', elRef.nativeElement.scrollTop);
    console.log('lastScrollTop', lastScrollTop);

    if (elRef.nativeElement.scrollTop == lastScrollTop && e.deltaY > 0) {
      e = e || window.event;
      if (e.preventDefault)
          e.preventDefault();
      e.returnValue = false; 
    }
    else if (elRef.nativeElement.scrollTop == 0) {
      lastScrollTop = -1;
    } 
    else {
      lastScrollTop = elRef.nativeElement.scrollTop;
    }


}, false)

However, the logic is ugly and doesn't work great. For example, when the user reaches the bottom, scrolls up a little and scrolls down again, the parent component moves slightly. Does anyone know how to deal with this? A (much) better implementation?

UPDATE 2:

This is much better, but it is late now, so I will check again tomorrow.

like image 318
Robert Smith Avatar asked Apr 07 '16 22:04

Robert Smith


People also ask

How do you stop an element from scrolling?

There's another way to disable scrolling that is commonly used when opening modals or scrollable floating elements. And it is simply by adding the CSS property overflow: hidden; on the element you want to prevent the scroll.

How do I stop parent scrolling element?

To do this (in Chrome, Firefox, and Edge), we can add the CSS property overscroll-behavior: contain to the overflow: auto element. This will prevent the "scroll chaining" behavior, which will, in turn, keep the mouse-wheel active within the target element.

How do I disable scroll behavior in CSS?

To hide the scrollbar and disable scrolling, we can use the CSS overflow property. This property determines what to do with content that extends beyond the boundaries of its container. To prevent scrolling with this property, just apply the rule overflow: hidden to the body (for the entire page) or a container element.


2 Answers

I would suggest something more straightforward in my opinion: add an arbitrary class to an arbitrary parent, and prevent scrolling via CSS overflow: hidden.

In this example, I wrote the directive to prevent the parent from scrolling while the element exists at all, as this was my desired behavior. Instead of OnDestroy and AfterViewInit, for your use case you should bind to mouseenter and mouseleave

HTML:

<div add-class="noscroll" to="body">Some Content Here</div>

CSS:

.noscroll { overflow: hidden; }

TS:

import {Directive, AfterViewInit, OnDestroy, Input} from "@angular/core";

@Directive({
  selector: '[add-class]'
})
export class AddClassDirective implements AfterViewInit, OnDestroy {
  @Input('add-class')
  className: string;

  @Input('to')
  selector: string;

  ngOnDestroy(): void {
    document.querySelector(this.selector).classList.remove(this.className);
  }

  ngAfterViewInit(): void {
    document.querySelector(this.selector).classList.add(this.className);
  }
}
like image 177
William B Avatar answered Sep 18 '22 18:09

William B


Robert' answer was really helpful, but it was outdated and didn't work for me first. I improved it, to make it work for me, and therefore I wanted to share it with you:

import { Component, Directive, Renderer2, ElementRef } from '@angular/core';

@Directive({
  selector: '[scroller-directive]',
})
export class ScrollerDirective {
  constructor(elRef: ElementRef, renderer: Renderer2) {
    renderer.listen(elRef.nativeElement, 'wheel', (event) => {
      const el = elRef.nativeElement;
      const conditions = ((el.scrollTop + el.offsetHeight >= el.scrollHeight)) && event.deltaY > 0 || el.scrollTop === 0 && event.deltaY < 0;
      if (conditions === true) {
        event.preventDefault();
        event.returnValue = false;
      }
    });
  }
}

Used in html like:

<div class="scrollable-inner-div" scroller-directive>
// Code
</div>
like image 44
Etoon Avatar answered Sep 19 '22 18:09

Etoon