Page Lifecycle Coordination

A design note on lifecycle control, coordination boundaries, and why initializeAsync-based page control prevents real business-screen failure patterns.

This note continues from CotomyElement Boundary . In the previous article, I explained why Cotomy starts from a DOM boundary through CotomyElement.

In this article, I want to move one layer up and explain why page lifecycle and coordination are centralized in CotomyPageController. I am not trying to show one more class pattern for its own sake. The real topic is what actually breaks on business screens when initialization and control flow are handled differently on every page.

Why Page Lifecycle Matters

Cotomy starts as a small reference module. You import what you need and use it directly.

import { CotomyElement, CotomyWindow } from "cotomy";

It is intentionally not a giant global framework that tries to own every runtime concern. Even so, business screens still have one unavoidable phase after page load: initialization.

Most screens need that phase because the UI is not truly operable at first paint. Inputs are not fully prepared, handlers are not connected yet, and remote data is still loading. In practice, initialization usually includes form setup for validation and submit contracts, page event binding for buttons and modals, startup loading for master and transaction data, restore logic for draft values or URL context, and failure behavior for unauthorized or expired sessions.

In Cotomy, CotomyWindow.ready is tied to the cotomy:ready custom event, so this timing is part of the lifecycle contract, not just an arbitrary callback point. Even on simple pages, lifecycle order matters in a very concrete way: the DOM becomes ready, form state is initialized, shared listeners are connected, required data arrives, and only then does the UI become safely operable. If each screen improvises this order, behavior starts to diverge almost immediately.

The Failure Pattern of Scattered Initialization

You can absolutely write page startup directly with DOMContentLoaded, like this:

document.addEventListener("DOMContentLoaded", () => {
  const forms = document.querySelectorAll(".form");
  forms.forEach(form => {
    form.addEventListener("submit", onSubmitEachForm);
  });
  loadInitialDataIndividually();
});

Technically, that works. The problem appears later, when each screen evolves in a slightly different way. One page uses DOMContentLoaded, another uses CotomyWindow.ready, one initializes a single form, another initializes multiple forms in a different order, and error fallback rules vary depending on who edited the screen last.

Then the familiar failures start to show up. Ready logic becomes page-specific and hard to compare, API exception handling is duplicated with inconsistent behavior, unauthorized handling becomes fragmented, and cross-form dependencies break because startup order is no longer reliable. You also see concrete regressions like summary totals updating before detail forms are ready, or click handlers binding before selector services exist.

The design point is simple: when page initialization is written outside the framework boundary on every screen, architecture drifts. You adopt a runtime model, but bypass its most important timing boundary.

What CotomyPageController Centralizes

CotomyPageController gives each page one clear control boundary. In other words, one page has one endpoint, one page has one control boundary, and that boundary is the page controller.

That single boundary keeps lifecycle responsibilities from fragmenting. It centralizes initializeAsync flow, form registration, screen event orchestration, and coordination points for shared failure policy.

import { CotomyEntityFillApiForm, CotomyPageController } from "cotomy";

CotomyPageController.set(class extends CotomyPageController {
  protected override async initializeAsync(): Promise<void> {
    await super.initializeAsync();
    this.setForm(
      CotomyEntityFillApiForm.byId<CotomyEntityFillApiForm>(
        "order-form",
        class extends CotomyEntityFillApiForm {}
      )!
    );
    this.setForm(
      CotomyEntityFillApiForm.byId<CotomyEntityFillApiForm>(
        "detail-form",
        class extends CotomyEntityFillApiForm {}
      )!
    );
  }
});

This sample is not about syntax tricks. It shows where lifecycle responsibility should live. Startup, form registration, and shared failure behavior stay inside one page boundary instead of being scattered across local handlers.

The same idea applies to unauthorized and session-expired flows. Re-auth can be implemented in different ways, but the decision point should remain in the page boundary, not per button and not per local ready block.

This gives you one predictable extension surface for page behavior. Screens no longer invent their own lifecycle style, and they follow one controller contract.

You can still use CotomyWindow.ready inside local components such as forms, but it should not replace page-level lifecycle control. In the current implementation, ready listens to the cotomy:ready event, and that event is fired only after the page load flow runs through window initialization and initializeAsync via CotomyPageController.set. So ready callbacks are local timing hooks under the same page boundary, not a separate lifecycle model.

Real Business Scenarios That Demand Coordination

Business pages are rarely single-form, single-action screens. Coordination work appears right away. Typical examples include related-entity search modals for customer or vendor selection, product dialogs with pricing and stock hints, re-auth during long editing sessions, initial load of master plus transaction data, rerender after save, cross-form value reflection, and multiple API calls that should share one failure policy.

CotomyPageController.set(class extends CotomyPageController {
  protected override async initializeAsync() {
    await super.initializeAsync();
    const orderForm = this.setForm(
      CotomyEntityFillApiForm.byId<CotomyEntityFillApiForm>(
        "order-form",
        class extends CotomyEntityFillApiForm {}
      )!
    );
    this.body.first("#select-customer")?.click(async () => {
      const selected = await this.app.customerSelector.open();
      orderForm.find("[name='customerId']").forEach(e => {
        e.value = selected.id;
      });
    });
  }
});

Another common case is a screen that depends on multiple startup loads in strict order. That order is part of the screen contract, so it needs to be expressed and preserved consistently across pages. This is not just data fetching; it is lifecycle control for operable UI state.

protected async initializeAsync() {
  await this.initializeScreenControllers();
  await this.applyInitialData();
  this.enableUserActions();
}

If you split this across ad-hoc ready handlers, race conditions become likely. Data can be applied before controllers or forms exist, handlers can run before dependencies are bound, and cross-form synchronization can start before all participants are registered. These are coordination failures, not component failures, so form-local logic alone cannot solve them.

Not SPA-Centric, But SPA-Compatible

Cotomy is not built as a giant SPA-first runtime, and that is still intentional. At the same time, page-level responsibility boundaries are not anti-SPA.

If each route or screen has one controller boundary and one lifecycle contract, the same design can scale into large SPA-style systems without immediate structural conflict. My current view is practical rather than absolute: this model is not tied only to traditional MPA, and the same coordination rules can work in SPA routing contexts as well.

I am already applying this model across multiple features in real applications, and I also plan to use it in a larger SPA project. So this direction is grounded in ongoing implementation work, not only in theory.

Separation Between UI Boundary and Application Logic

One more boundary matters here: Cotomy should not absorb everything.

Business screens need entity selection, auth control, and shared error policy, but those are not all core Cotomy responsibilities. A cleaner layering is to keep CotomyPageController as the UI boundary, keep screen use-case orchestration in the application service layer, and keep domain rules plus data authority in business logic and API layers.

CotomyPageController
Application Service Layer
Business Logic / API

At the page level, this looks like:

await this.app.auth.ensureAuthenticated();
const selected = await this.app.customerSelector.open();

With this split, roles stay clear. Cotomy handles UI boundary and lifecycle coordination, the application layer orchestrates screen use cases, and business/API layers own domain rules and authoritative data.

Without that separation, two problems show up quickly: UI controllers become pseudo-domain services, and domain calls leak directly into click handlers without a policy boundary.

In internal systems, this shared layer already covers more than entity fill behavior. It includes screen mode switching between view and edit, processing overlays, and side panels for related-entity selection, all implemented as application-layer features instead of Cotomy core features. Some of these patterns may be generalized in the future if they can be standardized without breaking boundary clarity.

Conclusion

Cotomy can begin as a small imported module, but business screens rapidly create lifecycle and coordination pressure. You can always write manual ready handlers, but long-term coherence breaks when every page does it differently.

CotomyPageController is used to keep initialization timing, form registration, shared failure policy, and cross-form coordination inside one control boundary. The goal is not abstraction for abstraction’s sake. The goal is to prevent operational drift in real business UI development.

Design Series

This article is part of the Cotomy Design Series, which explores architectural decisions behind the framework.

Series articles: CotomyElement Boundary , Page Lifecycle Coordination, Form AJAX Standardization , Inheritance and Composition in Business Application Design , API Exception Mapping and Validation Strategy , and Why Modern Developers Avoid Inheritance .

Links

Previous article: CotomyElement Boundary Next article: Form AJAX Standardization

Learn Cotomy

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