Standardizing CotomyPageController for Shared Screen Flows

A practical example of structuring business list screens with a reusable CotomyPageController base class.

This continues from CotomyPageController in Practice .

Business Screens Often Share the Same Structure

As discussed in other articles of this journal, business systems usually define many entities, and those entities are connected through complex relationships. Users still need to perform concrete operations on them every day: search, inspect, create, update, and delete.

Create and edit requirements can vary widely by business rule and approval flow. Some screens need simple forms, while others require conditional fields, embedded tables, or staged confirmation. Even then, the operational core is often still CRUD with the same user intent: find data, understand data, and change data safely.

Entity types are also broader than a short list can express. Depending on the system, targets can include users, products, suppliers, orders, production records, inventory snapshots, pricing rules, and many other domain-specific units. Display and search patterns differ, but teams usually keep the same operational screen flow so users can move across modules without relearning the UI.

That consistency is not only an implementation convenience. It helps users become productive faster and reduces hesitation during operations. For that reason, screens with completely different interaction styles should be introduced carefully and only when requirements truly demand it.

Example: A List Page Structure

A typical list page starts with a condition form and a table. Each row can navigate to a detail screen when clicked.

<form id="condition-form">
    <input type="text" name="keyword" placeholder="Search">
    <button type="submit">Search</button>
</form>

<table id="entities-table">
    <thead>
        <tr>
            <th data-sort="id">ID</th>
            <th data-sort="name">Name</th>
        </tr>
    </thead>
    <tbody>
        <tr data-entity-id="1">
            <td>1</td>
            <td>Example</td>
        </tr>
    </tbody>
</table>

This same pattern appears across many screens: user lists, product lists, and order lists.

Introducing CotomyListPageController

Because this structure repeats across many screens, it is natural to standardize the behavior around it.

A practical way to formalize this pattern is defining a controller class such as CotomyListPageController on top of CotomyPageController.

This is not presented as the only right architecture. It is one concrete pattern for building an application-level foundation: you define behavior categories first, then place each screen in a clear category boundary.

In this approach, a list controller is not only about reusing event handlers. It becomes a structural base for a screen category inside the application, which helps both implementation consistency and design decisions.

Class Structure

classDiagram
    CotomyPageController <|-- CotomyListPageController
    CotomyListPageController --> CotomyElement : entitiesTable
    CotomyListPageController --> CotomyQueryForm : conditionForm

Lazy Element Access Pattern

One useful pattern is storing CotomyElement | null members and resolving them lazily in getters with ??=.

import { CotomyPageController, CotomyElement } from "cotomy";

export class CotomyListPageController extends CotomyPageController {

    private _entitiesTable: CotomyElement | null = null;

    protected get entitiesTable(): CotomyElement {
        return this._entitiesTable ??= CotomyElement.byId("entities-table")!;
    }

}

This keeps DOM lookup logic centralized and makes page structure explicit inside the controller. It keeps DOM access predictable and avoids repeated lookups when the same element is used across multiple controller methods.

Initializing the List Behavior

List pages usually need the same initialization: condition form setup and row click navigation.

In CotomyPageController, initializeAsync is the right place for this screen-level wiring. CotomyQueryForm is a natural fit here because list screens usually express search state in the URL. It reads the form values, rebuilds the query string, and navigates to the updated URL, which keeps search and paging behavior explainable.

import { CotomyPageController, CotomyElement, CotomyQueryForm } from "cotomy";

export class CotomyListPageController extends CotomyPageController {

    private _entitiesTable: CotomyElement | null = null;

    protected get entitiesTable(): CotomyElement {
        return this._entitiesTable ??= CotomyElement.byId("entities-table")!;
    }

    protected async initializeAsync(): Promise<void> {
        await super.initializeAsync();

        const conditionForm =
            this.body.first("#condition-form", CotomyQueryForm);

        conditionForm?.initialize();

        this.entitiesTable.onSubTree(
            "click",
            "tr[data-entity-id]",
            e => {
                const row = (e.target as HTMLElement)
                    .closest("tr[data-entity-id]");

                if (!row) return;

                const id = row.getAttribute("data-entity-id");
                if (!id) return;

                location.href = `/entities/${id}`;
            }
        );

    }

}

In a real application, this often navigates to a detail page such as /products/{id} or /users/{id}. The /entities/{id} path here is only a generic example.

With this structure, every list screen can share the same base behavior, while each screen still controls its own endpoint and table schema.

Initialization Sequence

sequenceDiagram
    participant Script as Page Script
    participant W as CotomyWindow
    participant PC as CotomyPageController
    participant LC as CotomyListPageController
    participant QF as CotomyQueryForm
    participant T as entitiesTable

    Script->>PC: CotomyPageController.set()
    W->>LC: call initializeAsync() on window load
    LC->>PC: super.initializeAsync()
    LC->>QF: initialize condition form
    LC->>T: register onSubTree click handler
    W->>W: trigger cotomy:ready

Extending the Base Controller

In my own systems, the base controller layer is where operational common features are placed, especially for long-lived business screens.

For edit screens, one recurring concern is session timeout during AJAX operations. A shared controller can centralize re-login behavior and recovery flow so each screen does not reimplement the same failure handling.

The same applies to shared header and menu interactions. Navigation toggles, layout actions, and common shell controls are usually application-level concerns, so keeping them in a base controller keeps feature screens focused on business behavior.

In larger systems, I also separate controllers by segment, split by user group or business area. That segmentation can map to separate projects, for example by csproj units, and each segment can have its own page controller lineage.

This gives a stable structure such as: App-level base controller, then segment controller, then screen-category controller like list or detail, and finally the page-specific controller.

Why Page-Level Structure Matters

Common code can also be extracted as utility functions. That is valid, but page-level behavior often remains easier to reason about when grouped in page controllers.

Inheritance debates are common, and many teams prefer to avoid it entirely. In my own work, a shallow controller hierarchy has been a readable option for operational business screens, because it makes screen structure and shared behavior boundaries easy to see.

If inheritance does not fit your team style, composition with explicit wiring is also a good choice: reusable behaviors such as list navigation or pagination can be implemented as separate objects and injected into a page controller instead of inherited.

In practice, maintainability usually gets worse when the decision becomes ideological, either always inherit or never inherit. What matters more is whether boundaries stay clear as the system grows.

For my own projects, these checks are practical:

  1. If the shared behavior runs on the same page lifecycle, a base controller is often reasonable.
  2. If differences are mostly configuration, shallow inheritance usually stays readable.
  3. If the hierarchy grows deep, composition is usually safer.
  4. If impact analysis becomes hard, refactor boundaries before adding features.

More importantly, it gives designers and implementers a concrete base to define application feature categories such as list pages, detail pages, and editor pages. That categorization guides how new screens should be structured from the start.

Having said all that, as a side note, I personally still like solving this area with inheritance. I do avoid unnecessary depth, but if each class has a clear name and stands on its own meaning, I do not think deeper layering needs to be rejected automatically.

At the same time, I know many people think very differently about this. Especially developers who are used to frontend-heavy workflows often avoid inheritance itself, and that is a valid approach. React, for example, is generally structured around composition rather than class inheritance. So this is simply my personal preference from business-screen architecture, not something I want to impose on others. If Cotomy feels less suited to composition-heavy extension, that may simply be my own design preference showing through.

Conclusion

CotomyPageController makes it easier to stabilize repeated business screen patterns such as list, detail, and editor flows.

CotomyListPageController in this article is only one example, but standardizing behavior at the page level can significantly improve consistency and maintainability in long-lived business applications.

Practical Guide

This article is part of the Cotomy Practical Guide, which focuses on hands-on usage patterns for the framework.

Series articles: Working with CotomyElement , CotomyPageController in Practice , Standardizing CotomyPageController for Shared Screen Flows, and Building Business Screens with Cotomy .

Next

Next article: Building Business Screens with Cotomy , with a code-first Razor Pages example that wires one screen with CotomyPageController and CotomyEntityFillApiForm.

Links

Previous: CotomyPageController in Practice . More posts: /posts/ .

Learn Cotomy

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