hotwired / turbo

The speed of a single-page web application without having to write any JavaScript
https://turbo.hotwired.dev
MIT License
6.73k stars 430 forks source link

Adding SVG subelements via Turbo Streams leads to them not being rendered #1223

Open AsherWright opened 8 months ago

AsherWright commented 8 months ago

Overview

I'd like to edit SVG elements with turbo streams. One problem is that when I try to add an element via a turbo_stream, it isn't rendered. For instance, appending a <circle> inside of an SVG.

Code example

Showing the code for a rails app, but the idea would be the same with another framework:

html page:

<div class="w-3/4 h-96">
  <svg>
    <g>
      <g is="turbo-frame" id="circles">
        <circle r="15" fill="#004d40" transform="translate(50, 50)"></circle>
      </g>
    </g>
  </svg>
</div>
<div>
  <%= button_to "Action", action_path, method: :post %>
</div>

Turbo stream event:

<%= turbo_stream.append "circles" do %>
  <circle r="15" fill="#004d40" transform="translate(50, 100)"></circle>
<% end %>

After the event fires, we see it in the browser's inspect, and it's in the right spot. But it's not visible:

image

We can "fix" it by wrapping the <circle> in its own <svg>, but then there's a bunch of unneeded SVGs.

My uninformed hypothesis is that it has to do with element namespaces? And that SVGs may require the namespace when adding elements?

AsherWright commented 8 months ago

After checking the namespaceURI of each element, I do think it might be the problem. For the existing SVG circle, it's 'http://www.w3.org/2000/svg' and for the circle that the turbo stream adds it's 'http://www.w3.org/1999/xhtml'

omarluq commented 7 months ago

The append action uses document.append under the hood which by default adheres to the XHTML namespace http://www.w3.org/1999/xhtml and has very strict rules about it, you can get around this by using a custom turbo stream action that implements document.insertAdjacentHTML on 'beforeend' to get the same append behavior but more flexibility with namespaces.

This is an example of a custom action to handle this that I've tested

import { StreamActions } from "@hotwired/turbo"

StreamActions.appendSvgCircle = function() {
  this.removeDuplicateTargetChildren()
  this.targetElements.forEach((targetElement) => {
    let div = document.createElement('div');
    div.appendChild(this.templateContent.cloneNode(true));
    targetElement.insertAdjacentHTML('beforeend', div.innerHTML);
  })
}