Search code examples
angularsvgdynamiccomponents

Create angular svg components dynamically


This question is specifically about SVG components, that is to say components that use an svg template. I am aware of how to create components dynamically in Angular 18, and this question is more specific than that, in that it is about creating svg components dynamically.

I have created the requisite github repo to support this question and it can be found here

So onto the question, which is probably more about how to use attribute selectors in dynamically created components, but I don't want to lead the witness.

The desired outcome is to be able to have some sort of container component that houses the <svg> element, and be able to add specific components that house svg elements dynamically.

So in this example

export class DrawingSurfaceComponent {
  @ViewChild('svgRef', { read: ViewContainerRef })
  svgRef!: ViewContainerRef;
  registry:any[] = [];
  registerDrawingElementType(drawingElementType:any){
    this.registry.push(drawingElementType);
  }
  drawElement(drawingElementName:string){
    const [ el ] = this.registry.filter(r => r.name = drawingElementName);
    const newComponentRef = this.svgRef.createComponent(el.elementType);
  }
}

with the following template

<svg>
    <g #svgRef></g>
</svg>

What I want is for the drawElement method to dynamically add my rectangle component resulting in the following output

<svg>
  <g>
    <rect ... >
  </g>
</svg>

Instead what I get is

<svg>
  <g>
    <app-rectangle _nghost-ng-c578836850=""><rect _ngcontent-ng-c578836850="" width="200" height="100" x="10" y="10" rx="20" ry="20" fill="blue"></rect></app-rectangle>
  </g>
</svg>

Given that app-rectangle is not valid svg the rectangle is not drawn.

If I try and use an attribute selector like so

@Component({
  selector: '[my-rect]',
  standalone: true,
  imports: [],
  templateUrl: './rectangle.component.html',
  styleUrl: './rectangle.component.css',
  schemas: [
    NO_ERRORS_SCHEMA
  ]
})
export class RectangleComponent {

}

With the following template

<rect my-rect width="200" height="100" x="10" y="10" rx="20" ry="20" fill="blue" />

I get no output at all

<svg _ngcontent-ng-c3197269335=""><g _ngcontent-ng-c3197269335=""></g><!--container--></svg>

So how would I achieve this in Angular 18?


Solution

  • Strange that assuming that you have this HTML:

    <svg>
      <g #svgRef></g>
    </svg>
    

    and running

    this.svgRef.createComponent(el.elementType);
    

    you get:

    <svg>
      <g>
        <app-rectangle ...></rect></app-rectangle>
      </g>
    </svg>
    

    you app-rectangle component should be added as sibling of <g> and not as its child:

    <svg>
      <g></g>
      <app-rectangle ...></rect></app-rectangle>
    </svg>
    

    So, I would change the template to:

    <svg>
      <g>
        <ng-container #svgRef></ng-container>
      </g>
    </svg>
    

    What I suggest to do, is to wrap your partial SVG code (<rect ... /> in your example) with <ng-template> and expose it via public TemplateRef, something like:

    @Component({
      standalone: true,
      template: `
        <ng-template>
          <rect width="200" height="100" x="10" y="10" rx="20" ry="20" fill="blue" />
        <ng-template>  
      `,
      schemas: [NO_ERRORS_SCHEMA]
    })
    export class Rect {
      @ViewChild(TemplateRef, { static: true }) rectRef!: TemplateRef<unknown>;
    }
    

    then use it:

    drawElement(drawingElementName:string) {
      // clear view container
      this.svgRef.clear(); 
      const [el] = this.registry.filter(r => r.name = drawingElementName);
      // embed component
      const newComponentRef = this.svgRef.createComponent(el.elementType, { index: 0 });
      // embed template ref exposed by created component
      this.svgRef.createEmbeddedView(newComponentRef.instance.rectRef, { index: 1 });
      // remove component (we needed only its template ref)
      this.svgRef.remove(0);
    }
    

    I created STACKBLITZ to play with