Edit ############
I had omitted the fact that this is an extension code, which seems a crucial information.
I have realized the problem is probably not caused by race of any of my functions but rather:
- Extension script/module fetching, and
- Page teardown / reload
This would explain a bunch of Loading failed for the module with source “moz-extension://* messages under rapid reload. I have then to redesign this code to not overwrite settings even if not everything gets fetched.
################
I am trying to mimic the behavior of the HTMLSelectElement value setter to work at any time, but also retain lazy loading of my custom elements. I use a base class which cache elements of the same type to prevent their template from being loaded multiple times:
const templateCache = new Map();
export class DSElement extends HTMLElement {
constructor() {
super();
this._name = this.constructor._type;
this.attachShadow({ mode: 'open' });
this._internals = this.attachInternals();
}
async connectedCallback() {
let template = templateCache.get(this._name);
if (!template) {
const templateUrl =
chrome.runtime.getURL(`interface/templates/${this._name}.html`);
const response = await fetch(templateUrl);
const html = await response.text();
const wrapper = document.createElement('template');
wrapper.innerHTML = html;
const inner = wrapper.content.getElementById(`${this._name}-template`);
if (inner) {
template = inner;
templateCache.set(this._name, template);
}
requestAnimationFrame(() => this._after());
}
if (template)
this.shadowRoot.appendChild(template.content.cloneNode(true));
}
async _after() {
// Virtual method
}
}
Then I create a DSSelect class which handles drawing stuff inside the _after method:
import { DSElement } from './ds-element.js';
class DSSelect extends DSElement {
static _type = 'ds-select';
constructor() {
super();
this._value = null;
this._firstOption = null;
this._label = null;
this._internals.role = 'select';
}
async _after() {
this._select = this.shadowRoot.getElementById('select');
this._options = this.shadowRoot.getElementById('options');
this._labelSpan = this.shadowRoot.getElementById('label');
this.setInitialSelection();
this._select.addEventListener('click', () => this.toggle());
this.addEventListener('click', e => this.handleOptionClick(e));
}
setInitialSelection() {
let firstOption;
if (this._firstOption !== null)
firstOption = this._firstOption;
else
firstOption = this.querySelector('ds-opt');
if (firstOption)
this.selectOption(firstOption);
}
selectOption(option) {
this._value = option.getAttribute('value');
this._label = option.textContent;
this._labelSpan.innerText = this._label;
this.dispatchEvent(new Event('change', {bubbles: true}));
}
get value() {
return this._value;
}
set value(value) {
const match = Array.from(this.querySelectorAll('ds-opt')).find(
opt => opt.getAttribute('value') === value
);
if (match)
this._firstOption = match;
}
}
customElements.define(
DSSelect._type, DSSelect
);
And somewhere in my code I use the setter on DOMContentLoaded event:
const reloadOptions = () => {
const elMode = document.getElementById('mode');
elMode.value = 'selected';
};
document.addEventListener('DOMContentLoaded', reloadOptions);
This actually works 99% of the time, until I hold down F5 to reload quickly many times, then the select value jumps into the first defined value. I think there is an asynchronous race happening between reloadOptions and _after function and this.querySelectorAll('ds-opt') appears empty inside the DSElement setter when _after loose - thus setInitialSelection takes the else route.
I am not sure though what would be the correct approach here. Initially I thought calling requestAnimationFrame will make _after run after any function that runs on document load.
For completeness, the template for ds-select looks like this:
<template id="ds-select-template">
<style>
:host {
display: inline-block;
position: relative;
user-select: none;
}
#select {
cursor: pointer;
}
#options {
display: none;
position: absolute;
top: 100%;
left: 0;
right: 0;
}
#options.open {
display: block;
}
</style>
<div id="select">
<span id="label">n/a</span>
</div>
<div id="options">
<slot></slot>
</div>
</template>
And finally the ds-select element is used like:
<ds-select id="mode">
<ds-opt value="all">
Parse all
</ds-opt>
<ds-opt value="selected">
Parse selected
</ds-opt>
</ds-select>