Adaptive Components & Parent-Driven Behavior in Angular

Levent Arman Özak
Angular In Depth
Published in
6 min readNov 8, 2019

AngularInDepth is moving away from Medium. More recent articles are hosted on the new platform inDepth.dev. Thanks for being part of indepth movement!

It takes time to fully grasp concepts such as dumb/smart/stateful/stateless components, unidirectional data flow, content projection, views and DOM manipulation, and dependency injection in Angular. All that and probably some further information is necessary to build a consistent, scalable, and flexible component architecture. Then, just as you dig deeper and put what you learn into use, new questions and requirements arise. Should I really set this style up through an input property? Should a parent component be aware of how its child component works?

Occasionally, methods you apply with similar questions in mind are contrary to some best practices you know and implement by heart. They could even be identified as anti-patterns, yet still help you build your app. With this article, I will attempt to elaborate on an arguably convenient but possibly unorthodox approach: Employing adaptive components which adjust their own look and behavior with regard to an ancestor component, a directive, or an injectable in the dependency tree. You may think of them as dumb components that are, well, not totally dumb. Please note that what I am after is an examination rather than a suggestion and it may never be a good idea to create one at all.

A Sample Case

In order to explore how adaptive components can be thinkable, we are going to build two card components: One vertical and one horizontal. Here are the end results we are going to get:

A vertical and a horizontal card formed of a random scenery, a title (underlined), a body, and a green call-to-action.
The vertical and horizontal cards we will create

As you see, although there are minor visual and structural differences, from the point of members, they are identical: Both have an image, a title, a body text, and a call-to-action. Component classes are going to share members as well, wherefore both extend the same abstract card class. Let us take a look at the code:

Card components and how they are consumed

There are a few points I must clear up. First off, working with an abstract class here gives you a very important edge in terms of dependency injection. If you spot providers meta data in both class decorators of CardVerticalComponent and CardHorizontalComponent, you will realize that I am about to inject the abstract class somewhere else. Second, if you are unfamiliar with select property of ng-content, it works somewhat like a CSS selector and helps Angular place projected content to correct position in the DOM. Finally, those cards could have completely different contents, but I am too lazy for that. 🤷‍♂️

Now that you are acquainted with the context, it is time to go through the child components and find out how they will adapt to their parent.

Setting a Style on an Angular Component When a Parent Is Present

That, actually, is a common and basic need. Atomic components can often be reused in multiple component trees with only a slight style adjustment, frequently determined by the existence of an ancestor in dependency tree. Check out the following card image component:

A dumb card image component

The component can be used in both layouts, still its placement attribute has to be set as 'side' or 'top' depending on the layout. Now, looking into this scenario, we have following options:

  1. Keep CardImageComponent as is and suffer from not only repetitive work, but also another tiny detail about the component API.
  2. Add separate card image components or an attribute directive for vertical and horizontal layouts and suffer from an expanded API surface area, several additional bytes, and probably duplicate/useless code.
  3. Make this component smarter. 🤔

So, let us revisit that component and see what we can change:

A not-so-dumb card image component

We have completely removed the placement attribute and used dependency injection to our advantage. When this component is used inside a vertical card, the injected instance will have a truthy value and <img> element will get the card-img-side class. The Optional decorator, on the other hand, replaces an unprovided dependency with null. Therefore, when inside a horizontal card, vertical will have a falsy value and <img> element will get the card-img-top class.

Setting Styles on an Angular Component Depending on the Context (or How to Use :host-context)

The heading in our example has an underline when used in horizontal layout. We must achieve this by adding a border-bottom property to <h5> element, without giving the vertical card one. A global CSS rule is always an option, yes, but it is much more difficult to trace for modification or removal and unused CSS becomes a problem quite quickly. Here is how we can fix that:

Using :host-context selector to set a style based on context

The :host-context() CSS function selects the host element only if parameter matches element’s ancestor(s) and any descendant can also be targeted, like our example demonstrates. The immediate benefits are obvious: The border is only placed on horizontal card headings and it is easy to find and modify the rule. Nevertheless, there are some facts that you have to keep in mind:

  1. View encapsulation other than None is required for this selector to work. However, although :host-context() is a native CSS pseudo-class function for shadow DOM, it is experimental and the browser support is poor. Good news is, when emulated encapsulation is in effect, Angular will replace it with a simple selector scoped by an attribute containing a surrogate id.
  2. Angular compiler does not (and probably should not) validate the rule, so it cannot detect if you still need one. In other words, that CSS rule, inlined or not, shall not magically disappear from your bundle just because you do not have a horizontal card instance in your app anymore.

Using Properties on Parent Component in Angular

So far, we have styled child components based only on presence or absence of an ancestor. We could also bring parent properties and methods into play:

Parent color defines button color and action property is called on click

Indeed, CardButtonComponent sets its classes after referring to card color and calls its action property upon click event. Once again, our child component is characterized by an ancestor. Please pay attention to the practical use of AbstractCard class though. Since both card component classes extend and provide it, we can inject it to the child component and thus grant a type-safe reference to their shared interface.

Consuming ancestor properties directly may look like a terrific idea at first. Yet, there are some caveats:

  1. If ChangeDetectionStrategy is set to OnPush on a child component, it shall not be re-rendered on value changes of the parent property. In order to accomplish that, you will need a utility function the details of which I do not intend to include in this article. You may find a working example on the StackBlitz project linked below and here.
  2. Arrow functions as class properties can be called from child components without any trouble, because this will always refer to the class instance the property belongs to (AppComponent in our example). While consuming methods, however, you have to stay sharp about closures.
  3. Setting a new value to a parent property from within a child component is usually not a good idea, since it is not easy to discover where this is done and unintended overwrites are likely to happen. One-way data flow had better be applied.

Conclusion

Thanks to dependency injection in Angular, creating adaptive components characterized by an ancestor component, directive or service is available. We have a couple of these in our current project and they serve us marvelously. However, before you crucify me, let me emphasize once again that I am well aware of its drawbacks, merely displaying what can be done in Angular, and not recommending a general adoption of this technique.

You may find the StackBlitz project link below. Live long and prosper. 🖖

NG Turkey logo on right and “Follow NG Turkey on Twitter” message on left

--

--

Levent Arman Özak
Angular In Depth

Development Expert, SAP — Angular GDE — Founder, NG Turkey — Ex-team member, ABP framework