The document object model in JavaScript: Layout and styling

1. Layout

You may have noticed that different types of elements are laid out differ­ently. Some, such as paragraphs (<p>) or headings (<h1>), take up the whole width of the document and are rendered on separate lines. These are called block elements. Others, such as links (<a>) or the <strong> element, are ren­dered on the same line with their surrounding text. Such elements are called inline elements.

For any given document, browsers are able to compute a layout, which gives each element a size and position based on its type and content. This layout is then used to actually draw the document.

The size and position of an element can be accessed from JavaScript. The offsetWidth and offsetHeight properties give you the space the element takes up in pixels. A pixel is the basic unit of measurement in the browser. It traditionally corresponds to the smallest dot that the screen can draw, but on modern displays, which can draw very small dots, that may no longer be the case, and a browser pixel may span multiple display dots.

Similarly, clientWidth and clientHeight give you the size of the space inside the element, ignoring border width.

<p style=”border: 3px solid red”>

I’m boxed in

</p>

<script>

let para = document.body.getElementsByTagName(“p”)[0];

console.log(“clientHeight:”, para.clientHeight);

console.log(“offsetHeight:”, para.offsetHeight);

</script>

Giving a paragraph a border causes a rectangle to be drawn around it.

I’m boxed in

The most effective way to find the precise position of an element on the screen is the getBoundingClientRect method. It returns an object with top, bottom, left, and right properties, indicating the pixel positions of the sides of the element relative to the top left of the screen. If you want them relative to the whole document, you must add the current scroll position, which you can find in the pageXOffset and pageYOffset bindings.

Laying out a document can be quite a lot of work. In the interest of speed, browser engines do not immediately re-layout a document every time you change it but wait as long as they can. When a JavaScript program that changed the document finishes running, the browser will have to compute a new layout to draw the changed document to the screen. When a pro­gram asks for the position or size of something by reading properties such as offsetHeight or calling getBoundingClientRect, providing correct information also requires computing a layout.

A program that repeatedly alternates between reading DOM layout information and changing the DOM forces a lot of layout computations to happen and will consequently run very slowly. The following code is an example of this. It contains two different programs that build up a line of X characters 2,000 pixels wide and measures the time each one takes.

<p><span id=”one”></span></p>

<p><span id=”two”></span></p>

<script>

function time(name, action) {

let start = Date.now(); // Current time in milliseconds action();

console.log(name, “took”, Date.now() – start, “ms”);

}

time(“naive”, () => {

let target = document.getElementById(“one”);

while (target.offsetWidth < 2000) {

target.appendChild(document.createTextNode(“X”));

}

});

// → naive took 32 ms

time(“clever”, function() {

let target = document.getElementById(“two”);

target.appendChild(document.createTextNode(“XXXXX”));

let total = Math.ceil(2000 / (target.offsetWidth / 5));

target.firstChild.nodeValue = “X”.repeat(total);

});

// → clever took 1 ms </script>

2. Styling

We have seen that different HTML elements are drawn differently. Some are displayed as blocks, others inline. Some add styling—<strong> makes its content bold, and <a> makes it blue and underlines it.

The way an <img> tag shows an image or an <a> tag causes a link to be followed when it is clicked is strongly tied to the element type. But we can change the styling associated with an element, such as the text color or underline. Here is an example that uses the style property:

<p><a href=”.”>Normal link</a></p>

<p><a href=”.” style=”color: green”>Green link</a></p>

The second link will be green instead of the default link color.

Normal link

Green link

A style attribute may contain one or more declarations, which are a prop­erty (such as color) followed by a colon and a value (such as green). When there is more than one declaration, they must be separated by semicolons, as in “color:         red; border:                      none”.

A lot of aspects of the document can be influenced by styling. For example, the display property controls whether an element is displayed as a block or an inline element.

This text is displayed <strong>inline</strong>,

<strong style=”display: block”>as a block</strong>, and

<strong style=”display: none”>not at all</strong>.

The block tag will end up on its own line since block elements are not displayed inline with the text around them. The last tag is not displayed at all—display: none prevents an element from showing up on the screen. This is a way to hide elements. It is often preferable to removing them from the document entirely because it makes it easy to reveal them again later.

This text is displayed inline,

as a block

, and .

JavaScript code can directly manipulate the style of an element through the element’s style property. This property holds an object that has prop­erties for all possible style properties. The values of these properties are strings, which we can write to in order to change a particular aspect of the element’s style.

<p id=”para” style=”color: purple”>

Nice text

</p>

<script>

let para = document.getElementById(“para”);

console.log(para.style.color);

para.style.color = “magenta”;

</script>

There are some style property names that contain hyphens, such as font-family. Because such property names are awkward to work with in Java­Script (you’d have to say style[“font-family”]), the property names in the style object for such properties have their hyphens removed and the letters after them capitalized (style.fontFamily).

3. Cascading Styles

The styling system for HTML is called CSS, for Cascading Style Sheets. A style sheet is a set of rules for how to style elements in a document. It can be given inside a <style> tag.

<style> strong {

font-style: italic;

color: gray;

}

</style>

<p>Now <strong>strong text</strong> is italic and gray.</p>

The cascading in the name refers to the fact that multiple such rules are combined to produce the final style for an element. In the example, the default styling for <strong> tags, which gives them font-weight:  bold, is overlaid by the rule in the <style> tag, which adds font-style and color.

When multiple rules define a value for the same property, the most recently read rule gets a higher precedence and wins. So if the rule in the <style> tag included font-weight: normal, contradicting the default

font-weight rule, the text would be normal, not bold. Styles in a style attribute applied directly to the node have the highest precedence and always win.

It is possible to target things other than tag names in CSS rules. A rule for .abc applies to all elements with “abc” in their class attribute. A rule for #xyz applies to the element with an id attribute of “xyz” (which should be unique within the document).

.subtle {

color: gray; font-size: 80%;

}

#header {

background: blue; color: white;

}

/* p elements with id main and with classes a and b */

p#main.a.b {

margin-bottom: 20px;

}

The precedence rule favoring the most recently defined rule applies only when the rules have the same specificity. A rule’s specificity is a measure of how precisely it describes matching elements, determined by the number and kind (tag, class, or ID) of element aspects it requires. For example, a rule that targets p.a is more specific than rules that target p or just .a and would thus take precedence over them.

The notation p > a {…} applies the given styles to all <a> tags that are direct children of <p> tags. Similarly, p a {…} applies to all <a> tags inside <p> tags, whether they are direct or indirect children.

4. Query Selectors

We won’t be using style sheets all that much in this book. Understanding them is helpful when programming in the browser, but they are complicated enough to warrant a separate book.

The main reason I introduced selector syntax—the notation used in style sheets to determine which elements a set of styles apply to—is that we can use this same mini-language as an effective way to find DOM elements.

The querySelectorAll method, which is defined both on the document object and on element nodes, takes a selector string and returns a NodeList containing all the elements that it matches.

<p>And if you go chasing <span class=”animal”>rabbits</span></p>

<p>And you know you’re going to fall</p>

<p>Tell ’em a <span class=”character”>hookah smoking

<span class=”animal”>caterpillar</span></span></p>

<p>Has given you the call</p>

<script>

function count(selector) {

return document.querySelectorAll(selector).length;

}

console.log(count(“p”));     // All <p> elements

// → 4

console.log(count(“.animal”));    // Class animal

// → 2

console.log(count(“p .animal”)); // Animal inside of <p>

// → 2

console.log(count(“p > .animal”)); // Direct child of <p>

// → 1

</script>

Unlike methods such as getElementsByTagName, the object returned by querySelectorAll is not live. It won’t change when you change the document. It is still not a real array, though, so you still need to call Array.from if you want to treat it like one.

The querySelector method (without the All part) works in a similar way. This one is useful if you want a specific, single element. It will return only the first matching element or null when no element matches.

5. Positioning and Animating

The position style property influences layout in a powerful way. By default it has a value of static, meaning the element sits in its normal place in the document. When it is set to relative, the element still takes up space in the document, but now the top and left style properties can be used to move it relative to that normal place. When position is set to absolute, the element is removed from the normal document flow—that is, it no longer takes up space and may overlap with other elements. Also, its top and left proper­ties can be used to absolutely position it relative to the top-left corner of the nearest enclosing element whose position property isn’t static, or relative to the document if no such enclosing element exists.

We can use this to create an animation. The following document dis­plays a picture of a cat that moves around in an ellipse:

<p style=”text-align: center”>

<img src=”img/cat.png” style=”position: relative”>

</p>

<script>

let cat = document.querySelector(“img”);

let angle = Math.PI / 2;

function animate(time, lastTime) {

if (lastTime != null) {

angle += (time – lastTime) * 0.001;

}

cat.style.top = (Math.sin(angle) * 20) + “px”;

cat.style.left = (Math.cos(angle) * 200) + “px”;

requestAnimationFrame(newTime => animate(newTime, time));

}

requestAnimationFrame(animate);

</script>

The gray arrow shows the path along which the image moves.

Our picture is centered on the page and given a position of relative. We’ll repeatedly update that picture’s top and left styles to move it.

The script uses requestAnimationFrame to schedule the animate function to run whenever the browser is ready to repaint the screen. The animate function itself again calls requestAnimationFrame to schedule the next update. When the browser window (or tab) is active, this will cause updates to hap­pen at a rate of about 60 per second, which tends to produce a good-looking animation.

If we just updated the DOM in a loop, the page would freeze, and noth­ing would show up on the screen. Browsers do not update their display while a JavaScript program is running, nor do they allow any interaction with the page. This is why we need requestAnimationFrame—it lets the browser know that we are done for now, and it can go ahead and do the things that browsers do, such as updating the screen and responding to user actions.

The animation function is passed the current time as an argument. To ensure that the motion of the cat per millisecond is stable, it bases the speed at which the angle changes on the difference between the current time and the last time the function ran. If it just moved the angle by a fixed amount per step, the motion would stutter if, for example, another heavy task run­ning on the same computer were to prevent the function from running for a fraction of a second.

Moving in circles is done using the trigonometry functions Math.cos and Math.sin. For those who aren’t familiar with these, I’ll briefly introduce them since we will occasionally use them in this book.

Math.cos and Math.sin are useful for finding points that lie on a circle around point (0,0) with a radius of one. Both functions interpret their argu­ment as the position on this circle, with zero denoting the point on the far right of the circle, going clockwise until 2π (about 6.28) has taken us around the whole circle. Math.cos tells you the x-coordinate of the point that corre­sponds to the given position, and Math.sin yields the y-coordinate. Positions (or angles) greater than 2π or less than 0 are valid—the rotation repeats so that α + 2π refers to the same angle as a.

This unit for measuring angles is called radians—a full circle is 2π radi­ans, similar to how it is 360 degrees when measuring in degrees. The con­stant π is available as Math.PI in JavaScript.

The cat animation code keeps a counter, angle, for the current angle of the animation and increments it every time the animate function is called. It can then use this angle to compute the current position of the image ele­ment. The top style is computed with Math.sin and multiplied by 20, which is the vertical radius of our ellipse. The left style is based on Math.cos and multiplied by 200 so that the ellipse is much wider than it is high.

Note that styles usually need units. In this case, we have to append “px” to the number to tell the browser that we are counting in pixels (as opposed to centimeters, “ems,” or other units). This is easy to forget. Using numbers without units will result in your style being ignored—unless the number is 0, which always means the same thing, regardless of its unit.

Source: Haverbeke Marijn (2018), Eloquent JavaScript: A Modern Introduction to Programming, No Starch Press; 3rd edition.

Leave a Reply

Your email address will not be published. Required fields are marked *