Zach
Zach

Reputation: 219

Example: Angular 13 Directive to Replace HTML Elements with Angular Components

I thought I would post this example to show how I migrated some older code to Angular 13 that replaced HTML elements with Angular Components. My code originally used ComponentFactoryResolver which is now deprecated. If there is a better solution out there, I would be happy to update this!

Context: My use case was to add some customization to the display of parsed markdown by extending ngx-markdown. I picked directives as it fit well and was easy enough to implement. Specifically I was wanting to use Angular components to customize code blocks, images, and videos in a web app. This example shows what I did to replace video elements with my own, custom video player.

Original Directive

Here's the original way that I implemented my directive this using the ComponentFactoryResolver with pre-Angular 13.

import { DOCUMENT } from '@angular/common';
import { ApplicationRef, ComponentFactoryResolver, Directive, ElementRef, HostListener, Inject, Injector} from '@angular/core';

import { VideoPlayerComponent } from '../components/video-player/video-player.component';

@Directive({
  selector: 'markdown,[markdown]'
})
export class VideoReplacementDirective {
  constructor(
    @Inject(DOCUMENT) private document: Document,
    private injector: Injector,
    private applicationRef: ApplicationRef,
    private componentFactoryResolver: ComponentFactoryResolver,
    private element: ElementRef<HTMLElement>
  ) {}

  @HostListener('ready')
  public processVideos() {
    // find our videos to replace
    const els = this.element.nativeElement.querySelectorAll<HTMLVideoElement>('video');

    // process each element
    for (let i = 0; i < els.length; i++) {
      const v = els[i];

      // get the parent of our video and make sure it exists to make TS happy below
      const parent = v.parentElement;
      if (parent) {
        // create container element
        const container = this.document.createElement('div');

        // make our component
        const component = this.componentFactoryResolver
          .resolveComponentFactory(VideoPlayerComponent)
          .create(this.injector, [], container);
        this.applicationRef.attachView(component.hostView);

        // set the source for the video
        component.instance.src = v.getElementsByTagName('source')[0].src;

        // replace our element
        parent.replaceChild(container, v);
      }
    }
  }
}

Upvotes: 3

Views: 2811

Answers (1)

Zach
Zach

Reputation: 219

Updated Directive for Angular 13

With Angular 13, the docs and deprecated comments mention that you should use ViewContainerRef but don't tell you all the details to replicate what I was doing before.

Essentially I:

  1. Use ViewContainerRef to make my Angular component
  2. Update my Angular component to have its ElementRef as a public property
  3. Replace existing HTMLElement with elementRef of newly created Angular component

Here's the updated directive code which is a lot easier for me to understand and follow:

import { Directive, ElementRef, HostListener, ViewContainerRef } from '@angular/core';

import { VideoPlayerComponent } from '../components/video-player/video-player.component';

@Directive({
  selector: 'markdown,[markdown]'
})
export class VideoReplacementDirective {
  constructor(private view: ViewContainerRef, private element: ElementRef<HTMLElement>) {}

  @HostListener('ready')
  public processVideos() {
    // find our videos to replace
    const els = this.element.nativeElement.querySelectorAll<HTMLVideoElement>('video');

    // process each element
    for (let i = 0; i < els.length; i++) {
      const v = els[i];

      // get the parent of our video and make sure it exists to make TS happy below
      const parent = v.parentElement;
      if (parent) {
        // make our component
        const component = this.view.createComponent(VideoPlayerComponent);

        // set the source for the new video
        component.instance.src = v.getElementsByTagName('source')[0].src;

        // replace our element
        parent.replaceChild(component.instance.elementRef.nativeElement, v);
      }
    }
  }
}

And here is my updated component to show with the added ElementRef in the constructor to make this work.

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

@Component({
  selector: 'vis-ngx-markdown-video-player',
  templateUrl: './video-player.component.html',
  styleUrls: ['./video-player.component.scss']
})
export class VideoPlayerComponent {
  /** URL for the video to play */
  @Input() public src!: string;

  // add elementRef as public property
  constructor(public elementRef: ElementRef<HTMLElement>) {}
}

Upvotes: 3

Related Questions