Code Style Guide

This project follows strict engineering standards to ensure high-quality, maintainable code.

General Principles

  • Strict Mode: TypeScript strict mode is mandatory.
  • Standalone Components: Use the Angular standalone component model. There are no NgModules.
  • Immutability: Prefer immutable data patterns and readonly properties.
  • Signal-First: Use Angular Signals for state management where appropriate.

Architecture

  • Core: Contains singleton services, interceptors, and application-wide utilities.
  • Shared: Contains reusable components, pipes, and directives.
  • Features: Domain-specific components organized by feature area (e.g., clients, loans).
  • Layout: Contains top-level layout components like the header and sidebar.

Naming Conventions

  • Components: kebab-case.component.ts (e.g., user-profile.component.ts).
  • Services: kebab-case.service.ts (e.g., client-data.service.ts).
  • Interfaces/Models: kebab-case.model.ts (e.g., client-summary.model.ts).

UI Components: Ionic

The UI layer is Ionic (@ionic/angular v8). Ionic is configured in mode: 'md' (src/app/app.config.ts), so components render in Material Design styling.

Angular Material has been removed entirely, and npm run lint fails on any import of it. The table below records the equivalents, which are also the conventions for new code. @angular/cdk is retained and is fine to use for unstyled primitives (cdk-table, virtual scroll, a11y) — the shared data table is built on it.

Importing Ionic components

Always import individual components from the standalone entry point and list them in the component's own imports array. Never use IonicModule.

import { IonButton, IonIcon } from '@ionic/angular/standalone';

@Component({
  standalone: true,
  imports: [TranslateModule, IonButton, IonIcon],
  // ...
})

Component equivalents

Material (removed)Ionic equivalent
mat-cardion-card / ion-card-header / ion-card-title / ion-card-content
mat-form-field + matInpution-item fill="outline" + ion-label position="stacked" + ion-input
mat-select / mat-optionion-select / ion-select-option
button mat-buttonion-button
button mat-icon-button + mat-iconion-button fill="clear" + ion-icon slot="icon-only"
mat-checkbox / mat-slide-toggleion-checkbox / ion-toggle
mat-spinnerion-spinner
mat-progress-barion-progress-bar
mat-datepickerion-datetime-button + ion-modal + ion-datetime (see below)
matTooltip[attr.title], or [attr.aria-label] on icon-only controls
MatSnackBarNotificationService (see below)
MatDialogDialogService (see below)
mat-table + mat-paginatorthe shared app-data-table, or cdk-table + app-paginator directly

Ionic has no tooltip component. Use [attr.title] for hover text and always give icon-only buttons an [attr.aria-label].

Date pickers

<ion-item fill="outline">
  <ion-label position="stacked">{{ 'TELLERS.START_DATE' | translate }}</ion-label>
  <ion-datetime-button datetime="start-date-picker"></ion-datetime-button>
  <ion-modal [keepContentsMounted]="true">
    <ng-template>
      <ion-datetime
        id="start-date-picker"
        data-testid="start-date-picker"
        presentation="date"
        (ionChange)="onStartDateChange($event)"
      ></ion-datetime>
    </ng-template>
  </ion-modal>
</ion-item>

The datetime attribute on the button must match the id on ion-datetime, and [keepContentsMounted]="true" is required for the value to bind before first open.

Events

Ionic emits CustomEvents. Read detail.value, and keep a target.value fallback — unit tests dispatch plain DOM events:

onInput(event: Event): void {
  const value =
    (event as CustomEvent<{ value?: string }>).detail?.value ??
    (event.target as HTMLInputElement)?.value ??
    '';
  // ...
}

Bind (ionInput) for text entry and (ionChange) for selects, toggles and date pickers.

Icons

Icons are ionicons, referenced by kebab-case name:

<ion-icon name="create-outline" slot="icon-only"></ion-icon>

Every icon must be registered in src/app/core/icons.ts, which src/bootstrap.ts passes to addIcons() once at startup. An unregistered name renders as a blank space, silently. To add one, import it from ionicons/icons and add an entry to APP_ICONS; icons.spec.ts guards the registry's integrity.

Notifications and dialogs

Inject the services rather than Ionic's controllers directly — they keep durations, placement and confirm semantics consistent, and they are far easier to mock in tests.

private readonly notifications = inject(NotificationService);
private readonly dialogService = inject(DialogService);

this.notifications.success(this.translate.instant('CLIENTS.SAVED'));
this.notifications.error(message); // 10s, preserves newlines in stacked API errors

const confirmed = await this.dialogService.confirm({
  title: this.translate.instant('COMMON.DELETE'),
  message: this.translate.instant('LOANS.CONFIRM_DELETE_NOTE'),
  destructive: true,
});

Styling and theming

Design tokens live in src/styles/_common.scss (--primary-color, --card-bg, --border-color, …). src/styles/_ionic-theme.scss maps Ionic's --ion-color-* and surface variables onto them, so Ionic components inherit the Fineract palette automatically.

  • Style Ionic components through their CSS custom properties (--background, --border-radius, --padding-start) and shadow parts (::part(message)) — not by targeting internal classes.
  • Prefer the shared tokens over hard-coded colours.
  • Dark mode is driven by the [data-theme='dark'] attribute set by ThemeService. Do not introduce a second mechanism such as Ionic's .ion-palette-dark or a bare prefers-color-scheme query.
  • Component styles are inline in the styles: [] array. Keep them small — the production build errors above 8 kB per component.

Formatting and Linting

  • Prettier: Run npm run format to apply formatting; npm run format:check in CI.
  • ESLint: Run npm run lint. Note the sonarjs/void-use rule — the void operator is banned, so do not use void somePromise() to mark a floating promise.

Testing

  • Runner: Vitest (npm test). The jest.config.ts in the repo root is unused.

  • Unit Tests: Every service and complex component should have a corresponding test.ts.

  • Test Coverage: Aim for at least 80% statement coverage.

  • Ionic in TestBed: Components that use Ionic overlays — directly, or via NotificationService / DialogService — need Ionic's providers, or they fail with NG0201: No provider found for _ModalController:

    import { provideIonicTesting } from '../../testing/ionic-testing';
    
    TestBed.configureTestingModule({
      imports: [MyComponent, TranslateModule.forRoot()],
      providers: [provideIonicTesting()],
    });
    
  • Selectors: Give interactive elements both an id and a data-testid. The Playwright suite prefers getByRole/getByTestId; never write assertions against framework-internal classes.

Internationalization (i18n)

  • No Hardcoded Strings: All user-facing text must use translation keys and follow the project's i18n strategy. Run npm run i18n:check to verify translations are complete.