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

   ## What is happening
   
   Every form in the application binds like this:
   
   ```html
   <!-- src/app/features/products/loan-product-form.component.ts:159 -->
   <ion-input name="shortName" [(ngModel)]="product().shortName" />
   ```
   
   `product` is a signal. `product()` returns the object, and `[(ngModel)]` 
reads and writes a property *on that object*. Two separate problems follow.
   
   **The write does not notify anything.** Mutating `product().shortName` 
changes the object the signal already holds. The signal's reference is 
unchanged, so nothing that derives from it recomputes and nothing marks the 
view dirty. Angular 22 makes `OnPush` the default, so a `computed()` over 
`product`, or any sibling binding reading the same signal, can hold a stale 
value for as long as nothing else triggers a check.
   
   **The read fires NG0100 when the record loads.** Observed against a real 
backend on `/loans/view/3`:
   
   ```
   RuntimeError: NG0100: ExpressionChangedAfterItHasBeenCheckedError:
     Expression has changed after it was checked.
     Previous value: 'undefined'. Current value: 'NXA0'.
     Expression location: _LoanProductFormComponent component
       at Module.ɵɵtwoWayProperty
       at LoanProductFormComponent_Template
   ```
   
   `ɵɵtwoWayProperty` is this binding. `'NXA0'` is a real loan product short 
name arriving from the HTTP response between Angular's first pass and its 
verification pass.
   
   ## Business value
   
   This is the same correctness problem #184 was opened for, in the one shape 
that issue explicitly listed and did not finish. From its scope:
   
   > Split `[(ngModel)]` bindings that now target a signal into `[ngModel]` + 
`(ngModelChange)`, since a signal cannot be a two-way binding target.
   
   #184 is closed as completed; 275 of these bindings remain.
   
   The user-visible symptom is a form field that shows the old value, or a 
dependent field that does not react to an edit, with no error on screen. In 
loan product and loan application forms that is a field an officer believes 
they changed.
   
   It also blocks two things concretely:
   
   - **Enforcing the NG0100 check.** `e2e/fixtures.ts` watches for it and 
reports rather than fails, waiting for the sources to reach zero. This is one 
of the two remaining sources.
   - **`provideZonelessChangeDetection()`.** Zone.js currently supplies the 
accidental second trigger that hides these. Removing it without fixing them 
first breaks all 275 at once — #184 makes the same point.
   
   ## Finding them
   
   ```bash
   grep -rn '\[(ngModel)\]="[a-zA-Z_]*()\.' src/app --include=*.ts | wc -l   # 
275
   grep -rn '\[(ngModel)\]="[a-zA-Z_]*()\.' src/app --include=*.ts \
     | sed 's/:[0-9]*:.*//' | sort | uniq -c | sort -rn                      # 
45 components
   ```
   
   Largest first: `products/loan-product-form` (38), 
`working-capital/loan-products/wc-loan-product-form` (22), `loans/loan-form` 
(18), `clients/kyc/client-address-form` (13), 
`clients/kyc/client-family-member-form` (11), `security/users/user-form` (10), 
`clients/client-form` (10), `organization/staff/staff-form` (9), 
`loans/guarantors/guarantor-form` (9), `system/oidc-config` (8), 
`accounting/charges/charge-form` (8), `products/shares/share-product-form` (7), 
then a long tail.
   
   ## Describing the change
   
   Split the binding so the write goes through the signal rather than around it:
   
   ```html
   <!-- before -->
   <ion-input [(ngModel)]="product().shortName" />
   
   <!-- after -->
   <ion-input
     [ngModel]="product().shortName"
     (ngModelChange)="product.update((p) => ({ ...p, shortName: $event }))"
   />
   ```
   
   For a form with many fields, a single `patch(field, value)` helper on the 
component reads better than repeating the spread at each call site. Where the 
whole object is a form, consider whether a typed `FormGroup` is a better fit 
than 38 individual signal writes — but that is a larger decision and should not 
block the mechanical fix.
   
   **One pull request per form**, largest first. Each is independent.
   
   ## Scope
   
   In scope: the 275 bindings, and a test per form proving an edit is 
observable through the signal.
   
   Out of scope: converting template-driven forms to reactive forms wholesale, 
and the separate `ng-untouched` NG0100 shape tracked in the companion issue.
   
   ## A related regression worth noting
   
   `node scripts/audit-async-state.mjs` reports **5 plain async fields across 5 
components** — `products/deposit-closure-dialog` (`onAccountClosureId`), 
`products/savings-account-view` (`accountId`), `system/oidc-config` (`exists`), 
`system/report-definitions/report-definition-form` (`report`), 
`tellers/cashiers/cashier-transaction-form` (`currencyCode`). #184's acceptance 
criterion was zero. These five are a small, self-contained warm-up for anyone 
picking this up.
   
   ## Getting started
   
   - `npm run build` is the only check that type-checks templates, so it is 
what catches a signal read missing its call.
   - `npx playwright test --project=mocked` needs no backend and prints 
`[change-detection]` warnings per test; fixing a form should remove its 
component from that output.
   - `ENFORCE_CD_ERRORS=1` turns the warnings into failures — useful locally on 
a single spec to confirm a form is clean.
   - `scripts/codemod-signals.mjs` exists and may do some of the mechanical 
work; read it before hand-editing 38 bindings.
   


-- 
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