Why does a custom element collapse? - html

I try to define a custom field, containing either a SVG or a Canvas. But my example shows some strange rendering. I expect two boxes 400 pixel wide and 300 pixel high. But both boxes seem to collapse in different ways. How can I fix this?
class TestSvg extends HTMLElement
{
constructor() {
super();
const shadow = this.attachShadow({mode: 'open'});
const container = document.createElement('svg');
container.setAttribute('width', '400');
container.setAttribute('height', '300');
shadow.appendChild (container);
}
}
class TestCanvas extends HTMLElement
{
constructor() {
super();
const shadow = this.attachShadow({mode: 'open'});
const container = document.createElement('canvas');
container.setAttribute('width', '400');
container.setAttribute('height', '300');
shadow.appendChild (container);
}
}
customElements.define ('test-svg', TestSvg);
customElements.define ('test-canvas', TestCanvas);
test-svg, test-canvas {
border: 1px solid black;
}
svg
<test-svg>
</test-svg>
canvas
<test-canvas>
</test-canvas>
end
Same without custom elements:
svg, canvas {
border: 1px solid black;
}
svg
<svg width="400" height="300"></svg>
canvas
<canvas width="400" height="300"></canvas>
end
Why is there a difference between the version with custom elements and the version without custom elements?

Your SVG element is not being created correctly. It needs to be in the correct SVG namespace. Change it to this:
const container = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
By default, your custom elements will be display: inline. Set them to block or inline-block depending on your need.
test-svg, test-canvas {
display: inline-block;
border: 1px solid black;
}
Updated test:
class TestSvg extends HTMLElement
{
constructor() {
super();
const shadow = this.attachShadow({mode: 'open'});
const container = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
container.setAttribute('width', '400');
container.setAttribute('height', '300');
shadow.appendChild (container);
}
}
class TestCanvas extends HTMLElement
{
constructor() {
super();
const shadow = this.attachShadow({mode: 'open'});
const container = document.createElement('canvas');
container.setAttribute('width', '400');
container.setAttribute('height', '300');
shadow.appendChild (container);
}
}
customElements.define ('test-svg', TestSvg);
customElements.define ('test-canvas', TestCanvas);
test-svg, test-canvas {
display: inline-block;
border: 1px solid black;
}
svg
<test-svg>
</test-svg>
canvas
<test-canvas>
</test-canvas>
end

Good to see more people are combining Custom Elements and SVG, they are a good match
Your core problem was the SVG NameSpace, so Paul his answer is correct.
Some additional comments:
Your constructor can be optimized:
constructor() {
super();
const shadow = this.attachShadow({mode: 'open'});
const container = document.createElementNS('http://www.w3.org/2000/svg','svg');
container.setAttribute('width', '400');
container.setAttribute('height', '300');
shadow.appendChild (container);
}
super() returns the element scope
Google documentation that says to use super() first is wrong,
they mean: Call super() before you can use the 'this' scope reference.
attachShadow() boths sets AND returns this.shadowRoot for free
.appendChild() returns the created element
So you can chain everything:
constructor() {
const container = super()
.attachShadow({mode:'open'})
.appendChild (document.createElementNS('http://www.w3.org/2000/svg','svg'));
container.setAttribute('width', '400');
container.setAttribute('height', '300');
}
If you do not do anything special with this in memory Custom Element,
you can scrap the whole constructor and create the element with HTML in the connectedCallback
connectedCallback(){
this.innerHTML = `<svg width='400' height='300'
xmlns='http://www.w3.org/2000/svg'
viewBox='0 0 20 20'></svg>`;
}
Note: I also ditched shadowDOM above; if you do want shadowDOM its:
connectedCallback(){
this.attachShadow({mode:"open"})
.innerHTML = `<svg width='400' height='300'
xmlns='http://www.w3.org/2000/svg'
viewBox='0 0 20 20'></svg>`;
}
There is another sizing problem
Depending on your Font family and size there will be a gutter below your inline-block SVG Custom Element (see RED below) to allow for pgjq characters that stick out below the base line.
To counter that you have to set vertical-align: top on the SVG element:
<style>
body { font-size: 3em }
svg-circle { background: RED }
svg-circle svg {
background: grey;
display: inline-block;
width: 80px;
}
#correct svg { vertical-align: top }
</style>
<div>
<svg-circle radius="40%" color="green"></svg-circle>
<svg-circle x="50%" y="100%" color="blue"></svg-circle>
<svg-circle></svg-circle>
</div>
<div id="correct">
<svg-circle radius="40%" color="green"></svg-circle>
<svg-circle x="50%" y="100%" color="blue"></svg-circle>
<svg-circle></svg-circle>
</div>
<script>
customElements.define("svg-circle", class extends HTMLElement {
static get observedAttributes() { return ["x", "y", "radius", "color"] }
connectedCallback() { this.render() }
attributeChangedCallback() { this.render() }
render() {
let [x = "50%",y = "50%",radius = "50%", color = "rebeccapurple"] =
this.constructor.observedAttributes.map(x => this.getAttribute(x) || undefined);
this.innerHTML = `<svg viewBox='0 0 96 96'>
<circle cx='${x}' cy='${y}' r='${radius}' fill='${color}'/></svg>`;
}
});
</script>
Note: There is no shadowDOM attached to <svg-circle>, so the SVG inside can be styled with global CSS
Make it an IMG
If you do not want any CSS bleeding, and want to work with the SVG as if it is an image,
without pointer-events issues, then create an IMG:
this.innerHTML = `<img src="data:image/svg+xml,<svg viewBox='0 0 96 96'>
<circle cx='${x}' cy='${y}' r='${radius}' fill='${color}'/>
</svg>">`.replace(/#/g, "%23");
Note: The # is the only character that needs to be escaped here. In CSS url() usage you also need to escape the < and >
Add a grid
If you are going to create an Icon Toolbar or Chessboard like layout with SVGs, add a grid:
#correct {
display: grid;
grid-template-columns: repeat(3, 80px);
}
svg-circle svg {
background: grey;
display: inline-block;
width: 100%;
height: 100%;
}

In the first custom element TestSvg, change the element type from svg to canvas
const container = document.createElement('canvas');
HTML elements reference
https://developer.mozilla.org/en-US/docs/Web/HTML/Element
'canvas' is an HTML element.
where as 'svg' is not. 'svg' is Container element, structure element

Related

'box-shadow' on a custom component with only a shadow root attached applies as if component is 0x0px

I have a custom component that has only the shadow root attached and no child elements inside it. In Chrome dev tools, hover over the custom component shows its actual size on screen with the correct pixel numbers. However, when adding a box-shadow property to this component result in the shadow being applied to the top left corner of the component, as if the component itself is only 0px by 0px.
customElements.define('my-comp', class extends HTMLElement {
constructor() {
super();
this.attachShadow({
mode: 'open'
})
const div = document.createElement('div')
div.setAttribute('style', 'height: 100px; width: 100px; background: lime')
this.shadowRoot.append(div)
}
})
<my-comp style="box-shadow: 0px 0px 10px 10px brown"></my-comp>
Is this a known bug and is there a workaround? Or perhaps there's an error somewhere in my code?
Your custom component will have an inline display and you are adding a block element inside it. You are facing the behavior of "block element inside inline element"
Make your element inline-block to avoid dealing with such case
customElements.define('my-comp', class extends HTMLElement {
constructor() {
super();
this.attachShadow({
mode: 'open'
})
const div = document.createElement('div')
div.setAttribute('style', 'height: 100px; width: 100px; background: lime')
this.shadowRoot.append(div)
}
})
<my-comp style="box-shadow: 0px 0px 10px 10px brown;display:inline-block"></my-comp>

How can I style CSS parts using :root?

I have a button web component built that I am trying to style using CSS parts. In my web component below you can see that the button is colored tomato because I have used exportparts and then in my consumer CSS I have styled it using btn-comp::part(btn) { ... }.
However, I would prefer to not have to style it using btn-comp, instead I would like to just use :root to style it like this author does.
For example I should be able to do this:
:root::part(btn) { /* my styles here */ }
But that does not work, it only works when I use the component name. How can I style my CSS parts using :root?
const template = document.createElement('template');
template.innerHTML = `<button part="btn">I should be a green button</button>`;
class BtnComp extends HTMLElement {
connectedCallback() {
this.attachShadow({mode: 'open'});
this.shadowRoot.appendChild(template.content.cloneNode(true));
const button = this.shadowRoot.querySelector("button");
button.addEventListener("click", () => alert('Clicked'));
}
}
window.customElements.define('btn-comp', BtnComp);
:root::part(btn) {
background-color: green !important; /* I want this color to be applied! */
}
btn-comp::part(btn) {
background-color: tomato;
color: white;
padding: 1rem 2rem;
border: 0;
cursor: pointer;
}
<btn-comp exportparts="btn" />

Shadow DOM global css inherit possible?

Is there a way to inherit :host element css styles into shadow DOM?
The reason is if we start developing web components, each web component style must be consistent on a page.
The page can have global css, and this global css styles can be inherited to shadow DOM. There was ::shadow and /deep/, but it's deprecated now.
Or, is this against pattern? If so, why?
I found this Q/A, but seems outdated for me.
Can Shadow DOM elements inherit CSS?
http://plnkr.co/edit/qNSlM0?p=preview
const el = document.querySelector('my-element');
el.attachShadow({mode: 'open'}).innerHTML = `
<!-- SEE THIS 'red' is not red -->
<p class="red">This is defined in Shadow DOM. I want this red with class="red"</p>
<slot></slot>
`;
.red {
padding: 10px;
background: red;
font-size: 25px;
text-transform: uppercase;
color: white;
}
<!DOCTYPE html>
<html>
<head>
<!-- web components polyfills -->
<script src="//unpkg.com/#webcomponents/custom-elements"></script>
<script src="//unpkg.com/#webcomponents/webcomponentsjs"></script>
<script src="//unpkg.com/#webcomponents/shadydom"></script>
<script src="//unpkg.com/#webcomponents/shadycss#1.0.6/apply-shim.min.js"></script>
</head>
<body>
<div>
<p class="red">I'm outside the element (big/white)</p>
<my-element>
<p class="red">Light DOM content is also affected.</p>
</my-element>
<p class="red">I'm outside the element (big/white)</p>
</div>
</body>
</html>
As supersharp pointed out it's very simple but not obvious from the examples you can find on the internet. Take this base class as an example. Alternatively, you could make two different ones (e.g. Component and ShadowComponent). There is also the option to use adoptedStyleSheets or the ::part selector.
class HtmlComponent extends HTMLElement {
static ModeShadowRoot = 0;
static ModeRoot = 1;
static styleSheets = [];
static mode = HtmlComponent.ModeShadowRoot;
#root = null;
constructor() {
super();
if (this.constructor.mode === HtmlComponent.ModeShadowRoot) {
const shadowRoot = this.attachShadow({ mode: 'closed' });
shadowRoot.adoptedStyleSheets = this.constructor.styleSheets;
this.#root = shadowRoot;
} else {
this.#root = this;
}
}
get root() {
return this.#root;
}
init() {
this.root.innerHTML = this.render();
}
render() {
return '';
}
}
class Test extends HtmlComponent {
static mode = HtmlComponent.ModeRoot;
constructor() {
super();
super.init();
}
render() {
return `
<div>
<x-nested-component></x-nested-component>
</div>
`;
}
}
One of the features of Shadow DOM is to isolate CSS styles.
So if you want your Custom Elements to inherit styles from the main page, don't use Shadow DOM. It's not mandatory.

Using a non-shadow DOM custom element both inside and outside the shadow DOM

I have a custom element (without shadow DOM) that I'd like to be able to use anywhere, even inside another custom element that might use shadow DOM. However, I'm not sure how to get the styles working in both places.
For example, lets say I create a simple fancy-button element:
class fancyButton extends HTMLElement {
constructor() {
super();
this.innerHTML = `
<style>
fancy-button button {
padding: 10px 15px;
background: rgb(62,118,194);
color: white;
border: none;
border-radius: 4px
}
</style>
<button>Click Me</button>`;
}
}
customElements.define('fancy-button', fancyButton);
<fancy-button></fancy-button>
Inside a shadow DOM element, the inserted style tag will allow the fancy-button styles to work. However, if this component gets used outside of a shadow DOM element, the style tag will be duplicated every time the element is used.
If instead I add the style tag as part of the html import file, then the styles only work outside of the shadow DOM but at least they are only declared once.
<!-- fancy-button.html -->
<style>
fancy-button button {
padding: 10px 15px;
background: rgb(62,118,194);
color: white;
border: none;
border-radius: 4px
}
</style>
<script>
class fancyButton extends HTMLElement {
constructor() {
super();
this.innerHTML = `<button>Click Me</button>`;
}
}
customElements.define('fancy-button', fancyButton);
</script>
What's the best way to add custom element styles that handles both being used inside and outside the shadow DOM?
So I was able to find a solution thanks to Supersharp suggestions about checking if we're in the shadow DOM.
First you add the styles as part of the import file so that the styles apply outside of the shadow DOM by default. Then when element is added to the DOM, we check getRootNode() to see if it's been added to a ShadowRoot node. If it has, and the styles haven't already been injected into the root, then we can inject the styles manually.
var div = document.createElement('div');
var shadow = div.attachShadow({mode: 'open'});
shadow.innerHTML = '<fancy-button></fancy-button>';
document.body.appendChild(div);
<style data-fs-dialog>
fancy-button button {
padding: 10px 15px;
background: rgb(62,118,194);
color: white;
border: none;
border-radius: 4px
}
</style>
<script>
class fancyButton extends HTMLElement {
constructor() {
super();
}
connectedCallback() {
this.innerHTML = `<button>Click Me</button>`;
var root = this.getRootNode();
// In polyfilled browsers there is no shadow DOM so global styles still style
// the "fake" shadow DOM. We need to test for truly native support so we know
// when to inject styles into the shadow dom. The best way I've found to do that
// is to test the toString output of a shadowroot since `instanceof ShadowRoot`
// returns true when it's just a document-fragment in polyfilled browsers
if (root.toString() === '[object ShadowRoot]' && !root.querySelector('style[data-fs-dialog]')) {
var styles = document.querySelector('style[data-fs-dialog]').cloneNode(true);
root.appendChild(styles);
}
}
}
customElements.define('fancy-button', fancyButton);
</script>
<fancy-button></fancy-button>
When all browsers support <link rel=stylesheet> in the shadow DOM, then the inline script can turn into an external stylesheet as robdodson suggested, and the code is a bit cleaner.
You'll probably want to put the styles in a separate CSS file that you vend along with your element's JS. But as you've pointed out, if you put the element inside the Shadow DOM of another element then the styles won't work in that scope. For this reason it's usually best to just create a shadow root and pop your styles in there. Any reason why you wouldn't want to do that?

Polymer 2.0 : listeners not working

<dom-module id="polymer-starterkit-app">
<template>
<style>
:host {
display: block;
}
#box{
width: 200px;
height: 100px;
border: 1px solid #000;
}
</style>
<h2>Hello, [[prop1]]!</h2>
<paper-input label="hello">
</paper-input>
<div id="box" on-click="boxTap"></div>
</template>
<script>
/** #polymerElement */
class PolymerStarterkitApp extends Polymer.Element {
static get is() { return 'polymer-starterkit-app'; }
static get properties() {
return {
prop1: {
type: String,
value: 'polymer-starterkit-app'
},
listeners:{
'click':'regular'
},
regular:function(){
console.log('regular')
}
};
}
boxTap(){
console.log('boxTap')
}
}
window.customElements.define(PolymerStarterkitApp.is, PolymerStarterkitApp);
</script>
</dom-module>
As shown in the code above, I have tried to define a simple listener on-tap on my div with the class box but it doesn't seem to work!
I think I'm using the wrong syntax.
Also, why should we use listeners if we can simply use predefined listeners like on-click and on-tap?
I would really appreciate any type of help!
Edit: I helped updating Polymer's documentation. It's now very clear and detailed. https://www.polymer-project.org/2.0/docs/devguide/events#imperative-listeners Just read that and you're good. TL;DR: The listeners object is no more in Polymer 2.0, but there's a new way to do it.
You could simply set them up in ready(). There is no need to use .bind() in this case because this will be your custom element in the callback because it's the event's current target.
ready () {
super.ready()
this.addEventListener('my-event', this._onMyEvent)
}
_onMyEvent (event) { /* ... */ }
If you need to listen for events on something that is not your custom element itself (e.g. window), then do it the way it is shown in the Polymer documentation:
constructor() {
super();
this._boundListener = this._myLocationListener.bind(this);
}
connectedCallback() {
super.connectedCallback();
window.addEventListener('hashchange', this._boundListener);
}
disconnectedCallback() {
super.disconnectedCallback();
window.removeEventListener('hashchange', this._boundListener);
}
Source: https://www.polymer-project.org/2.0/docs/devguide/events#imperative-listeners
You must create the listener manually
connectedCallback() {
super.connectedCallback();
this.addEventListener('click', this.regular.bind(this));
}
disconnectedCallback() {
super.disconnetedCallback();
this.removeEventListener('click', this.regular);
}
regular() {
console.log('hello');
}
However, to add a listener to an element like the div, you need to add Polymer.GestureEventListeners
class PolymerStarterkitApp extends Polymer.GestureEventListeners(Polymer.Element) {
}