Skip to content

Drive customElements.define() from the registry — replace the hand-rolled registerWebComponents() list #161

@dlrice

Description

@dlrice

Context
src/protvista-uniprot.ts still defines a hand-rolled registerWebComponents() method that calls loadComponent() once per built-in nightingale tag (ten entries: nightingale-navigation, nightingale-track-canvas, nightingale-colored-sequence, nightingale-sequence, nightingale-variation, nightingale-linegraph-track, nightingale-filter, nightingale-manager, nightingale-sequence-heatmap, plus protvista-uniprot-structure). The list is byte-identical to the pre-refactor element except for one entry (nightingale-interpro-track, removed in commit 5f8250b when features-interpro migrated to track-canvas).

The config-as-data refactor moved component names into the registry — every semantic kind resolves to a component: string — but the actual customElements.define() call still happens via the hand-rolled list. Consumer-defined semantic kinds whose components live outside this list cannot register their own tag through the registry; the embedder has to call loadComponent() themselves, defeating the point of registerSemanticKind().

Captured in docs/hardcoded-assumptions-audit.md as item B18.

Task
Drive component registration from the registry instead. After loadConfig() resolves the config and the registry knows every component name referenced by every active semantic kind, walk the resolved set and call loadComponent(name, ctor) for each — looking up the constructor via a (name → ctor) map that built-ins seed at boot and consumers extend via a new element.registerComponent(name, ctor) API.

Scope:

  • Add a components bucket to the registry (registerComponent, getComponent, hasComponent, listComponents), seeded with the ten built-in (name, ctor) pairs currently in registerWebComponents(). Mirror the collision behaviour of the existing buckets — re-registering a built-in throws RegistryCollisionError.
  • Expose registerComponent(name, ctor) on the element's runtime API alongside registerAdapter / registerSemanticKind / registerTheme.
  • After the loader has produced a NormalizedConfig, walk the resolved kinds + explicit component: overrides, collect the unique set of component names, look each up in the registry, and call loadComponent(name, ctor) for them. Skip any name already defined on customElements (the existing loadComponent helper already does this guard).
  • Delete registerWebComponents() from src/protvista-uniprot.ts. The init flow becomes: loadConfig → register the components the config actually uses → mount.
  • Update docs/architecture.md's "How a config becomes pixels" section: the registry now owns component resolution end to end, not just name-mapping.

Acceptance:

  • A consumer-defined semantic kind whose component: resolves to a name not in the built-in set works end-to-end without the consumer touching customElements.define themselves — they just call element.registerComponent('my-track', MyTrack) once and reference it via kind:.
  • No regression on the existing default config — the same 10 nightingale tags are still defined after mount, but via the registry walk rather than the hand-rolled list.
  • src/protvista-uniprot.ts no longer imports any @nightingale-elements/* constructor directly; those imports move to a small src/built-in-components.ts module that the registry seeds from.
  • Tests cover: (a) the built-in walk registers every component the default config references; (b) a consumer-registered component is picked up when its kind appears in the config; (c) a kind referencing an unknown component fails validation with a useful message before mount, not at customElements time.

Notes:

  • The loadComponent helper in src/utils/ stays as-is — it's the right level of abstraction for the actual customElements.define call (with the already-defined guard). What changes is who calls it, not how.
  • Keep the built-in seeding in a dedicated module (src/built-in-components.ts alongside registerBuiltinAdapters) so consumers wanting a leaner build can tree-shake the constructors they don't use.

Metadata

Metadata

Assignees

No one assigned

    Labels

    nextIssue which pertains to the next version of ProtVista.

    Type

    No type

    Projects

    Status

    Backlog

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions