50

I'm programming a single-page application using the Angular framework. I'm new to it. I've read this guide to help me understand the fundamental differences between jQuery and Angular and I'd like to follow this guidance as much as possible and NOT use jQuery.

Except that jQuery helps get around some of the browser incompatibilities and provides a useful library of functions, like being able to know the top position of an element from the top of the window, as in $('element').offset().top. No plain Javascript seems to be able to come close without rewriting this function, at which point wouldn't it be a better idea to use a jQuery or jQuery like library?

Specifically, what I'm trying to do is set up a directive that fixes an element in place once the top of it is scrolled to a certain position in the window. Here's what it looks like:

directives.scrollfix = function () {
    return {
        restrict: 'C',
        link: function (scope, element, $window) {

            var $page = angular.element(window)
            var $el   = element[0]
            var elScrollTopOriginal = $($el).offset().top - 40

            $page.bind('scroll', function () {

                var windowScrollTop = $page[0].pageYOffset
                var elScrollTop     = $($el).offset().top

                if ( windowScrollTop > elScrollTop - 40) {
                    elScrollTopOriginal = elScrollTop - 40
                    element.css('position', 'fixed').css('top', '40px').css('margin-left', '3px');
                }
                else if ( windowScrollTop < elScrollTopOriginal) {
                    element.css('position', 'relative').css('top', '0').css('margin-left', '0');
                }
            })

        }
    }
}

If there's a much better way to achieve this in Angular that I'm still just not getting, I'd appreciate the advice.

1
  • the accepted solution doesn't take the current scroll position into account, see my answer below
    – schellmax
    Commented Jul 8, 2014 at 9:43

6 Answers 6

67

use getBoundingClientRect if $el is the actual DOM object:

var top = $el.getBoundingClientRect().top;

JSFiddle

Fiddle will show that this will get the same value that jquery's offset top will give you

Edit: as mentioned in comments this does not account for scrolled content, below is the code that jQuery uses

https://github.com/jquery/jquery/blob/master/src/offset.js (5/13/2015)

offset: function( options ) {
    //...

    var docElem, win, rect, doc,
        elem = this[ 0 ];

    if ( !elem ) {
        return;
    }

    rect = elem.getBoundingClientRect();

    // Make sure element is not hidden (display: none) or disconnected
    if ( rect.width || rect.height || elem.getClientRects().length ) {
        doc = elem.ownerDocument;
        win = getWindow( doc );
        docElem = doc.documentElement;

        return {
            top: rect.top + win.pageYOffset - docElem.clientTop,
            left: rect.left + win.pageXOffset - docElem.clientLeft
        };
    }
}
6
  • 1
    Looks like the cross browser compatibility on this is great too. Thanks.
    – saikofish
    Commented Sep 23, 2013 at 7:48
  • 4
    note that this solution doesn't account for the current scrolling position, see my answer below
    – schellmax
    Commented Jul 8, 2014 at 9:46
  • 4
    No, it's not the same. This is the same: $el.getBoundingClientRect().top + document.body.scrollTop Commented Jan 10, 2015 at 14:21
  • Well jQuery uses more than that... You should include getWindow, isWindow etc...
    – T J
    Commented Apr 20, 2016 at 8:11
  • @TJ, why? Users should be able to infer what getWindow does, can look up the code of the utility function themselves since I have linked the github repo, and I believe adding in the code just to explain a utility function would just clutter the answer. Commented Apr 20, 2016 at 14:56
49

Here is a function that will do it without jQuery:

function getElementOffset(element)
{
    var de = document.documentElement;
    var box = element.getBoundingClientRect();
    var top = box.top + window.pageYOffset - de.clientTop;
    var left = box.left + window.pageXOffset - de.clientLeft;
    return { top: top, left: left };
}
8
  • Reinventing the wheel (not universal one, by the way) is definitely better then using "cancer", yes, sure...
    – Regent
    Commented Dec 4, 2015 at 12:36
  • 19
    @Regent Not everyone wants to include a giant library to do something that can otherwise be done in 5 lines of code. Commented Dec 5, 2015 at 13:07
  • For example, jquery-2.1.3.min.js (83KB) is not giant file. What about projects (not AngularJS ones) where jQuery is used in many places? Do you suggest to write your one functions to make code (which works with DOM) shorter? Not well tested, with bugs, without good cross-browsing? I do understand that jQuery shouldn't be used in all projects, but by insulting jQuery you state that it should't be used anywhere (and this is wrong statement, by the way), don't you?
    – Regent
    Commented Dec 5, 2015 at 18:06
  • 15
    I'm not sure where I said that it "shouldn't be used anywhere". If jQuery is already a requirement, then sure, make use of it. But as time goes on and browser incompatibilities are resolved, it's value begins to diminish. And while 83kb might not be a lot on your first-world fiberoptic connection, much of the developing world isn't so fortunate. It's also frustrating as a JS library designer that so many SO posts continually punt to jQuery at times when you really need to understand how things work. See youmightnotneedjquery.com Commented Dec 5, 2015 at 20:14
  • 1
    I've been looking for an answer like this and all i've found were jQuery code. I think most of people are confusing JS with JQ.. And now I learnt how things works in the affix environment! Thanks! Commented May 9, 2017 at 12:07
11

Seems you can just use the prop method on the angular element:

var top = $el.prop('offsetTop');

Works for me. Does anyone know any downside to this?

3
  • When I add an element to the DOM with the element.prepend(); offsetTop & Left are always 0. However in the element inspector, it's not 0. Commented Aug 1, 2015 at 8:04
  • The offsetTop property only returns the offset relative to the element's parent (developer.mozilla.org/en-US/docs/Web/API/HTMLElement/offsetTop), and not to the whole document like jQuery's offset() (api.jquery.com/offset) does. Commented Sep 15, 2015 at 13:57
  • It's a bit of a rigamarole in the cases where your DOM element is loaded asynchronously after a function looks for $el on the first pass through the digest. In combination with the solution provided by Darren - I had to first use plain JS querySelector, wrap it in angular element and then was able to call the prop.
    – lakewood
    Commented Oct 27, 2015 at 15:37
3

the accepted solution by Patrick Evans doesn't take scrolling into account. i've slightly changed his jsfiddle to demonstrate this:

css: add some random height to make sure we got some space to scroll

body{height:3000px;} 

js: set some scroll position

jQuery(window).scrollTop(100);

as a result the two reported values differ now: http://jsfiddle.net/sNLMe/66/

UPDATE Feb. 14 2015

there is a pull request for jqLite waiting, including its own offset method (taking care of current scroll position). have a look at the source in case you want to implement it yourself: https://github.com/angular/angular.js/pull/3799/files

2
  • The cvmlrobotics link is dead. Commented Feb 11, 2015 at 13:21
  • @user2602152 thanks for your comment, i've replaced the dead link by additional info i recently found.
    – schellmax
    Commented Feb 14, 2015 at 20:00
1

For Angular 2+ to get the offset of the current element (this.el.nativeElement is equvalent of $(this) in jquery):

export class MyComponent implements  OnInit {

constructor(private el: ElementRef) {}

    ngOnInit() {
      //This is the important line you can use in your function in the code
      //-------------------------------------------------------------------------- 
        let offset = this.el.nativeElement.getBoundingClientRect().top;
      //--------------------------------------------------------------------------    

        console.log(offset);
    }

}
0

You can try element[0].scrollTop, in my opinion this solution is faster.

Here you have bigger example - http://cvmlrobotics.blogspot.de/2013/03/angularjs-get-element-offset-position.html

1
  • 4
    The link to cvmlrobotics is dead. Commented Feb 11, 2015 at 13:33

Not the answer you're looking for? Browse other questions tagged or ask your own question.