bug: unable to render foreignObject with attributes inside SVG
Prerequisites
- [X] I have read the Contributing Guidelines.
- [X] I agree to follow the Code of Conduct.
- [X] I have searched for existing issues that already report this problem, without success.
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
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>
);
}
}
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!
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
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);
});
});
};