): boolean {
const link = cell.getAsNormalizedFullLink();
return Array.isArray(link?.path) && link.path.length > 0;
}
private async _renderCell() {
this._log("_renderCell called");
// Prevent concurrent renders
if (this._isRenderInProgress) {
this._log("render already in progress, skipping");
return;
}
// Early exits
if (!this._renderContainer || !this.cell) {
this._log("missing container or cell, returning");
return;
}
const isSubPath = this._isSubPath(this.cell);
// For root charm cells (not subpaths), check if value is defined.
// If not, wait for subscription to trigger - this handles async loading
// where the Cell exists but the charm data hasn't loaded yet.
//
// For subpath cells (like .key("fabUI") or .key("sidebarUI")), we should
// render immediately even if the value is undefined/null - the pattern
// may intentionally set these properties to undefined (e.g., sidebarUI: undefined).
//
// See _isSubPath() comment for why this is a heuristic, not a principled solution.
if (!isSubPath) {
let cellValue: unknown;
try {
cellValue = this.cell.get();
} catch {
cellValue = undefined;
}
if (cellValue === undefined || cellValue === null) {
this._log(
"root cell value is undefined/null, waiting for async load",
);
// Don't set _hasRendered - subscription will trigger render when value becomes available
return;
}
}
// Mark render as in progress
this._isRenderInProgress = true;
try {
// Clean up any previous render
this._cleanupPreviousRender();
// start() handles all cases: syncs if needed, loads recipe, runs nodes.
// For subpaths or cells without recipes, it's a no-op.
await this.cell.runtime.start(this.cell);
await this._renderUiFromCell(this.cell);
// Mark as rendered and trigger re-render to hide spinner
this._hasRendered = true;
this.requestUpdate();
} catch (error) {
this._handleRenderError(error);
} finally {
this._isRenderInProgress = false;
}
}
private _cleanupPreviousRender() {
if (this._cleanup) {
this._log("cleaning up previous render");
this._cleanup();
this._cleanup = undefined;
}
}
private _cleanupCellValueSubscription() {
if (this._cellValueUnsubscribe) {
this._log("cleaning up cell value subscription");
this._cellValueUnsubscribe();
this._cellValueUnsubscribe = undefined;
}
}
private _handleRenderError(error: unknown) {
console.error("[ct-render] Error rendering cell:", error);
if (this._renderContainer) {
this._renderContainer.innerHTML =
`Error rendering content: ${
error instanceof Error ? error.message : "Unknown error"
}
`;
}
}
override disconnectedCallback() {
this._log("disconnectedCallback called");
super.disconnectedCallback();
// Cancel any in-progress renders
this._isRenderInProgress = false;
// Reset render state
this._hasRendered = false;
// Clean up
this._cleanupCellValueSubscription();
this._cleanupPreviousRender();
}
}
globalThis.customElements.define("ct-render", CTRender);
declare global {
interface HTMLElementTagNameMap {
"ct-render": CTRender;
}
}