Nested Element (Web Component) Can't Get Its Template

Nested element (web component) can't get its template

document.currentScript contains a reference to the script that is currently parsed and executed. Therefore it is not valid anymore for your purpose when the constructor() function is called (from another script).

Instead you shoud save its value in a variable at the beginning of the script, and use this variable in the constructor:

<script>
var currentScript = document.currentScript
customElements.define( ... )
...
</script>

If you have multiple scripts, you should use distinct names.

Alternately, you can encapsulate the ephemeral value in a closure:

(function(owner) {
customElements.define('app-container', class extends HTMLElement {
constructor() {
super();
let shadowRoot = this.attachShadow({ mode: 'open' });
const content = owner.querySelector('#app-container').content;
shadowRoot.appendChild(content.cloneNode(true));
}
});
})(document.currentScript.ownerDocument);

Here the value document.currentScript.ownerDocument is assigned to the owner argument which is still defined correctly when constructor() is called.

owner is locally defined so you can use the same name in the other document.

Lit: nested template doesn't render

Anytime we implement lifecycle callback methods, it's necessary to call the parent class's methods (so the superclass features work as-expected).

If you need to customize any of the standard custom element lifecycle methods, make sure to call the super implementation (such as super.connectedCallback()) so the standard Lit functionality is maintained.

Standard custom element lifecycle

  connectedCallback(): void {
super.connectedCallback()
console.log("UL-MAIL CHILD: connected now"); // triggers.
}

Nested Webcomponents issue

It was a duplicate.

Github project updated if anyone is interested.

Contents of nested slots in web component are not visible

You can't have nested <slot> elements kinda like you can't have nested <li> elements.

So like with <li> where you add an extra <ul> container,

you have to add an extra Web Component which passes on slot content:

update: added exportparts and part to show how global CSS styles nested shadowDOMs

<script>
class MyCoolBaseClass extends HTMLElement {
constructor(html) {
let style = "<style>:host{display:inline-block;background:pink}</style>";
super().attachShadow({mode:'open'}).innerHTML = style + html;
}
}
customElements.define('el-one', class extends MyCoolBaseClass {
constructor() {
super(`<el-two exportparts="title"><slot name="ONE" slot="TWO" ></slot></el-two>`);
}
});
</script>

<style>
*::part(title){
color:green;
}
</style>
<el-one>
<b slot="ONE">Fantastic!</b>
</el-one>

<script>
customElements.define('el-two', class extends MyCoolBaseClass {
constructor() {
super(`Hello <slot name="TWO" ></slot> <b part="title">Web Components</b>`);
}
});
</script>

web component (vanilla, no polymer): how to load template content?

It's because when you do:

document.querySelector( '#my-element' );

...document refers to the main document index.html

If you want to get the template, you should use instead document.currentScript.ownerDocument

var importedDoc = document.currentScript.ownerDocument;
customElements.define('my-element', class extends HTMLElement {
constructor() {
super();
let shadowRoot = this.attachShadow({mode: 'open'});
const t = importedDoc.querySelector('#my-element');
const instance = t.content.cloneNode(true);
shadowRoot.appendChild(instance);
}
});

Note that document.currentScript is a global variable, so it refers to your imported document only when it is currently parsed. That's why it's value is saved in a variable (here: importedDoc) to be reusable later (in the constrcutor call)

If you have multiple imported document you may want to isolate it in a closure (as explained in this post):

( function ( importedDoc )
{
//register element
} )(document.currentScript.ownerDocument);

Style nested web component where child can be same type as, but needs different styles to, parent

Took some time to fully understand what you want (and I could be wrong)

  • You want to specify the margin-top for all CHILDREN (except the first child)

    with: <vertical-layout childmargin="2em">
  • For nested <vertical-layout> the element should have the margin-top of its PARENT container

Problem with your: <vertical-layout style="--spacing-size: 2em">, is that the 2em is set on the <vertical-layout> itself (and all its children)

You want it applied to children only

You can't do that with CSS in shadowDOM; because that doesn't style slotted content.

See: ::slotted CSS selector for nested children in shadowDOM slot


I have changed your HTML and attributes to reflect the margins you want:

(px notation for better comprehension)

    0px <vertical-layout id="Level1" childmargin="15px">
15px <div>child1-1</div>
15px <div>child1-2</div>
15px <div>child1-3</div>
15px <vertical-layout id="Level2" childmargin="10px">
0px <div>child2-1</div>
10px <div>child2-2</div>
10px <vertical-layout id="Level3" childmargin="5px">
5px <div>child3-1</div>
5px <div>child3-2</div>
5px <div>child3-3</div>
</vertical-layout>
10px <div>child2-3</div>
</vertical-layout>
15px <div>child1-4</div>
15px <div>child1-5</div>
</vertical-layout>

CSS can not read that childmargin value; so JS is required to apply that value to childelements

As you also don't want to style the first-child...

The code for the connectedCallback is:

    connectedCallback() {
let margin = this.getAttribute("childmargin");
setTimeout(() => {
let children = [...this.querySelectorAll("*:not(:first-child)")];
children.forEach(child=>child.style.setProperty("--childmargin", margin));
});
}

Notes

  • * is a bit brutal.. you might want to use a more specific selector if you have loads of child elements; maybe:
[...this.children].forEach((child,idx)=>{
if(idx) ....
};
  • You are looping all children; could also set the style direct here.. no need for CSS then

  • The setTimeoutis required because all child have not been parsed yet when the connectedCallback fires.

Because all your <vertical-layout> are in GLOBAL DOM (and get refelected to <slot> elements)

You style everything in GLOBAL CSS:

  vertical-layout > *:not(:first-child)  {
margin-top: var(--childmargin);
}

Then all Web Component code required is:

customElements.define("vertical-layout", class extends HTMLElement {
constructor() {
super()
.attachShadow({mode:"open"})
.innerHTML = "<style>:host{display:flex;flex-direction:column}</style><slot></slot>";
}
connectedCallback() {
let margin = this.getAttribute("childmargin");
setTimeout(() => {
let children = [...this.querySelectorAll("*:not(:first-child)")];
children.forEach(child=>child.style.setProperty("--childmargin", margin));
});
}
});

Sample Image

<vertical-layout id="Level1" childmargin="15px">
<div>child1-1</div>
<div>child1-2</div>
<div>child1-3</div>
<vertical-layout id="Level2" childmargin="10px">
<div>child2-1</div>
<div>child2-2</div>
<vertical-layout id="Level3" childmargin="5px">
<div>child3-1</div>
<div>child3-2</div>
<div>child3-3</div>
</vertical-layout>
<div>child2-3</div>
</vertical-layout>
<div>child1-4</div>
<div>child1-5</div>
</vertical-layout>

<style>
body {
font: 12px arial;
}
vertical-layout > *:not(:first-child) {
font-weight: bold;
margin-top: var(--childmargin);
}
vertical-layout::before {
content: "<vertical-layout " attr(id) " childmargin=" attr(childmargin);
}
vertical-layout > vertical-layout {
background: lightblue;
border-top: 4px dashed red;
}
vertical-layout > vertical-layout > vertical-layout {
background: lightcoral;
}
</style>

<script>
customElements.define("vertical-layout", class extends HTMLElement {
constructor() {
super()
.attachShadow({
mode: "open"
})
.innerHTML =
`<style>
:host {
display: flex;
flex-direction: column;
background:lightgreen;
padding-left:20px;
border:2px solid red;
}
::slotted(*){margin-left:20px}
:host([childmargin]) ::slotted(:not(:first-child)) {
color:blue;
}
</style>
<slot>
<slot></slot>
</slot>`;
}
connectedCallback() {
let margin = this.getAttribute("childmargin");
setTimeout(() => {
let children = [...this.querySelectorAll("*:not(:first-child)")];
children.map(child=>{
child.style.setProperty("--childmargin", margin);
child.append(` margin-top: ${margin}`);
})
});
}
});

</script>

createElement on Custom Element breaks template

Solved thanks to the awesome @Supersharp and their Stack Overflow post

Basically, in order to preserve the correct document.currentScript.ownerDocument I need to declare it in a var before the class then use that var in the class.

Old:

class PuzzlePiece extends HTMLElement{
constructor(){
super();
const t = document.currentScript.ownerDocument.getElementById('piece-puzzle');
const shadowRoot = this.attachShadow({mode: 'open'});
shadowRoot.appendChild(t.content.cloneNode(true));}

New:

var importedDoc = document.currentScript.ownerDocument;
class PuzzlePiece extends HTMLElement{
constructor(){
super();
const t = importedDoc.getElementById('piece-puzzle');
const shadowRoot = this.attachShadow({mode: 'open'});
shadowRoot.appendChild(t.content.cloneNode(true));}

How to use Custom Elements with template?

While inside the template, don't use the document global:

<template id="template">
<div>Welcome to my app!</div>
</template>

<script>
class MyApp extends HTMLElement {
constructor() {
super();
const shadow = this.attachShadow({mode: "open"});

// while inside the imported HTML, `currentDocument` should be used instead of `document`
const currentDocument = document.currentScript.ownerDocument;
// notice the usage of `currentDocument`
const template = currentDocument.querySelector('#template');

const clone = document.importNode(template.content, true);
shadow.appendChild(clone);
}
}
customElements.define("my-app", MyApp);
</script>

Plunker demo: https://plnkr.co/edit/USvbddEDWCSotYrHic7n?p=preview


PS: Notes com compatibility here, though I assume you know HTML imports are to be deprecated very soon.

How does a child element access custom web component class functions and vars?

There's a couple of very simple rules for decoupling WebComponents:

  • Parent to child: one-way data-binding or HTML5 attributes for passing data and calling child.func() for performing actions.

  • Child to parent: Child fires an event. The parent(s) listen for that event. Because of event bubbling you can have an arbitrary number of parents picking up that event with no extra effort.

This pattern is used by standard web components as well (input, textbox etc). It also allows easy testing since you don't have to mock the parent.

This was the recommended pattern described by the Polymer library, which was/is a wrapper over WebComponents. Unfortunately I can't find that specific article that described this pattern.

polymer element communication diagram

In the following example, the parent listens for events from children and calls functions on itself to affect its state, or functions on the children to make them do something. None of the children know about the parent.

Diagram showing parent listening for child events and performing actions on itself or calling

Do web components allow for nesting of HTML forms?

This seems like a very valid question to me.

Out of curiosity, I made a quick fiddle (provided below) which tests use case of nested forms, where one is inside shadow root.

var template = document.querySelector('template');var clone = document.importNode(template.content, true);
var root = document.querySelector('#host').createShadowRoot();root.appendChild(clone);
document.querySelector('button').onclick = function(e) { var formValues = $('#docForm').serialize(); alert(formValues); $('#result').text(formValues); return false;}
document.querySelector('#host').shadowRoot.querySelector('button').onclick = function(e) { var form = document.querySelector('#host').shadowRoot.querySelector('#shadowForm'); alert($(form).serialize()); $('#result').text($(form).serialize()); return false;}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script><template id="template">    <form id="shadowForm">        <input type="text" name="text"/>        <button type="submit">Submit shadow form</button>    </form></template>

<form id="docForm" > <table> <tr> <td> <input type="checkbox" name="checkbox"/> </td> </tr> <tr> <td> <input type="text" val="" name="text"/> </td> </tr> <tr> <td> <select name="multiple" multiple="multiple"> <option>A</option> <option>B</option> <option>C</option> </select> </td> </tr> <tr> <td> <div id="host"></div> <button type="submit"> Submit document Form</button> </td> </tr> </table></form>

<div id="result"></div>


Related Topics



Leave a reply



Submit