I came accross an odd problem using WebComponents and composing them. I created a minimal HTML page to reproduce the problem.
I’m basically trying to build the following structure :
- custom list
- containing custom items
- each item containing a custom input to show the item name
My custom input is a wrapper to add label to a basic input and I use get
and set
on my custom input so that its value mirror the basic input value.
It works perfectly in isolation : getting and setting the value of my custom input gets and sets the value of the basic input inside it.
It also works fine inside my custom item : getting and setting its item correctly mirrors the item name on the custom input.
But when I manage items through my custom list, it breaks. The getter and setter of my custom input seem to have disappeared and fallback to a normal property with no accessors functions.
Here’s the minimal code to reproduce the problem :
const customInputTemplate = document.createElement("template");
customInputTemplate.innerHTML = `
<label id="label" for="input"></label>
<input id="input" type="text" />
`;
window.customElements.define("my-custom-input", class MyCustomInput extends HTMLElement {
static get observedAttributes() {
return ["label"]
}
constructor() {
super();
this.attachShadow({ mode: "open"});
this.shadowRoot.appendChild(customInputTemplate.content.cloneNode(true));
this.labelElemenet = this.shadowRoot.getElementById("label");
this.inputElement = this.shadowRoot.getElementById("input");
}
get value() { // This seems to be ignored or unset in certain cases
return this.inputElement.value;
}
set value(newValue) { // This also has the problem
this.inputElement.value = newValue;
}
attributeChangedCallback(name, oldValue, newValue) {
if (name === "label") {
this.labelElemenet.innerText = newValue;
}
}
});
const customItemTemplate = document.createElement("template");
customItemTemplate.innerHTML = `
<li>
<my-custom-input id="custom-input" label="Item :"></my-custom-input>
</li>
`;
window.customElements.define("my-custom-item", class MyCustomItem extends HTMLElement {
#item;
constructor() {
super();
this.attachShadow({ mode: "open"});
this.shadowRoot.appendChild(customItemTemplate.content.cloneNode(true));
this.customInputElement = this.shadowRoot.getElementById("custom-input");
}
get item() {
return this.#item;
}
set item(newItem) {
this.#item = newItem;
this.#render();
}
#render() {
if (this.customInputElement.value !== this.#item.name) {
this.customInputElement.value = this.#item.name
}
}
});
const customListTemplate = document.createElement("template");
customListTemplate.innerHTML = `
<ul>
<slot></slot>
</ul>
`;
window.customElements.define("my-custom-list", class MyCustomList extends HTMLElement {
#items;
constructor() {
super();
this.attachShadow({ mode: "open"});
this.shadowRoot.appendChild(customListTemplate.content.cloneNode(true));
}
get items() {
return this.#items;
}
set items(newItems) {
this.#items = newItems;
this.#render();
}
#render() {
this.replaceChildren(...this.#items.map(item => {
const itemElement = document.createElement("my-custom-item");
itemElement.item = item; // This is where things get broken
return itemElement;
}));
}
});
const customInputElement = document.getElementById("my-custom-input");
customInputElement.value = "Some value for my custom input";
const customItemElement = document.getElementById("my-custom-item");
customItemElement.item = { id: "Z", name: "Some name for my custom item"};
const listElement = document.getElementById("my-list");
listElement.items = [
{ id: "A", name: "Item A" },
{ id: "B", name: "Item B" },
{ id: "C", name: "Item C" },
];
// All inputs are empty instead of showing the item names
const firstItem = listElement.querySelector("my-custom-item");
const customInputOfFirstItem = firstItem.shadowRoot.querySelector("my-custom-input");
console.log("customInputOfFirstItem.value = ", customInputOfFirstItem.value); // Correctly logs "Item A"
// Trying to force value
customInputOfFirstItem.value = "Updated item A";
console.log("After forcing value, customInputOfFirstItem.value = ", customInputOfFirstItem.value); // Correctly logs "Updated item A"
// But ...
const originalInput = customInputOfFirstItem.shadowRoot.getElementById("input");
console.log("Original input value = ", originalInput.value); // Logs empty value
// Forcing it
originalInput.value = "Forced inside value for item A";
console.log("After forcing inside value, customInputOfFirstItem.value = ", customInputOfFirstItem.value); // Still logs "Updated item A"
// It's acting as if getter (line 37) and setter (line 41) where ignored or somewhat unset in the process
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Web Component</title>
</head>
<body>
<h3>Custom input, works fine</h3>
<my-custom-input id="my-custom-input" label="Custom input"></my-custom-input>
<h3>Custom item, also works fine</h3>
<my-custom-item id="my-custom-item"></my-custom-item>
<h3>Inside custom list, does not work</h3>
<my-custom-list id="my-list"></my-custom-list>
</body>
</html>