28

I'm tring to create a directive that will center a div.

So far, I have this code:

app.directive("setcenter", function () {
    return {
        scope:{
            setcenter: '='
        },
        link: function (scope, element, attrs) {

            scope.$watch('setcenter', function (newValue) {
                if (newValue == true) {
                    var width = element.width();
                    element.css('position', 'absolute');
                    element.css('top', '80px');
                    element.css('left', '50%');
                    element.css('z-index', '200');
                    element.css('margin-left', '-' + width / 2 + 'px');
                }
            });
        }
    }
});

The problem is the width of the element. The whole point for this directive is that the div that uses this directive, don't have a width set. I want this directive to figure out the width and center the div.

The problem I encounter is that when the directive is invoked, the actual width of the div is not yet known. When I use this in my situation, the div is 800px, but when the page is finished loading, the div is 221px.

So, what can I do to wait till the actual width is known of the div?

Martijn
  • 24,441
  • 60
  • 174
  • 261
  • Shouldn't the title of the question be about width instead of height ? About your question, maybe you could simply add a dom load event listener in your link function and call your centering function there. – Yann Jul 18 '13 at 15:34
  • I've tried to put `$(document).ready(...)`in the `if(newValue == true)` statement, but it didn't work. – Martijn Jul 18 '13 at 15:42
  • I think the problem is that you can never really now when the div has its final size, since the div could contain bindings (ng-style, ng-bind, etc...) that change its size dynamically. If you aren't too worried about performances, I think you could just set up a $watch on rootscope and call your centering method every time. – Yann Jul 18 '13 at 15:45
  • 2
    I'd recommend just use CSS. Sounds like you're trying to over complicate a really simple design requirement. Create a CSS class and just attach it to your element, and that CSS class will automatically center the DIV in whatever parent container you want, regardless of the width. It can even be responsive to the user resizing their browser, changing font sizes, whatever. http://stackoverflow.com/questions/114543/how-to-center-a-div-in-a-div-horizontally – Mike Pugh Jul 18 '13 at 15:49
  • 1
    Did you try do use element.clientWidth instead of element.width? – Deividi Cavarzan Jul 19 '13 at 02:30

4 Answers4

31

First, I only have used this logic when I defined a controller for a directive rather than a link function. So defining it in a link function instead may cause different behavior, but I suggest you try it there first and if you can't get it to work then switch to using a controller.

As far as I can tell, the only change you would need to make this work would be to change $scope to scope calls and $element to element since the dependency injected objects become standard link function parameters.

  $scope.getElementDimensions = function () {
    return { 'h': $element.height(), 'w': $element.width() };
  };

  $scope.$watch($scope.getElementDimensions, function (newValue, oldValue) {
    //<<perform your logic here using newValue.w and set your variables on the scope>>
  }, true);

  $element.bind('resize', function () {
    $scope.$apply();
  });

The idea for this usage came to me after reading a very similar type of usage about watching the $window rather than the current element, the original work can be found here.

Angular.js: set element height on page load

Community
  • 1
  • 1
James Eby
  • 1,784
  • 20
  • 25
  • For those cutting and pasting this example, don't make the mistake of forgetting the third argument to $watch which is "true" - to do a deep comparison. See https://groups.google.com/forum/#!topic/angular/slJ7Xj3SYOk – Amir Mar 20 '14 at 01:57
  • 1
    Doesn't quite work with the current 1.3.16 version of AngularJS. For some reasons it initially throws negative width + height amidst proper values, which often on initial update breaks things. I don't know if it is a bug or something changed, but I had to use the following work-around inside the watch: if(newValue.w > 0 && newValue.h > 0) {then do the update} – vitaly-t Jun 10 '15 at 10:58
  • Must be a change in behavior to newer angular versions. I was using multiple versions up to and including angular 1.2.X.Thanks for the info, when I update my project that relies on this solution to a newer version of angular I will remember this change in behavior. – James Eby Jun 10 '15 at 14:35
  • Did you use any style settings to have the directive to take up the whole screen (`width:100%`)? If not, I think those dimension values will never actually change. – orange Jul 23 '15 at 04:02
  • 1
    `$element.height()` and `$element.width()` are not functions. – Hobbyist Jun 18 '16 at 20:38
  • I see lots of answers everywhere referring to $element.width(), but as far as I can tell, it's really not a function. Why is everybody talking as if it is? – mcv Oct 20 '16 at 13:31
7

James' answer led me to:

app.directive('measureInto', function () {
        return {
            restrict: 'A',
            link: function (scope, element, attrs) {
                scope.$watch(function() {
                    return element[0].clientWidth;
                }, function(value){
                    scope[attrs.measureInto] = element[0].clientWidth + 10;
                });
            }
        };
    });

So, at runtime, I add this and assign into whatever scope variable I want the width of the element I'm looking for

Michael Burdi
  • 71
  • 1
  • 1
  • I tried this. But it detects width only on page load. See this plunker: http://plnkr.co/edit/NwrT91YU4ddXzDHGeFWQ?p=preview – zendu Jul 27 '17 at 02:22
1

I had a similar issue and found that the dimensions were reliably correct when all the ng-ifs (or anything else using ngAnimate) on the page had been resolved - it's possible something similar is happening here. If so, this would do the trick without adding any new listeners:

$scope.tryGetElementDimensions = function () {
    if (!angular.element("your-element") || ((angular.element("your-element")[0].classList).contains("ng-animate")
       $timeout(function() {
           $scope.tryGetElementDimensions()
       })
    }
    else {
        $scope.getElementDimensions()
    } 

$scope.getElementDimensions = function (){
    //whatever you actually wanted to do
}

link: function (scope, element, attrs) {
    $scope.tryGetElementDimensions()
}

Angular adds ng-animate and ng-enter, ng-leave classes while it's animating and you can be confident it's finished when these classes have all been removed. $timeout without a second argument just waits for the next digest.

strom2357
  • 11
  • 2
1

Can't comment yet, therefore this answer. Found a similar solution like the one strom2357 is suggesting. $timeout works really well to let you know when the dom is ready, and it is super simple. I am using this solution to get the ui-view element width. Found it in a fiddle.

var app = angular.module('app', []);

app.controller('MyController', function($timeout, $scope){

  $timeout(function(){
    //This is where you would get width or height of an element
    alert('DOM ready');
  });

  alert('DOM not ready');

});
Jette
  • 2,459
  • 28
  • 37