Headless Angular Components

Post Editor

A headless component is one that provides behavior to its children, and allows the children to decide the actual UI to render while incorporating the behavior provided by the parent. In this article, we explore an example of a headless component, and the problems they help us solve.

7 min read
post

Headless Angular Components

A headless component is one that provides behavior to its children, and allows the children to decide the actual UI to render while incorporating the behavior provided by the parent. In this article, we explore an example of a headless component, and the problems they help us solve.

post
post
7 min read

Big thanks to Stephen Cooper (@SCooperDev) for reviewing this article

A headless component is one that provides behavior to its children, and allows the children to decide the actual UI to render while incorporating the behavior provided by the parent. Headless components encapsulate the implementation details of complex behaviors from the specific UI rendered on the page. By not being opinionated about the UI, headless components enable greater customization by letting us apply the reusable behaviors to a wider range of UI elements.

For the purposes of this article, when we say UI, we mean the visual elements the user sees on the page. Behavior refers to the actual functionality or effect that a user might see when interacting with elements on the page.

The concept of headless components has existed in the front end world for a couple years now, but has never really taken off in the Angular community. In React, Michael Jackson paved the way for headless components in his popular talk, "Never Write Another HoC," advocating for the Render Prop pattern, which is used to create headless React components. Kent C. Dodds later popularized the idea of headless components in React with the library, downshift, and his material on Advanced React Patterns. In 2018, Isaac Mann wrote a series of articles, translating Kent's Advanced React Patterns to Angular. Among the articles in that series, "Use <ng-template\>" shows how <ng-template> can be used to replicate React's Render Prop pattern. Stephen Cooper further advanced this idea in his 2019 talk: "ngTemplateOutlet: The secret to customisation".

In this article, we explore an example of a headless component, and introduce a slightly different syntax for creating headless components in Angular. This is my effort to help further socialize the concept of headless components in the Angular community.

File Select
Link to this section

Suppose we have to build a file select for our app. The good news is, the browser does a lot of the heavy lifting for us, but we still have to do a little bit of work to harness the native file input and make it look and behave as we want. So we might build something like this.

Starting off, this works great. We have a simple file select, and users can select whatever files they want. As others start using the file select, though, they will inevitably want to customize the UI for their own needs. For the first change, suppose we have different brand colors, and while we only ever want the primary color, other people want to use the file select with other colors. Not a huge problem. We can add an @Input() to control the button color.

<>Copy
` <button (click)="openFileSelectDialog()" [ngClass]="color"> Pick a file </button> ` export class FileSelectComponent { @Input() color = "primary"; }
@Input to control button color

Our component has increased slightly in complexity, but it still works and now everyone can use any brand color they want. At this point, it's still a pretty simple component, but we have more feature requests on the way!

Next, someone else on the team sees this file select interaction, and they want to use their <cool-button> component to trigger the file select dialog instead of a normal button. We could copy and paste the UI logic to programmatically trigger the click on the hidden input, but something seems wrong about straight copy and pasting, especially within the same component. So instead, we add another @Input() to control which UI element opens the file select dialog.

<>Copy
` <button *ngIf="!useCoolButton" (click)="openFileSelectDialog()" [ngClass]="color" > Pick a file </button> <cool-button *ngIf="useCoolButton" (click)="openFileSelectDialog()" > Pick a cool file </cool-button> ` export class FileSelectComponent { @Input() useCoolButton = false; }
@Input to control which button to use

At this point, it's starting to feel like this component is responsible for too much, but it gets the job done.

Next, someone wants the component to include a list of the selected files. If we were to satisfy this request, we might build out the markup for a list and add yet another @Input() to show and hide the list. At this point, it's time to stop and rethink our approach to maintaining this component. Ideally, it would be nice to find a way to make it work for everyone else without us having to maintain their specific UI needs.

The Problem with Customization
Link to this section

This is a slightly contrived example, as there's not much variation in a file select, but this still demonstrates the problems we're trying to solve with headless components. We've all written or seen code that works like this. Whether it’s a universal feature like selecting files or something application specific, we’re often tempted to manage every possible component customization in the same place. So what's wrong with our approach to this component so far?

For starters, we don't want to ship everyone else's code in our app. We may never use some of the variations added to this component, but that code has to be included in our app anyways. It's also harder to manage the code with all possible use cases located in one place. Code changes overtime, and with all of these unrelated pieces of UI cobbled together, it's easy to accidentally break someone else's use case when making a seemingly unrelated change. And as more UI variations are added to this component, think about the length of this file. As this file gets longer, it will be harder to read and manage the code.

Maybe we made all of these change unnecessarily though? What if we allowed users to apply their own "theme" to this component by overriding default css?

Similar to the problem of shipping everyone else's UI in our app, we're still doing the same thing with CSS: shipping default styles even though we've overridden them. If we already have our own design system, it’s unnecessary to duplicate those same styles. Even when we can override styles though, we still can't control the markup rendered to the page. Some UI changes are difficult or impossible to make via CSS alone and require different markdown altogether.

So how can we provide this native file select behavior in a way that allows other developers to use their own UI?

Headless File Select
Link to this section

As it turns out, Angular gives us more tools than just @Input() to customize components. Refactored into a headless component, this is how our file select looks now.

Let's step through the code to unpack how this works.

CallbackTemplateDirective
Link to this section

Notice first the *callbackTemplate directive.

<>Copy
<button *callbackTemplate="let context" class="primary" (click)="context.openFileSelectDialog()" > pick a file </button>
the *callbackTemplate directive in use

I'll typically name this directive something more application-specific, but for now we'll call it callbackTemplate for clarity. (Soon, we'll see how it's in some ways analogous to a callback function). You can name this directive whatever suits you, though. The star on the front indicates that this is a structural directive. Structural directives are special in that they are responsible for deciding when to render the element to which they are applied. This is similar to how our friend *ngIf works. Under the hood, the host element is actually wrapped up in an <ng-template> and provided to the structural directive as a TemplateRef, which the directive can render to the page.

But take a look at the class definition of CallbackTemplateDirective.

<>Copy
constructor( public template: TemplateRef<{ $implicit: TImplicitContext }> ) {}
CallbackTemplateDirective class definition

There's not much going on in this directive. All we have is a constructor with an injected TemplateRef. So who actually renders the template? Notice that the access modifier is set to public …

FileSelectComponent
Link to this section

The real magic happens in the FileSelectComponent, itself. Notice first, the @ContentChild decorator.

<>Copy
@ContentChild(CallbackTemplateDirective) callback: CallbackTemplateDirective;
FileSelectComponent.callback

That's a special decorator that tells Angular we want to get the first occurrence of CallbackTemplateDirective within its content children. "What are content children?" you ask. A parent component's content children are any elements, components, or directives placed within the parent's starting and closing tags. The @ContentChild decorator is kind of like Angular's version of querySelector except that we can query for instances of components and directives in addition to native html elements.

Now that we have access to the callbackTemplate directive, we also have access to its injected TemplateRef because we made it public. Next, the file select component can render callback.template to the page using ngTemplateOutlet.

<>Copy
<ng-container [ngTemplateOutlet]="callback.template" [ngTemplateOutletContext]="templateContext" ></ng-container>
rendering callback.template

The beautiful thing here is FileSelectComponent doesn't have to know what it's rendering. It just knows it has a template, and it knows where to render it. The user of the component decides what to render. We have a clear separation of concerns that allows us to render any UI to activate the file select.

But how does the custom UI actually open the dialog? When rendering a template, we can provide some context for the template to use [ngTemplateOutletContext]="templateContext".

<>Copy
templateContext = { $implicit: { // this has to be a lambda or else we get `this` problems openFileSelectDialog: () => this.openFileSelectDialog() } };
context used when rendering callback.template

The $implicit key in the context object may look confusing. The value of this object is what's passed to our template input variable let context. We can actually add more keys to the context object, but that leads to a lot more syntax in the template. I prefer to put context data into $implicit for simplicity because we can use any name we want for our template context variable.

<>Copy
<button *callbackTemplate="let context" class="primary" (click)="context.openFileSelectDialog()" > pick a file </button>
using the template context

When our *callbackTemplate is rendered, context is populated with the contents of templateContext.$implicit.

Now that the parent <file-select> component renders the TemplateRef from callbackTemplate and provides the method to open the file select dialog, the child content is free to open the file select dialog from any UI element it wants. From Isaac and Stephen's examples mentioned in the intro, we see that we can also use <ng-template> directly rather than a structural directive, but I don't like the syntax as much. But either way, it's the same pattern using the same Angular features. Just different syntax.

Final Thoughts
Link to this section

Building components in this way is certainly a paradigm shift, but I hope you can see the value in being able to share UI behavior without polluting your code or forcing a specific UI. In Angular, we're used to thinking about @Input() and @Output() as the primary means for components to communicate with each other, but as we see here there exist other means by which we can create more flexible and more expressive component APIs.

I'll leave you with a final example to explore on your own. This example uses the same pattern to simplify creating and opening modals, which is typically a painful experience with most Angular libraries. For what it's worth, both the file select and the modal examples come from code that I've sent to production. The other developers I work with have also come to  appreciate the simplicity of this approach. As you'll see from the modal example, the parent component might render some basic UI, so it's not strictly "headless". When building your API of components, you can decide where to draw the line between implementation details and customization based on what's appropriate for your application. A more specific headless component may only allow for a small amount of customization, while a more general-purpose headless component may not render anything at all to allow for full customization.

Share

About the author

author_image

Hayden Braxton is a software developer from Richmond, Virginia. When not writing code, you can find him trying to learn n + 1 things or digging holes in the ground.

author_image

About the author

Hayden Braxton

Hayden Braxton is a software developer from Richmond, Virginia. When not writing code, you can find him trying to learn n + 1 things or digging holes in the ground.

About the author

author_image

Hayden Braxton is a software developer from Richmond, Virginia. When not writing code, you can find him trying to learn n + 1 things or digging holes in the ground.

Looking for a JS job?
Job logo
Senior Full-Stack Developer (Node+Angular)

A-Listware

Ukraine
Remote
$48k - $78k
Job logo
Senior Full stack (Angular+Node)

Monolith

Ukraine
Remote
$60k - $84k
Job logo
AngularJS Developer/.net Core - Remote Contract

InfoMagnus

United States
Remote
$115k - $134k
Job logo
Angular Web Developer

NTT Data Services, Inc.

United States
Remote
$115k - $134k
More jobs
NxAngularCli
NxAngularCli
NxAngularCli

Featured articles

Angularpost
13 September 20218 min read
Tracking user interaction area

Explore one of the most complex pieces of Taiga UI — ActiveZone directive that keeps an eye on what region user is working with. It touches on low-level native DOM events API, advanced RxJS and Dependency Injection, ShadowDOM and more!

Angularpost
13 September 20218 min read
Tracking user interaction area

Explore one of the most complex pieces of Taiga UI — ActiveZone directive that keeps an eye on what region user is working with. It touches on low-level native DOM events API, advanced RxJS and Dependency Injection, ShadowDOM and more!

Read more
AngularpostTracking user interaction area

13 September 2021

8 min read

Explore one of the most complex pieces of Taiga UI — ActiveZone directive that keeps an eye on what region user is working with. It touches on low-level native DOM events API, advanced RxJS and Dependency Injection, ShadowDOM and more!

Read more
Angularpost
7 September 202122 min read
Designing Angular architecture - Container-Presentation pattern

Designing architecture could be tricky, especially in the agile world, where requirement changes are frequent. So your design has to support that and provides extendibility without the need for serious modification. In such cases, you will find the Container-Presentation pattern instrumental.

micro frontendspost
6 September 202125 min read
Taking micro-frontends to the next level

The micro-frontends concept has been out there for quite a while. We’ve been using this architecture in Wix since around 2013, long before it was even given this name. In this article I’d like to share some of the things we did in order to evolve the concept of developing big scale micro-frontends.

micro frontendspost
6 September 202125 min read
Taking micro-frontends to the next level

The micro-frontends concept has been out there for quite a while. We’ve been using this architecture in Wix since around 2013, long before it was even given this name. In this article I’d like to share some of the things we did in order to evolve the concept of developing big scale micro-frontends.

Read more
micro frontendspostTaking micro-frontends to the next level

6 September 2021

25 min read

The micro-frontends concept has been out there for quite a while. We’ve been using this architecture in Wix since around 2013, long before it was even given this name. In this article I’d like to share some of the things we did in order to evolve the concept of developing big scale micro-frontends.

Read more