Lit-Element: which event to use for DOM updates? - javascript

The documentation over at github.com/Polymer/lit-element describes the lifecycl, if a property of some lit-element is changed. However, I can not seem to find any documentation about a lifecycle if the DOM content of the element is changed.
So assume I have some nested DOM structure and my outermost element should display something based on the DOM content. For sake of simplicity the example below will just display the number of child-elements of the given type.
Now at some point my application inserts a new nested element (click the test button below). At this point I would like to update the shown count.
From my tests it seems that render() is not called again in that case, neither is updated().
Which event do I need to listen or which function do I need to implement for to recognize such a change?
My only current workaround is to use requestUpdate() manually after the DOM update, but I think such changes should be handled by lit-element itself.
document.querySelector( 'button' )
.addEventListener( 'click', () => {
const el = document.querySelector( 'my-element' );
el.insertAdjacentHTML( 'beforeend', '<my-nested-element>new addition</my-nested-element>' );
})
my-element, my-nested-element {
display: block;
}
<script src="https://unpkg.com/#webcomponents/webcomponentsjs#latest/webcomponents-loader.js"></script>
<!-- Works only on browsers that support Javascript modules like Chrome, Safari, Firefox 60, Edge 17 -->
<script type="module">
import {LitElement, html} from 'https://unpkg.com/#polymer/lit-element/lit-element.js?module';
class MyElement extends LitElement {
constructor(){
super();
this.number = this.querySelectorAll( 'my-nested-element' ).length;
}
render() {
return html`<p>number of my-nested-element: ${this.number}</p>
<slot></slot>`;
}
}
customElements.define('my-element', MyElement);
class MyNestedElement extends LitElement {
render() {
return html`<slot></slot>`;
}
}
customElements.define('my-nested-element', MyNestedElement);
</script>
<my-element>
<my-nested-element>first</my-nested-element>
<my-nested-element>second</my-nested-element>
</my-element>
<button>test</button>

In order to detect a new element inserted from the Light DOM through a <slot> element, you can listen to slotchange events on the <slot> element, or on the Shadow DOM root itself.
See the running example below:
document.querySelector('button').onclick = () =>
document.querySelector('my-element').insertAdjacentHTML('beforeend', '<my-nested-element>new addition</my-nested-element>');
my-element,
my-nested-element {
display: block;
}
<script type="module">
import {LitElement, html} from 'https://unpkg.com/#polymer/lit-element/lit-element.js?module';
class MyElement extends LitElement {
firstUpdated() {
var shadow = this.shadowRoot
var nb = shadow.querySelector( 'span#nb' )
shadow.addEventListener( 'slotchange', () =>
nb.textContent = this.querySelectorAll( 'my-nested-element').length
)
}
render() {
return html`<p>number of my-nested-element: <span id="nb"></span></p>
<slot></slot>`;
}
}
customElements.define('my-element', MyElement);
</script>
<my-element>
<my-nested-element>first</my-nested-element>
<my-nested-element>second</my-nested-element>
</my-element>
<button>test</button>

Related

Using slots in WebComponents without using shadow DOM

I'm trying to build a WebComponent without using ShadowDOM - so far it mostly just worked, but now I want to build a component that wraps other components like you would do with Angular's #ViewChild / #ViewChildren. (the library I'm using here to render is uhtml similar to lit-html)
export class Dropdown extends HTMLElement {
private open: boolean = false;
static observedAttributes = ["open"]
constructor() {
super();
}
attributeChangedCallback(name: string, oldValue: string, newValue: string) {
switch (name) {
case "open":
this.open = Boolean(newValue);
break;
}
this.display()
}
connectedCallback() {
this.display()
}
display = () => {
render(this, html`
<div>
<slot name="item">
</slot>
</div>
`)
}
static register = () => customElements.define("my-dropdown", Dropdown)
}
If I now use this component
Dropdown.register();
render(document.body, html`
<my-dropdown open="true">
<strong slot="item">test</strong>
</my-dropdown>
`)
I'm expecting to see
<my-dropdown>
<div>
<strong>test</test>
</div>
</my-dropdown>
but the slot part is not working.
If I switch to ShadowDOM, it just works, but now I have to deal with ShadowDOM's sandbox with regards to styling which I'd rather not do.
constructor() {
super();
this.attachShadow({mode: "open"})
}
display = () => {
render(this.shadowRoot as Node, html`
Is it possible to make slots work without shadowDOM?
If not, is there different way to grab the content defined inside the component and use it inside display?
<my-component>
<div>some content here</div>
</my-component>
should render as
<my-component>
<header>
random header
</header>
<section>
<!-- my custom content -->
<div>some content here</div>
</section>
</my-component>
Any suggestions?
No, <slot> are part of the shadowDOM API
You can fake it, but since there is no shadowDOM you would have to store that content someplace else.
Could be a <template> you read and parse your (light)DOM content into.
That is a sh*load of DOM mutations.
Might be easier to just learn to style shadowDOM with:
CSS properties
inheritable styles
::part
constructable stylesheets

Changing shadowRoot.innerHTML stop inherited behavior for attributes and events

Below ake-class2 inherits from/extends ake-class1.
Adding <select> element to ake-class2.shadowRoot.
console.log this.clickme button to make sure it's inherited correctly.
clickme button doesn't work without adding again lines after comment These 3 lines in ake-class2.
I couldn't understand why this behavior happen.
why this happpens ?
<html>
<head>
<title>AKE Front</title>
<script>
class1_html = `
<div class="container">
<button class="clickme">Click Me</button>
</div>
`
class2_html = `
<select></select>
`
/*--------------------------------------------------------------------------------*/
class AKEclass1 extends HTMLElement { //custom-component class
constructor() {
super(); // always call super() first in the constructor.
//const root = this.createShadowRoot(); //chrome only - deprecated
const root = this.attachShadow({mode: 'open'}); //By calling attachShadow with mode: 'open', we are telling our element to save a reference to the shadow root on the element.shadowRoot property
this.shadowRoot.innerHTML = class1_html;
// These 3 lines
this.container = this.shadowRoot.querySelector("div.container");
this.clickme = this.container.querySelector("button.clickme");
this.clickme.addEventListener("click", this.clickMe.bind(this));
}
clickMe() {
alert("Hello !");
}
}
customElements.define('ake-class1', AKEclass1);
/*--------------------------------------------------------------------------------*/
class AKEclass2 extends AKEclass1 { //custom-component class
constructor() {
super(); // always call super() first in the constructor.
this.shadowRoot.innerHTML += class2_html;
// These 3 lines
//this.container = this.shadowRoot.querySelector("div.container");
//this.clickme = this.container.querySelector("button.clickme");
//this.clickme.addEventListener("click", this.clickMe.bind(this));
}
}
customElements.define('ake-class2', AKEclass2);
/*--------------------------------------------------------------------------------*/
</script>
</head>
<body>
<ake-class2 class="ake_window"></ake-class2>
</body>
</html>
As mentioned in the comments .innerHTML += is the culprit.
What it does:
Create a NEW string by concatening .innerHTML + NEWString
delete the innerHTML DOM tree
and then Garbage Collection (GC) kicks in:
Delete all existing DOM elements, thus remove all connected listeners
set the NEW String as innerHTML
Some 'gurus' say this makes innerHTML evil, I say you need to understand what it does.
In the SO snippet below you see the listener being connected twice, but only executed once when clicked
<script>
class BaseClass extends HTMLElement {
constructor() {
super().attachShadow({mode:'open'})
.innerHTML = `<button>Click ${this.nodeName}</button>`;
this.listen();// but removed by GC
}
listen(){
console.log("add listener on", this.nodeName);
this.shadowRoot
.querySelector("button")
.onclick = (evt) => this.clicked(evt);
}
clicked(evt){
console.log("clicked", this.nodeName)
}
}
//customElements.define('element-1', BaseClass);
customElements.define('element-2', class extends BaseClass {
connectedCallback(){
this.shadowRoot.innerHTML += ` with concatenated HTML`;
this.listen();
}
});
</script>
<element-2></element-2>
Notes:
Using the inline onclick handler, it only allows for one handler where addEventListener can add more (you can use it here if you like)
No need for oldskool .bind(this) by defining lexical scope with a arrow function, not a function reference
all can be chained because
super() sets AND returns the this scope
attachShadow sets AND returns this.shadowRoot

Slots does not work on a html web component without shadow dom

I have a html web component without shadow dom and I try to add a slot. For some reason it does not work.
I expected it to switch "Foo bar" to "Hello world" but that does not happen.
Does slots only works with shadow dom and a template?
How can I get it to work?
class HelloWorld extends HTMLElement {
constructor() {
super();
}
connectedCallback() {
this.innerHTML = `
<div>
<slot name="element-name">Foo Bar</slot>
</div>
`;
}
}
customElements.define("hello-world", HelloWorld);
<hello-world>
<span slot="element-name">Hello World</span>
</hello-world>
Yes, <slot> only works in shadowDOM
Slotted content is reflected lightDOM content
See: ::slotted CSS selector for nested children in shadowDOM slot
A Web Component without shadowDOM only has innerHTML
If you do this.innerHTML= on such a Web Component it replaces the innerHTML, just like on any other HTML tag
with shadowDOM:
<hello-world>
<b slot="none">Mighty</b>
<span slot="title">Web Components</span>
Hello!
</hello-world>
<script>
customElements.define("hello-world", class extends HTMLElement {
constructor() {
super()
.attachShadow({mode:"open"})
.innerHTML = `<div><slot></slot><slot name="title">Foo Bar</slot></div>`;
this.onclick = (evt) => this.querySelector('b').slot = "title";
}
});
</script>

HTML web component does not use shadow DOM style

I have created a vanilla web component or HTML element. It just displays two links.
To encapsulate the thing, I use shadow DOM. However it does not seem to be encapsulated. In the DOM tree it's inside #shadow-root which is good.
Why does the web component use the global style instead of the style I provided in the template for my web component?
The text is red and I expected it to be green.
class MyEl extends HTMLElement {
constructor() {
super();
this.shadow = this.attachShadow({ mode: "open" });
}
connectedCallback() {
const template = `
<style>
a {
color: green;
}
</style>
<slot></slot>`;
this.shadow.innerHTML = template;
}
}
window.customElements.define("my-el", MyEl);
a {
color: red
}
<my-el>
Item1
Item2
</my-el>
While this question already has an accepted answer, moving a slot's children to the shadowRoot isn't desirable for most use cases.
What you probably want to do is to use the ::slotted() selector.
Just bear in mind that styles applied to a slot's children through the ::slotted() selector only act as "default" styles and can still be overridden by using styles in light DOM.
For example, check this edited version of your snippet:
As you can see, this time my-el tries to apply both a color and a text-decoration style to anchor (<a>) children in any of it's slots.
However, in light dom, we have a a.special selector that overrides the color, so the <a class="special"> will be red, not green
class MyEl extends HTMLElement {
constructor() {
super();
this.shadow = this.attachShadow({ mode: "open" });
}
connectedCallback() {
const template = `
<style>
::slotted(a) {
color: green;
text-decoration: none;
}
</style>
<slot></slot>`;
this.shadow.innerHTML = template;
}
}
window.customElements.define("my-el", MyEl);
a.special {
color: red
}
<my-el>
Item1
<a class="special" href="example.com">Item2</a>
</my-el>
The full, detailed explanation is in: ::slotted CSS selector for nested children in shadowDOM slot
TL;DR
Your links are in lightDOM and thus styled by its DOM (in your code the document DOM)
Moving the nodes from lightDOM to shadowDOM is one "solution"; but you are not using slots then.
FYI, your code can be compacted to:
class MyEl extends HTMLElement {
constructor() {
super().attachShadow({ mode: "open" })
.innerHTML = `<style>a{color:green}</style><slot></slot>`;
}
}
window.customElements.define("my-el", MyEl);
More SLOT related answers can be found with StackOverflow Search: Custom Elements SLOTs
observe this line, you have to move/copy elements to shadow for example with:
this.shadow.innerHTML = this.innerHTML + template;
I've added this to demonstrate that only inline style will be applied to shadow dom elements .. so copied links in SD are using your style :)
so red will be GLOBAL, green will be SHADOW elements
class MyEl extends HTMLElement {
constructor() {
super();
}
connectedCallback() {
this.shadow = this.attachShadow({ mode: "open" });
const template = `
<style>
a {
color: green;
}
</style>
<slot></slot>`;
this.shadow.innerHTML = this.innerHTML + template;
}
}
window.customElements.define("my-el", MyEl);
a {
color: red
}
<my-el>
Item1
Item2
</my-el>

Polymer 2.0: pass this to another element. Making a comments section

So basically what I'm struggling with is how to pass the reference of the parent element to its child element i.e the custom remove element?
can anyone please help me out!
*******************this is the el-insert element********************
// this element recieves data from another element(username and comment)
<link rel="import" href="./remove.html">
<dom-module id="el-insert">
<template >
<div id="userComment"><span>{{username}}</span>: {{saveComment}}</div>
<input id="edit" type="text" value={{saveComment::input}}>
<hr>
<reply-comment></reply-comment>
<button on-click="edit">Edit</button>
<remove-comment ></remove-comment>
</template>
<script>
class elInsert extends Polymer.Element {
static get is() { return 'el-insert'; }
static get properties(){
return {
saveComment:{
type:String,
notify:true
},
username:{
type:String,
notify:true
}
}//return ends
}
edit (){
$(this.$.userComment).toggleClass('hide');
var display = $(this.$.edit).css('display');
if(display == 'none'){
$(this.$.edit).css('display','block');
}else{
$(this.$.edit).css('display','none');
}
}
}
window.customElements.define(elInsert.is, elInsert);
</script>
</dom-module>
***************the remove button element******************
<link rel="import" href="../bower_components/polymer/polymer.html">
<dom-module id="remove-comment">
<template>
<button on-click="remove">el-Remove</button>
</template>
<script>
class removeComment extends Polymer.Element {
static get is() { return 'remove-comment'; }
static get properties(){
return{
}//return ends
}
remove(){
var element = this.parentNode.host;
$(element).remove();
}
}
window.customElements.define(removeComment.is,removeComment);
</script>
</dom-module>
This code works perfectly for me.
But how will I do the same thing using fire or dispatch as mentioned in the answer by Nicolas.
How to pass event details from parent element to child element?
Also, I want to make this element reusable so that I can simply drop it into another place like in a reply to delete that also.
(Also, I'm new very new to polymer so if there is anything else that I can improve upon in this code then please let me know)
I hope now you guys can help me out.
You do not want to do that - in general you should just pass properties to the child element and catch events from the child in the parent elements.
properties go down and events go up -
In you case you do not have to pass anything to the child element. The child element should just fire an event and the parent should respond to it when it catches it.
Something like that:
<dom-module id="child-element">
<template>
<div on-tap="_deleteComment"></div>
</template>
<script>
class ChildElement extends Polymer.Element {
...
_deleteComment() : {
// fire event to be caught by parent
const deleteEvent = new CustomEvent('deleteComment', {detail: {
whatever: {
you: 'want',
},
}});
this.dispatchEvent(deleteEvent);
}
}
...
</script>
</dom-module>
<dom-module id="parent-element">
<template>
<child-element on-delete-comment="_deleteComment"></child-element>
</template>
<script>
class ParentElement extends Polymer.Element {
...
_deleteComment(evt) : {
// handle evt
}
}
...
</script>
</dom-module>
#daKmoR is right - more context and some code would help -

Categories