Previous article: Page Representation and the Birth of a Controller Structure
Why talk about a constructor?
Even though CotomyElement wraps many DOM methods, the real idea is inside the constructor.
In the previous articles, I explained how TypeScript stabilized fragile code through type constraints, how ElementWrap improved locality around DOM handling, and how scoped CSS reduced the distance between UI and styling responsibility. The constructor is where those lines finally converge into one boundary I can actually use every day.
I do not see it as syntax convenience. I see it as a decision about ownership.
The original form: HTMLElement in, wrapper out
In the ElementWrap era, the constructor started with one simple shape: HTMLElement in, wrapper out.
That fit my actual workload. Most HTML was server-rendered, so the dominant flow was to find known nodes and wrap them for typed handling. That pattern still describes most of the systems I build.
There are two different searches in that world. One is page-wide search from document scope. The other is nested search under an already selected element. Both are useful, and I never wanted to force every lookup into a parent-threading pattern if a document-level query was clearer in that moment.
Why static finders exist and why byId is static only
CotomyElement provides static finder methods so I can enter from page root quickly. Methods like first, find, and byId work as small entry points from document scope.
It also supports nested search from an existing instance, so child-level selection stays local when I already have a component boundary.
Modern Cotomy has CotomyWindow as a broader boundary, but for daily use I still wanted CotomyElement itself to keep a minimal root-level entry style.
The byId decision is intentional. An id is page-unique by definition, so it belongs to page scope rather than instance scope. For that reason, byId exists as a static method and there is no equivalent instance method.
The four constructor patterns
Current CotomyElement accepts four input kinds.
Pattern A: HTMLElement
When I already have an element from existing markup, I pass it directly.
const button = document.querySelector("#save-btn") as HTMLElement;
const el = new CotomyElement(button);
Pattern B: html + optional css object
This is the constructor shape that binds creation and scoped styling in one local point.
const panel = new CotomyElement({
html: `<section><h2>Summary</h2><p>Ready</p></section>`,
css: `[root] { padding: 8px; border: 1px solid #ccc; }`,
});
Pattern C: tagname + optional text + optional css object
This was added later mostly for small convenience cases.
const message = new CotomyElement({
tagname: "p",
text: "No records found.",
css: `[root] { color: #666; margin: 0; }`,
});
Pattern D: html string only
If scoped CSS is unnecessary, a plain html string is often the cleanest call.
const row = new CotomyElement(`<li class="item">Alpha</li>`);
Why the html+css pattern is the biggest feature, but not always used
If I had to pick one constructor pattern as CotomyElement’s largest characteristic, it is the html plus css object pattern. In a practical sense, I built this boundary mainly for that. It compresses structure and styling into a single local decision.
At the same time, I have to be honest about real usage volume. In my systems, this pattern is not everywhere.
Most large structure is still server-rendered. Frame-level styling is usually centralized in a large shared stylesheet. Page or shared-part styling also lives in Razor scoped CSS. CotomyElement scoped CSS is mostly used for TypeScript-generated pieces where local ownership matters more than global reuse.
That is why I describe the core gain as searchability and locality, not universal replacement.
The benefit: scoped CSS that cannot leak
When I build a UI piece from API data, I can attach only the CSS needed for that generated component and keep it isolated from the rest of the page.
That matters under two pressures: parent styles I did not author and app-wide rules that can shift over time. In this constructor model, those pressures do not need to dictate local component styling behavior.
How I decided to treat scoped CSS
At first, I considered relying on inline style as the simplest path. It looks straightforward when the target is one element and one visual tweak.
That approach broke down quickly for normal UI work. Child selectors, pseudo-classes, and responsive rules all become awkward or impossible when style has to stay inline, so I needed real CSS instead of style attributes pretending to be a full styling system.
The current CotomyElement approach is to inject a scoped style tag and bind it to a dedicated CSS scope identifier.
In useScopedCss, it creates a style element and normalizes selectors with [root].
Then it rewrites [root] into [data-cotomy-scopeid="
I keep scopeId separate from instanceId on purpose. scopeId is the CSS boundary marker, while instanceId is wrapper identity used by event-related ownership such as EventRegistry. InstanceId is about behavior ownership; scopeId is about styling ownership. They solve different problems even when they live on the same element, and when scoped CSS is used, scopeId is generated per element.
Cleanup also matters. On removed, CotomyElement defensively checks whether an element with that scopeId is still present in the DOM, and removes the corresponding style tag only when it is no longer present. I do not claim measured performance wins here, but this avoids style-node accumulation risk on long-lived screens with repeated dynamic generation.
It also helps extensibility. A base class can define a default structure, and subclasses can apply small diffs without turning style ownership into a cross-file hunt.
I know this sounds like I am praising my own idea a little too much, and yes, I probably am.
Still, the useful part is not pride. The useful part is fewer unintended effects when real screens evolve.
Evolution and removed patterns
Two constructor-era patterns were removed on purpose.
First, I used to pass html and css as separate parameters. I dropped that because unstable arity grew quickly and the signature became too easy to misuse as variants increased.
Second, I used to allow constructing from another wrapper instance. I removed that because multiple wrapper identities around the same DOM node was structurally wrong for the boundary I wanted. One DOM identity should have one clear wrapper ownership path. Multiple wrappers around the same identity blur ownership.
These were boundary decisions, not simple refactors.
The lifecycle problem: creation is not enough
Creation is only half of a DOM wrapper story. The other half is deletion.
A wrapper has a specific failure mode: the DOM node can disappear while the wrapper instance still exists in memory. When that happens, event registrations and internal state can turn into ghost state, and later operations can accidentally run against a detached element. The bug is often quiet until a second action path touches it.
Garbage collection does not solve that ownership problem by itself. If controller state, caches, or closures still reference the wrapper, the object stays alive even after the DOM node is gone.
I considered patterns like strict manual dispose calls, explicit parent-managed lifetimes, and checking attachment state everywhere before each operation. Those patterns can work, but in my daily flow they were too easy to forget or apply inconsistently.
In the current Cotomy implementation, lifecycle detection is handled through MutationObserver in CotomyWindow.initialize(). It watches body subtree removals, wraps each removed HTMLElement, and triggers a removed event when the node is truly detached and not in a moving state. Here, moving state means temporary transit relocation, where data-cotomy-moving is set to avoid false removal handling during movement. CotomyElement already registers a removed hook during construction, and that hook swaps the internal element to a data-cotomy-invalidated placeholder and clears EventRegistry entries for that instance.
This does not make lifetime perfect in every possible usage pattern, but it does turn the common removal path into automatic invalidation instead of manual cleanup discipline. I did not want my future self to debug another invisible lifetime bug at 2 AM.
CotomyWindow as a boundary
CotomyWindow is the broader page-level boundary for window events, layout-change propagation, and mutation observation setup.
Even with that boundary, CotomyElement still needs lifecycle correctness on its own side, because wrappers can be created from many places in application code. CotomyWindow helps detect removal at page scope, while CotomyElement keeps per-instance ownership behavior consistent after removal.
Closing
Compared with modern frontend ecosystems, Cotomy is very small.
But this constructor boundary is the grain of sand that contains what I actually wanted to build: local DOM ownership, practical search entry points, and scoped styling where it matters.
It is not a grand framework claim. It is a precise tool shaped by my constraints and repeated screen implementation work.
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 .
Previous article: Page Representation and the Birth of a Controller Structure Next article: Dynamic HTML Boundaries in CotomyElement