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 themargin-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
setTimeout
is required because all child have not been parsed yet when theconnectedCallback
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));
});
}
});
<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.
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.
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
How to Access Accelerometer/Gyroscope Data from JavaScript
Filtering an Array with a Function That Returns a Promise
Difference Between the JavaScript String Type and String Object
How to Import a CSS File in a React Component
JavaScript Es6 Cross-Browser Detection
How to Access Xhr Responsebody (For Binary Data) from JavaScript in Ie
Differencebetween Document and Document in JavaScript
Access to Es6 Array Element Index Inside For-Of Loop
Put JavaScript in One .Js File or Break It Out into Multiple .Js Files
Adding Post Parameters Before Submit
How to Convert CSV to JSON in Node.Js
Jquery Clone Form Fields and Increment Id