Search code examples
angulartypescriptsvgcustom-directive

Adding an svg programmatically using an angular custom directive


I have an Angular 12 application, where I'd like to add a custom directive for Elements that adds an external link icon (svg). The icon should be added after the link, like this: enter image description here

The directive should be used like this: <a externalLink [href]="url">View in Jira</a>

I've not been able to insert an svg or an icon programmatically. We have a component library that has an icon (similarly to MatIcon) so a possible solution might be to use ComponentFactoryResolver to dynamically create an icon component, or another possible solution might be to create an svg element directly and add that.. I haven't gotten either to work yet, so I'm open for suggestions and tips! :)

I'd like to realise this as a directive, because I think it should be possible, and then it's just neater and more lightweight to use. But of course creating this as a component would be easier, because it doesn't require any programmatic messing with the dom.. please let me know if/how this would be possible to do using a directive :)

My directive so far looks like this (a lot of trial and error):

import { ComponentFactory, ComponentFactoryResolver, Directive, ElementRef, HostBinding, OnInit, Renderer2, TemplateRef, ViewContainerRef, } from '@angular/core';
import { CustomIcon } from '@company/components-lib/custom-icon';

@Directive({
  selector: 'a[externalLink]',
})
export class ExternalLinkDirective implements OnInit {

  // @HostBinding('id') readonly elementClass = 'external-link';

/* enforce external links to open in a new tab */

  @HostBinding('attr.target') readonly target = '_blank';

  svgContent = '';

  constructor(
    private el: ElementRef,
    private renderer: Renderer2,
    private componentFactory: ComponentFactory<unknown>,
    private readonly templateRef: TemplateRef<unknown>,
    private readonly viewContainer: ViewContainerRef,
    private readonly componentFactoryResolver: ComponentFactoryResolver
  ) {
  }

  ngOnInit(): void {


/* tried to add the actual svg to the innerHTML of the svg comoponent */

    this.svgContent = 
`<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">`+
  `<path d="M18.75 11.8869V18.75H5.25V5.25H12.1131L9.86306 3H3.75C3.33577 3 3 3.33581 3 3.75V20.25C3 20.6642 3.33577 21 3.75 21H20.25C20.6642 21 21 20.6642 21 20.25V14.1369L18.75 11.8869V11.8869Z" fill="#3ad4dd" />`+
  `<path d="M13.0449 3L15.9617 5.9168L9.87854 12L11.9999 14.1213L18.0831 8.03812L20.9999 10.955V3H13.0449Z" fill="#3ad4dd" />`+
`</svg>`;

    let svg = this.renderer.createElement('svg');
    this.renderer.setAttribute(svg, 'innerHTML', this.svgContent);
    console.log(this.svgContent);

/* tried to add the component using ComponentFactoryResolver, since ComponentFactory is marked as deprecated in Angular 14 https://angular.io/api/core/ComponentFactory */

    // let icon = this.componentFactoryResolver.resolveComponentFactory(CustomIcon);
    // let icon = this.renderer.createElement('custom-icon');
    // icon.setAttribute('class', 'external-link');

    let parent = this.renderer.parentNode(this.el.nativeElement);
    // parent.appendChild(icon);
    parent.appendChild(svg);

    // let iconElement = document.createElement('custom-icon', { name: 'external-link' });
  }

}


Solution

  • I played around with it a little and you can use a directive without a component by using elementRef and renderer

    @Directive({ selector: '[external]' })
    export class ExternalLinkDirective implements OnInit {
      @Input() size: string = '16';
      @Input() color: string = '#3ad4dd';
      @Input() viewBox = '0 0 24 24';
    
      constructor(
        private elementRef: ElementRef,
        private render: Renderer2
      ) {}
    
      ngOnInit() {
        const svg = this.render.createElement('svg', 'svg');
        this.render.setAttribute(svg, 'width', this.size);
        this.render.setAttribute(svg, 'height', this.size);
        this.render.setAttribute(svg, 'viewBox', this.viewBox);
        this.render.setAttribute(svg, 'fill', 'none');
        const p1 = this.render.createElement('path', 'svg');
        const p2 = this.render.createElement('path', 'svg');
        this.render.setAttribute(
          p1,
          'd',
          'M18.75 11.8869V18.75H5.25V5.25H12.1131L9.86306 3H3.75C3.33577 3 3 3.33581 3 3.75V20.25C3 20.6642 3.33577 21 3.75 21H20.25C20.6642 21 21 20.6642 21 20.25V14.1369L18.75 11.8869V11.8869Z'
        );
        this.render.setAttribute(
          p2,
          'd',
          'M13.0449 3L15.9617 5.9168L9.87854 12L11.9999 14.1213L18.0831 8.03812L20.9999 10.955V3H13.0449Z'
        );
        this.render.setAttribute(p1, 'fill', this.color);
        this.render.setAttribute(p2, 'fill', this.color);
        this.render.appendChild(svg, p1);
        this.render.appendChild(svg, p2);
        this.render.appendChild(this.elementRef.nativeElement, svg);
      }
    }
    
    <!-- app.component.html -->
    <a href="https://google.com" external color="red">google</a>
    <a href="https://linkedin.com" external color="green">LinkedIn</a>
    <a href="https://ebay.com" external color="#ffcc00">E-Bay</a>
    

    Result

    Here's a stackblitz