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.

Community
  • 1
  • 1
saikofish
  • 1,744
  • 1
  • 15
  • 12

6 Answers6

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
        };
    }
}
Patrick Evans
  • 41,991
  • 6
  • 74
  • 87
  • 1
    Looks like the cross browser compatibility on this is great too. Thanks. – saikofish Sep 23 '13 at 07:48
  • 4
    note that this solution doesn't account for the current scrolling position, see my answer below – schellmax Jul 08 '14 at 09:46
  • 4
    No, it's not the same. This is the same: `$el.getBoundingClientRect().top + document.body.scrollTop` – Alex Fedoseev Jan 10 '15 at 14:21
  • Well jQuery uses more than that... You should include `getWindow`, `isWindow` etc... – T J Apr 20 '16 at 08: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. – Patrick Evans Apr 20 '16 at 14:56
  • To get a handle on the window object I injected `$window` into my directive and replaced `win` with `$window`. – Ian G Oct 24 '16 at 19:13
48

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 };
}
Regent
  • 5,142
  • 3
  • 21
  • 35
Paul Gordon
  • 2,532
  • 3
  • 26
  • 24
  • Reinventing the wheel (not universal one, by the way) is definitely better then using "cancer", yes, sure... – Regent Dec 04 '15 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. – Paul Gordon Dec 05 '15 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 Dec 05 '15 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 http://youmightnotneedjquery.com/ – Paul Gordon Dec 05 '15 at 20:14
  • I 100% agree @Wolverine I can't stand it when devs include a library just to do one or two things. If they can't understand why, then they have a lot of learning to do. If already required or used, use it, but to include an entire library to to do one thing is insane. – LifeOfCoding Aug 11 '16 at 16:03
  • 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! – celsomtrindade May 09 '17 at 12:07
  • Tnx! this helped me a lot !!! in a project with complex template of angular2 and no JQuery. – Dudi Sep 13 '17 at 09:28
  • I also agree with @Wolverine. By itself, 83 KB might not seem like much, but when it's bundled in your JavaScript and requested tens of millions of times a month like it is in our web app, it becomes terabytes of bandwidth that Amazon is happy to charge you for. – Rob Johansen Oct 19 '17 at 16:39
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?

Darren
  • 9,014
  • 2
  • 39
  • 50
  • 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. – Steven Ryssaert Aug 01 '15 at 08:04
  • The offsetTop property only returns the offset relative to the element's parent (https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/offsetTop), and not to the whole document like jQuery's offset() (http://api.jquery.com/offset/) does. – gluecksmelodie Sep 15 '15 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 Oct 27 '15 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

schellmax
  • 5,678
  • 4
  • 37
  • 48
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);
    }

}
Combine
  • 3,894
  • 2
  • 27
  • 30
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

bmazurek
  • 169
  • 4