Hopefully the snippet is self explanatory, most of the (albeit rather broken) magic happens in conntectedCallback(). Essentially, if you type into the first text field at the very top you will see a string being printed to the console. But typing into the second editable text field (which unlike the first one is nested inside another webcomponent) nothing gets printed.
Any thoughts or pointers would be most appreciated.
<!DOCTYPE html>
<html>
<head>
<script>
class HtmlGenMachinery extends HTMLElement {
disconnectedCallback() {
console.log("disconnectedCallback for '" + String(this.tagName) + "' element");
}
attributeChangedCallback() {
console.log("attributeChangedCallback for '" + String(this.tagName) + "' element");
}
adoptedCallback() {
console.log("adoptedCallback for '" + String(this.tagName) + "' element");
}
constructor() {
super();
console.log("constructor for '" + String(this.tagName) + "' element");
this.attachShadow({ mode: "open" });
}
connectedCallback() {
console.log("connectedCallback for '" + String(this.tagName) + "' element");
setTimeout(() => {
console.log("connectedCallback setTimeout lambda for '" + String(this.tagName) + "' element");
// Parse html
this.sourceHtml = this.sourceHtml
.replaceAll('{shadowRoot}', '(el{id}.shadowRoot)')
.replaceAll('¤%&/📍$', '`')
.replaceAll('¤%&/🧭$', '</sc' + 'ript>')
.replaceAll(`{content}`, this.innerHTML);
if (this.sourceHtml.includes('el{id}')) {
this.sourceHtml = "<script>var el{id}=document.getElementById('{id}');</sc" + "ript>" + this.sourceHtml;
}
for (const argStr of this.arguments) {
if (Array.isArray(argStr)) {
this.sourceHtml = this.sourceHtml.replaceAll(`{${argStr[0]}}`, this.getAttribute(argStr[1]));
} else {
this.sourceHtml = this.sourceHtml.replaceAll(`{${argStr}}`, this.getAttribute(argStr));
}
}
const newContent = document.createRange().createContextualFragment(this.sourceHtml);
this.shadowRoot.appendChild(newContent);
}, 0)
}
}
class HtmlGen extends HtmlGenMachinery { sourceHtml = `{content}`; arguments = []; }
window.customElements.define('f-html-gen', HtmlGen);
class HtmlGen_Box extends HtmlGenMachinery {
sourceHtml = `<div style="background-color: {color}">{content}</div>`; arguments = ["color"];
}
window.customElements.define('f-html-gen-box', HtmlGen_Box);
class HtmlGen_StringProperty extends HtmlGenMachinery {
sourceHtml = `<blockquote><p contenteditable=true id=propertyParagraph>{text}</p></blockquote>
<script type="module">
let el={shadowRoot}.getElementById("propertyParagraph");
console.log("howdy from {id} shadowRoot: " + String(Object.getOwnPropertyNames({shadowRoot})));
setTimeout(()=>{
console.log("Latent message from {id}");
},0)
el.addEventListener("input", function() {
console.log("Contents of {id} is ->\\n" + el.innerHTML + "<-\\n");
{changedCallback}
}, false);
¤%&/🧭$`;
arguments = ["text", ["changedCallback","changed-callback"], "id"];
}
window.customElements.define('f-html-gen-string-property', HtmlGen_StringProperty);
class HtmlGen_Text extends HtmlGenMachinery { sourceHtml = `<p>{text}</p>`; arguments = ["text"]; }
window.customElements.define('f-html-gen-text', HtmlGen_Text);
</script>
</head>
<body>
<f-html-gen-string-property
text="Latent functions here work just fine! To confirm this, try clicking on me and typing, then look in the console"
id=f8d3c6b4c21754686 changed-callback="console.log('changed-callback');"></f-html-gen-string-property>
<f-html-gen>
<f-html-gen-box color=gray>
<f-html-gen-text text="Lorum Ipsum"></f-html-gen-text>
</f-html-gen-box>
<f-html-gen-box color=gray>
<f-html-gen-string-property
text="However, when nested inside other webcomponents, latent functions stop working. Hence, if you try clicking on me and start tying, you wont see anything printed to the console!"
id=dzd3b64lc13t56ki4 changed-callback="console.log('changed-callback');">
</f-html-gen-string-property>
</f-html-gen-box>
</f-html-gen>
</body>
</html>
Ray is right, its lightDOM
You can slim all your code to HTML with Declarative shadow DOM (shadowrootmode
attribute)
<input id="A1" placeholder="a1" onkeyup="console.log(this.id,this.value)" />
<input-two>
<input id="A2" placeholder="a2" onkeyup="console.log(this.id,this.value)" />
</input-two>
<hr>
<input id="B1" placeholder="b1" onkeyup="console.log(this.id,this.value)" />
<input-two>
<template shadowrootmode="open">
<input id="B3" placeholder="b3" onkeyup="console.log(this.id,this.value)" />
</template>
<input id="B2" placeholder="b2" onkeyup="console.log(this.id,this.value)" />
</input-two>
input b2 is no longer available in the UI,
because the shadowRoot prevents it from rendering in the UI
Note! B2 is still in the DOM (as lightDOM) So you can do anything you want with it.
Adding a <slot>
can slot/reflect <input B2>
from lightDOM to shadowDOM.
<input id="B1" placeholder="b1" onkeyup="console.log(this.id,this.value)" />
<input-two>
<template shadowrootmode="open">
<slot></slot>
<input id="B3" placeholder="b3" onkeyup="console.log(this.id,this.value)" />
</template>
<input id="B2" placeholder="b2" onkeyup="console.log(this.id,this.value)" />
</input-two>
Side note: Long read on slotted
content: ::slotted CSS selector for nested children in shadowDOM slot
So you get lost in the woods/DOM because your script is working on the invisible b2
... which you reference with a document global scope reference
<script>var el{id}=document.getElementById('{id}');
so this will never work for nested shadowRoots
and its not required because the browser will create (equally named) variables for every ID in global scope anyway.
It has done so since early IE versions,
and because IE had 80+ percent marketshare back then (25+ years ago) every next browser copied its behavior
shadowDOM content is NOT in global scope, so will not create those global variables!
I refactored your code; see link below
Here is the problem with your code:
You create Custom Elements green and red with a shadowDOM
But you use global variables to point to those 2 elements
So when your wrapper takes its innerHTML
to create the third Custom Element inside shadowDOM (gold)
The listener for gold is again attached to red (lightDOM)
and since the wrapper now got shadowDOM, its original content (lightDOM) red is no longer visible in the UI
I refactored your code to the bare essentials,
StackOverflow can't do formatted console,
see: https://jsfiddle.net/WebComponents/z125htfo/
For console output:
Your logic problem is that you have one BaseClass that handles 2 types of Web Components (input and wrapper) Which is fine when you handle above scenario in your code. It all depends on what you want to achieve.
The best way out is to not use global variables and this weird <script>
injection.
Use <template>
to create new DOM, and script (now having the correct scope!) inside your Web Component methods to attach functionality.
And yes, this can be done in dozens of ways,
it all depends on what you want to achieve
fix your code
Instead of creating shadowDOM with this.innerHTML
create a <slot>
to prevent a TWO duplicate and reflect the original TWO from lightDOM to shadowDOM (see JSFiddle)
But that weird script injection will bite you again in the future
<input-element id="ONE"></input-element>
<wrap-element id="WRAP"><input-element id="TWO"></input-element></wrap-element>
<script>
const createElement = (tag, props = {}) => Object.assign(document.createElement(tag), props);
class BaseClass extends HTMLElement {
connectedCallback() {
if (this.nodeName == "INPUT-ELEMENT") {
this.attachShadow({mode: "open" })
.append(
createElement("input", {
placeholder: `${this.localName} ${this.id}`,
onkeyup: (evt) => console.log(evt.target.value)
})
)
} else {
console.log("do something? for:", this.nodeName)
}
}
}
customElements.define("wrap-element", class extends BaseClass {})
customElements.define("input-element", class extends BaseClass {})
</script>