0

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>
5
  • 1
    Can you write your code in StackOverflow Snippet syntax With the [<>] button in the editor. It will help readers execute your code with one click. And help create answers with one click. Thank you. Commented Jul 28 at 7:59
  • Not really. The template in my code is loaded from a drive - how would I do it without a server? Commented Jul 28 at 10:40
  • Mock it... you are describing a clinet-side problem, if it is an error client-side than any back-end code can't cause it Commented Jul 28 at 13:02
  • I have scratched the whole idea of racing between setter and initialization methods as it seemed kinds silly. Instead I added a promise to my custom elements which resolves when they are ready and then the setter can overwrite it's value. This seems like the most robust method so far. Commented Jul 28 at 16:24
  • Ah, you fell into the connectedCallback trap, see my blogpost: dev.to/dannyengelman/… Commented Jul 28 at 19:21

0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.