Before we deal with directives, we should know what are ViewContainerRef, TemplateRef, ElementRef.
Also we should understand how the Angular microsyntax works.

Difference between attribute directives and structural directives

In regards to the syntax in typescript, attribute directives and structural directives are the same but their semantics are different!

The most important difference to know is, what instances (ViewContainerRef, TemplateRef, ElementRef) you get injected in the constructor of your directive. You will see them listed below the examples.

Structural directive

A structural directive is all about <ng-template />. We use them to conditionally show/hide a template (TemplateRef) or as a template for multiple items (for loop).

I implemented my own "appIf" structural directive to ilustrate how a structural directive works.

@Directive({ selector: "[appIf]" })
export class AppIfDirective<T> implements OnChanges {
  private thenViewRef: EmbeddedViewRef<AppIfContext<T>> | null = null;
  private elseViewRef: EmbeddedViewRef<AppIfContext<T>> | null = null;
  private context: AppIfContext<T> = {
    $implicit: null,
    // Same name as the directive "appIf" to support the "as" binding
    // *appIf="state$ | async as state"
    // Without this property 'appIf', the state variable would be undefined.
    appIf: null,
  };

  @Input('appIf') condition: boolean | null = false;
  @Input('appIfElse') elseTemplateRef?: TemplateRef;

  constructor(
    private readonly viewContainterRef: ViewContainerRef,
    private readonly thenTemplateRef: TemplateRef,
  ) { }

  ngOnChanges(changes: SimpleChanges): void {
    this.context.$implicit = this.context.appIf = this.condition;

    if (this.condition) {
      this.elseViewRef?.destroy();
      this.elseViewRef = null;

      if (!this.thenViewRef) {
        this.thenViewRef = this.viewContainterRef.createEmbeddedView(this.thenTemplateRef, this.context);
      }
    } else {
      this.thenViewRef?.destroy();
      this.thenViewRef = null;

      if (this.elseTemplateRef) {
        if (!this.elseViewRef || changes.elseTemplateRef) {
          this.elseViewRef = this.viewContainterRef.createEmbeddedView(this.elseTemplateRef, this.context);
        }
      }
    }
  }
}

To use a structural directive we apply a * (star) prefix (microsyntax) to the element.

<div *appIf="name$ | async as name">Hello {{name}}!</div>

Which Angular will transform to an attribute directive.

<ng-template [appIf]="name$ | async" let-name"appIf">Hello {{name}}!</ng-template>
<div *appIf="name$ | async as name">Hello {{name}}!</div>
</ng-template>
  • ViewContainerRef - will point to the ViewContainerRef of the current <ng-template /> element.
  • TemplateRef - will point to the current <ng-template /> element.
  • ElementRef - will point to the current element which is the <ng-template />, which will represent as a #comment in the DOM
    (usually we do NOT care about the ElementRef in a structural directive).

Attribute directive

Adds behavior to an element (ElementRef), listen to a click event (HostListener), modify the element (Renderer2).

@Directive({ selector: "[appHightlight]" })
export class HighlightDirective {
  constructor(
    private readonly elementRef: ElementRef,
  ) { }

  @HostListener('click')
  onClick() {
    this.elementRef.nativeElement.style.backgroundColor = 'yellow';

    // Or better by using the Renderer2:
    this.renderer.setStyle(this.elementRef.nativeElement, 'backgroundColor', 'yellow');
  }
}

To use a attribute directive we just apply the selector to the element.

<div appHightlight>Hello!</div>
  • ElementRef - will point to the current <div /> element.
  • ViewContainerRef - will point to the ViewContainerRef of the current <div /> element.
  • TemplateRef - will be undefined. (because this is a <div /> and not a <ng-template /> element.