17

We're facing a fairly scary issue in JavaScript that none of us seems to be quite capable of resolving:

How do we get the width and height of a DOM element, including children, entire box model etc. without the component actually being displayed on the page?

Remember: I'm looking for suggestions. Even answers which don't answer the question fully (or don't quite fit with the specified parameters) might, and probably will, be helpful.

Main goal: I'm adding HTML elements into the page via Javascript - HTML elements with sizes and styles from a database. Problem is that they misbehave, usually bad aligment, one element is larger than another due to padding/margin whatever, and so I need to check their actual size to fix these issues.

The resulting application is going to be a, as BigMacAttack has described it in the comments, a 'tightly knit mosaic of 3rd-party HTML controls' would pretty much be spot-on. It needs to look a lot like full-fledged desktop application, and HTML seems to hate the idea with passion. Not that I blame it.

Anyway, here's some example code:

JavaScript:

function exampleElement(caption, content) {
    this.caption = caption;
    this.content = content;
    this.rootElement = document.createElement("div");
}

exampleElement.prototype.constructElement = function() {
    var otherElement = document.createElement("p");
    this.rootElement.className = "exampleElement";
    this.rootElement.textContent = this.caption; 
    otherElement.className = "exampleP";
    otherElement.textContent = this.content;
    this.rootElement.appendChild(otherElement);
    /*I need to know size of the otherElement here*/
    /*here goes code adding stuff into rootElement*/
};

window.onload = function() {
    var ex = new exampleElement("Hello", "Here's text");
    ex.constructElement();
    document.body.appendChild(ex.rootElement);
};

CSS:

.exampleElement {
    padding: 5px;
    margin: 6px;
}

.exampleElement .exampleP {
    padding: 20px;
    margin: 6px;
}

A fiddle

Now, we need our page to dynamically react to size of the window and to contents of individual components, that's why it's important to be able to get size of an object before even displaying it. It's also important that creation of an object is clearly separated into three phases:

  • creation via new

  • construction of DOM tree (constructElement)

  • addition into the document (either directly into body or into another DOM tree)

It's important that we know sizes of individual elements during the construction phase.

So far we've tried measuring it via jQuery, DOM width and height attributes, but none of that works with DOM object not being directly displayed on page. Another approach I have tried were several functions adding the object into document.body, getting width and height, and then immediately removing it - however, since our CSS files are very specific, this is unreliable unless you insert the entire rootElement, which will be a terrible performance and memory hog as our components get fairly complex.

I suppose an approach of dropping .CSS files completely and defining styles directly trough JS would solve at least part of our predicament, but there has to be a better way.

Starting bounty to get more ideas and suggestions. Just shoot people, even if answer is not entirely within the boundaries of the question (how would/did you do it etc.) - the goal I'm trying to achieve is for my JS generated HTML controls to properly fit together.

Fenixp
  • 645
  • 5
  • 22
  • Would inserting the object into a hidden `iframe` work? – Mathijs Flietstra Sep 19 '13 at 09:54
  • @Mathijs Flietstra Well I might as well insert it directly into body and get size of its children, problem is that it gets too problematic when it comes to performance (either that or styles don't get applied, take your pick) – Fenixp Sep 19 '13 at 10:10
  • 2
    Couldn't you have `position:absolute; opacity:0;` and then remove that when you have the sizes? – ediblecode Sep 19 '13 at 10:12
  • 6
    You can't. Just add it to the body, measure it, then remove it. If you do so within the same block of code, it's guaranteed to not even flicker on the screen, and it is very fast (if this becomes a bottleneck you need to rethink how you're doing things; calculating the CSS will take longer than appending to the DOM structure) – Dave Sep 19 '13 at 10:14
  • I have tried similar things in the past, but have never built something so complex that adding it to the document would be of much harm. When I get trapped into these width issues in Javascript, I usually add them to the document or use fixed sizes... – mavrosxristoforos Sep 23 '13 at 05:26
  • @Dave if the biggest dream of your life is to have a chance of getting 50 more rep, feel free to post your comment as an actual answer. As long as there isn't a more constructive one, you're the first one who honestly said 'Nope.' – Fenixp Sep 23 '13 at 05:26
  • @Fenixp , i have a suggestion, what if you serialize the dom element, and save it to a file, and then get the size of this file , what do you think ? – mostafa khansa Sep 23 '13 at 08:20
  • @mostafa khansa Size as in width and height, not size as in ... Well, size. You're right, edited question to clear that up. – Fenixp Sep 23 '13 at 08:28
  • @mostafakhansa I don't think that he want the size in KB.. – avrahamcool Sep 23 '13 at 08:28
  • html2canvas might be able to reliably guess the size of non-dom elements, since it uses it's own layout engine. – dandavis Sep 23 '13 at 20:24
  • @dandavis If you look at the source html2canvas calculates element bounds using [getBoundingClientRect](https://developer.mozilla.org/en-US/docs/Web/API/element.getBoundingClientRect) so elements still need to be attached to the page for that to work. – Kerry Liu Sep 23 '13 at 21:06
  • You can't because the size of the `rootElement` is affected by where it will be inserted in the DOM .. So you cannot calculate its children sizes without first calculating its own. So you need to insert the whole structure in the DOM to see how it is affected.. – Gabriele Petrioli Sep 23 '13 at 23:44
  • @Gaby aka G. Petrioli Often enough I need it precisely the other way around - to calculate size of an innermost child. What angers me is that styles don't get applied on such element, so I have to append entire rootElement regardless. – Fenixp Sep 24 '13 at 07:07
  • Maybe you can find any possible solution in [this post](http://stackoverflow.com/questions/526347/css-javascript-how-do-you-get-the-rendered-height-of-an-element) – Martin Sep 24 '13 at 11:17
  • What is the end goal here? Are you wanting to create a tightly knit mosaic of 3rd-party HTML controls or something? I'm thinking that better understanding your end goal could aid others in coming up with some alternative approaches. – BigMacAttack Sep 24 '13 at 15:38
  • @BigMacAttack "Are you wanting to create a tightly knit mosaic of 3rd-party HTML controls or something?" <- yes, that. I'll edit the OP a little more. – Fenixp Sep 24 '13 at 20:59
  • Do you intend to use your own mosaic/tiling algorithm or do you plan on using an existing one? There are actually quite a few javascript libraries for creating mosaics. For example, there is a very popular one called [Masonry](http://masonry.desandro.com/) that appears to be pretty good at what it does. What about placing your HTML elements in container elements and letting something like Masonry figure out the tiling of the containers? – BigMacAttack Sep 24 '13 at 21:34

7 Answers7

10

You run into this often if you need to initially hide components like accordions, sliders, and other things that require bounding box information to work properly.

A simple trick is to just add css that hides the visibility of the content in question and ensures that it doesn't flicker or cause interfere with your layout.

It can be something as simple as:

{   visibility:hidden;
    position:absolute;
    left: -9999px;
}

And then setting position back to static or relative and visibility back to visible when you're ready to show the component.

The fiddle is here, but there's not much to it: http://jsfiddle.net/gwwar/ZqQtz/1/

Kerry Liu
  • 2,072
  • 16
  • 16
  • That would be the first thing I'd do as well, however we can never be sure what will end up displaying on the page - elements to display, their sizes, positions and styles get fetched from DB, and even that based on user input. +1 for future googlers tho, thank you for the effort. – Fenixp Sep 23 '13 at 05:59
  • Hmm, if you're doing something quite complicated, and want to offload some of that work to the server, it almost sounds like you want to use a headless browser. You'd need to create special pages for the components and it'd be super hacky, but the calculations would be pretty accurate provided that you had the proper fonts installed on your machine. Scripting things is pretty simple with PhantomJS and CasperJS. Anyway, I might consider doing that for a test integration, but I wouldn't recommend this for prod. – Kerry Liu Sep 23 '13 at 06:12
  • The other requirement is that the application must be, with certain limitations, functional offline. But doing the calculations server-side is not a terrible idea, after all we do have access to both JS and CSS files and we have to send them to client at some point. Still would be hacky. My brain has to process that, thanks – Fenixp Sep 23 '13 at 06:24
  • The only other thing I can think of that may be of interest would be the [shadow dom](http://www.w3.org/TR/shadow-dom/) This is experimental and only supported on chrome. I haven't played with this myself yet, but you may want to read up on this. – Kerry Liu Sep 23 '13 at 07:04
  • Well... Now I know of another extremely cool feature that I won't be able to use due to browser restrictions. I need to ask someone why we didn't start development a year later. – Fenixp Sep 23 '13 at 07:58
6

Getting the rendered width and height of a box-model DOM node using javascript without actually adding it to the DOM to be displayed is not possible.

To prove this, let's walk through how the rendered height and width of a DOM node is calculated internally to the browser. I will reference how WebKit handles this since it is the most commonly used layout engine.

As the document is parsed and DOM nodes are added to the "DOM Tree", Renderers are created for the DOM nodes that need to be displayed. This is how the "Render Tree" gets built.

Here is an excerpt from an article entitled "WebCore Rendering I – The Basics" by Dave Hyatt on the official WebKit Blog:

"Renderers are created through a process on the DOM called attachment. As a document is parsed and DOM nodes are added, a method called attach gets called on the DOM nodes to create the renderers.

void attach()

The attach method computes style information for the DOM node. If the display CSS property for the element is set to none or if the node is a descendant of an element with display: none set, then no renderer will be created."

So, in order to be efficient, the browser does not even bother computing style information for elements with display set to none. As a result, the information is not available to be accessed via javascript. However, if the display property is not set to none, the following occurs:

"During attachment the DOM queries CSS to obtain style information for an element. The resultant information is stored in an object called a RenderStyle... The RenderStyle can be accessed from a RenderObject using the style() method... One of the principal workhorse subclasses of RenderObject is RenderBox. This subclass represents objects that obey the CSS box model. These include any objects that have borders, padding, margins, width and height."

So if your use case allows for you to retrieve the box-model rendering height and width via C/C++ directly from the browser and pass it to your javascript code via some other means, then you could query the height/width methods of the RenderBox subclass of each DOM element. This is basically how the WebKit Developer Tools gets this information.

Fenixp
  • 645
  • 5
  • 22
BigMacAttack
  • 4,479
  • 3
  • 30
  • 39
  • Thank you for your effort put into the answer. I actually am aware of why it can't be done - regardless, I need it done somehow. The C/C++ route would be interesting, but not all that useful for a released application. I suppose displaying the elements, getting their size and hiding them is the only way to go. – Fenixp Sep 24 '13 at 07:03
  • @Fenixp *"I suppose displaying the elements, getting their size and hiding them is the only way to go."* - Correct. If you wish to get the actual rendered size (i.e. w/o guessing by using libraries such as jquery.actual) then this is the only way to go. Be sure to look at [this SO question](http://stackoverflow.com/questions/1473584/need-to-find-height-of-hidden-div-on-page-set-to-displaynone) and its answers for some implementation ideas. – BigMacAttack Sep 24 '13 at 15:36
1

You can try this jQuery plugin : https://github.com/dreamerslab/jquery.actual

lefoy
  • 1,158
  • 2
  • 13
  • 25
  • Thanks for the link! It basically appends the element to the page, gets size, and then removes it afterwards - I'm also not entirely sure whether it even works for elements which are not hidden (i. e. - not in the page's DOM tree at all). Lukily enough, the library will come in handy for completely unrelated project of mine. Thank you! – Fenixp Sep 24 '13 at 07:09
1

Since you'd asked for related answers...

If your DOM elements are not to be dynamically sized by it's contents, I would suggest the following workflow.

Although it can make your DOM structure a bit more bloated, it is often better off to utilize "container" objects. These objects would possess no borders, padding or margins to keep their DOM dimensions predictable. Then you can force the child to expand snuggly within it's container's bounds.

Your container object would be use for querying specific sizing and placement, since there are no extra attributes to account for size distortion.

http://jsfiddle.net/aywS6/

CSS

#container {
    position: absolute;
    margin:0px;
    padding:0px;
    width: 300px;
    height: 100px;
    background: #aaa;
    border: 0 none;
}

#subject {
    position: absolute;
    top: 0px;
    bottom: 0px;
    left: 0px;
    right: 0px;
    padding: 10px 20px;
    background: #aaf;
}

HTML

<div id="container">
    <div id="subject">
        contents...
    </div>
</div>
eriksssss
  • 410
  • 6
  • 6
1

The problem here is you can't get the element size before you add this element on the page.

On your method "constructElement", you adding the element to a root element, but just on onload you adding to the page. Your property "rootElement" not have the size properties setted.

Rethink the structure of your object/method if you want to manipulate these properties.

Look my update of your fiddle

$(function ()
{
    var ex = new exampleElement("Hello", "Here's text");
    ex.constructElement();
    document.body.appendChild(ex.rootElement);

    $("<span/>")
        .text(" => Width is " + $("div").width() + ", and Height is " + $("div").height())
        .appendTo($("div p"));
});
Willian Andrade
  • 332
  • 3
  • 11
0

Don't know if that helps, but since you asked for any answer :) here is what I have.

I had a similar issue where I wanted to have nice jQuery effect to resize an HTML container to "auto" height and width according to it's inner content. The idea is still similar to what some people already mentioned here: you have a container somewhere "off the screen", you put your stuff there, measer the dimentions and delete it. So here is the function I have found here

jQuery.fn.animateAuto = function(prop, speed, callback) {
    var elem, height, width;
    return this.each(function(i, el) {
        el = jQuery(el), elem = el.clone().css({
            "height" : "auto",
            "width" : "auto"
        }).appendTo("body");
        height = elem.css("height"), width = elem.css("width"), elem.remove();

        if (prop === "height")
            el.animate({
                "height" : height
            }, speed, callback);
        else if (prop === "width")
            el.animate({
                "width" : width
            }, speed, callback);
        else if (prop === "both")
            el.animate({
                "width" : width,
                "height" : height
            }, speed, callback);
    });
};

This code has a little more than you need but the part that you might be interested in is this:

elem = el.clone().css({
                "height" : "auto",
                "width" : "auto"
}).appendTo("body");
height = elem.css("height")
width = elem.css("width")

Hope this helps

Pinny
  • 206
  • 2
  • 10
0

If the main problem is that you can't get the size before placing it onto the screen I would recommend creating a class that allows the div to be hidden under the page's background. This should work cross browser.

.exampleHidden{
    position: absolute;
    z-index: -1;
}

If you set the main background of the page's z-index to 0 then attaching the hidden class to your new element it would put it under the background. Assuming it isn't larger than your background it should be hidden for now to allow you to measure and set the height and width before actually bringing it back to the foreground by removing the .exampleHidden class.