Previous article: Reaching Closures to Remove Event Handlers Later
ElementWrap was never meant to control an entire page
When I first created ElementWrap, the intention was narrow. I wanted to simplify DOM operations so daily screen work would be less repetitive.
At that stage, I was not trying to create a page-level orchestration model. I only wanted a practical wrapper around common element operations.
Many business systems, especially CRUD-centered systems, do not need heavy TypeScript behavior on the client side. Most screens can run with minimal interaction logic. That assumption was realistic in many of my earlier projects.
Why that assumption started to break
Later, one project required more coordination than that assumption could support. It was a small sales management system I developed as an individual contract.
The domain itself was not unusual. It handled products, customer-specific quotation prices, orders, shipping destinations, and consignor handling. In Japanese business practice, shipments are often sent under a trading company acting as the consignor, so that data relationship had to be represented correctly.
The system generated delivery slips, invoices, shipping data, and order records. A key objective was to import data from purchase orders whenever possible and reduce manual document preparation.
In other words, it was a practical business system with modest scale but non-trivial coordination needs.
Why the system was built from scratch
Before implementation, we evaluated existing SaaS products. None matched the required workflow cleanly. Customization cost was high, and ongoing management fees would accumulate over time.
Because there was some technical knowledge inside the company, we judged that Azure hosting plus a custom system was operationally manageable.
The scope was intentionally limited. It covered users, products, packaging, quotations, orders, deliveries, and billing. The plan assumed around 20 hours per week and roughly one year for design and implementation.
This was not a large digital transformation project. It was a focused attempt to build exactly what operations needed.
The limits of everything starting from the load event
In older systems built with ElementWrap, I mostly used a simple pattern. Register initialization logic in the load event, then let user events drive the rest.
Because many interactions were event-driven, this pattern worked better than expected for a long time.
But the order entry screen in this project became much more complex. Order creation needed customer selection, quotation selection, product selection, shipping destination selection, and consignor selection. Each choice influenced other choices. Some fields appeared or disappeared depending on earlier decisions.
Technically it was possible to place everything inside one entry-point handler. But architecturally that would have produced one very large and fragile script. The entry point itself would become a maintenance risk.
The idea of a PageController
That was the point where the page controller idea became necessary.
The idea itself was not original. It is a common pattern, and I do not remember exactly where I first learned it. What mattered was that the pattern matched the problem at the right time.
I needed one place that could own page-level coordination.
The first idea: run()
My first design attempt was straightforward. PageController had a run() method, and entry code would instantiate and run the controller.
const controller = new OrderPageController();
controller.run();
This idea actually came from older application architectures I had used in C# desktop systems. In hindsight, this was a clear failure on my side. I carried a desktop pattern into browser runtime design without enough adaptation. I implemented it once, but noticed the mismatch early in development and replaced it before production use. The typical relationship was simple. A controller instance is created, run() is invoked, the controller prepares the screen, the screen exposes an interface, and the controller handles callbacks through that interface.
public partial class OrderForm : Form
{
public interface IOrder
{
void OnSubmit(Order order);
}
private readonly IOrder _events;
public OrderForm(IOrder events)
{
_events = events;
}
}
public class OrderController : OrderForm.IOrder
{
public void Run()
{
var form = new OrderForm(this);
form.Show();
}
public void OnSubmit(Order order)
{
// process order
}
}
Conceptually this was clean in desktop architecture. In browser architecture, it was the wrong fit.
Why run() failed in the browser
In browser code, run() could execute before the DOM was fully ready. That creates timing fragility immediately and turns initialization order into a source of hidden defects.
There were obvious workarounds. I could add DOM-ready handlers or move script tags to the bottom of the page. But those fixes felt procedural, not structural. Since jQuery workflows normally add startup logic through ready handling, not doing that consistently at first was also a clear reflection point for me.
Very early in development, I judged this design as architecturally incomplete and dropped it. I did not want page initialization correctness to depend on local script placement habits. This experience became a direct reminder that reusing patterns across different architecture types is risky. A pattern that is valid in desktop UI can fail when lifecycle ownership and execution timing are controlled by the browser.
The design that survived: static registration
The approach that survived changed ownership of initialization. Instead of creating a controller instance directly in entry code, the page registers a controller type. The framework then controls when the instance is created and initialized.
This is the direction that became CotomyPageController.
CotomyPageController.set(class extends CotomyPageController {
protected override async initializeAsync(): Promise<void> {
// page initialization logic
}
});
Internally, set() registers a load-time path and runs initialization only when the page is ready. That moved timing responsibility from each screen script into one framework-level mechanism.
Why this structure worked
This structure solved several practical problems at once.
One controller per page became explicit. Page-level coordination had a stable home. Related UI references could be stored as controller properties. Multiple forms could be managed as one coordinated unit. List pages and detail pages could share the same control style.
Real screen behavior became easier to reason about once controller subclasses were shared as reusable page types. With common subclasses for list and detail flows, a row click could navigate to detail consistently. A page restored by browser back could reload data through one predictable path. Behavior moved from scattered handlers toward centralized lifecycle control through those shared controller boundaries.
In practice it is mostly for shared behavior
In day-to-day work, most pages are not extremely complex. So PageController is often used to standardize shared patterns rather than to host elaborate logic.
That is why base classes such as ListPageController and DetailPageController became useful in practice. They encapsulate common behavior, clarify responsibility, and reduce repeated setup code.
The long-term effect is simple. Development becomes faster, and maintenance becomes calmer, because each page has one obvious coordination boundary. Looking back, that small decision quietly changed how I structured every screen afterward. More importantly, this was the point where the design moved from a utility class library toward an actual framework boundary.
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: The next memoir topic is currently being planned.