Coding Conventions

Naming

Element Rule Example

Component selector

pyb- prefix, kebab-case

pyb-advance-line-widget

Component class

Suffix Component, Page, View, Base or Modal

SellerCardPage, LtcSummaryModalComponent

Service

Suffix Service, root-provided

PermissionsService

Guard

Suffix Guard

DocuSignGuard

Flow

Suffix Flow or FlowService

AdvanceLineWithdrawFlow

Task

Suffix Task

WithdrawAdvanceLineOfferTask

Enum

Suffix Enum or a plural type name

PermissionsEnum, AdvanceLineEventTypes

RxJS subject field

Suffix Subject

toggleFilterSubject

Observable field

Suffix $

user$, currentUser$

Mock

<Resource><Type>Mock

UserServiceMock, PermissionsServiceMock

NgRX action

[Feature] Description

[DashboardNotifications] Fetch Dashboard Notification

DI token

SCREAMING_SNAKE_CASE + _TOKEN

FEE_REPORT_COLUMNS_CONFIG_TOKEN

File organisation

  • One concept per file. A feature slice is a folder with .actions.ts, .reducer.ts, .selectors.ts, .effects.ts, *.module.ts.

  • Every component that is used outside its own folder ships its own *.module.ts that declares and exports it.

  • Specs sit beside the file they test: foo.service.ts / foo.service.spec.ts.

  • Barrels (index.ts) exist for core/auth, core/pyb/services, core/pyb/features, core/pyb/entities and core/pyb/constants. Import through the barrel where one exists.

Imports

Never use a relative import across module boundaries. Use the configured path aliases — see Path Aliases. Relative imports are acceptable only within the same folder or a direct child.

// good
import { PermissionsService } from '@pyb-core/pyb/services/permissions.service';
import { WelcomeModalComponent } from '@pyb-modals/welcome-modal/welcome-modal.component';

// bad
import { PermissionsService } from '../../../../core/pyb/services/permissions.service';

Layering

The intended dependency direction is one-way — views/ imports from core/, not the reverse:

core/

Business logic, state, services, guards. Should know nothing about pages.

views/

Presentation. Selects from the store, dispatches actions, calls core services.

vendors/

Third-party wrappers. Imported by core/, never the reverse.

The codebase does not currently hold this line

About 49 files under core/ import from views/ — roughly 90 import statements. They cluster into four recognisable patterns:

Modal components

core/pyb/tasks/, flows/ and effects import concrete modal components and their data interfaces from @pyb-modals/, because a task’s whole job is to open a dialog.

Report types

core/pyb/datasources/ and core/reports/ import filter and row interfaces from views/pages/reports/…, where the column configs live.

Form vocabulary

FormHelpersService and cancellation-survey-modal.config.ts import AvailableFields, FormFieldConfig and DynamicFieldType from the views layer.

Onboarding models

core/pyb/interfaces/approved-products.interface.ts imports the three ProductOffering*Class models from views/onboarding/.

Nothing enforces the direction — there is no lint rule for it. When adding code, prefer passing view knowledge in (a DI token, an action payload, a method argument) over importing it, and prefer moving a shared type into core/ over adding another upward import. Do not treat the existing imports as precedent for new ones.

Change detection

ChangeDetectionStrategy.OnPush is the default for new components. Consequences:

  • pass immutable data through @Input(),

  • prefer the async pipe over manual subscribe() in templates,

  • if you must mutate local state outside Angular’s zone, call ChangeDetectorRef.markForCheck().

Subscription hygiene

Extend Subscriber (@pyb-core/pyb/utils/subscriber.class) and pipe through takeUntil(this.ngUnsubscribe$). This applies to services with long-lived subscriptions as well as components.

export class SomeComponent extends Subscriber implements OnInit {
    ngOnInit(): void {
        this.store.select(selectCurrentUser)
            .pipe(takeUntil(this.ngUnsubscribe$))
            .subscribe(user => { /* … */ });
    }
}

NgRX rules

  • Reducers are pure. storeFreeze is active outside production and will throw on any mutation.

  • Side effects — HTTP, localStorage, router.navigate, dialogs — belong in effects or services, never in reducers.

  • Selectors are memoized with createSelector and colocated with their slice.

  • Components select; they never reach into state directly.

Translations

No user-visible string is hardcoded. Every label goes through TranslateService or the translate pipe, with the key defined in src/assets/i18n/en.ts. See Internationalization.

Analytics

Every user-initiated action that matters to the business emits a Segment event through EventLoggerService, using a constant from src/app/core/pyb/enums/event-types.enum.ts. Ad-hoc string literals are not acceptable. See Event Logger.

Tooling

Command Effect

npm run lint

ESLint over ./src with .eslintrc.js.

npm run lint:fix

Lint with autofix.

npm run format-all

Prettier over src/app/*/.{ts,js}.

npm run test:ci

lint:quiet then headless Karma with coverage.

Husky hooks run pretty-quick --staged and lint-staged on commit, and the unit suite on push.

Things not to do

Don’t Because

Import CoreModule from a feature module

It is a singleton; a second import duplicates interceptors and providers.

Edit src/environments/environment.ts

It is generated by config/pre-build.js and overwritten on every build.

Drop --max_old_space_size=4096 from an npm script

The Angular 12 build runs out of heap without it.

Mutate NgRX state

storeFreeze throws in dev and the bug silently corrupts state in prod.

Add a gated feature without a PermissionsEnum entry

The permission tree in permissions.const.ts is what ngx-permissions reads; an unregistered permission is invisible to guards and directives.

Hardcode a route string

URLS_ROUTER is the single source of truth.