1

I am using Angular 6 trying to create an HTML element (table headers) that is positioned in the middle of the page but will stick to the top of the page when the user scrolls past that point. I am trying to grab that element's position using @ViewChild but am unable to retrieve it. What might be missing?

Currently, the 'sticky' class is always applied but I want to only apply the sticky class when the element's position is <= to window.pageYOffset.

I am trying to achieve a similar effect to THIS using THIS tutorial.

My HTML:

<table id="tabletop" class="table" >
            <thead  >
                <tr #stickyMenu [class.sticky] = "stickyMenu" >
                    <th scope="col" id="th0">Monitoring Point</th>
                    <th scope="col" id="th1">Network Health<br><small id="more_detail">(Click icon to see more detail)</small></th>
                    <th scope="col" id="th2">Active Alerts</th>
                    <th scope="col" id="th3">Velocity (ft/s)</th>
                    <th scope="col" id="th4">Water Temperature (°F)</th>
                    <th scope="col" id="th5">Depth Readings (m)</th>               
                </tr>
            </thead>
  </table>

The css:

.sticky{
    position: fixed;
    top: 0;
    /* overflow: hidden; */
    z-index: 10;
    background-color: #fff;
    width:100%;
    padding-right:20px!important;
  }

My component:

import { Component, OnInit, ViewChild , ViewEncapsulation, HostListener, ElementRef, AfterViewInit} from '@angular/core';
import { ActivatedRoute, ParamMap, Router } from '@angular/router';
import { AuthService } from '../../services/auth.service';
import { SiteService } from '../../services/site.service';
import { SiteModel } from '../../models/site-model';
import { BaseComponent } from '../base.component';
import { DomSanitizer } from '@angular/platform-browser';
import { DateTime }  from 'luxon';

@Component({
  selector: 'app-site',
  templateUrl: './site.component.html',
  styleUrls: ['./site.component.css','../../../vizuly/lib/styles/vizuly.css']
})

export class SiteComponent extends BaseComponent implements OnInit, AfterViewInit {
  siteHid;
  scute_data;
  window_narrow;
  scute_data_ready: Boolean;
  sticky: boolean = false;
  elementPosition: any;  

constructor(public sanitizer: DomSanitizer, private route: ActivatedRoute, protected router: Router, private authService: AuthService, private siteService: SiteService) {
  super();
}

 @ViewChild("stickyMenu", {read: ElementRef}) tr: ElementRef;

ngOnInit() {
    this.route.paramMap.subscribe((params: ParamMap) => {
    this.siteHid = params.get('siteHid');
    this.loadSite(this.siteHid); 
 });
}

ngAfterViewInit(){
    console.log(this.tr)
    this.elementPosition = this.tr.nativeElement
    // this.elementPosition = this.stickyMenu.nativeElement.offsetTop;
    console.log(this.elementPosition)  //undefined
  }

@HostListener('window:scroll', ['$event'])
  handleScroll() {
    console.log(this.sticky, window.pageYOffset, this.elementPosition)
    this.sticky = (window.pageYOffset >= this.elementPosition) ? true : false;
    console.log(this.sticky)
   }

loadSite(siteHid){
   }

}
Devstar34
  • 1,027
  • 2
  • 21
  • 47

4 Answers4

2

You are binding the wrong attribute here:

[class.sticky] = "stickyMenu"

should be

[class.sticky] = "sticky"
  • the class wouldn't apply at all then – Devstar34 May 13 '19 at 20:49
  • 3
    The `elementPosition` attribute is not updated inside `handleScroll()`, so it always holds the value it was assigned on the component init. – Alexander Gasnikov May 13 '19 at 21:00
  • This is correct - the RHS should refer to boolean property, and your controlling property is named 'sticky' not 'stickyMenu'. Now, the question is - what appears in your console log within `handleScroll()`? – Richard Matsen May 13 '19 at 21:08
  • I think thats the second step ,she is not able to get the position of the element its printing in console as undefined – dota2pro May 13 '19 at 21:20
  • Thanks! Step one seems to be working with your help so far. The next issue is that the sticky class will not un-apply once the window.pageYOffset is less than the element position because the element position reassigns to 0. Do I need to try to store the original element position in a separate variable? – Devstar34 May 13 '19 at 21:59
1

Can you please try to reproduce your problem in this blitzy I am getting offset top 0 instead of undefined as you mentioned.

Also use the position directly

console.log(this.tr)
//this.elementPosition = this.tr.nativeElement
console.log(this.tr.nativeElement);
console.log(this.tr.nativeElement.offsetTop);  
dota2pro
  • 7,220
  • 7
  • 44
  • 79
  • Thanks. unfortunately the main problem is that cannot get the element position to apply the class upon a scroll past that point. – Devstar34 May 13 '19 at 21:03
1

I suppose you want to uncomment 1 line and change to this:

 ngAfterViewInit(){
    this.elementPosition = this.tr.nativeElement.offsetTop;
    console.log(this.elementPosition);
 }

Because stickyMenu is named only in DOM.

Musical Echo
  • 143
  • 2
  • 7
1

All the other answers have pieces of the puzzle, but one thing is missing IMO, grabbing this.elementPosition from this.tr.nativeElement.offsetTop is returning a value that's too small.

Looking at MDN - HTMLElement​.offsetTop

The HTMLElement.offsetTop read-only property returns the distance of the current element relative to the top of the offsetParent node.

but the offsetParent in this case is <thead>, not the top of the window which is what needs to be compared to window.scrollTop in handleScroll().
(Giovanni Chiodi has his sticky element close to the window top, so offsetTop works ok for him).

This SO question How to get an element's top position relative to the browser's viewport has a better way to get the element position,

var viewportOffset = el.getBoundingClientRect();
// these are relative to the viewport, i.e. the window
var top = viewportOffset.top;

So I would change your code to:

template

<table id="tabletop" class="table" >
  <thead  >
    <tr #stickyMenu [class.sticky] = "sticky">
    ...

component

@ViewChild("stickyMenu", { read: ElementRef }) tr: ElementRef;

ngAfterViewInit() {
  this.elementPosition = this.tr.nativeElement.getBoundingClientRect().top;
}

@HostListener('window:scroll', ['$event']) 
handleScroll() {
  this.sticky = (window.pageYOffset >= this.elementPosition);
}
Richard Matsen
  • 20,671
  • 3
  • 43
  • 77
  • This was exactly what I needed thank you! It took me a little extra research because apparently you can't read an ElementRef if it's within the same element as an *ngIF statement, which mine was. – Devstar34 May 29 '19 at 21:27