Skip to content

Error when using x-for to generate SVG elements #637

Closed

Description

@Calinou

Alpine version: 2.4.1

I'm getting an error when using x-for on a template tag to generate SVG elements. With the following HTML:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Rectangle Editor</title>
    <script defer src="https://cdn.jsdelivr.net/npm/alpinejs@2.4.1/dist/alpine.js"></script>
</head>
<body>
    <div x-data="rectangleEditor()">
        <svg width="1024" height="1024">
            <!-- Error -->
            <template x-for="rectangle in rectangles" :key="rectangle">
                <rect :x="rectangle.x" :y="rectangle.y" :width="rectangle.width" :height="rectangle.height" />
            </template>
        </svg>
        <!-- No error -->
        <template x-for="rectangle in rectangles" :key="rectangle">
            <div :x="rectangle.x" :y="rectangle.y" :width="rectangle.width" :height="rectangle.height" />
        </template>
    </div>

    <script>
        function rectangleEditor() {
            return {
                rectangles: [
                    { x: 20, y: 40, width: 200, height: 400 },
                    { x: 60, y: 100, width: 500, height: 500 },
                    { x: 200, y: 40, width: 100, height: 400 },
                    { x: 300, y: 40, width: 100, height: 400 },
                ],
            }
        };
    </script>
</body>
</html>

I don't get such an error when using x-for outside a svg element. But as soon as I add elements in a svg element using x-for (even standard HTML elements such as div), I get the following stack trace:

Chromium 81:

alpine.js:76 Uncaught TypeError: Cannot read property 'childElementCount' of undefined
    at warnIfMalformedTemplate (alpine.js:76)
    at handleForDirective (alpine.js:438)
    at alpine.js:1570
    at Array.forEach (<anonymous>)
    at Component.resolveBoundAttributes (alpine.js:1530)
    at Component.initializeElement (alpine.js:1446)
    at alpine.js:1430
    at alpine.js:1420
    at walk (alpine.js:84)
    at walk (alpine.js:88)

Firefox 78:

Uncaught TypeError: el.content is undefined
    alpinejs 2.4.1/dist/alpine.js:76
    alpinejs 2.4.1/dist/alpine.js:438
    alpinejs 2.4.1/dist/alpine.js:1570
    alpinejs 2.4.1/dist/alpine.js:1530
    alpinejs 2.4.1/dist/alpine.js:1446
    alpinejs 2.4.1/dist/alpine.js:1430
    alpinejs 2.4.1/dist/alpine.js:1420
    alpinejs 2.4.1/dist/alpine.js:84
    alpinejs 2.4.1/dist/alpine.js:88
    alpinejs 2.4.1/dist/alpine.js:88
    alpinejs 2.4.1/dist/alpine.js:1408
    alpinejs 2.4.1/dist/alpine.js:1425
    alpinejs 2.4.1/dist/alpine.js:1355
    alpinejs 2.4.1/dist/alpine.js:1735
    alpinejs 2.4.1/dist/alpine.js:1678
    alpinejs 2.4.1/dist/alpine.js:1694
    alpinejs 2.4.1/dist/alpine.js:1693
    alpinejs 2.4.1/dist/alpine.js:1677

If this isn't supported, then this should be documented in the README. (I'm willing to open a PR for that if this is the case.)

Activity

  1. allmarkedup commented on Jul 7, 2020

    @allmarkedup
    Contributor

    FWIW I suspect this is not an Alpine issue - if you are using HTML tags (like template) inside an SVG element they will have an SVG namespace so HTML elements like HTMLTemplateElement won't be implemented by the browser. I ran into something similar a while back trying to use vanilla JS and templates in SVG elements.

    Edit: Sounds like Polymer ran into something similar and there is now a WHATWG feature request open to provide support: whatwg/html#3563 - not much help in the short term though.

  2. SimoTod commented on Jul 7, 2020

    @SimoTod
    Collaborator

    Yeah, it's a DOM issue. templates inside an svg tag are not implemented and they miss the content property (you can see it using the inspector without alpine).

    You need to include a polyfil like the one below (adapted from one mentioned on the polymer issue)

    (function(){ 
      var templates = document.querySelectorAll('svg template');
      var el, template, attribs, attrib, count, child, content;
      for (var i=0; i<templates.length; i++) {
        el = templates[i];
        template = el.ownerDocument.createElement('template');
        el.parentNode.insertBefore(template, el);
        attribs = el.attributes;
        count = attribs.length;
        while (count-- > 0) {
          attrib = attribs[count];
          template.setAttribute(attrib.name, attrib.value);
          el.removeAttribute(attrib.name);
        }
        el.parentNode.removeChild(el);
        content = template.content;
        while ((child = el.firstChild)) {
          content.appendChild(child);
        }
      }
    })();
    
  3. Calinou commented on Jul 7, 2020

    @Calinou
    ContributorAuthor

    @SimoTod That polyfill seems to be working (tested in Firefox and Chromium). Thanks a lot 🙂

  4. SimoTod commented on Jul 7, 2020

    @SimoTod
    Collaborator

    No worries. Make sure you test it on all browsers you support (IE11, old edge, Safari, etc). It should work but I did not check. 👍

  5. added a commit that references this issue on Jul 7, 2020
    488c649
  6. added 2 commits that reference this issue on Jul 16, 2020
    05e595f
    6586fcb
  7. Corkle commented on Dec 19, 2020

    @Corkle

    I'm curious why x-for is only supported on template tags. Is this due to a technical limitation? It would be nice to be able to use it on other tag types that are supported within svg.

  8. Calinou commented on Dec 19, 2020

    @Calinou
    ContributorAuthor

    Is this due to a technical limitation?

    Yes, Alpine.js doesn't use a virtual DOM which means you can only use x-for on template tags.

  9. HugoDF commented on Dec 20, 2020

    @HugoDF
    Contributor

    Is this due to a technical limitation?

    Yes, Alpine.js doesn't use a virtual DOM which means you can only use x-for on template tags.

    Hmmm I don't think it's strictly VDOM vs not, I think it's a trade-off (admittedly partly due to no VDOM). You probably could have an x-for without template but you would need to hide the relevant Nodes/elements until x-for has run. The big benefits of template is that by default it doesn't show visually and is easy to use to generate DOM Nodes.

  10. ocharles commented on May 21, 2022

    @ocharles

    Edit: sorry, you can ignore this comment. I was applying the polyfill too early.

    Here's an updated example that works:

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Rectangle Editor</title>
        <script defer src="https://cdn.jsdelivr.net/npm/alpinejs@2.4.1/dist/alpine.js"></script>
    </head>
    <body>
        <div x-data="rectangleEditor()">
            <svg width="1024" height="1024">
                <!-- Error -->
                <template x-for="rectangle in rectangles" :key="rectangle">
                    <rect :x="rectangle.x" :y="rectangle.y" :width="rectangle.width" :height="rectangle.height" />
                </template>
            </svg>
            <!-- No error -->
            <template x-for="rectangle in rectangles" :key="rectangle">
                <div :x="rectangle.x" :y="rectangle.y" :width="rectangle.width" :height="rectangle.height" />
            </template>
        </div>
    
        <script>
            function rectangleEditor() {
                return {
                    rectangles: [
                        { x: 20, y: 40, width: 200, height: 400 },
                        { x: 60, y: 100, width: 500, height: 500 },
                        { x: 200, y: 40, width: 100, height: 400 },
                        { x: 300, y: 40, width: 100, height: 400 },
                    ],
                }
            };
    
          (function(){
            var templates = document.querySelectorAll('svg template');
            var el, template, attribs, attrib, count, child, content;
            for (var i=0; i<templates.length; i++) {
              el = templates[i];
              template = el.ownerDocument.createElement('template');
              el.parentNode.insertBefore(template, el);
              attribs = el.attributes;
              count = attribs.length;
              while (count-- > 0) {
                attrib = attribs[count];
                template.setAttribute(attrib.name, attrib.value);
                el.removeAttribute(attrib.name);
              }
              el.parentNode.removeChild(el);
              content = template.content;
              while ((child = el.firstChild)) {
                content.appendChild(child);
              }
            }
          })();
        </script>
    </body>
    </html>

    For posterity, here's my original comment:

    Details Can anyone supply a small example page using `x-for` with `svg` and the latest Alpine? With the polyfill above, I still get errors.

    This is what I'm trying at the moment:

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Rectangle Editor</title>
        <script defer src="https://cdn.jsdelivr.net/npm/alpinejs@2.4.1/dist/alpine.js"></script>
        <script>
          (function(){
            var templates = document.querySelectorAll('svg template');
            var el, template, attribs, attrib, count, child, content;
            for (var i=0; i<templates.length; i++) {
              el = templates[i];
              template = el.ownerDocument.createElement('template');
              el.parentNode.insertBefore(template, el);
              attribs = el.attributes;
              count = attribs.length;
              while (count-- > 0) {
                attrib = attribs[count];
                template.setAttribute(attrib.name, attrib.value);
                el.removeAttribute(attrib.name);
              }
              el.parentNode.removeChild(el);
              content = template.content;
              while ((child = el.firstChild)) {
                content.appendChild(child);
              }
            }
          })();
        </script>
    </head>
    <body>
        <div x-data="rectangleEditor()">
            <svg width="1024" height="1024">
                <!-- Error -->
                <template x-for="rectangle in rectangles" :key="rectangle">
                    <rect :x="rectangle.x" :y="rectangle.y" :width="rectangle.width" :height="rectangle.height" />
                </template>
            </svg>
            <!-- No error -->
            <template x-for="rectangle in rectangles" :key="rectangle">
                <div :x="rectangle.x" :y="rectangle.y" :width="rectangle.width" :height="rectangle.height" />
            </template>
        </div>
    
        <script>
            function rectangleEditor() {
                return {
                    rectangles: [
                        { x: 20, y: 40, width: 200, height: 400 },
                        { x: 60, y: 100, width: 500, height: 500 },
                        { x: 200, y: 40, width: 100, height: 400 },
                        { x: 300, y: 40, width: 100, height: 400 },
                    ],
                }
            };
        </script>
    </body>
    </html>
  11. flytreeleft commented on May 1, 2026

    @flytreeleft

    Actually, if you are using Vite, the <template/> in <svg/> will cause [vite] Internal server error: Cannot read properties of undefined (reading 'nodeName').

    To avoid this error, you can add a namespace to the template tag (example <x:template/>), then change your initialization code as following (based on #3944):

    document.addEventListener('DOMContentLoaded', () => {
      recreateTemplateInSvg(document);
    });
    
    function recreateTemplateInSvg(node) {
      node.querySelectorAll('svg x\\:template').forEach((el) => {
        const template = el.ownerDocument.createElement('template');
    
        for (const attr of el.attributes) {
          template.setAttributeNode(attr.cloneNode());
        }
        template.content.append(...el.children);
    
        el.replaceWith(template);
      });
    
      node.querySelectorAll('template').forEach((el) => {
        recreateTemplateInSvg(el.content);
      });
    }

    If your <svg/> is nested in <template/>, you should call recreateTemplateInSvg recursively with the content of <template/> node, because querySelectorAll will not match the elements which are in <template/>.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    documentationImprovements or additions to documentation

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions