3

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);
      }
    }
  }
}
2
  • There is no question in here really. You might consider doing a blog post if you like to share something. Not what SO is for. Commented Mar 9, 2022 at 19:22
  • I’m voting to close this question because it’s not a question. Commented Mar 9, 2022 at 19:23

1 Answer 1

3

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>) {}
}
Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.