Skip to main content
Ben Nadel at Scotch On The Rock (SOTR) 2010 (Brussels) with: Ray Camden and Alison Huselid and David Huselid
Ben Nadel at Scotch On The Rock (SOTR) 2010 (Brussels) with: Ray Camden@cfjedimaster ) , Alison Huselid , and David Huselid

You Can Render Anything In Angular

By on

The other day, I heard someone say something to the effect of: In an Angular application you're stuck doing it "The Angular Way". The person saying this was implying that it's somehow easier to perform low-level programming in React. If anything, I'd argue that the opposite is more true; however, I don't wish to compare Angular to React - I just want to flesh-out what might be a missing mental for some developers. In case you didn't know, in Angular, you can render anything you want. Breaking out of the Angular application life-cycle and performing custom work is relatively straightforward.

Run this demo in my JavaScript Demos project on GitHub.

View this code in my JavaScript Demos project on GitHub.

To explore this idea, consider the following TypeScript class, Messenger. It accepts a DOM (Document Object Model) element and exposes a renderMessage() method. This class does not know anything about Angular at all. It doesn't know anything about any framework that might be consuming it. It doesn't import any classes or apply any directives or decorators. It's just vanilla TypeScript / JavaScript that performs its own DOM manipulations:

/**
* I DO NOT KNOW ANYTHING ABOUT THE ANGULAR APPLICATION. I am just a class that renders
* some stuff to the given DOM Element when told to do some things. I don't know who or
* what tells me to do those things. I have complete freedom to be me!
*/
export class Messenger {

	private element: HTMLElement;

	/**
	* I initialize the messenger for the given DOM element.
	*/
	constructor( element: HTMLElement ) {

		this.element = element;
		this.element.style.display = "block";
		this.element.addEventListener( "mouseenter", this.handleMouseenter );
		this.element.addEventListener( "mouseleave", this.handleMouseleave );

	}

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

	/**
	* I update the rendered message using the given name.
	*/
	public renderMessage( name: string ) : void {

		this.element.textContent = `Hello ${ name }, how goes it?`;

	}

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

	/**
	* I update the rendering of the message on mouse-enter.
	*/
	private handleMouseenter = ( event: MouseEvent ) : void => {

		this.element.style.fontWeight = "bolder";
		this.element.style.backgroundColor = "yellow";

	}


	/**
	* I update the rendering of the message on mouse-leave.
	*/
	private handleMouseleave = ( event: MouseEvent ) : void => {

		this.element.style.fontWeight = "normal";
		this.element.style.backgroundColor = "transparent";

	}

}

Now, this JavaScript class won't magically instantiate itself - we have to create something in Angular world that invokes the Messenger constructor and passes in a DOM element on which the Messenger class can act. To do this, I'm going to create a light-weight wrapper component that acts as the liaison between the Angular life-cycle and the Messenger life-cycle.

This wrapper / liaison component does two main things:

  1. It makes sure to instantiate the Messenger class outside of the Angular Zone. Angular simplifies state management / DOM reconciliation by monkey-patching a number of low-level DOM methods (ex, setTimeout() and addEventListener()). This way, Angular knows to trigger a change-detection life-cycle when low-level actions take place inside the application.

    In our case, however, since we intend to render something wholly custom within our Angular application, we don't want the monkey-patched methods to be used by our Messenger class. As such, when we instantiate the Messenger class, we're going to do it via Zone's runOutsideAngular() method. This way, Messenger can only access the original, native DOM methods.

  2. It pipes any necessary view-model changes into the Messenger instance. In our case, that means calling the .renderMessage() method whenever the relevant name value changes. In our demo, that's going to be done via a simple NgModel two-way data-bindings.

The following light-weight wrapper component exposes an Input, name. We then use the ngOnChanges() life-cycle method to make sure that new value gets pumped into the Messenger instance via renderMessage():

// Import core Angular modules.
import { Component } from "@angular/core";
import { ElementRef } from "@angular/core";
import { NgZone } from "@angular/core";

// Import application modules.
import { Messenger } from "./messenger";

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

@Component({
	selector: "app-messenger",
	standalone: true,
	inputs: [ "name" ],
	template: ""
})
export class MessengerComponent {

	public name: string;

	private messenger: Messenger;
	private zone: NgZone;

	/**
	* I initialize the messenger wrapper with the given host element. This component is a
	* light-weight wrapper that connects the Angular runtime to the underlying messenger
	* instance.
	*/
	constructor(
		elementRef: ElementRef,
		zone: NgZone
		) {

		this.zone = zone;
		this.name = "";

		// Since the Messenger class might use methods (such as addEventListener()) that
		// are monkey-patched by Zone.js, we want to make sure to construct the Messenger
		// instance outside of the Angular zone. This way, when it performs actions
		// internally, it won't trigger unnecessary change detection cycles in Angular.
		this.messenger = this.zone.runOutsideAngular(
			() => {

				return( new Messenger( elementRef.nativeElement ) );

			}
		);

	}

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

	/**
	* I push name changes in Angular down into the Messenger instance.
	*/
	public ngOnChanges() : void {

		this.messenger.renderMessage( this.name );

	}

}

As you can see, this wrapper component is acting as an extremely light-weight conduit between the Angular application and the underlying Messenger class.

To see this light-weight wrapper in action, let's use it in our App component. Notice that we're using [(ngModel)] two-way data bindings to seamlessly and effortlessly pipe view-model changes into our wrapper:

<h1>
	You Can Render Anything In Angular 15
</h1>

<p class="entry">
	<strong>Name:</strong> <input [(ngModel)]="name" />
</p>

<!--
	This Component is just a light-weight wrapper that acts an interface between the
	Angular application and whatever random thing you are rendering under the hood.
	Essentially, this component is just the "calling context" that you would need in any
	kind of application that renders the "thing" in question.
-->
<app-messenger [name]="name"></app-messenger>

And, when we run this Angular 15 code, we get the following output:

An input field, that when change, causes the underlying Messenger instance to be re-rendered inside the Angular application.

As you can see, our updated name value in Angular's view-model is pushed down into the Messenger instance via the ngOnChanges() life-cycle method. And, the mouseenter and mouseleave events - bound inside the Messenger class - have no problem updating the DOM.

If you step back for a minute and think about what "Angular" is at a high-level, it's really just a way to take state (your Classes) and reconcile that state with the DOM (your templates). To render something "completely custom" inside an Angular application, all you're really doing is using a "template" to render the "custom thing". And then, optionally, hooking into the state reconciliation life-cycle in order to push changes down into that template.

If anything, Angular's stateful component approach more closely mirrors the browser's native stateful DOM approach (when compared to some other frameworks). This should make it easier to build completely custom things in Angular - not harder.

Hopefully this helps clear up any confusion or misconceptions that newer developers might have about the Angular web application framework.

Want to use code from this post? Check out the license.

Reader Comments

21 Comments

interesting read. i have worked with angular and react, and react also provides a similar escape hatch. my guess is all of frameworks allow an escape hatch like this.

15,334 Comments

@Hassam,

I've only played with earlier versions of React that had life-cycle methods (like componentWillMount() and stuff). In that case, it feels like a relatively similar approach. When it comes to "new" React with Hooks, I don't know how it would work. I feel like you'd have to create a special Effects hook or something that depends on the prop ... I'm talking out of my depth though.

15,334 Comments

@Hassam,

❤️ Very cool ❤️ I really appreciate you putting that together. It took me a minute or two, looking at the code, to see what was going on. The Hooks approach to building React application really requires me to turn my brain a bit inside out. I see that you have three different hooks to get this all working as expected.

I know people love Hooks; but, the life-cycle methods still feel more natural to me. Though, I guess you just get used to a new paradigm.

Regardless, thanks again - it's great for people to know that they can pretty much do whatever they want in any of the modern frameworks. None of them holds you back.

21 Comments

Thanks for the kind words Ben! I too miss the old react with life cycle hooks, i know there are better ways to write the sample which I did but it should be the job of the framework to make right approach easy and bad approach hard.

Post A Comment — I'd Love To Hear From You!

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.