Reputation: 4668
I have created a very basic custom element, that can change its value based on a provided attribute person
. But whenever I'm loading my custom element I get this error: Cannot set property 'innerHTML' of null
. When I add a breakpoint to the attributeChangedCallback function I can indeed see that on load the element is not there. When I continue loading though the element loads perfectly.
I could imagine because I'm using webpack to bundle all my files that the issue comes from loading the element at the end of the body instead of loading the element inside my head.
my-element.js:
class MyElement extends HTMLElement {
constructor() {
super();
this.shadow = this.attachShadow({mode: 'open'});
this._person = '';
}
get person() {
return this._name;
}
set person(val) {
this.setAttribute('person', val);
}
static get observedAttributes() {
return ['person'];
}
attributeChangedCallback(attrName, oldVal, newVal) {
let myElementInner = this.shadow.querySelector('.my-element-inner');
switch (attrName) {
case 'person':
this._person = newVal;
// ======================
// The error occures here
// ======================
myElementInner.innerHTML = `My name is ${this._person}`;
}
}
connectedCallback() {
var template =
`
<style>
.my-element-inner {
outline: blue dashed 1px;
background-color: rgba(0,0,255,.1);
}
</style>
<span class="my-element-inner">My name is ${this._person}</span>
`
this.shadow.innerHTML = template;
}
}
customElements.define('my-element', MyElement);
index.html:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>WebPack Test Page</title>
</head>
<body>
<my-element person="André"></my-element>
<!-- Here goes the bundle.js -->
</body>
</html>
Upvotes: 1
Views: 1023
Reputation: 106
Since the spec encourages pushing as much setup as possible to the connectedCallback
, this might be preferable for some developers.
Note that attributeChangedCallback
can still be called while the raw HTML is being parsed if any (observed) attributes are present on the element. For cases like these, I use a private #mounted
property. This property is useful for preventing chunks of logic in connectedCallback
from running redundantly (see the link to the spec above), but it can also be useful for preventing attributeChangedCallback
from running prematurely.
class MyElement extends HTMLElement {
#mounted = false;
#person = '';
constructor() {
super();
this.shadow = this.attachShadow({mode: 'open'});
}
get person() {
return this.#person;
}
set person(val) {
this.setAttribute('person', val);
}
static get observedAttributes() {
return ['person'];
}
attributeChangedCallback(attrName, oldVal, newVal) {
if (!this.#mounted) return;
let myElementInner = this.shadow.querySelector('.my-element-inner');
switch (attrName) {
case 'person':
this.#person = newVal;
myElementInner.innerHTML = `My name is ${this.#person}`;
}
}
connectedCallback() {
const template = `
<style>
.my-element-inner {
outline: blue dashed 1px;
background-color: rgba(0,0,255,.1);
}
</style>
<span class="my-element-inner">My name is ${this.#person}</span>
`
this.shadow.innerHTML = template;
this.#mounted = true;
}
}
customElements.define('my-element', MyElement);
Upvotes: 0
Reputation: 3527
The attributeChangedCallback()
can be called before or after the connectedCallback
depending on how your custom element is used.
If you move the connectedCallback
logic to the constructor then things will be fine
Another option would be to check if myElementInner
is null
and keep your code in the connectedCallback
class MyElement extends HTMLElement {
constructor() {
super();
this.shadow = this.attachShadow({mode: 'open'});
this._person = '';
var template =
`
<style>
.my-element-inner {
outline: blue dashed 1px;
background-color: rgba(0,0,255,.1);
}
</style>
<span class="my-element-inner">My name is ${this._person}</span>
`
this.shadow.innerHTML = template;
}
get person() {
return this._person;
}
set person(val) {
this.setAttribute('person', val);
}
static get observedAttributes() {
return ['person'];
}
attributeChangedCallback(attrName, oldVal, newVal) {
let myElementInner = this.shadow.querySelector('.my-element-inner');
switch (attrName) {
case 'person':
this._person = newVal;
if (myElementInner) {
myElementInner.innerHTML = `My name is ${this._person}`;
}
}
}
}
customElements.define('my-element', MyElement);
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>WebPack Test Page</title>
</head>
<body>
<my-element person="André"></my-element>
<!-- Here goes the bundle.js -->
</body>
</html>
Upvotes: 2