When I try to query child elements within connectedCallback, I get an empty nodelist - javascript

<script>
class SomeClass extends HTMLElement {
constructor() {
super();
}
connectedCallback() {
// returns empty nodelist
console.log(this.querySelectorAll('.thing'));
}
}
customElements.define('my-component', SomeClass);
</script>
<my-component>
<div class="thing"></div>
<div class="thing"></div>
<div class="thing"></div>
</my-component>
When I try to query child elements within connectedCallback, I get an empty nodelist.
If I move script tag after <my-component> - it starts working:
<my-component>
<div class="thing"></div>
<div class="thing"></div>
<div class="thing"></div>
</my-component>
<script>
class SomeClass extends HTMLElement {
constructor() {
super();
}
connectedCallback() {
// returns empty nodelist
console.log(this.querySelectorAll('.thing'));
}
}
customElements.define('my-component', SomeClass);
</script>
Is there some callback that triggers when all child elements are available? (no matter where <script> was added). Do I really have to use something like document.ready or mutation observer? What's the most efficient way?

There appear to be a different behavior between browsers, where it works on e.g. Firefox, but Chrome (Blink) need "Mutation Observers".
https://github.com/w3c/webcomponents/issues/551
Optionally one can place the script after the component, for the same reason DOM work in general.
Why does jQuery or a DOM method such as getElementById not find the element?
Or make the customElements.define() call after DOM been loaded
<script>
class SomeClass extends HTMLElement {
constructor() {
super();
}
connectedCallback() {
// returns empty nodelist
console.log(this.querySelectorAll('.thing'));
}
}
window.addEventListener('DOMContentLoaded', function() {
customElements.define('my-component', SomeClass);
});
</script>
<my-component>
<div class="thing"></div>
<div class="thing"></div>
<div class="thing"></div>
</my-component>

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>

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

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>

How to access functions defined in js file inside the template of Polymer element?

I have created a function in global.function.js file as
function getData(flag) {
if (flag === 1) {
return "one";
}
else {
return "not one";
}
}
which then is imported using custom-js-import.html element:
<script src="global.function.js"></script>
When I tried to access the above function in custom-element.html, I am able to access it in the script part but not in the template part.
Is there any way I can access the function inside the HTML element?
<!-- custom-element.html -->
<link rel="import" href="https://polygit.org/components/polymer/polymer-element.html">
<link rel="import" href="custom-js-import.html">
<dom-module id="custom-element">
<template>
<div>
Hello
</div>
<div id="data"></div>
<div>{{getData(1)}}</div><!-- Unable to access this from here -->
<div>{{getLocalData()}}</div>
</template>
<script>
// Define the class for a new element called custom-element
class CustomElement extends Polymer.Element {
static get is() { return "custom-element"; }
constructor() {
super();
}
ready(){
super.ready();
this.$.data.textContent = "I'm a custom-element.";
console.log(getData(1));//can be easily accessed from here
}
getLocalData(){
return "local";
}
}
// Register the new element with the browser
customElements.define(CustomElement.is, CustomElement);
</script>
</dom-module>
Sample Code
Is there any way I can access the function inside the HTML element?
Not really. In order to use data in a template you need to bind it to a property (Polymer calls this data binding).
Polymer's data binding system is designed for binding values to a template. Those values are typically just literals (e.g. strings and numbers) or plain ole javascript objects e.g. {a: 'someval', b: 5}. Polymer's data binding system is not designed to bind functions to a template and you can't just use javascript inside of a template. (If you're really into that idea, check out React as a replacement to polymer).
The polymer way to do what you're trying to do is to use a computed property. Instead of calling a function inside the template, create a computed property that reacts to changes of other variables. When the state of a property changes, the computed property will change too. This state can be thought of as the argument of your function.
I think it's better just to see the code working yeah (tested in chrome)?
<link rel="import" href="https://polygit.org/components/polymer/polymer-element.html">
<link rel="import" href="custom-js-import.html">
<dom-module id="custom-element">
<template>
<div>
Hello
</div>
<label>
<input type="number" value="{{flag::input}}">
</label>
<h1>from flag: [[flag]]</h1>
<div id="data"></div>
<div>{{boundComputedData}}</div><!-- Unable to access this from here -->
<div>{{getLocalData()}}</div>
</template>
<script>
// Define the class for a new element called custom-element
class CustomElement extends Polymer.Element {
static get is() {
return "custom-element";
}
constructor() {
super();
}
getData(flag) {
const flagAsNumber = parseInt(flag);
if (flagAsNumber === 1) {
return "one";
} else {
return "not one";
}
}
ready() {
super.ready();
this.$.data.textContent = "I'm a custom-element.";
console.log(this.getData(1)); //can be easily accessed from here
}
getLocalData() {
return "local";
}
static get properties() {
return {
flag: {
type: Number,
value: 0
},
boundComputedData: {
type: String,
computed: 'getData(flag)'
}
};
}
}
// Register the new element with the browser
customElements.define(CustomElement.is, CustomElement);
</script>
</dom-module>
<custom-element></custom-element>
So What I'm doing here is:
creating a computed property boundComputedData and setting the computed property to 'getData(flag)' which will make it use the class function getData.
Now whenever the state the property flag changes, the computed property will update.
Hope it helps!
Thanks to Rico Kahler for suggesting me to use a mixin. Using mixin solved my problem. You can view the full working sample here.
All the global functions can be defined in the mixin.
<!--custom-mixin.html-->
<script>
const CustomMixin = superclass => class extends superclass {
static get properties() {
return {};
}
connectedCallback() {
super.connectedCallback();
}
getData(flag) {
if (flag === 1) {
return "one(From Mixin)";
} else {
return "not one(From Mixin)";
}
}
};
</script>
And remember to import the mixin file and add that mixin to your element.
<!-- custom-element.html -->
<link rel="import" href="https://polygit.org/components/polymer/polymer-element.html">
<link rel="import" href="custom-mixin.html">
<dom-module id="custom-element">
<template>
<div>
Hello
</div>
<div id="data"></div>
<div>{{getData(1)}}</div>
<!-- Unable to access this from here -->
<div>{{getLocalData()}}</div>
</template>
<script>
// Define the class for a new element called custom-element
class CustomElement extends CustomMixin(Polymer.Element) {
static get is() {
return "custom-element";
}
constructor() {
super();
}
ready() {
super.ready();
this.$.data.textContent = "I'm a custom-element.";
console.log(getData(1)); //can be easily accessed from here
}
getLocalData() {
return "local";
}
}
// Register the new element with the browser
customElements.define(CustomElement.is, CustomElement);
</script>
</dom-module>

Categories