Dynamic HTML Boundaries in CotomyElement

A reflective look at single-root constraints, parser wrapping for standalone tags, scoped CSS limits, and instance and id ownership in CotomyElement.

Previous article: The CotomyElement Constructor Is the Core

Why dynamic HTML is not trivial

Passing a string into a DOM wrapper sounds simple, but it immediately asks harder questions. Where is the ownership boundary? What counts as valid structure? Which safety checks are strict, and which are best-effort?

In the early phase of CotomyElement, this was one of the most fragile areas. Small changes in parsing behavior could silently alter runtime shape, and that kind of instability is hard to debug later in controller code.

The single root element rule

One question came first: how much freedom should this constructor allow?

If multiple root nodes were accepted, one instance would need to hold multiple HTMLElements. That would change the class model, event ownership, and almost every method contract that assumes one underlying element.

So I drew a boundary: one instance means one element. If the input resolves to multiple roots, construction fails.

This was not a technical limitation. I could have modeled a fragment-like wrapper. I rejected that direction because ambiguity at construction time would spread everywhere else.

const doc = parser.parseFromString(wrappedHtml, "text/html");
if (doc.body.children.length !== 1) {
    throw new Error(`CotomyElement requires a single root element, but got ${doc.body.children.length}.`);
}

The invalid standalone tag problem

The first implementation used DOMParser directly, and that works for many tags. But some tags cannot stand alone in valid HTML parsing contexts. td, tr, thead, and option are typical examples.

This was not theoretical. Dynamically generating rows and cells was common in actual screens, so ignoring the problem would make the API unreliable exactly where dynamic UI was needed most.

The pragmatic fix was to wrap such tags with required structural parents before parsing, then extract the intended element.

For example, td is parsed by temporarily placing it inside table, tbody, and tr, then selecting td from the parsed result.

const wrapperMap: Record<string, { prefix: string, suffix: string }> = {
    tr: { prefix: "<table><tbody>", suffix: "</tbody></table>" },
    td: { prefix: "<table><tbody><tr>", suffix: "</tr></tbody></table>" },
    thead: { prefix: "<table>", suffix: "</table>" },
    option: { prefix: "<select>", suffix: "</select>" }
};

const wrap = wrapperMap[tag];
const wrappedHtml = wrap ? `${wrap.prefix}${html}${wrap.suffix}` : html;

I do not consider this beautiful. I consider it stable. wrapperMap exists because browser parsers enforce structural context, and CotomyElement needs consistent behavior for dynamic markup that would otherwise fail depending on tag type.

Scoped CSS complications

Scoped CSS introduced another boundary question. If CSS is provided, should CotomyElement always apply it?

For roots like style, script, meta, or link, scoped CSS is almost certainly a mistake. Throwing exceptions for every such case felt too defensive for day-to-day usage, so I took a softer boundary: apply scoped CSS only when the element is stylable.

If not stylable, the CSS is ignored.

This is a practical compromise. It avoids turning minor misuse into hard failures while keeping normal usage predictable.

public get stylable(): boolean {
    return !["script", "style", "link", "meta"].includes(this.tagname);
}

private useScopedCss(css: string): this {
    if (css && this.stylable) {
        const hasRoot = /\[root\]/.test(css);
        const normalizedCss = hasRoot ? css : `[root] ${css}`;
        const writeCss = normalizedCss.replace(
            /\[root\]/g,
            `[data-cotomy-scopeid="${this.scopeId}"]`
        );
        // style tag injection into head...
    }
    return this;
}

The root selector is normalized to root when missing, then rewritten into a concrete data-cotomy-scopeid selector. That keeps local CSS scoped to the current element identity without requiring manual selector rewriting in every call.

The id problem

CotomyElement carries two internal identities: instanceId and scopeId.

instanceId is data-cotomy-instance and is used for event registry and lifecycle ownership. scopeId is data-cotomy-scopeid and is used for scoped CSS isolation.

But HTML uniqueness is defined by id. That is a separate concern.

I kept a strict boundary here: Cotomy does not modify id by default. For controlled elements such as forms managed through CotomyPageController, if id is missing, generateId is called so controller-level lookup remains predictable.

// CotomyElement
public generateId(prefix: string = "__cotomy_elem__"): this {
    if (!this.id) {
        this.attribute("id", `${prefix}${cuid()}`);
    }
    return this;
}
// CotomyPageController
protected setForm<T extends CotomyForm = CotomyForm>(form: T): T {
    if (!form.id) {
        form.generateId();
    }
    this._forms[form.id!] = form;
    return form.initialize();
}

This is not a perfect answer. It is a boundary tradeoff. The platform id model stays untouched unless ownership is explicit, and page-level orchestration still gets stable identifiers where it needs them.

A future major version may revisit this line.

Closing

I have only explained a fraction of CotomyElement so far, but these boundaries were some of the most important decisions during development.

My vocabulary may not always be enough to express the full design intent, but the intent itself was real: keep dynamic behavior usable without letting ambiguity spread through the entire API.

If you are shaping your own development environment or framework, even partially, I hope these reflections are useful in your own boundary decisions.

Development Backstory

This article is part of the Cotomy Development Backstory, which traces how Cotomy’s architecture emerged through real project constraints.

Series articles: Building Systems Alone , Early Architecture Attempts , The First DOM Wrapper and Scoped CSS , API Standardization and the Birth of Form Architecture , Page Representation and the Birth of a Controller Structure , The CotomyElement Constructor Is the Core , Dynamic HTML Boundaries in CotomyElement, Reaching Closures to Remove Event Handlers Later , and The Birth of the Page Controller .

Next article: Reaching Closures to Remove Event Handlers Later

Learn Cotomy

Cotomy is a DOM-first UI runtime for long-lived business applications.