Build client UI as structured web components

Best practice Recommended
When a client-side Docly solution grows beyond a single page, structure the UI as native Web Components (custom elements) instead of one large script that manipulates the DOM by hand. Encapsulating markup, behavior and state inside reusable elements keeps the frontend maintainable, testable and portable across pages — and works without any framework or build step.

What you'll see

A client-side Docly app starts as a single index.html with one big app.js that queries elements by id, builds markup with string concatenation, and wires up event handlers globally. It works at first. As the app grows — more screens, more lists, more forms calling more #/API/ endpoints — the script becomes a tangle of document.getElementById, duplicated render logic, and shared mutable state. A change to one widget breaks another, the same card markup is copy-pasted across pages, and nobody can tell which function owns which part of the DOM.

What's actually happening

Docly serves plain HTML, CSS and JavaScript for client-side apps and handles the backend through #/API/ endpoints. Nothing about that requires — or excludes — a particular frontend architecture. The browser's own Web Components standard (custom elements, shadow DOM, templates) is available everywhere, needs no framework, no bundler and no build step, which makes it a natural fit for the zero-config Docly model.

A custom element bundles three things that otherwise drift apart: the markup it renders, the behavior that reacts to user input, and the state it owns. Instead of a global script reaching into shared DOM, each element is responsible for its own subtree. Shadow DOM scopes its styles so one component's CSS can't leak into another's. The element exposes a small surface — attributes and properties in, events out — so pages compose features by placing tags, not by orchestrating imperative DOM code.

The payoff grows with the app. A <customer-card> defined once renders identically on the dashboard, the search results and the detail page. A <data-table> that fetches from a #/API/ endpoint can be dropped onto any page with a single attribute. Bugs stay local to the component that owns them, and a component can be tested in isolation because it has no hidden dependency on the rest of the page.

What to do

Once a client-side app has more than a couple of repeated UI pieces or more than one page, define those pieces as custom elements rather than ad-hoc DOM scripts.

  • One component per file — keep each customElements.define(...) in its own JS file under your UI folder, named after the tag.
  • Encapsulate state — hold the component's data in instance fields, not global variables. Re-render from render() when it changes.
  • Attributes in, events out — pass data down via attributes/properties; signal up via this.dispatchEvent(new CustomEvent(...)). Don't reach across components.
  • Fetch with ~/ — call endpoints as fetch('~/API/...') so the component is portable across working copies (see related entry).
  • Scope styles — use shadow DOM (or a clear class prefix) so component CSS doesn't leak.

Don't — one global script owning the whole DOM:

// app.js
async function loadCustomers() {
  const res = await fetch('/API/customers');   // also wrong: leading slash
  const data = await res.json();
  const list = document.getElementById('list');
  list.innerHTML = data.map(c =>
    `<div class="card">${c.name}</div>`).join('');
}
loadCustomers();

Do — a self-contained component:

// /ui/customer-list.js
class CustomerList extends HTMLElement {
  connectedCallback() { this.load(); }

  async load() {
    const res = await fetch('~/API/customers');
    this.customers = await res.json();
    this.render();
  }

  render() {
    this.innerHTML = '';
    for (const c of this.customers) {
      const card = document.createElement('customer-card');
      card.customer = c;
      this.appendChild(card);
    }
  }
}
customElements.define('customer-list', CustomerList);

Compose pages by placing tags:

<!-- /ui/index.html -->
<script src="/ui/customer-card.js"></script>
<script src="/ui/customer-list.js"></script>

<customer-list></customer-list>

The same <customer-card> now renders on every page that needs it, the list owns its own fetch-and-render cycle, and a change to either stays contained. The app scales by adding tags, not by growing one script.