-1

I've been reading through many different blog posts and topics on here to see how or even if what I'm trying to do with templates can be done, but have not found anything that works.

We have an icon component that is quite simple in structure which is set up by simply using the component and specifying which icon and size is needed in the template using it:

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

@Component({
   selector: 'comp-icon',
   template: `
        <span class="{{ class }}" *ngIf="icon === 'error'" >
          <svg [attr.height]="size" viewBox="0 0 48 48" [attr.width]="size" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"></svg>
        </span>
        <span class="{{ class }}" *ngIf="icon === 'success'" >
          <svg [attr.height]="size" viewBox="0 0 48 48" [attr.width]="size" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"></svg>
        </span>
    `
})
export class IconComponent {
   @Input() icon: string;
   @Input() size: number;
   @Input() class: string;

   constructor() {
   }
}

The problem is that the above repeats for many more lines and we are trying to include the ability to add custom svg images to the list from the various applications that will be using the library that is implementing this component. So far this is what I've managed which basically illustrates what we're trying to accomplish:

import {Component, Input, OnInit, ViewChild} from '@angular/core';
import {Icons} from './icons';

@Component({
   selector: 'comp-icon',
   template: `
        <span class="{{ class }}">
            <ng-container [ngTemplateOutlet]="iconContent"></ng-container>
        </span>
    `
})
export class IconComponent implements OnInit {
   @Input() icon: string;
   @Input() size: number;
   @Input() class: string;

   @ViewChild('iconContent') iconContent: any;

   constructor() {
   }

   ngOnInit() {
       this.iconContent = (Icons.find(icon => icon.name === this.icon) || { name: '', content: '' }).content;
   }
}

And the corresponding icons.ts looks like this:

export interface IIcon {
    name: string;
    content: string;
}

export const Icons: IIcon[] = [
    {
        name: 'error',
        content: `<svg [attr.height]="{SIZE}" [attr.width]="{SIZE}" viewBox="0 0 48 48" xmlns="http://www.w3.org/2000/svg">
        </svg>`
    },
    {
        name: 'success',
        content: `<svg [attr.height]="{SIZE}" [attr.width]="{SIZE}" viewBox="0 0 48 48" xmlns="http://www.w3.org/2000/svg">
        </svg>`
    },
];

Is it at all possible to dynamically specify the contents of a section of a component in this way, in this case the svg? I've read some about directives being able to accomplish this, but nothing really sticks out or I haven't really figured out how.

DavidIQ
  • 1
  • 3
  • You want to use components or angular attribute and other bindings, then you need to compile the component at runtime. See for example https://stackoverflow.com/questions/38888008/how-can-i-use-create-dynamic-template-to-compile-dynamic-component-with-angular – Günter Zöchbauer Sep 22 '17 at 20:42
  • Well I don't necessarily _want_ to do that, but it doesn't look like I have much of a choice since `innerHTML` doesn't like svgs. Thanks for the link, I'll look it over. P.S. Not sure what the down-vote was for, whoever did it. – DavidIQ Sep 22 '17 at 21:15
  • You can use svg with innerHTML, but no bindings. You might need to mark as safe https://stackoverflow.com/questions/31548311/angular-2-html-binding/41089093#41089093. You can use TypeScript string interpolation **before** you pass it to innerHTML instead of bindings. – Günter Zöchbauer Sep 23 '17 at 08:27
  • @GünterZöchbauer thanks. That gave me enough direction to be able to do this by using ElementRef through which I don't have to implement a pipe to disable security for this. – DavidIQ Sep 28 '17 at 21:04

2 Answers2

0

You could use innerHTML for property binding, if you want to create a component then you should checkout Günter Zöchbauer's suggetion.

<div [innerHTML]="iconContent">
</div>

Another possible solution could be

export const Icons: IIcon[] = [
    {
        name: 'error',
        width: '50px',
        heigth: '50px',
        viewBox: '0 0 48 48',
        xmlns: "http://www.w3.org/2000/svg"
    }
];

And then in your component html

<svg [attr.width]="iconContent.width" [attr.heigth]="iconContent.height" [attr.viewBox]="iconContent.viewBox" [attr.xmlns]="xmlns"></svg>
JSingh
  • 375
  • 1
  • 4
  • Yeah I've tried this, but Angular gives me a nice `WARNING: sanitizing HTML stripped some content` message, which apparently means stripping out the entire svg. – DavidIQ Sep 22 '17 at 21:09
  • @DavidlQ you'd need to tell angular you know what you are doing an disable output sanitization for the svg content check https://angular.io/guide/security – Leonard Brünings Sep 22 '17 at 22:43
0

The proper way to do this was to indeed use ngComponentOutlet as I was originally attempting. Based on this post I've implemented it, pretty much verbatim, in the following manner:

import { Compiler, Component, ElementRef, Input, NgModule, NgModuleFactory, OnInit, ViewChild } from '@angular/core';
import { IconsProvider } from './icons.provider';

@Component({
   selector: 'comp-icon',
   template: `
        <ng-container *ngComponentOutlet="dynamicIconComponent;
                        ngModuleFactory: dynamicIconModule;"></ng-container>
    `
})
export class IconComponent implements OnInit {
   @Input() icon: string;
   @Input() size: number;
   @Input() class: string;

   public dynamicIconComponent: any;
   public dynamicIconModule: NgModuleFactory<any>;

   constructor (private _iconsProvider: IconsProvider, private _compiler: Compiler) {
   }

   ngOnInit() {
      const selectedIcon: string = this._iconsProvider.getIcon(this.icon);

      if (selectedIcon != null) {
          this.dynamicIconComponent = this.createNewIconComponent(selectedIcon, this.class, this.size);
          this.dynamicIconModule = this._compiler.compileModuleSync(this.createIconComponentModule(this.dynamicIconComponent));
      }
   }

   private createIconComponentModule(iconComponentType: any) {
       @NgModule({
           imports: [],
           declarations: [
               iconComponentType
           ],
           entryComponents: [iconComponentType]
       })
       class RuntimeIconComponentModule {}

       return RuntimeIconComponentModule;
   }

   private createNewIconComponent(iconContents: string, iconClass: string, iconSize: number) {
       @Component({
           selector: 'dynamic-icon-component',
           template: `<span class="{{ iconClass }}">${iconContents}</span>`
       })
       class DynamicIconComponent implements OnInit {
           private iconContents: string;
           private iconClass: string;
           private size: number;

           ngOnInit() {
               this.iconContents = iconContents;
               this.iconClass = iconClass;
               this.size = iconSize;
           }
       }

       return DynamicIconComponent;
   }
}

The comments and suggestions were very useful in eventually finding my way to the answer. Thank you.

DavidIQ
  • 1
  • 3