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>
.
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()
.
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.
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.
DOMContentLoaded
. It's not relevant.EntityTable
two properties:
items
- An array of data items to be rendered by the table. Defaults to null
. When the items array is set, render the table to the shadow DOM. Optional, dispatch a custom event "items-change".value
- Represents the data item that is selected. Defaults to null
. When the value
is set, select the associated row in the table. Optional, dispatch a custom event "change".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:
items
property on the EntityTable
instance.value
.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.
EntityTable
internally in its own Shadow DOM or that can be used to wrap around an EntityTable
in Light DOM.loadingState
- Represents the status of the data loading process. This can have one of three values: "not-loaded", "loading", and "loaded". The initial value is "not-loaded". Optional, when the value is set to "loaded", dispatch a custom "loaded" event.src
- When src changes, update loadingState
to "loading" and then fetch the data. When the data is loaded, set the items
property on the EntityTable
instance, and then update loadingState
to "loaded".src
attribute's initial value in the connectedCallback
and use that value to set the property, kicking off the loading process (if src
has a value). Also, do the same thing from the attributeChangedCallback
so that src
can be handled at connect and on both property and attribute changes.value
and items
that pass through to the orchestrated EntityTable
. If you made the EntityTable
events bubble and composed, then those will pass through automatically.