Angular Components and Directives

With a development environment ready and a grasp on the advantages of the MVC pattern, we can turn our attention to the core building blocks of Angular.

This portion of the platform acts as the foundation for everything else built on top of it. While you're likely already acquainted with many of Angular Core's features, including well-known decorators like @Component, it's worth pausing to consider the bigger picture:

What is the single unifying purpose that these various tools and features are designed to accomplish? What is the fundamental function of Angular Core?

The Core Purpose of Angular's View Layer

When we examine the individual features of Angular Core in isolation, it's easy to lose sight of the overarching goal they work towards.

Collectively, the view layer features of Angular Core supply us with everything needed to become, in a sense, an extension of the browser itself:

Angular Core enables us to expand the browser's built-in functionality by crafting our own custom HTML tags and attributes, and attaching specific behavior to them.

This is the central idea behind Angular Core, and most of its view layer functionality is focused on this single concept.

You can think of Angular Core (specifically the view layer) as providing a kind of missing browser extensibility toolkit. We'll explore why and put it to use shortly.

To begin, let's establish a key framework concept: what exactly constitutes a Directive?

Understanding Directives

The term "Directive" isn't exclusive to Angular, though AngularJS certainly brought the term into common usage and defined it clearly.

In fact, a concept very similar to Directives has existed since the very origins of browsers and the web itself.

Consider a basic input field, the kind we all recognize:

At first glance, this appears to be a simple leaf element in our HTML document, with an XML-like API for configuration. While it isn't strictly valid XML due to the missing closing tag, it's close.

A Closer Look at a Standard HTML Input

But there's more happening inside this simple input box than is immediately apparent, at least within the Chrome browser. If we open Chrome's Dev Tools (Ctrl + Shift + I), go to Settings, and enable the "Show User Agent Shadow DOM" option, we can see the internals.

We'll clarify what that means later; for now, know that this setting allows us to peer *inside* standard HTML elements, including a plain input.

As it turns out, an HTML input isn't a leaf element at all! Let's examine its internal structure using the Dev Tools:

What we find is additional HTML within a supposedly simple tag. This raises a question: how is this possible?

The Shadow DOM and Built-in Browser Directives

The input widget isn't rendered natively by the operating system. Instead, it's constructed internally from HTML and CSS, including an internal stylesheet:

The Inner Workings of an HTML Input

It appears that an input with a placeholder is fundamentally implemented as a bordered div, containing a special region that handles keyboard input and updates the displayed text. The placeholder text itself is just another div styled and positioned within the input's border.

The browser builds complex HTML elements by composing simpler ones, like divs, until it ultimately reaches the native rendering primitives of the underlying operating system.

Until recently, these internal HTML tags were hidden from developers, as Dev Tools didn't expose them. These elements collectively form what is known as the Shadow DOM of the input component.

What Exactly is the Shadow DOM?

The Shadow DOM is a hidden document sub-tree that can be embedded within what appears to be a leaf component, like an HTML input. A crucial point is that the styles within this sub-tree are scoped and isolated; they don't leak out into the main page's styling, remaining effective only within the component itself.

So, the browser has this built-in mechanism that's quite useful for creating new HTML elements from pre-existing ones. This functionality enables us to:

  • expose a public, XML-like API for an element
  • define the element's appearance using HTML
  • attach behavior to the new element
  • apply styles to it while keeping those styles fully isolated

The combined specification of appearance, API, and behavior forms a powerful concept. Let's give this concept a name: a Directive.

While the term is most commonly associated with Angular, we can see that it's fundamentally a browser functionality that has been used internally for a long time, even if it wasn't directly accessible to developers.

A Toolkit for Extending the Browser

What if we could harness this browser mechanism to create our own custom-designed HTML tags?

That would be incredibly powerful, but the native browser functionality isn't exposed for us to use directly.

However, the good news is that Angular Core provides a JavaScript-based mechanism that achieves nearly the same result, effectively simulating what the browser's internal Directive mechanism allows us to do.

To demonstrate this, we will use Angular to build our own HTML input directive with a placeholder and compare it to the native one. Our implementation will be remarkably similar to what we observed in the browser's shadow DOM: a bordered div, some text, and the necessary keyboard handling logic. Let's build our first Angular Directive.

Creating Our Own HTML Input

First, we need to define the API of our new directive. We want it to be used in the page like this:

This looks nearly identical to a standard HTML input, except for its custom tag name and the presence of a closing tag.

Our new directive is a special type of HTML directive that not only contains behavior but also has an associated template for its appearance. We call this type of directive a Component - essentially a Directive with a template.

Let's begin implementing our input box. The end result will be nearly indistinguishable from a native browser input.

Writing Our First Custom HTML Element

Here's the full implementation, which we'll then dissect step-by-step. This is our first Angular component:

Deconstructing Our First Component

This small snippet of code contains all the elements we identified in a browser Directive:

  • It defines an API (the placeholder input).
  • It defines a look and feel (the template and styles).
  • It defines behavior (the onKeyUp method).

If you were to run this, you'd be surprised by how close it matches the appearance of a real input, except for the blinking cursor, which could be simulated with a "|" character and some animation. Looking at the implementation of our my-input component, the similarity to the native input's shadow DOM is clear.

Defining the Public API

The code is a plain ES6 class decorated with @Component(). Let's focus specifically on the part that defines the public API:

The @Component() decorator signals to Angular that this class holds the functional specification for a component. Within the decorator's configuration, the selector property defines a CSS selector that indicates which elements this component's behavior and appearance apply to. In this case, every element named my-input in the page will get this component's template and behavior.

This component also has an @Input() property named placeholder, a string. The combination of the custom tag name, my-input, and the names of its @Input() properties defines the public API for this new HTML element. This tells us that directives are a great way to define custom elements with a specific API for configuration.

Defining the Component's Look and Feel

Within the template, the component renders as a focusable div (thanks to the tabindex attribute) with a CSS class my-input applied to it.

Notice the special {{}} syntax inside the template. This is a template expression that Angular evaluates against the component's class instance. So, the expression {{placeholder}} is evaluated as this.placeholder, where this refers to the specific component instance.

In this case, a ternary operator ? is used to display the placeholder text when the internal value variable is empty. It's worth noting that value is not part of the public API but rather an internal implementation detail of the component. Besides the template, we also define styles associated with the component. These styles, by default, operate in a manner highly reminiscent of the Shadow DOM mechanism.

Angular's Style Isolation

The styles defined for our component are isolated from the main page, similar to the webkit-input-placeholder we saw in the native input's shadow DOM. This is a powerful feature, allowing us to ensure a component will maintain its intended appearance regardless of where it's used on a page.

*By default*, Angular's mechanism for style isolation isn't technically the Shadow DOM's encapsulation, but it achieves a comparable effect out of the box. Angular transparently increases the specificity of the component's styles. It does this by adding a unique property to each element in the component's template at runtime and then using that property as an attribute selector in the generated CSS. This ensures these component styles will generally override external page styles, guaranteeing a consistent look across different contexts.

However, this encapsulation isn't absolute. An external style rule, *for example*, one that uses !important, could still override our component's styles. But in practice, this provides excellent style safety.

Adding Keyboard Behavior

In the template, we detect the browser's native keyup event on the div using the (keyup) event syntax, which binds the event to the component's onKeyUp method.

So, the component's class serves multiple essential roles: it defines the component's public API, it stores the template's state variables, and it also contains the logic dictating how the component reacts to user interactions.

Our keyboard handling logic is quite straightforward:

  • If the key pressed is the backspace key, we'll remove the last character from the value string.
  • If any other non-modifier key (not Ctrl, Cmd, etc.) is pressed, we'll append that character to the value string.

Notice that we never directly manipulate the DOM. We only update the value variable, and Angular automatically detects this state change and updates the view to reflect the new value. This is a simplified model to illuminate the parallels between the browser's Shadow DOM functionality and Angular's component system, which provides roughly equivalent capabilities.

Summary of Key Concepts

In essence, the view layer of Angular Core is all about providing a toolkit for extending the browser with our own custom HTML elements, complete with their own APIs, appearances, and behaviors. The design philosophy is to extend HTML rather than replace it, offering a JavaScript implementation of functionality that was until recently an internal browser mechanism. The ultimate goal is to compose our applications from our own custom elements, mirroring how the browser itself works internally.

Here is the ongoing Angular for Beginners series:

If you are just getting started learning Angular, have a look at the Angular for Beginners Course:

Angular For Beginners - Components vs Directives — figure 1

If you enjoyed this post, have also a look also at other popular posts that you might find interesting: