Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
Ben Nadel at InVision In Real Life (IRL) 2019 (Phoenix, AZ) with: Clark Valberg and David Fraga
Ben Nadel at InVision In Real Life (IRL) 2019 (Phoenix, AZ) with: Clark Valberg@clarkvalberg ) and David Fraga@davidfraga )

Having Fun With Position: Fixed And Element.getBoundingClientRect() In Angular 9.0.0-rc.2

By Ben Nadel on

Over the years, I've gotten more confident in my ability to position elements, both fixed and absolute. But, for the most part, these elements are all predictably positioned within the web page. Where I still lack confidence is in my ability to position an element based on another arbitrary element whose position will not be known until runtime. I believe that the key to such positioning lies in the Element.getBoundingClientRect() DOM (Document Object Model) method. And, as such, I wanted to invest some time in getting more comfortable with this method in Angular 9.0.0-rc.2.

Run this demo in my JavaScript Demos project on GitHub.

View this code in my JavaScript Demos project on GitHub.

I have played with the Element.getBoundingClientRect() method a little bit in the past. For example, I've used it to outline Text selections on the page, inspired by Medium's contextual tools feature. But, I haven't really used it in a production application yet. As such, I don't have that muscle memory for how it works; or, where it makes sense.

To try and work this muscle, I wanted to create a fun little Selection feature, inspired by the Inspect tool at InVision. Inside Inspect, you can click into the shapes within a Prototype and see the dimensions and offsets of those shapes in relation to the overall screen.

Now, I didn't want to try and tackle something quite as complicated; so, I'm just going to show the distance from the selected Element to the edge of the Viewport. And, as our luck would have it, this is exactly what Element.getBoundingClientRect() gives us.

When the user clicks on an Element, I'm going to show a position: fixed outline of that Element. And, if the user continues to click on an Element within the local DOM branch, I'm going to gradually expand the scope of the selection until we hit the Document Element.

Since this is a very "visual" effect, I think it would be helpful to look at the demo before we look at the code. Here's me clicking on various elements within the demo content:

A red outline dynamically renders above a selected element within the DOM using Angular 9.0.0-rc.2.

As you can see, when I click on an Element, a red outline appears accompanied by guidelines that indicate the distance from said Element to the edge of the viewport. And, as I click from Element to another, the outline follows or expands-upon my selection.

This was a lot of fun to build! And it's gotten my very excited about possible use-cases for Element.getBoundingClientRect().

With that, let's look at the code. This Angular app has two core components: the App component, which captures the selection and measures the bounding rectangle; and, the Overlay component which renders the red outline and the guidelines.

Let's look at the App component first:

// Import the core angular services.
import { Component } from "@angular/core";

// ----------------------------------------------------------------------------------- //
// ----------------------------------------------------------------------------------- //

interface Overlay {
	left: number;
	height: number;
	top: number;
	width: number;
}

// This class will be injected into the DOM in order to identify which element is
// currently being "focused" by the overlay. This has no technical function - it just
// makes it easier to see what is happening in the Elements panel of the dev-tools.
var TRACER_CLASS = "-+-+-+-+-+-+-+-target-element-+-+-+-+-+-+-+-";

@Component({
	selector: "app-root",
	host: {
		"(document:click)": "handleClick( $event )",
		"(window:keydown.Esc)": "reset()",
		"(window:resize)": "reset()"
	},
	styleUrls: [ "./app.component.less" ],
	template:
	`
		<!--
			Just moving the "demo content" to another component so that we don't
			have a ton of noise in the app component.
		-->
		<app-demo-content></app-demo-content>

		<app-overlay
			*ngIf="targetOverlay"
			[height]="targetOverlay.height"
			[left]="targetOverlay.left"
			[top]="targetOverlay.top"
			[width]="targetOverlay.width">
		</app-overlay>
	`
})
export class AppComponent {

	public targetOverlay: Overlay | null;

	private targetElement: HTMLElement | null;

	// I initialize the app component.
	constructor() {

		this.targetOverlay = null;
		this.targetElement = null;

	}

	// ---
	// PUBLIC METHODS.
	// ---

	// I handle the mouse click on the document.
	public handleClick( event: MouseEvent ) : void {

		// If we have an existing element, remove the tracer class.
		// --
		// NOTE: This class serves no functional purpose other than to indicate where in
		// the DOM tree the selected target lives. This will show up in the Elements pane
		// of the Chrome Dev Tools, and will clearly illustrate the journey of the
		// selection as the user continues to click within a single DOM branch.
		if ( this.targetElement ) {

			this.targetElement.classList.remove( TRACER_CLASS );

		}

		this.targetElement = this.getNextTarget( event );
		this.targetOverlay = null;

		if ( this.targetElement ) {

			this.targetElement.classList.add( TRACER_CLASS );

			// The bounding client rectangle contains the FIXED location of the given
			// element within the browser's viewport.
			var rect = this.targetElement.getBoundingClientRect();
			
			this.targetOverlay = {
				height: rect.height,
				left: rect.left,
				top: rect.top,
				width: rect.width
			};

		}

	}


	// I reset the state, clearing the target element and overlay.
	public reset() : void {

		this.targetElement = null;
		this.targetOverlay = null;

	}

	// ---
	// PRIVATE METHODS.
	// ---

	// I get the next target element based on the given mouse click event.
	private getNextTarget( event: MouseEvent ) : HTMLElement | null {

		var target = ( event.target as HTMLElement );

		// If we have an existing target element and the user clicked below the target
		// element but within the same DOM branch, let's move up one level in the DOM
		// tree.
		if ( this.targetElement && this.targetElement.contains( target ) ) {

			return( this.targetElement.parentElement || null );

		}

		// Otherwise, just use the user's click target.
		return( target );

	}

}

There's not too much going on here. When you get past the event-handlers and the template, basically, this component is a glorified call to:

this.targetElement.getBoundingClientRect();

... which then populates a simple Object that provides the input-bindings for the Overlay component. The Overlay component then renders the outline and the four guidelines. This component uses position: fixed, which means that its coordinates line up quite nicely with the results of the .getBoundingClientRect() call. However, I do have to do a tiny bit of math in order to render the guidelines:

// Import the core angular services.
import { Component } from "@angular/core";

// ----------------------------------------------------------------------------------- //
// ----------------------------------------------------------------------------------- //

@Component({
	selector: "app-overlay",
	inputs: [
		"height",
		"left",
		"top",
		"width"
	],
	styleUrls: [ "./overlay.component.less" ],
	template:
	`
		<div
			class="target"
			[style.opacity]="1"
			[style.top.px]="fixedTop"
			[style.right.px]="fixedRight"
			[style.bottom.px]="fixedBottom"
			[style.left.px]="fixedLeft">
		</div>

		<div
			class="vertical-offset"
			[style.opacity]="( ( fixedTop > 30 ) ? 1 : 0 )"
			[style.top.px]="0"
			[style.right.px]="fixedRight"
			[style.left.px]="fixedLeft"
			[style.height.px]="fixedTop">
			<span class="size">
				{{ fixedTop }}
			</span>
		</div>

		<div
			class="vertical-offset"
			[style.opacity]="( ( fixedBottom > 30 ) ? 1 : 0 )"
			[style.right.px]="fixedRight"
			[style.bottom.px]="0"
			[style.left.px]="fixedLeft"
			[style.height.px]="fixedBottom">
			<span class="size">
				{{ fixedBottom }}
			</span>
		</div>
		
		<div
			class="horizontal-offset"
			[style.opacity]="( ( fixedRight > 50 ) ? 1 : 0 )"
			[style.top.px]="top"
			[style.right.px]="0"
			[style.bottom.px]="fixedBottom"
			[style.width.px]="fixedRight">
			<span class="size">
				{{ fixedRight }}
			</span>
		</div>

		<div
			class="horizontal-offset"
			[style.opacity]="( ( fixedLeft > 50 ) ? 1 : 0 )"
			[style.top.px]="fixedTop"
			[style.bottom.px]="fixedBottom"
			[style.left.px]="0"
			[style.width.px]="fixedLeft">
			<span class="size">
				{{ fixedLeft }}
			</span>
		</div>
	`
})
export class OverlayComponent {

	public height!: number;
	public left!: number;
	public top!: number;
	public width!: number;

	public fixedBottom: number;
	public fixedLeft: number;
	public fixedRight: number;
	public fixedTop: number;

	// I initialize the overlay component.
	constructor() {

		this.fixedBottom = 0;
		this.fixedLeft = 0;
		this.fixedRight = 0;
		this.fixedTop = 0;

	}

	// ---
	// PUBLIC METHODS.
	// ---

	// I get called whenever the input bindings change.
	public ngOnChanges() : void {

		// Translate the input bindings to "fixed" coordinates.
		// --
		// NOTE: The Top/Left inputs are already intended to be "fixed", but we need to
		// calculate the Right/Bottom based on the dimensions of the window.
		this.fixedTop = Math.floor( this.top );
		this.fixedLeft = Math.floor( this.left );
		this.fixedRight = Math.floor( document.documentElement.clientWidth - ( this.left + this.width ) );
		this.fixedBottom = Math.floor( document.documentElement.clientHeight - ( this.top + this.height ) );

	}

}

Anyway, just a fun little Friday code kata in Angular 9.0.0-rc.2. This post, and the one before it, are both building up to something more interesting; however, I wanted to prime my brain for working with Element.getBoundingClientRect() before I tried to wrap my head around something more complicated.

Have a great weekend!



Reader Comments

This is very cool. And it worked on my iPhone as well!

Yes. I love:

getBoundingClientRect()

It provides just about every piece of geometric/co-ordinate data that is ever needed for a DOM element. And, I think it has existed since the very early days of JavaScript!

Reply to this Comment

@Charles,

Word up -- MDN (Mozilla Developer Network) says it goes back to IE4?! How is it possible that I didn't start using this like 10 years ago? Probably because jQuery did all my positioning back then :D Now that I'm going a bit more "native", time to learn this stuff.

Reply to this Comment

Yes. Totally. I only started using:

getBoundingClientRect()

A year or so ago!

Before then, I used to struggle with things like:

style.left
style.top
style.width
style.height

But, sometimes, these do not return values for one reason or another.

Reply to this Comment

Post A Comment

You — Get Out Of My Dreams, Get Into My Comments
Live in the Now
Oops!
NEW: Some basic markdown formatting is now supported: bold, italic, blockquotes, lists, fenced code-blocks. Read more about markdown syntax »
Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.