Angular elements: Communication between components 🤝

Denis Severin
Fundamental Library

--

A brief overview đź“‹

Web components are a standard of modern web development. They allow us to write modular, scalable, reusable, and encapsulated HTML elements with complex business logic inside, which is supported by all modern browsers.

The @angular/elements library provides the ability to wrap Angular components into a standalone Web component relatively easily.

Current state of @angular/elements 🎩

Although @angular/elements provides great opportunity for generating standalone and encapsulated components which could be used almost anywhere, it has some flaws:

  • Complex NgModule structure;
  • Components are bound to it’s module Injector;
  • No support for ContentChild and ContentChildren;

Fixing the Injector đź› 

The reason for the complex NgModules is simple: components need to be aware of each other. This task is handled by the module Injector.

But what if we want to keep our NgModules simple and straightforward?

In this case, child elements from different NgModule will not be aware of the parent components from the other NgModule. Sometimes this issue can break whole applications. In Fundamental-ngx, for complex components, e.g. Platform Table, we rely on it’s awareness of the parent container for styling, passing data and retrieving one.

If we take a look into the implementation of the createCustomElement, we will see that for options it supports passing custom strategyFactory property which is actually responsible for generating the web component.

So let’s take advantage over this factory class and make web component aware of it’s parents!

What we need to do, is to grab the parent element injector and inherit it for our current injector. Here’s simple schema how it works:

Logical schema how to get parent injector

The code snippet below is an extended code from ComponentNgElementStrategyFactory class provided by @angular/elements itself.

In the code above we do the following:

  • When element is placed inside the DOM tree, we try to get the parent component for it’s injector
  • If the injector is found, we extend our current injector for the current component with the found parent injector, thus, giving the ability to inject things from parent elements. Similarly how default Angular DI works.
  • After we initiated our injector, we set it as an element property for further usages by child or parent components.

This actually solves one of the annoying things of @angular/elements such as complex NgModules.

Before, you had to have some root NgModule which imports all secondary NgModules for sharing the Injector and declaring the components. Now you can have much simpler NgModules which are responsible for individual set of components. For example, TabModule which declares and generates only Tab-related components:

In the code snippet above we export two types of module:

  • One for native Angular application;
  • Second is for web component usage.

The second module is extended from BaseWebComponentModule which holds common business logic for generating the Web components:

Finding lost children 🔎

Handling child-parent communication was pretty simple and straightforward, right?

Now, let’s try fixing another communication issue: parent with children.

Due to Angular’s rendering engine specifics, @ContentChild and @ContentChildren wont work in Web component. So, we’ll have to write our own solution for it, yet, keeping the backwards compatibility.

Let’s try making our @WebComponentContentChildren!

So, the plan is:

  • It should be a decorator which can be placed on top of @ContentChildren;
  • It should return QueryList same way @ContentChildren does;
  • It should react on Subtree changes;
  • It should support the same syntax as @ContentChildren with passing Class, Injection token, string, etc.
  • It should be performant;
  • ???
  • PROFIT!!!

Let’s start with the decorator:

In the code above we do simple task: storing information about current component @ContentChildren condition.

Now, backwards compatibility and Subtree changes reaction.

In the code above, we initialise MutationObserver for our Web component, which will notify us on any subtree changes. In this example it reacts on almost any change, which is not needed in real case scenarios.

Now, let’s think how we can support the same Selector syntax @ContentChildren has.

Remember, in the first part we exposed Component’s injector as a public property?

We are going to use it to check if current element fits the condition!

The code above is pretty self-explanatory, but let’s describe it a bit more.

  • If passed selector is a string, let’s threat it as CSS selector, and use QuerySelectorAll. If your application uses string providers, you can remove this part of the code.
  • Second check is for injection tokens. Since we are using Shadow DOM encapsulation, getting all child elements is not a performance-breaker, since it will not get elements from children Shadow DOM, and we like to keep our components simple and split complex logic with smaller components or directives. After we get all child elements, we check whether element exposes the injector. If so, we try to receive token value from it’s injector, but keeping the scope to the element itself (without going to the parent injectors for the value), and flagging it as optional so if there’s no value, we don’t get an injector error.

Now, all we need to do is to notify our component of the changes, when they actually happening:

And now the usage part:

That’s all, folks. We made @angular/elements even more powerful with the support of such beloved features as DI and child elements communication.

If you like to know more, you can play around with our library’s implementation of web components wrapper.

And also check the demo app.

--

--