Hi I am new to Web Components concept. I wanted to know if we can access a slotted element using shadowRoot.querySelector
I have implemented an input field as slot for setting some values dynamically. And have a class added to it 'column-title'. I am trying to access this element in connectedCallback() like this
var titleInput = this.shadowRoot.querySelector('.column-title')
But it returns null.
Please help.
Going off of #royyko 's response. The better answer here is instead of giving a class or id with the purpose to grab the slot... use the already provided identifier, its name. To do this you would do the following this.shadowRoot.querySelector('slot[name=interior]')
I'm not sure what the issue is, but you should be able to get a slot using querySelector and its class. I've mocked up an example below.
The console will print out references to the slot. If you're attaching a class just for the purpose of finding the slot then it's probably better to set an id (assuming that it's within a shadow root) and find it with this.shadowRoot.getElementById('my-slot-id')
customElements.define('blue-box',
class extends HTMLElement {
constructor() {
super();
var template = document
.getElementById('blue-box')
.content;
const shadowRoot = this.attachShadow({
mode: 'open'
})
.appendChild(template.cloneNode(true));
}
connectedCallback() {
console.log(this.shadowRoot.querySelector('slot.target-slot'));
}
});
<template id="blue-box">
<style>
.blue-border {
border: 2px solid blue;
padding: 5px;
margin: 5px;
}
</style>
<div class='blue-border'>
<slot name='interior' class='target-slot'>NEED INTERIOR</slot>
</div>
</template>
<blue-box>
<span slot='interior'>My interior text</span>
</blue-box>
<blue-box>
<span slot='interior'>
Check Here: <input type='checkbox'>
</span>
</blue-box>
Related
I have a web component with a shadow DOM and a default slot.
I need to apply certain styling based on the presence or absence of specific a light DOM descendant. Please note that I don't need a specific workaround for this specific styling, it's just an example and in the real world the example is alot more complex.
I also cannot work with regular DOM CSS like x-y:has(div) since I need to apply styles to an element in the shadow DOM based on the presence of the div in the light DOM.
Please note that the code snippet only works in browsers that support constructable stylesheets (e.g. Safari won't).
const styleStr = `
:host {
display: block;
border: 3px dotted red;
}
:host(:has(div)) {
border-color: green;
}
`;
let css;
try {
css = new CSSStyleSheet;
css.replaceSync(styleStr);
} catch(e) { console.error(e) }
customElements.define('x-y', class extends HTMLElement {
constructor() {
super().attachShadow({mode: 'open'}).adoptedStyleSheets.push(css);
this.shadowRoot.append(document.createElement('slot'))
}
})
<x-y>no div - should have red border</x-y>
<x-y>
<div>div, should have green border</div>
</x-y>
I was trying to find if maybe :host() is not accepting :has(), but was unable to find anything on it, neither in the spec, nor on MDN or caniuse.
Does anyone have definitive knowledge/reference about this, and can point me to some documentation?
You want to style slotted content based on an element inside the slot
Since <slot> are reflected, (deep dive: ::slotted CSS selector for nested children in shadowDOM slot)
you need to style a <slot> in its container element.
If you want that logic to be done from inside the Component,
you could do it from the slotchange Event, which checks if a slotted element contains that DIV
Then creates a <style> element in the container element
Disclaimer: Provided code is a Proof of Concept, not production ready
<my-component>
Hello Web Component
</my-component>
<!-- <my-component> will add a STYLE element here -->
<my-component>
<!-- <my-component> will assign a unique ID to the DIV -->
<div>Web Component with a DIV in the slot</div>
</my-component>
<script>
customElements.define("my-component", class extends HTMLElement {
constructor() {
super().attachShadow({mode: "open"}).innerHTML = `<slot/>`;
let slot = this.shadowRoot.querySelector("slot");
slot.addEventListener("slotchange", (evt) => {
[...slot.assignedNodes()].forEach(el => {
if (el.nodeName == "DIV") {
el.id = "unique" + new Date() / 1;
// inject a <style> before! <my-component>
this.before( Object.assign( document.createElement("STYLE"), {
innerHTML : `#${el.id} { background:lightgreen } `
}));
}
});
});
}
})
</script>
PS. Don't dynamically add any content inside <my-component>, because that slotchange will fire again...
While messing around with custom elements I wondered if one could use custom attributes within the elements (and possibly within their children too). I know VueJS does something similar with attributes like v-bind, v-for, etc; and I know there's probably a lot more going on under the hood there than I realize. I've tried registering custom elements and attempting to retrieve them like so:
<new-element cool="Awesome!"> </new-element>
class NewElement extends HTMLElement {
constructor() {
super();
this.coolAttr = this.getAttribute("cool");
}
}
customElements.define("new-element", NewElement);
However, when loading the page (in Google Chrome for me) the "custom" attributes disappear, and any attempt at getting them retrieves null. Is there a way to "register" these custom attributes, or do I have to stick with data- attributes?
Attributes become available in the connectedCallback,
they are not available yet in the constructor
Unless the Custom Element is PARSED (in the DOM) BEFORE the Element is defined!!
Also be aware the attributeChangedCallback runs before the connectedCallback
for Observed attributes
Also see: https://andyogo.github.io/custom-element-reactions-diagram/
.as-console-row-code {
font: 12px Arial!important;
background:yellow;
color:darkred;
}
.as-console-row:after{ display:none!important }
<before-element cool="Awesome?">FOO</before-element>
<script>
class NewElement extends HTMLElement {
log( ...args ){
console.log(this.nodeName, `cool:${this.getAttribute("cool")}`,"\t\t\t",...args );
}
static get observedAttributes() {
return ["cool"];
}
constructor() {
const name = "constructor"; // CAN! run code BEFORE super()!
// super() sets AND returns the 'this' scope
super().log(name);
}
connectedCallback() {
this.log("connectedCallback", this.innerHTML || "innerHTML not parsed yet");
// be aware this.innerHTML is only available for PARSED elements
// use setTimeout(()=>{...},0) if you do need this.innerHTML
}
attributeChangedCallback(name, oldValue, newValue) {
this.log(`attributeChangedCallback name:${name}, old:${oldValue}, new:${newValue}`);
}
}
customElements.define("before-element", class extends NewElement {});
customElements.define("after-element", class extends NewElement {});
</script>
<after-element cool="Awesome!!">BAR</after-element>
It can be easily solved by adding the name of the attributes after "data-".
<New-element data-*="Anything you want" />
for example, you can have:
<element data-cool="value">
You can have as many custom attributes as you want.
Let's say I have a dialog component like
class ModalDialog extends HTMLElement {
constructor(){
super()
this._shadow = this.attachShadow({mode: 'closed'})
}
connectedCallback(){
const template = `
<style>
... lots of style that doesn't matter to this question ...
</style>
<div class="dialog">
<div class="dialog-content">
<div class="dialog-header">
<slot name="header"></slot>
<span class="close">×</span>
</div>
<div class="dialog-body"><slot name="body"></slot></div>
<div class="dialog-footer"><slot name="footer"></slot></div>
</div>
</div>
`
this._shadow.innerHTML = template
this._shadow.querySelector('.close').onclick = () => this.hide()
const dialog = this._shadow.querySelector('.dialog')
dialog.onclick = evt => {
if(evt.target == dialog){ //only if clicking on the overlay
this.hide()
}
}
this.hide()
}
show() {
this.style.display = 'block'
}
hide(){
this.style.display = 'none'
}
}
window.customElements.define('modal-dialog', ModalDialog)
Now let's say I want to create dedicated dialogs ... e.g. one that allows a user to pick an image.
I could do it like this
import {} from './modal-dialog.js'
class ImageSelector extends HTMLElement {
constructor(){
super()
this._shadow = this.attachShadow({mode: 'closed'})
}
connectedCallback(){
const template = `
<style>
... more style that doesn't matter ...
</style>
<modal-dialog>
<div slot="header"><h3>Select Image</h3></div>
<div slot="body">
... pretend there's some fancy image selection stuff here ...
</div>
</modal-dialog>
`
this._shadow.innerHTML = template
}
show(){
this._shadow.querySelector('modal-dialog').show()
}
hide(){
this._shadow.querySelector('modal-dialog').hide()
}
}
window.customElements.define('image-selector', ImageSelector)
but I don't like the show and hide methods, there.
Another option would be to inherit from the dialog rather than from HTMLElement...
import {} from './modal-dialog.js'
class ImageSelector extends customElements.get('modal-dialog'){
constructor(){
super()
}
connectedCallback(){
... now what? ...
}
}
window.customElements.define('image-selector', ImageSelector)
but if I do that, how do I actually fill the slots?
Naive approach would of course be to just use _shadow and put it into the slots' inner html, but I have a feeling that's not the way to go.
TLDR; It is impossible to use both inheritance and composition at same time.
Long answer:
You are actually mixing two distinct but alternative concepts:
Inheritance - Use when overriding/overloading the existing behavior
Composition - Use when using the behavior of some other entity and add some more behavior around it.
You can substitute one for another and in general, in Web UI programming, Composition is always preferred over the Inheritance for the loose coupling it provides.
In you case, you actually want to use the template and not actually override it. So, composition is a better choice here. But that also means that you will actually have to write some more boilerplate code i.e. wrapper implementations of show and hide method.
In theory, inheritance was invented to promote code re-use and avoid repetitive code but that comes as a cost as compared to composition.
Good info from Harshals' answer; here is the userland code
Unless you are doing SSR, the very first file read is the HTML file.
So put the Template content there and let one generic Modal Web Component read the templates
<template id="MODAL-DIALOG">
<style>
:host { display: block; background: lightgreen; padding:.5em }
[choice]{ cursor: pointer }
</style>
<slot></slot>
<button choice="yes">Yes</button>
<button choice="no" >No</button>
</template>
<template id="MODAL-DIALOG-IMAGES">
<style>
:host { background: lightblue; } /* overrule base template CSS */
img { height: 60px; }
button { display: none; } /* hide stuff from base template */
</style>
<h3>Select the Framework that is not Web Components friendly</h3>
<img choice="React" src="https://image.pngaaa.com/896/2507896-middle.png">
<img choice="Vue" src="https://upload.wikimedia.org/wikipedia/commons/thumb/9/95/Vue.js_Logo_2.svg/1184px-Vue.js_Logo_2.svg.png">
<img choice="Svelte" src="https://upload.wikimedia.org/wikipedia/commons/thumb/1/1b/Svelte_Logo.svg/1200px-Svelte_Logo.svg.png">
<slot><!-- remove slot from BaseTemplate, then this slot works --></slot>
</template>
<modal-dialog>Standard Modal</modal-dialog>
<modal-dialog template="-IMAGES">Images Modal</modal-dialog>
<script>
document.addEventListener("modal-dialog", (evt) => {
alert(`You selected: ${evt.detail.getAttribute("choice")}`)
});
customElements.define('modal-dialog', class extends HTMLElement {
constructor() {
let template = (id="") => {// if docs say "use super() first" then docs are wrong
let templ = document.getElementById(this.nodeName + id);
if (templ) return templ.content.cloneNode(true);
else return []; // empty content for .append
}
super().attachShadow({mode:"open"})
.append( template(),
template( this.getAttribute("template") ));
this.onclick = (evt) => {
let choice = evt.composedPath()[0].hasAttribute("choice");
if (choice)
this.dispatchEvent(
new CustomEvent("modal-dialog", {
bubbles: true,
composed: true,
detail: evt.composedPath()[0]
})
);
// else this.remove();
}
}
connectedCallback() {}
});
</script>
Notes
Ofcourse you can wrap the <TEMPLATES> in a JS String
You can't add <script> in a Template
well, you can.. but it runs in Global Scope, not Component Scope
For more complex Dialogs and <SLOT> you will probably have to remove the unwanted slots from a Basetemplate with code
Don't make it too complex:
Good Components do a handful of things very good.
Bad Components try to do everything... create another Component
I've been doing the research on Slack and elsewhere, but I am not able to find an answer to my question. I feel I lack some basic knowledge of OOP, which probably will take me hours of researching and coding before I get to the answer. But somehow I am perplexed that it might be such a complex issue.
The question is:
I have two buttons with the same class on a page (this is just for example). I create JS to handle the behaviour via Class function. In constructor I define an element I want the Class to point to, namely child. However, I want the Class to point to one of two child separately when clicking on them. However, this.child in constructor always points to two child elements.
Can you please help and tell what I am doing wrong?
const selectors = {
childElement: '.child'
},
$ = jQuery;
class Child {
constructor() {
this.child = $(selectors.childElement);
this.bindUiEvents();
}
bindUiEvents() {
$(this.child).on('click', this.addStyles);
}
addStyles() {
$(this).addClass('coloured');
}
}
new Child();
.child {
height: 100px;
width: 100px;
border: 1px solid black;
}
.child.coloured {
background: blue;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div class="parent">
<button class="child">First</button>
<button class="child">Second</button>
</div>
If you need to create an instance of class Child for each button, you have to get the elements outside the class.
First we modify the Child class to accept a button during instantiation.
class Child {
constructor(child) {
this.child = child; //you would want to pass a single element here
this.bindUiEvents();
}
bindUiEvents() {
$(this.child).on('click', this.addStyles);
}
addStyles() {
$(this.child).addClass('.coloured');
}
}
Then we iterate each button outside
const buttons = $('.child');
const arry = []; //we'll put each Child instance here
//iterate on each button
buttons.each((idx, b) => {
const clss = new Child(b); //pass each button element to their own Child class
arry.push(clss); //add in arry for later access;
})
As #Liam said in the comments, you can do all of these without all this class-based jibber jabber. But I'm just gonna go assume you have other reasons for this extra complexities
class UioKey extends HTMLElement {
...
eKey(){windows.alert('class eKey function')}
}
function eKey(){
eKey(){windows.alert('document eKey function')}
<template id="uio-key-temp">
<div class="uio-key">
<div class="i" onclick="eKey()"></div><span></span>
</div>
</template>
when clikcing on the .i div agot the document ekey that is firing, i want
the class ekey() to be fired
if i omit the dodument eKey() fuction i got function eKey() undefined
onclick will only work with globally defined functions.
Here is a very quick hack that allows you to use a class function.
// Class for `<uio-key>`
class UioKey extends HTMLElement {
constructor() {
super();
let shadow = this.attachShadow({mode: 'open'});
shadow.innerHTML = '<div><div on-click="eKey">div</div><span>span</span></div>';
let a = shadow.querySelectorAll('[on-click]');
a.forEach(
el => {
const handlerName = el.getAttribute('on-click');
el.addEventListener('click', this[handlerName]);
}
);
}
eKey() {
window.alert('class eKey function');
}
}
// Define our web component
customElements.define('uio-key', UioKey);
<hr/>
<uio-key></uio-key>
<hr/>
I use a custom attribute on-click as a way to grab all elements that want a click handler then I take the value of that attribute and use it as the class function name and pass it into the addEventListener function.
Alternatly to #Intervalia's answer, you could use the getRootNode() method and then the host property to access the Custom Element object from inside the Shadow DOM.
class UioKey extends HTMLElement {
constructor() {
super()
this.attachShadow( { mode: 'open' } )
.innerHTML = uio-key-temp.innerHTML
}
eKey(){
window.alert('class eKey function' )
}
}
customElements.define( 'uio-key', UioKey )
<template id="uioKeyTemp">
<style> .i { background: gray ; height: 10pt } </style>
<div class="uio-key">
<div class="i" onclick="this.getRootNode().host.eKey()"></div>
</div>
</template>
<uio-key></uio-key>
Note that it's always a good practice to use inline scripts because in some situations they can be disabled (for security reasons).