112
votes

It seems like in jQuery when an element is not visible width() returns 0. Makes sense, but I need to get the width of a table in order to set the width of the parent before I show the parent.

As noted below, there is text in the parent, that makes the parent skew and look nasty. I want the parent to be only as wide as the table and have the text wrap.

<div id="parent">
    Text here ... Can get very long and skew the parent
    <table> ... </table>
    Text here too ... which is why I want to shrink the parent based on the table
</div>

CSS:

#parent
{
    display: none;
}

Javascript:

var tableWidth = $('#parent').children('table').outerWidth();
if (tableWidth > $('#parent').width())
{
    $('#parent').width(tableWidth);
}

tableWidth always returns 0 since it is not visible (is my guess since it gives me a number when visible). Is there a way to get the width of the table without making the parent visible?

12

12 Answers

97
votes

Here is a trick I have used. It involves adding some CSS properties to make jQuery think the element is visible, but in fact it is still hidden.

var $table = $("#parent").children("table");
$table.css({ position: "absolute", visibility: "hidden", display: "block" });
var tableWidth = $table.outerWidth();
$table.css({ position: "", visibility: "", display: "" });

It is kind of a hack, but it seems to work fine for me.

UPDATE

I have since written a blog post that covers this topic. The method used above has the potential to be problematic since you are resetting the CSS properties to empty values. What if they had values previously? The updated solution uses the swap() method that was found in the jQuery source code.

Code from referenced blog post:

//Optional parameter includeMargin is used when calculating outer dimensions  
(function ($) {
$.fn.getHiddenDimensions = function (includeMargin) {
    var $item = this,
    props = { position: 'absolute', visibility: 'hidden', display: 'block' },
    dim = { width: 0, height: 0, innerWidth: 0, innerHeight: 0, outerWidth: 0, outerHeight: 0 },
    $hiddenParents = $item.parents().andSelf().not(':visible'),
    includeMargin = (includeMargin == null) ? false : includeMargin;

    var oldProps = [];
    $hiddenParents.each(function () {
        var old = {};

        for (var name in props) {
            old[name] = this.style[name];
            this.style[name] = props[name];
        }

        oldProps.push(old);
    });

    dim.width = $item.width();
    dim.outerWidth = $item.outerWidth(includeMargin);
    dim.innerWidth = $item.innerWidth();
    dim.height = $item.height();
    dim.innerHeight = $item.innerHeight();
    dim.outerHeight = $item.outerHeight(includeMargin);

    $hiddenParents.each(function (i) {
        var old = oldProps[i];
        for (var name in props) {
            this.style[name] = old[name];
        }
    });

    return dim;
}
}(jQuery));
41
votes
function realWidth(obj){
    var clone = obj.clone();
    clone.css("visibility","hidden");
    $('body').append(clone);
    var width = clone.outerWidth();
    clone.remove();
    return width;
}
realWidth($("#parent").find("table:first"));
8
votes

Based on Roberts answer, here is my function. This works for me if the element or its parent have been faded out via jQuery, can either get inner or outer dimensions and also returns the offset values.

/edit1: rewrote the function. it's now smaller and can be called directly on the object

/edit2: the function will now insert the clone just after the original element instead of the body, making it possible for the clone to maintain inherited dimensions.

$.fn.getRealDimensions = function (outer) {
    var $this = $(this);
    if ($this.length == 0) {
        return false;
    }
    var $clone = $this.clone()
        .show()
        .css('visibility','hidden')
        .insertAfter($this);        
    var result = {
        width:      (outer) ? $clone.outerWidth() : $clone.innerWidth(), 
        height:     (outer) ? $clone.outerHeight() : $clone.innerHeight(), 
        offsetTop:  $clone.offset().top, 
        offsetLeft: $clone.offset().left
    };
    $clone.remove();
    return result;
}

var dimensions = $('.hidden').getRealDimensions();
3
votes

Thank you for posting the realWidth function above, it really helped me. Based on "realWidth" function above, I wrote, a CSS reset, (reason described below).

function getUnvisibleDimensions(obj) {
    if ($(obj).length == 0) {
        return false;
    }

    var clone = obj.clone();
    clone.css({
        visibility:'hidden',
        width : '',
        height: '',
        maxWidth : '',
        maxHeight: ''
    });
    $('body').append(clone);
    var width = clone.outerWidth(),
        height = clone.outerHeight();
    clone.remove();
    return {w:width, h:height};
}

"realWidth" gets the width of an existing tag. I tested this with some image tags. The problem was, when the image has given CSS dimension per width (or max-width), you will never get the real dimension of that image. Perhaps, the img has "max-width: 100%", the "realWidth" function clone it and append it to the body. If the original size of the image is bigger than the body, then you get the size of the body and not the real size of that image.

3
votes

I try to find working function for hidden element but I realize that CSS is much complex than everyone think. There are a lot of new layout techniques in CSS3 that might not work for all previous answers like flexible box, grid, column or even element inside complex parent element.

flexibox example enter image description here

I think the only sustainable & simple solution is real-time rendering. At that time, browser should give you that correct element size.

Sadly, JavaScript does not provide any direct event to notify when element is showed or hidden. However, I create some function based on DOM Attribute Modified API that will execute callback function when visibility of element is changed.

$('[selector]').onVisibleChanged(function(e, isVisible)
{
    var realWidth = $('[selector]').width();
    var realHeight = $('[selector]').height();

    // render or adjust something
});

For more information, Please visit at my project GitHub.

https://github.com/Soul-Master/visible.event.js

demo: http://jsbin.com/ETiGIre/7

3
votes

If you need the width of something that's hidden and you can't un-hide it for whatever reason, you can clone it, change the CSS so it displays off the page, make it invisible, and then measure it. The user will be none the wiser if it's hidden and deleted afterwards.

Some of the other answers here just make the visibility hidden which works, but it will take up a blank spot on your page for a fraction of a second.

Example:

$itemClone = $('.hidden-item').clone().css({
        'visibility': 'hidden',
        'position': 'absolute',
        'z-index': '-99999',
        'left': '99999999px',
        'top': '0px'
    }).appendTo('body');
var width = $itemClone.width();
$itemClone.remove();
2
votes

Before take the width make the parent display show ,then take the width and finally make the parent display hide. Just like following

$('#parent').show();
var tableWidth = $('#parent').children('table').outerWidth();
 $('#parent').hide();
if (tableWidth > $('#parent').width())
{
    $('#parent').width() = tableWidth;
}
2
votes

One solution, though it won't work in all situations, is to hide the element by setting the opacity to 0. A completely transparent element will have width.

The draw back is that the element will still take up space, but that won't be an issue in all cases.

For example:

$(img).css("opacity", 0)  //element cannot be seen
width = $(img).width()    //but has width
1
votes

The biggest issue being missed by most solutions here is that an element's width is often changed by CSS based on where it is scoped in html.

If I was to determine offsetWidth of an element by appending a clone of it to body when it has styles that only apply in its current scope I would get the wrong width.

for example:

//css

.module-container .my-elem{ border: 60px solid black; }

now when I try to determine my-elem's width in context of body it will be out by 120px. You could clone the module container instead, but your JS shouldn't have to know these details.

I haven't tested it but essentially Soul_Master's solution appears to be the only one that could work properly. But unfortunately looking at the implementation it will likely be costly to use and bad for performance (as most of the solutions presented here are as well.)

If at all possible then use visibility: hidden instead. This will still render the element, allowing you to calculate width without all the fuss.

0
votes

In addition to the answer posted by Tim Banks, which followed to solving my issue I had to edit one simple thing.

Someone else might have the same issue; I'm working with a Bootstrap dropdown in a dropdown. But the text can be wider as the current content at this point (and there aren't many good ways to resolve that through css).

I used:

$table.css({ position: "absolute", visibility: "hidden", display: "table" });

instead, which sets the container to a table which always scales in width if the contents are wider.

0
votes
jQuery(".megamenu li.level0 .dropdown-container .sub-column ul .level1").on("mouseover", function () {
    var sum = 0;
    jQuery(this).find('ul li.level2').each(function(){
    sum -= jQuery(this).height();
    jQuery(this).parent('ul').css('margin-top', sum / 1.8);
    console.log(sum);
    });
});

On hover we can calculate the list item height.

0
votes

As has been said before, the clone and attach elsewhere method does not guarantee the same results as styling may be different.

Below is my approach. It travels up the parents looking for the parent responsible for the hiding, then temporarily unhides it to calculate the required width, height, etc.

    var width = parseInt($image.width(), 10);
    var height = parseInt($image.height(), 10);

    if (width === 0) {

        if ($image.css("display") === "none") {

            $image.css("display", "block");
            width = parseInt($image.width(), 10);
            height = parseInt($image.height(), 10);
            $image.css("display", "none");
        }
        else {

            $image.parents().each(function () {

                var $parent = $(this);
                if ($parent.css("display") === "none") {

                    $parent.css("display", "block");
                    width = parseInt($image.width(), 10);
                    height = parseInt($image.height(), 10);
                    $parent.css("display", "none");
                }
            });
        }
    }