Engineering Journal
Schema Editor
Schema Editor

One Codebase, N Deployment Contexts: The Web Component Configuration Pattern

2026-06-04

TLDR

Maintaining four copies of a 900-line HTML file, one per domain variant, guarantees drift within weeks. A web component with a domain attribute interface reduces each domain page to a thin shell. Bug fixes apply once and propagate everywhere. The component encapsulates the tool; the host page provides the context.


The Problem Class

Many developer tools run in multiple contexts: the same editor for different domains (electrical, architectural, software), the same dashboard for different clients, the same UI component in light and dark themes. The question is how to share the core implementation across contexts while varying the configuration.

The options are: N separate files (simple to create, guaranteed to drift), a build system with N entry points (correct but adds tooling), or a web component with an attribute interface (correct, zero build step, and composable with any host page).

The Naive Approach

Copy the working file. Change the domain-specific parts. Repeat for each variant. This is the fastest path to a working second variant. It produces N independent files that share no code at the implementation level.

The first month: the files diverge by the amount of intentional domain customization. This is expected and correct.

The second month: a bug is fixed in one file. It is manually applied to two others. The third file is missed.

The third month: a feature is added to the electrical variant. It requires refactoring the canvas engine in that file. The same refactor is needed in the other three files. It is done in two. The other two are now on a different architecture.

Why It Breaks

Copy-paste code sharing is not code sharing. It is code divergence with a shared starting point. The only way to keep N copies in sync is to apply every change to every copy. The probability that this happens perfectly across all changes, for all developers, in all circumstances, is zero.

The divergence is self-reinforcing. Once two copies diverge, merging changes between them requires understanding both. The cost of merging grows with divergence. So developers apply changes to one copy (the "main" one) and schedule the others for "later." Later never comes.

The Better Model

A web component encapsulates the tool implementation. Domain pages host the component and pass configuration through attributes:

// The component: reads domain attribute, configures itself
class SchemaEditor extends HTMLElement {
    static get observedAttributes() { return ['domain']; }

connectedCallback() { const domain = this.getAttribute('domain') || 'general'; this._init(domain); }

attributeChangedCallback(name, oldVal, newVal) { if (name === 'domain' && oldVal !== newVal) { this._switchDomain(newVal); } }

_init(domain) { // Single initialization path, domain-specific config loaded by name const config = DOMAIN_CONFIGS[domain]; this._setupPalette(config.palette); this._setupRules(config.rules); this._setupExport(config.exportFormats); } } customElements.define('schema-editor', SchemaEditor);

Domain pages become thin shells:

<!-- electrical/index.html -->
<!DOCTYPE html>
<html>
<head>
    <title>Electrical Schematic Editor</title>
    <script src="/tools/schema-editor/schema-editor.js" type="module"></script>
</head>
<body>
    <schema-editor domain="electrical"></schema-editor>
</body>
</html>

The electrical page is 15 lines. The construction page is 15 lines with domain="construction". Every domain page is identical except the attribute value and the page title.

Domain-specific configuration lives in the component as a data object, not as branching logic:

const DOMAIN_CONFIGS = {
    electrical: {
        palette: ['resistor', 'capacitor', 'ic', 'wire', 'gnd'],
        rules: { orthogonalWires: true, snapGrid: 10 },
        exportFormats: ['svg', 'netlist', 'bom'],
    },
    construction: {
        palette: ['wall', 'door', 'window', 'hvac'],
        rules: { orthogonalWires: false, snapGrid: 50 },
        exportFormats: ['svg', 'dxf'],
    },
};

Adding a new domain means adding a config object and a new 15-line host page. No duplication of component logic.

Tradeoffs

The web component approach requires that all domain variants use the same underlying implementation. If the electrical and construction domains have genuinely different canvas architectures (not just different configs), the component cannot unify them without branching inside the component.

The custom elements API has adoption costs: not all teams are familiar with the connectedCallback/attributeChangedCallback lifecycle. Documentation needs to cover the attribute interface. But these are one-time costs.

The One Thing to Watch For

The connectedCallback fires before child elements are parsed. If the component's initialization relies on content inside the element (slotted elements, inline config), use a setTimeout(0) or requestAnimationFrame to defer initialization, or use the slot API explicitly. Domain attribute configuration avoids this issue entirely because it reads from attributes, not children.

Read this post in the full Engineering Journal →