Using Host Instead of “replace: true” in Angular 4

Posted on by in Development

Let’s say that you’re working on an Angular 4 app that displays some images. You want to add a directive you can apply to any image tag to make it look fancy when you mouse over it. You also want a component that will take up 100% of its parent container’s width and display an array of images in a flex row. Let’s call these FancyImageDirective and ImageRowComponent.

If you want to see what we’re building in action, or just skip right to the code, you can check out the Plunker here.

Back in the AngularJS 1.2 days, you might have used a directive with replace: true for both of these features. In Angular, every component has a wrapping element – app-image-row for our ImageRowComponent for instance – that in the final markup goes around whatever is in your template. Replace: true removed that wrapping element, exposing the template directly. That meant that if you wanted your directive to just render an image with some calculated classes and event handlers, or set width: 100% on an outer div, you could do it.

That was pretty convenient, but also made it less clear what was going on in the final generated DOM. In any case, Angular 1.3 deprecated it, and the option is long gone in Angular 4. So, what’s an Angular developer to do?

Enter “Host”

“Host” is a term from the Web Components world, and refers to the custom component that wraps elements in its Shadow DOM. Since Angular usually either uses or emulates Shadow DOM (depending on your configuration), the framework uses the same term.

So app-image-row is the host for ImageRowComponent. Since it wraps whatever you put in your template, you can’t interact with it at the template level. Instead, Angular gives you a few special tools to do what you need – but the documentation isn’t great just yet.

Directly styling a host element

Angular’s isolated stylesheets are great for avoiding collisions in your CSS class names. But because they’re based on the Shadow DOM, one of their quirks is they can’t target the host by element name. So, if you wanted to make your image row a 100% width flex row, this wouldn’t work in image-row.component.css:

// Doesn’t work
app-image-row {
  width : 100%;
  display: flex;
}

You could freely target app-image-row in the parent’s stylesheet, but here we want the component to style its own host. So instead, we can use the special shadow-dom :host selector:

:host {
  width : 100%;
  display: flex;
}

This works whether you’re using or emulating Shadow DOM.

Adding classes to the host

So styling the host directly is all well and good, but sometimes you want to put a specific class on it. This is particularly useful for directives like our FancyImageDirective, which doesn’t have its own isolated stylesheet, but wants to conditionally add classes to its associated image tag on mouse over.

One option is to dependency inject ElementRef, and manipulate the nativeElement directly to add or remove classes. Something like this:

constructor(elementRef: ElementRef) {
  // We can do better!
  elementRef.nativeElement.onmouseover = () => {
    elementRef.nativeElement.classList = 'is-hovered';
  }
}

But the Angular Docs recommend against using ElementRef, saying it’s a security hole. And anyway, the imperative style of going in and directly adjusting the classes feels like a throwback to the jQuery days.

Happily, there’s a better way – the HostBinding decorator. Like decorators for @Input() or @Output(), @HostBinding lets you tag a property in your class with special meaning – in this case, that it controls some property on the host element.

First, you’ll want to import HostBinding:

import { HostBinding } from '@angular/core';

Then, you can set all the classes on the element with:

@HostBinding('class') imageClasses: string = 'fancy-image';

Any time you change the imageClasses property, the host’s classes will change.

According to the (very limited) docs for HostBinding, the binding should always be a string. But if you want to toggle a specific class, you can actually do that by having a boolean HostBinding, like this:

@HostBinding('class.is-hovered’) isHovered: boolean;

Setting this.isHovered to true will add the class, and setting it to false will remove it.

If you want to set your classes even more dynamically, you can use a getter function instead of a static property, like this:

@HostBinding('class.is-hovered’)
  get isHovered(): boolean {
  return someLogic();
}

Note that HostBinding is mostly useful with directives, but does work with components as well.

Adding attributes to the host

Classes aren’t the only things you can set with HostBinding – styles and attributes also work:

@HostBinding('style.width') imageWidth: string;
@HostBinding('attr.title’) imageTitle: string;

This gives your directives the ability to make a lot of changes to their host components, without having to fall back on ElementRef.

Adding listeners to the host

Another common use case is to want to add listeners to the host, for things like click or mouseover events. For that, Angular provides HostListener, which is similar to HostBinding. HostListener lets you connect a class method to a DOM event on the host, like this:

import { HostListener} from '@angular/core';</code>

@HostListener('click')
  onMouseover() {
  isHovered = true;
}

HostListener supports all the standard DOM events, like onClick and onFocus.

Putting it all together

Putting it all together, our component and directive might look something like this:

@Component({
  selector: 'app-image-row',
  template: `<img />`,
  styles: [`
    :host {
      width: 100%;
      display: flex;
      align-items: center;
    }
    .is-hovered { filter: brightness(2); }
  `]
})
export class ImageRowComponent {
  @Input() images: string[];
}
@Directive({
  selector: '[appFancyImage]'
})
export class FancyImageDirective {
  viewCount: number;

  @HostBinding('class.is-hovered') isHovered: boolean;

  @HostBinding('style.width') width: string = '100%';

  @HostBinding('attr.title')
  get countViews(): string {
    return `Views: ${this.viewCount}`;
  }

  @HostListener('mouseover')
  onMousein() {
    this.isHovered = true;
  }

  @HostListener('mouseout')
  onMouseout() {
    this.isHovered = false;
  }

  constructor() {
    this.viewCount = Math.floor(Math.random() * 10000);
  }
}

And there you have it – a nice declarative way for directives and components to work with their hosts, no ElementRef or replace: true required.