apache / apache/fineract-backoffice-ui
Make the component library plug-and-play behind an adapter boundary
- Dominant language
- TypeScript
- Stars
- 15
- Forks
- 60
- Avg merge
- 10h 15m
- Merged PRs (30d)
- 108
Description
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` `` 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 `ControlValueAccessor`s**. 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 ``, 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.
Contributor guide
Research direction
This is a design proposal rather than a bounded edit. Start by reading DOCS/adr/0003-adapter-boundary.md and AGENTS.md:84, then inspect src/app/ui/, e2e/utils/, eslint-suppressions.json, and scripts/codemod-aria-labels.mjs. Done requires an agreed adapter design and an incremental migration plan covering source, lint enforcement, form contracts, and the E2E test seam.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- angular, typescript
- Domain
- frontend
- Issue type
- Refactor
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 25/100