Creating HTML Comments in Angular

March 16th 2018 Angular

Although comments in HTML markup usually don't play an important role (they are comments after all), they could have a meaning for parsers which post-process the HTML document. When I recently encountered such a requirement, it turned out that generating custom HTML comments with an Angular application is not as easy as one might expect.

The most obvious approach would be to simply put the comments in the component markup:

<!-- {{contents}} -->

Unfortunately for my case, Angular removes all comments from the template during processing. Hence this first attempt resulted in an empty component.

It was time to use some JavaScript code: Angular would bind the comment contents to the component so that the code could read it and replace the whole component with a comment:

{{contents}}
import { Component, OnInit, Input, ElementRef } from '@angular/core';

@Component({
  selector: 'app-comment',
  templateUrl: './comment.component.html',
  styleUrls: ['./comment.component.css']
})
export class CommentComponent implements OnInit {

  @Input() contents: string;

  constructor(private elementRef: ElementRef) { }

  ngOnInit() {
    setTimeout(() => this.createComment(), 0);
  }

  private createComment() {
    const htmlElement = this.elementRef.nativeElement as HTMLElement;
    htmlElement.outerHTML = `<!-- ${htmlElement.textContent} -->`;
  }
}

The setTimeout call is used in order for HTML rendering to complete before createComment is called. This works, but has the unfortunate side effect of removing the component, making it impossible for Angular to update it if the bound value changed. Still, this simple approach might be good enough for certain scenarios.

To support dynamic updating of comments when the bound value changes, the component will need to remain intact. To achieve that I decided to insert the comment as its sibling:

import { Component, Input, ElementRef } from '@angular/core';

@Component({
  selector: 'app-comment',
  templateUrl: './comment.component.html',
  styleUrls: ['./comment.component.css']
})
export class CommentComponent {

  private contentsValue: string;
  @Input()
  get contents(): string {
    return this.contentsValue;
  }
  set contents(value: string) {
    this.contentsValue = value;
    setTimeout(() => this.createComment());
  }

  private commentCreated = false;

  constructor(private elementRef: ElementRef) { }

  private createComment() {
    const htmlElement = this.elementRef.nativeElement as HTMLElement;
    if (this.commentCreated) {
      htmlElement.parentNode.removeChild(htmlElement.previousSibling);
    }
    htmlElement.parentNode.insertBefore(
      document.createComment(htmlElement.textContent), htmlElement);
    this.commentCreated = true;
  }
}

The createComment function now needs to be called whenever the bound value changes, therefore I'm triggering it in the property accessor. I'm tracking whether the comment was already created so that I can delete the existing one before creating a new one. Additionally, I use createComment instead of string concatenation to generate the comment which ensures a valid syntax, no matter the contents.

With this approach, the component remains in the DOM, therefore I added some CSS to hide it:

:host {
  position: absolute;
  visibility: hidden;
}

The code might still break in some cases and there might even be a better way to achieve the same, but this works good enough for me.

Copyright
Creative Commons License