6

Is it possible to inject dynamic components that are child svg elements?

For instance, I have a main component (CanvasComponent) , and its template is something like this

<svg width="400" height="400">
 <ng-template appCanvasAnchor></ng-template>
</svg>

That ng-template is the anchor used to dynamically inject components as per https://angular.io/guide/dynamic-component-loader

The dynamic (SquareComponent) has got this template

<svg:rect width="100" height="100" style="fill:red" />

Now, because of the way dynamic components get added to the DOM, when my Square component gets added DOM it'll be contained by a div element like below.

<svg width="400" height="400">
  <div app-square>
   <rect width="100" height="100" style="fill:red" />
 </div>
</svg>

In theory that all works pretty well, except for the fact that this is SVG, and therefore my square won't render because it's got unknown markup (the div). The question here is, can I somehow change what element the injector uses so that rather than using a div, it uses and svg know element like g?

Pilsen
  • 125
  • 7
  • what you want to do with SVG? do you want to change any values in SVG elements in run time with business conditions? – Prithivi Raj Nov 08 '17 at 07:32
  • Not only that, I need to produce floor-plan like images, so I figure I'd be much easier to do it with SVG. There's a good post on how to deal with SVG in angular https://teropa.info/blog/2016/12/12/graphics-in-angular-2.html but it doesn't address dynamic content. I did another round of research and did find a post pretty much like this one and it wasn't very promising. Might have to change the approach :( – Pilsen Nov 08 '17 at 22:28
  • @Pilsen This question is old, but SVGs in Angular are just HTML for the most part. I am mostly adding this comment for anyone else with a similar question, but have you tried content projection? https://angular.io/guide/content-projection – dj11223344 Dec 07 '22 at 17:59
  • If you want to load external SVG, then load external SVG: https://dev.to/dannyengelman/load-file-web-component-add-external-content-to-the-dom-1nd No Angular voodoo components required – Danny '365CSI' Engelman Dec 08 '22 at 12:28
  • SVG files are XML files and can have their own ` – Rene van der Lende Dec 08 '22 at 15:58

4 Answers4

1

Might be easier with a native JavaScript Web Component

  • <svg-floorplan> processes its innerHTML
  • and generates the <SVG> in shadowDOM
  • based on HTMLUnknownElements <room> (make for great semantic HTML)
    which declare the x,y,width,height,fill attributes for a proper SVG <rect>
  • based on the <rect> a Label can be positioned smack in the middle
  • any remaining SVG elements in innerHTML are moved into the new <SVG>
  • Your JavaScript skills can add much more...
    <svg-floorplan viewBox="0 0 300 100">
      <style>text{fill:gold}</style>

      <room rect="0 0 200 50">living</room>
      <room rect="200 0 100 100 blue">kitchen</room>
      <room rect="0 50 100 50">bedroom</room>
      <room rect="100 50 100 50 green">bathroom</room>

      <circle fill="red" cx="25" cy="25" r="10"></circle>
    </svg-floorplan>

creates:

<svg-floorplan viewBox="0 0 300 100">
  <style>text{fill:gold}</style>
  <room rect="0 0 200 50">living</room>
  <room rect="200 0 100 100 blue">kitchen</room>
  <room rect="0 50 100 50">bedroom</room>
  <room rect="100 50 100 50 green">bathroom</room>
  <circle fill="red" cx="25" cy="25" r="10"></circle>
</svg-floorplan>

<script>
  customElements.define("svg-floorplan", class extends HTMLElement {
    connectedCallback() {
      setTimeout(() => { // make sure innerHTML is parsed
        let svg = `<svg viewBox="${this.getAttribute("viewBox")}">`;
        svg += `<rect x="0" y="0" width="100%" height="100%" fill="grey"/>`;
        this.querySelectorAll('room[rect]').forEach(rm => {
          let [x, y, width, height, fill="grey"] = rm.getAttribute("rect").split(" ");
          svg += `<rect x="${x}" y="${y}" width="${width}" height="${height}" stroke="black" fill="${fill}"/>`;
          let label = rm.innerHTML;
          svg += `<path id="${label}" pathLength="100" d="M${x} ${~~y+height/2}h${width}" stroke="none"></path>
           <text><textPath href="#${label}" startoffset="50" 
                           dominant-baseline="middle" text-anchor="middle">${label}</textPath></text>`;
          rm.remove();
        });
        svg += `</svg>`;
        this.attachShadow({mode:"open"}).innerHTML = svg;
        this.shadowRoot.querySelector("svg")
                       .insertAdjacentHTML("beforeend",this.innerHTML)
      })
    }
  })
</script>
Danny '365CSI' Engelman
  • 16,526
  • 2
  • 32
  • 49
1

Not sure about templates but it is possible to inject dynamically svg-s with content projection, with the help of some workarounds.

The main workaround is to find a way to remove the host elements of the component which can be handled by creating a directive that like the following, that will handle the "unwrapping" of certain elements. As seen below this directive is doing something similar to the spread operator in js, it takes all children of the element on which it is attached and spreads them on the level of the parent element

@Directive({
  selector: '[appRemoveHost]',
  standalone: true,
})
export class RemoveHostDirective {
  constructor(private el: ElementRef) {}

  //wait for the component to render completely
  ngOnInit() {
    var nativeElement: HTMLElement = this.el.nativeElement,
      parentElement: HTMLElement = nativeElement.parentElement;
    // move all children out of the element
    while (nativeElement.firstChild) {
      parentElement.insertBefore(nativeElement.firstChild, nativeElement);
    }
    // remove the empty element(the host)
    parentElement.removeChild(nativeElement);
  }
}

Kudos to this question and answer Remove the host HTML element selectors created by angular component

After that, you just have to create wrapper components for the SVG elements that you want to use.

Sample canvas component can look like so

@Component({
  selector: 'app-canvas',
  template: `<svg width="400" height="400">
                <ng-content></ng-content>
              </svg>`,
  standalone: true,
  imports: [CommonModule],
})
export class CanvasComponent {}

a custom SVG element can look like so

@Component({
  selector: 'app-custom-rect,[app-custom-rect]',
  template: `<svg appRemoveHost>
              <rect  [attr.x]="x" [attr.y]="y" [attr.width]="width" [attr.height]="height" style="{{ style }}" />
             </svg>`,
  standalone: true,
  imports: [CommonModule, RemoveHostDirective],
})
export class CustomRectComponent {
  @Input() style = '';
  @Input() x = '';
  @Input() y = '';
  @Input() width = '';
  @Input() height = '';
}

Here it is worth mentioning that we need to create svg wrapper element otherwise Only void and foreign elements can be self-closed "rect" as rect element can exist only in svg-s. As visible here we are using the appRemoveHost, thanks to that when our component is being rendered the wrapper will be removed.

Finally, at the place where we want to use our canvas, we should do the following

<app-canvas>
  <app-custom-rect
    appRemoveHost
    [y]="'50'"
    [x]="'0'"
    [width]="'100'"
    [height]="'100'"
    [style]="'fill:red'"
  ></app-custom-rect>
  <app-canvas/>

Here we once again need to use the appRemoveHost directive in order to remove the leftover wrapper of the rect component.

Here you can find Working Stackblitz

1

Is it possible to inject dynamic components that are child svg elements?

Yes, definitely. Angular has been supporting that for a few years already, but I'm not sure I've seen this very well documented.

When you create an Angular component, your selector can be a lot of things. It doesn't have to be a tag necessarily. And we're going to take advantage of that to use an attribute instead.

First, lets build our main component that'll host the SVG:

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

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

And the HTML:

<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
  <svg:g house></svg:g>
</svg>

As you probably guessed already, the trick is here: <svg:g house></svg:g>. We create a group, that won't render anything. But we add an attribute house to it.

And then to "inject" a component there, we can have a component selector target an svg tag with a group, and a house attribute:

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

@Component({
  selector: 'svg:g[house]',
  templateUrl: './house.component.html',
  styleUrls: ['./house.component.css'],
})
export class HouseComponent {}

And final piece of the puzzle that's pretty much just SVG: The one to inject

<svg:polygon points="25,10 40,30 10,30" style="fill:rgb(0,0,255);" />

<svg:rect width="30" height="30" x="10" y="30" style="fill:rgb(0,0,255);" />

If we look at the rendered HTML:

<app-main _ngcontent-rcx-c227="" _nghost-rcx-c226=""
  ><svg
    _ngcontent-rcx-c226=""
    viewBox="0 0 100 100"
    xmlns="http://www.w3.org/2000/svg"
  >
    <g _ngcontent-rcx-c226="" house="" _nghost-rcx-c217="">
      <polygon
        _ngcontent-rcx-c217=""
        points="25,10 40,30 10,30"
        style="fill: rgb(0,0,255);"
      ></polygon>
      <rect
        _ngcontent-rcx-c217=""
        width="30"
        height="30"
        x="10"
        y="30"
        style="fill: rgb(0,0,255);"
      ></rect>
    </g></svg
></app-main>

Not a single trace of our inner component. Of course, you can have as many and as nested as you wish. You can also parameterize them by passing data as input like you'd with any other component or inject services.

Here's a live demo on Stackblitz.

maxime1992
  • 22,502
  • 10
  • 80
  • 121
  • The original question is how the components can be injected "dynamically" from the code using `ng-template` and something like `viewContainerRef.createComponent<>` that works fine otherwise but adds a
    loading svg components. One alternative approach that works is using `ngSwitch` on and switch the components based on the condition but this approach does not provide the expected "dynamicity".
    – KDR Dec 13 '22 at 18:53
-1

The documentation might help you on that :

You can use SVG files as templates in your Angular applications. When you use an SVG as the template, you are able to use directives and bindings just like with HTML templates. Use these features to dynamically generate interactive graphics.

I would suggest you create a SVG component that you can control in an Angular fashion, and for the dynamic template display, what you can do is this :

<div class="container" *appCanvasAnchor>
</div>
.container {
  width: 400px;
  height: 400px;
  svg {
    width: 100%;
    height: 100%;
  }
}
MGX
  • 2,534
  • 2
  • 14