javascriptasync-awaitshadow-domhtmlelements

How Can ShadowRoot Elements in a Custom HtmlElement Data Table Be Accessed After Async Functions Complete?


I've created a working (mostly) custom EntityTable (entity-table) HtmlElement that successfully fetches and displays JSON data from a custom RESTful microservice in an HTML <table>.

enter image description here

From another code module (app.js) I want to select (and highlight) the first table row (<tr>), including the integer entity id value it contains in its first cell (<td>), and pass the row (as the default selected row) to a listener that will use the id to populate a second, related EntityTable.

But when I set a breakpoint in app.js only the <link> table <table> elements seem to exist in the EntityTable's ShadowRoot element, which was attached in open mode in its constructor().

Developer Tools Sources Tab

Developer Tools Elements Tab

index.html

    <section id="definitions">
      <entity-table id="entitytypedefinition" baseUrl="http://localhost:8080/entityTypes/" entityTypeName="EntityTypeDefinition" whereClause="%22Id%22%20%3E%200" sortClause="%22Ordinal%22%20ASC" pageNumber="1" pageSize="20" includeColumns="['Id', 'LocalizedName', 'LocalizedDescription', 'LocalizedAbbreviation']" zeroWidthColumns="['Id']" eventListener="entityTableCreated"></entity-table>
    </section>

EntityTable.js

//NOTE: Copyright © 2003-2025 Deceptively Simple Technologies Inc. Some rights reserved. Please see the aafdata/LICENSE.txt file for details.

class EntityTable extends HTMLElement {
  constructor() {
    super();
    const shadow = this.attachShadow({ mode: 'open' });

    console.log(`EntityTable constructor ends.`);
  }

  //NOTE: Called each time this custom element is added to the document, and the specification recommends that custom element setup be performed in this callback rather than in the constructor
  connectedCallback() {
    const link = document.createElement('link');
    link.setAttribute('rel', 'stylesheet');
    link.setAttribute('href', 'css/style.css');
    this.shadowRoot.appendChild(link);

    //TODO: Get live authentication token
    document.cookie = "Authentication=XXX";

    const table = document.createElement('table');

    console.log(`EntityTable connectedCallback() Explicit parent id: ${this.getAttribute('id')}`);
    table.setAttribute('id', this.getAttribute('id') + '-23456' || 'entitytypedefinition-34567')   //TODO: Generate and append a unique id
    table.setAttribute('class', 'entity-table');

    console.log(`EntityTable connectedCallback() Not fetching data yet!`);

    //TODO: Should this be put back into the constructor???
    //TODO: And should the event listener be added to the document or to the table???
    this.addEventListener(this.getAttribute('eventListener') || 'DOMContentLoaded', async () => {
      console.log('EntityTable connectedCallback() ' + this.getAttribute('eventListener') + ` event listener added.`);
      try {
        if ((this.getAttribute('eventListener') == 'entityTableCreated') || (this.getAttribute('eventListener') == 'entityTableRowClicked')) {
          const data = await fetchData(this.getAttribute('baseUrl') || 'http://localhost:8080/entityTypes/', this.getAttribute('entityTypeName') || 'EntityTypeDefinition', this.getAttribute('whereClause') || '%22Id%22%20%3E%20-2', this.getAttribute('sortClause') || '%22Ordinal%22%20ASC', this.getAttribute('pageSize') || 20, this.getAttribute('pageNumber') || 1);
          await displayData(data, table, this.getAttribute('includeColumns') || ['Id', 'EntitySubtypeId', 'TextKey'], this.getAttribute('zeroWidthColumns') || []);
        }
      }
      catch (error) {
        console.error('Error fetching data:', error);
      }
    });

    this.shadowRoot.appendChild(table);
    console.log(`EntityTable connectedCallback() Data fetched and displayed!`);
  }
}

customElements.define('entity-table', EntityTable)

async function fetchData( ...

async function displayData( ...

app.js

//NOTE: Copyright © 2003-2025 Deceptively Simple Technologies Inc. Some rights reserved. Please see the aafdata/LICENSE.txt file for details.

console.log(`app.js executing! No DOMContentLoaded listener added yet.`);

//NOTE: Constructors for both EntityTables defined in index.html are called here

//NOTE: "Wire up" the two EntityTables so that when a row is clicked in the first EntityTable, the data is sent to the second EntityTable
document.addEventListener('DOMContentLoaded', () => {
  const entityTableDefinitions = document.getElementById('entitytypedefinition');
  const entityTableAttributes = document.getElementById('entitytypeattribute');

  console.log(`app.js still executing! DOMContentLoaded listener now added to page.`);

  //NOTE: Populate the definitions table with data by adding custom listener and dispatching custom event
  const definitionsEvent = new CustomEvent('entityTableCreated', {
    detail: { entityTableDefinitions }
  });

  console.log(`app.js still executing! Dispatching entityTableCreated event ...`);
  entityTableDefinitions.dispatchEvent(definitionsEvent);

  const selectedRow = entityTableDefinitions.shadowRoot.innerHTML; //NOTE: Get the first row in the table
  //.querySelectorAll('tr').length; //NOTE: Get the first row in the table
  //.querySelectorAll('table')[0]
  //.querySelectorAll('tbody');
  //.querySelectorAll('tr')[0]; //NOTE: Get the first row in the table

  //NOTE: Populate the attributes table with data by adding custom listener and dispatching custom event
  const attributesEvent = new CustomEvent('entityTableRowClicked', {
    detail: { selectedRow }
  });

  console.log(`app.js still executing! Dispatching entityTableRowClicked event: ${selectedRow} ...`);
  entityTableAttributes.dispatchEvent(attributesEvent);

  //TODO: Add click event listener to each row in the first table??? (unless passing the EntityTable reference is sufficient)

});

I've tried moving the this.shadowRoot.appendChild(table) statement up just under the await displayData() in the EntityTable's this.addEventListener() method and making it await, but this actually resulted in less elements (only the <link>) being available.

I suspect that the app.js code is continuing to execute while the fetchData() and `displayData() promise(s) are awaited, but I'm stuck on how to prevent this.

I'm not a front-end guy. Any help would be greatly appreciated.


Solution

  • The Problem

    The problem you are having has nothing to do with Shadow DOM. It's all related to async programming and the way you are using events. The situation is exacerbated by the mixing of UI and data loading in a single component.

    Improved Methodology

    Revamp the EnityTable

    That's all you want in your basic table component. At this level, completely extract the data loading so that you have a pure UI component.

    Next, from your external code you would:

    Higher Order Component

    If you want, you can take the previous three steps and encapsulate them in a higher order component that orchestrates fetching the data and setting the items.