Aman-Mittal opened a new issue, #287:
URL: https://github.com/apache/fineract-backoffice-ui/issues/287

   ## Business value
   
   Every form control in the app is written out by hand in the same shape:
   
   ```html
   <ion-item fill="outline">
     <ion-label position="stacked">{{ 'COMMON.NAME' | translate }}</ion-label>
     <ion-input
       [attr.aria-label]="'COMMON.NAME' | translate"
       [name]="'colName' + i"
       [(ngModel)]="column.name"
       required
     ></ion-input>
   </ion-item>
   ```
   
   Measured across `src/app`:
   
   | | |
   |---|---|
   | `ion-label position="stacked"` | **627** |
   | `[attr.aria-label]` on controls | **633** |
   | `ion-input` using Ionic 8's native `label=` | **0** |
   | Components re-declaring `.form-container { padding: 24px; max-width: N; 
margin: 0 auto }` | **87**, in **6** different max-widths |
   
   Three separate costs.
   
   **1. The visible label does not label the input.** In Ionic 8 the 
association comes from the component's own `label` property or a slotted label 
— `node_modules/@ionic/core/dist/collection/components/input/input.js:467-476`:
   
   ```js
   getLabelledById() {
       if (this.inheritedAttributes['aria-label']) { return undefined; }
       if (this.label !== undefined) { return this.labelTextId; }
       return this.labelSlot?.id || undefined;
   }
   ```
   
   A sibling `<ion-label>` sitting next to the input inside an `<ion-item>` is 
neither. It is the Ionic 7 pattern, and `STYLE.md:76` still prescribes it. So 
the visible text is decorative, and the accessible name comes **only** from the 
`[attr.aria-label]` — which is why all 633 of them exist. It works today, which 
is exactly why nobody has noticed that 627 of them are a workaround for markup 
that is one major version out of date. And it is fragile in a specific way: 
delete an `aria-label` as redundant-looking and the field silently becomes 
unnamed.
   
   **2. Roughly 4,000 lines of duplicated template.** 555 non-date control 
blocks average ~14 lines each; 73 date-picker triads (`ion-datetime-button` + 
`ion-modal` + `ion-datetime`) average ~18. The two largest forms are ~65% 
template by line count, and that template is almost entirely field markup.
   
   **3. Nowhere to put a validation message.** There is essentially no inline 
validation feedback anywhere in the app — one hand-rolled cross-field message 
in `journal-entry-form.component.ts:283-285`, and nothing else. Server-side 
errors go to a global toast that names the parameter in backend terms 
(`error.interceptor.ts:63-77`). A field-level error has no home to go to, and 
adding one today means editing hundreds of blocks.
   
   ## Why this is worth doing before any forms rework
   
   There is a live question about whether to move off template-driven forms. 
**This change is worth doing either way, and it makes whichever answer you pick 
dramatically cheaper.** It is the load-bearing step: once field markup lives in 
one component, swapping what drives it is an internal change rather than a 
128-file rewrite.
   
   For context on that separate question — `@angular/[email protected]` already 
ships `./signals` and `./signals/compat` as stable public API. 
`form<TModel>(model: WritableSignal<TModel>, schema)` takes a writable signal 
holding a plain object, which is exactly the shape this codebase already uses 
(`readonly product = signal<PostLoanProductsRequest>({...})`), and 
`validateHttp` maps an HTTP error response onto field errors. That is a strong 
argument against migrating to reactive forms as an intermediate step. **But 
that decision is not this issue** — do not make it here.
   
   ## Describing the change
   
   Two or three components in `src/app/shared/components/`:
   
   - `<app-form-field>` — label, control projection, required marker, hint, 
error slot
   - `<app-select-field>` — same, wrapping `ion-select`
   - `<app-date-field>` — the `ion-datetime-button` + `ion-modal` + 
`ion-datetime` triad, which is the most-copied and most error-prone block
   
   Use Ionic 8's native labelling — `label="…"` and `labelPlacement="stacked"` 
on the control itself — so the association is real and the `aria-label` becomes 
unnecessary rather than load-bearing.
   
   Signal inputs, per the house style (73 `input()`, one `@Input()` left in the 
codebase). Take the label as a translation key and translate inside, matching 
how `ColumnDef.label` already works.
   
   **This is deliberately not a full migration.** Land the components plus 
**one** converted form as the proof, and stop. Pick a mid-sized form — 
`src/app/features/organization/offices/office-form.component.ts` or similar — 
not `loan-product-form.component.ts`. Converting 150 forms in one PR is 
unreviewable and will conflict with everything.
   
   **Four things to get right.**
   
   1. **Do not break `[(ngModel)]`.** 574 bindings depend on it, and the 
wrapper must project the control rather than own it, or every call site changes 
shape. Content projection with the consumer supplying the `ion-input` is the 
safe design; a wrapper that renders its own input and re-exposes a value 
binding is a much larger change and should not be attempted here.
   2. **Keep `name` on the control.** Template-driven forms register by `name`; 
a wrapper that swallows it silently breaks form validity, and 
`[disabled]="form.invalid"` on 92 submit buttons then never enables.
   3. **Verify the accessible name after converting.** Confirm the control's 
accessible name is the label — via `getByLabel()` in a Playwright check, or 
`getByRole('textbox', { name: … })`. This is the whole point of using the 
native property; assert it rather than assuming.
   4. **Fold the `.form-container` styles into `src/styles/_common.scss`** as a 
shared class while you are here. 87 copies in 6 max-width variants is a visual 
inconsistency as much as a duplication, and it is a two-line change per file 
afterwards.
   
   ## Testing
   
   No platform acceptance test applies — this is presentation, and the platform 
contract is unchanged. That is the property the tests should pin down.
   
   - **A unit spec** for each new component: label renders, translation key 
resolves, required marker appears, the projected control keeps its `name`, and 
the accessible name equals the label.
   - **The converted form's existing spec must pass untouched.** If you find 
yourself editing its assertions, the wrapper has changed behaviour and the 
design needs revisiting. This is the strongest signal available that the 
refactor is faithful — say in the PR whether you had to touch it.
   - **An e2e assertion using `getByLabel()`** on the converted form. The suite 
currently has **zero** `getByLabel` calls, which is itself a symptom: labels 
are not associated, so nobody could write one. A passing `getByLabel` is direct 
proof the association now works.
   
   ## Scope
   
   In scope: the shared components, the shared container styles, and one 
converted form.
   
   Out of scope: converting the rest; any change to how form state is held; 
validation-message rendering beyond leaving a slot for it; the choice between 
template-driven, reactive and signal forms.
   
   ## Getting started
   
   - Existing shared components for house style: 
`src/app/shared/components/data-table/`, 
`src/app/shared/components/status-badge/`
   - Style guidance: `STYLE.md` — note it prescribes the Ionic 7 label pattern 
this issue is moving away from. **Update it in the same PR**, or the next 
contributor will reintroduce the old shape.
   - Adapter boundary: `DOCS/adr/0003-adapter-boundary.md`. Use `| 
appTranslate` from `src/app/core/adapters` in new components; importing 
`@ngx-translate/core` directly is banned by lint outside the composition root.
   - `npm test`, `npm run lint:prune`, `npm run i18n:check` and `npm run build` 
must pass.
   - Worth agreeing the component API on the issue before writing much — it 
will end up at hundreds of call sites, so the input names are worth ten minutes 
of discussion.
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to