Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
Ben Nadel at cf.Objective() 2014 (Bloomington, MN) with: Mehul Saiya
Ben Nadel at cf.Objective() 2014 (Bloomington, MN) with: Mehul Saiya@mehulsaiya )

Scrolling An Overflow-Container Back To The Top On Content Change In Angular 7.2.7

By Ben Nadel on

Yesterday, I shared my feelings on tightly-coupled DOM (Document Object Model) access in an Angular 7.2.7 application; and, about how I'm going to stop bending over backwards in an attempt to fulfill some dogmatic separation of concerns when a payoff is never going to be realized. Along the same lines, I wanted to quickly look at another DOM-access situation that comes up often for me in my Single-Page Applications (SPA): having to scroll an overflow container back to the top when its content changes. And, just as with my previous post, I'm going to use tightly-coupled DOM-access methods to accomplish this in Angular 7.2.7.


 
 
 

 
 
 
 
 

Run this demo in my JavaScript Demos project on GitHub.

View this code in my JavaScript Demos project on GitHub.

To quickly explore this idea, let's imagine a tabbed-content area User Interface (UI). If the tab-body container, in this UI, is an overflow container, there's a good chance that the tab-body scroll-offset will have to reset when the user navigates from one tab to the next tab.

To implement this scroll-offset reset, we're going to reach into the View of our Angular Component, find the native HTML Element, and imperatively call its .scrollTo() method when the selected Tab content changes. This will tightly couple our Angular Component to the view-rendering layer; but, it will make our lives much easier.

Now, in yesterday's demo, I used the native .querySelector() method on the Host element in order to locate the HTML Element in question. So today, in an effort to mix things up a little bit, I'm going to use a ViewChild() query to inject our target element's "ElementRef" wrapper into our Angular Component. Then, when I need to reset the scroll-offset of the tab content, I can use the "nativeElement" property of the injected ViewChild query.

  • // Import the core angular services.
  • import { Component } from "@angular/core";
  • import { ElementRef } from "@angular/core";
  • import { ViewChild } from "@angular/core";
  •  
  • // ----------------------------------------------------------------------------------- //
  • // ----------------------------------------------------------------------------------- //
  •  
  • @Component({
  • selector: "my-app",
  • queries: {
  • "tabsContentRef": new ViewChild( "tabsContentRef" )
  • },
  • styleUrls: [ "./app.component.less" ],
  • template:
  • `
  • <div class="tabs">
  • <nav class="tabs__nav">
  • <a
  • (click)="show( 'one' )"
  • class="tabs__nav-item"
  • [class.tabs__nav-item--on]="( selectedTab === 'one' )">
  • Show One
  • </a>
  • <a
  • (click)="show( 'two' )"
  • class="tabs__nav-item"
  • [class.tabs__nav-item--on]="( selectedTab === 'two' )">
  • Show Two
  • </a>
  • </nav>
  • <div #tabsContentRef class="tabs__content" [ngSwitch]="selectedTab">
  • <div *ngSwitchCase="( 'one' )" class="tabs__tab">
  •  
  • <h2>
  • Tab One
  • </h2>
  •  
  • <p *ngFor="let i of [0,1,2,3,4,5,6,7,8,9,10]">
  • This is tab one content ......
  • </p>
  •  
  • </div>
  • <div *ngSwitchCase="( 'two' )" class="tabs__tab">
  •  
  • <h2>
  • Tab Two
  • </h2>
  •  
  • <p *ngFor="let i of [0,1,2,3,4,5,6,7,8,9,10]">
  • This is tab two content ......
  • </p>
  •  
  • </div>
  • </div>
  • </div>
  • `
  • })
  • export class AppComponent {
  •  
  • public selectedTab: "one" | "two";
  • public tabsContentRef!: ElementRef; // Using "definite assignment" assertion (query).
  •  
  • // I initialize the app component.
  • constructor() {
  •  
  • this.selectedTab = "one";
  •  
  • }
  •  
  • // ---
  • // PUBLIC METHODS.
  • // ---
  •  
  • // I show the given tab.
  • public show( tab: "one" | "two" ) : void {
  •  
  • this.selectedTab = tab;
  • // By default - the default behavior of the browser - when we change the content
  • // of an overflow-container, the overflow-container doesn't change its scroll
  • // offset unless it suddenly has less content than it did before. As such, when
  • // the tab-content changes, we have to explicitly scroll the overflow-container
  • // back to the top.
  • this.scrollTabContentToTop();
  •  
  • }
  •  
  • // ---
  • // PRIVATE METHODS.
  • // ---
  •  
  • // I scroll the tab-content overflow-container back to the top.
  • private scrollTabContentToTop() : void {
  •  
  • this.tabsContentRef.nativeElement.scrollTo( 0, 0 );
  •  
  • }
  •  
  • }

As you can see, when a user clicks on one of the tabs, we update the Angular Component's view-model in order to change the tab-content. And then, we immediately call the .scrollTabContentToTop() method. This method reaches into the DOM and invokes the native DOM-method, .scrollTo(), which will scroll our overflow-container back to the top.

Now, if we run this Angular application in the browser and switch from tab to tab, we can see that the tab-content container is scrolled back to the top on each tab-change:


 
 
 

 
 Embracing tightly-coupled DOM-access in order to call scollTo() and reset the scroll-offset of an overflow container in Angular 7.2.7. 
 
 
 

This kind of approach used to feel "dirty" to me because I didn't want my Angular Component classes to know about the rendered DOM. But, in some cases, like this, reaching directly into the DOM in order to manipulate it makes the code easier to understand and maintain. And, again, if we do ever need to create better separation between our Classes and our Templates, we can always refactor the code to use more indirection.



Reader Comments

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.