Using Expando DOM Properties In Angular 9.1.6
In the vast, vast majority of cases, the View-Model in Angular is the source of truth for the application; and, the HTML template is little more than the manifestation of said truth. However, in some edge-cases, representing all information in the view-model is either inelegant or costly in terms of performance. In those edge-cases, it can be helpful to connect the DOM (Document Object Model) to an in-memory value using an "expando" property. An expando property is just a unique attribute value that can be used to map an in-memory value to a DOM element. Let's take a quick look at expando properties in Angular 9.1.6.
Run this demo in my JavaScript Demos project on GitHub.
View this code in my JavaScript Demos project on GitHub.
The idea of an "expando" property is rather old. jQuery has always used an expando properties to store .data()
associated with an Element. Internally, jQuery uses a numeric counter that it increments every time it has to inject an expando property into the DOM. In Angular, we can create an Expando Service that encapsulates this counter for us:
// Import the core angular services.
import { Injectable } from "@angular/core";
// ----------------------------------------------------------------------------------- //
// ----------------------------------------------------------------------------------- //
@Injectable({
providedIn: "root"
})
export class Expando {
private id: number;
private attributeName: string;
// I initialize the expando service.
constructor() {
this.id = 0;
this.attributeName = ( "data-expando" + Date.now() );
}
// ---
// PUBLIC METHODS.
// ---
// I add an expando property to the given element, returning the unique ID.
public add( element: Element ) : number {
var nextID = ++this.id;
var value = String( nextID );
element.setAttribute( this.attributeName, value );
return( nextID );
}
// I return the unique ID of the expando property on the given element. Or, if there
// is no expando property, I return zero.
public get( element: Element ) : number {
return( Number( element.getAttribute( this.attributeName ) ) );
}
// I remove the expando from the given element, returning the unique ID.
public remove( element: Element ) : number {
var value = this.get( element );
element.removeAttribute( this.attributeName );
return( value );
}
}
As you can see, this Angular service is doing next to nothing. It has .add()
, .get()
, and .remove()
methods which inject, read, and remove the expando property for a given HTML element respectively. Every time the .add()
method is called, a unique value is added to the DOM and returned to the calling context.
The unique value returned from the .add()
method can then be used to store in-memory information associated with the given HTML element. Illustrating a use-case for this goes beyond the scope of this article; but, let's take a quick look at how it works in the calling context.
As a trite example, I'm using the expando
service in my App component to add and remove the expando property on a set of Paragraph tags:
// Import the core angular services.
import { Component } from "@angular/core";
// Import the application components and services.
import { Expando } from "./expando";
// ----------------------------------------------------------------------------------- //
// ----------------------------------------------------------------------------------- //
@Component({
selector: "app-root",
styleUrls: [ "./app.component.less" ],
template:
`
<p (click)="toggle( $event.target )"> Roses are red. </p>
<p (click)="toggle( $event.target )"> Violets are blue. </p>
<p (click)="toggle( $event.target )"> Expando properties are cool. </p>
<p (click)="toggle( $event.target )"> And so is Angular. </p>
`
})
export class AppComponent {
private expando: Expando;
// I initialize the app component.
constructor( expando: Expando ) {
this.expando = expando;
}
// ---
// PUBLIC METHODS.
// ---
// I toggle the expando property on the given element.
public toggle( element: HTMLElement ) : void {
if ( this.expando.get( element ) ) {
var value = this.expando.remove( element );
console.group( "%cRemoving Expando Property", "color: red ;" );
console.log( "Value:", value );
console.log( element );
console.log( element.dataset );
console.groupEnd();
} else {
var value = this.expando.add( element );
console.group( "%cInjecting Expando Property", "color: green ;" );
console.log( "Value:", value );
console.log( element );
console.log( element.dataset );
console.groupEnd();
}
}
}
As you can see, when the user clicks on one of the Paragraph tags, I'm checking to see if the given HTML element has an expando property; then, I'm either adding or removing it based on those results. And, when we run the following Angular app in Chrome, we get the following output:
As you can see, when the Expando
service injects the expando property into the DOM, it shows up as a data-*
attribute that contains a numeric value. This numeric value is what we can use (in a future post) to associate an Element with an in-memory data cache.
Most of the time, you'll have to think about something like this - you just build your view-model and Angular's template reconciliation takes care of the rest. When this is not possible, however, an expando property can help keep the "kludgey" solutions a bit more elegant. Which is what I hope to showcase in my next Angular post.
Want to use code from this post? Check out the license.
Reader Comments
Hi, Ben Thanks for everything that you are doing it's really cool and informative , I just want to request you fr a tutorial on gRPC in Angular like how we can do this in Enterprise application also how to call API and show the data. I really feel that very complex so it will be very helpful if you made one tutorial on that Thanks :)
@Sourish,
Thanks for the kind comments. Unfortunately, I have zero experience with gRPC. My rough understanding is that it's a way to pack data in an HTTP request using fixed-length byte-offsets (as opposed to a JSON-like key-value pairing). I assume you would need a special AJAX client that understands how to pack and unpack these type of payloads. But, I've not tried this personally.
@Ben
Thanks for the replay yes actually this one is light weighted and mostly I just want to learn that protocol buffer instead of JSON it saves the size of the data but it is a bit tough to handle like complexity is there anyways let see and Thanks for everything you do :)
@All,
As a follow-up to this post, I wanted to look at how we can use Expando properties in Angular to power some clean interactions with the
IntersectionObserver
API:www.bennadel.com/blog/3826-using-expando-dom-properties-to-power-the-intersectionobserver-api-in-angular-9-1-6.htm
@Sourish,
My pleasure. Personally, I try to err on the side of simplicity. I know a lot of people love the idea of compression and efficiency -- but, sometimes, you just can't beat the ease of use that JSON over the network gives you (especially for debuggin).