stencil icon indicating copy to clipboard operation
stencil copied to clipboard

bug: unable to render foreignObject with attributes inside SVG

Open peterpeterparker opened this issue 3 years ago • 3 comments

Prerequisites

Stencil Version

v2.x

Current Behavior

foreignObject with attributes in SVG cannot be rendered.

Stencil expect the foreignObject to be provided as foreign-object in order to be able to render attributes but, the browser expect the foreignObject to be provided as <foreignObject/> and not <foreign-object/>.

It seems Stencil has an issue only with a subset of SVGAttributes respectively width, height, x and y.

Expected Behavior

Being able to use <foreignObject/> with attributes withing svg in Stencil render functions.

Steps to Reproduce

import { Component, h } from '@stencil/core';

@Component({
  tag: 'my-component',
  styleUrl: 'my-component.css',
  shadow: true,
})
export class MyComponent {
  render() {
    return (
      <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 300 300" x="0" y="0" height="300" width="300">
        <circle r="142" cx="150" cy="150" fill="none" stroke="#000000" stroke-width="2" />

        <foreignObject x={0} y={0} width="200" height="200">
          <p>Hello World</p>
        </foreignObject>
      </svg>
    );
  }
}

Code Reproduction URL

https://github.com/peterpeterparker/stencil-svg-foreignobject

Additional Information

Notes

a fixed issue regarding foreign object has been opened in the past, see #1733

Screenshots

Capture d’écran 2021-12-20 à 09 43 49 Capture d’écran 2021-12-20 à 09 47 29

Workaround

import {Component, h, ComponentInterface} from '@stencil/core';

@Component({
  tag: 'deckgo-social-img',
  styleUrl: 'social-img.scss',
  shadow: true
})
export class SocialImg implements ComponentInterface {
  private foreignObjectRef!: SVGForeignObjectElement;

  componentDidLoad() {
    this.setForeignObjectAttributes();
  }

  private setForeignObjectAttributes() {
    this.foreignObjectRef?.setAttribute('x', '0');
    this.foreignObjectRef?.setAttribute('y', '0');
    this.foreignObjectRef?.setAttribute('width', '200');
    this.foreignObjectRef?.setAttribute('height', '100');
  }

  render() {
    return (
      <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 300 300" x="0" y="0" height="300" width="300">
        <circle r="142" cx="150" cy="150" fill="none" stroke="#000000" stroke-width="2" />

        <foreignObject ref={(el) => (this.foreignObjectRef = el as SVGForeignObjectElement)}>
          <p>Hello World</p>
        </foreignObject>
      </svg>
    );
  }
}

peterpeterparker avatar Dec 20 '21 09:12 peterpeterparker

Thanks for the issue! This issue has been labeled as holiday triage. With the winter holidays quickly approaching, much of the Stencil Team will soon be taking time off. During this time, issue triaging and PR review will be delayed until the team begins to return. After this period, we will work to ensure that all new issues are properly triaged and that new PRs are reviewed. In the meantime, please read our Winter Holiday Triage Guide for information on how to ensure that your issue is triaged correctly. Thank you!

ionitron-bot[bot] avatar Dec 20 '21 09:12 ionitron-bot[bot]

Thanks for the reproduction case! I can confirm this isn't working as expected. I'm going to get this ingested for the team to triage & get in our internal backlog

rwaskiewicz avatar Jan 12 '22 13:01 rwaskiewicz

I finally up ending in a more scalable workaround

The idea is to encapsulate the needed data in the parent element and use a helper to apply it when needed

<g x="10" y="10" width="200" height="300">
    <foreignObject>
    <div>TEST</div>
    </foreignObject>
</g>

Then in componentDidLoad process all found matches

  componentDidLoad() {
    setForeignObjectAttributes(this.el.shadowRoot);
  }
export const inheritAttributes = (el: HTMLElement, attributes: string[] = []) => {
  const attributeObject: { [k: string]: any } = {};

  attributes.forEach((attr) => {
    if (el.hasAttribute(attr)) {
      const value = el.getAttribute(attr);
      if (value !== null) {
        attributeObject[attr] = el.getAttribute(attr);
      }
      el.removeAttribute(attr);
    }
  });

  return attributeObject;
};

export const setForeignObjectAttributes = (el: HTMLElement | ShadowRoot) => {
  Array.from(el.querySelectorAll("foreignObject"))
    .map((e) => {
      return { source: e.parentElement, target: e };
    })
    .forEach(({ source, target }) => {
      const attrs = inheritAttributes(source, ["x", "y", "width", "height"]);
      Object.entries(attrs).forEach(([key, value]) => {
        target.setAttribute(key, value);
      });
    });
};

xperiments avatar May 27 '22 12:05 xperiments