Skip to content

Make the component library plug-and-play behind an adapter boundary #530

Description

@Aman-Mittal

A proposal for discussion. Nothing here has been implemented.

Goal: swapping the component library should mean reimplementing an adapter, not editing 296 files. The same change makes the app more customizable, because whatever the app owns is what a deployment can change.

Where we are

  • 5,424 <ion-*> occurrences across 296 files; 299 files import @ionic/angular/standalone.
  • About 62% of that is cosmetic — cards, buttons, icons, grid.
  • ~770 ngModel bindings across six control types, with zero custom ControlValueAccessors. Ionic currently defines what a value is.
  • Good news: there is no Ionic app shell (no ion-app, ion-content, ion-router-outlet, ion-menu, ion-tabs). The layout is ours already.

The catch

Wrapping components fixes the source, but the source is the cheap half.

Would need changing
Unit tests ~15–20% of 237 files, mostly one-line import swaps
Source 296 files, but ~62% cosmetic
E2E 39 of 52 specs, 16 of which bypass our helper layer entirely

E2E asserts against rendered DOM, so wrapping components does nothing for it by itself. The boundary has to cover the test seam too, or it is not plug and play.

Proposed design

src/app/ui/ holds app-owned primitives (app-button, app-select, app-input, …). The public API is ours; the vendor lives inside. A swap reimplements that directory.

Three tiers, because they have different answers:

  1. Cosmetic — card, button, icon, layout. A template swap. High volume, low risk.
  2. Form controls — each owns its ControlValueAccessor. This is the important one: it moves the value contract (ISO date strings, select values) into the app, so a swap fails loudly instead of corrupting data quietly. Keeping today's selectors means ~770 bindings move without touching 296 templates.
  3. Behavioural (select popup, tabs, overlays) — build on @angular/cdk, which AGENTS.md already retains "for unstyled primitives" and which @angular/cdk/table already uses in 28 files. CDK is headless, so this tier becomes permanently vendor-independent rather than merely swappable.

Test seam: each primitive exposes a stable data-testid and known ARIA, so e2e/utils/ targets our contract instead of Ionic's internals. That is what makes the 39-spec number fall.

Theming: _ionic-theme.scss already maps app tokens onto Ionic's in the right direction. Make that the whole contract, so a deployment is not capped at the current 16 BRANDABLE_TOKENS by what Ionic happens to expose.

Getting there incrementally

The repo has already proven this mechanism twice: @angular/material was banned by no-restricted-imports and reached zero, and @ngx-translate is on a ratchet that has gone 435 → 323.

  1. Ban @ionic/angular/* outside src/app/ui/**.
  2. Seed eslint-suppressions.json with today's offenders, so nothing breaks now.
  3. --prune-suppressions means the count can only fall; new Ionic imports outside ui/ fail lint immediately.
  4. Migrate one primitive at a time — tier 3 first (smallest and riskiest), then tier 2, then the test seam, then the cosmetic tail.

Why it is worth doing

We have done this before. The Material → Ionic migration left five dead CSS rules for mat-form-field, eleven files hand-tracking state the old library used to own, and a nav doc that still says "Material icon name". Stale mat-select selectors in the guided tour pointed at nothing until #526.

For scale: scripts/codemod-aria-labels.mjs records that Ionic's 7 → 8 label change alone touched 132 files. That is a point release, without this boundary.

Notes

  • DOCS/adr/0003-adapter-boundary.md deliberately excluded <ion-*>, while saying the boundary "is built so this can be added later". This is that later. AGENTS.md:84 states the current policy and would need updating too.
  • Unrelated but worth knowing: the "323 call sites" in npm run ga:check is not Ionic. It is 285 ngx-translate call sites plus 15 storage ones; zero are Ionic components. Easy to misread as this problem.
  • Assumes one library at a time. Supporting two at once is a much larger design.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions