Coding Conventions
Naming
| Element | Rule | Example |
|---|---|---|
Component selector |
|
|
Component class |
Suffix |
|
Service |
Suffix |
|
Guard |
Suffix |
|
Flow |
Suffix |
|
Task |
Suffix |
|
Enum |
Suffix |
|
RxJS subject field |
Suffix |
|
Observable field |
Suffix |
|
Mock |
|
|
NgRX action |
|
|
DI 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.tsthat declares and exports it. -
Specs sit beside the file they test:
foo.service.ts/foo.service.spec.ts. -
Barrels (
index.ts) exist forcore/auth,core/pyb/services,core/pyb/features,core/pyb/entitiesandcore/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:
|
Business logic, state, services, guards. Should know nothing about pages. |
|
Presentation. Selects from the store, dispatches actions, calls core services. |
|
Third-party wrappers. Imported by |
|
The codebase does not currently hold this line
About 49 files under
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 |
Change detection
ChangeDetectionStrategy.OnPush is the default for new components. Consequences:
-
pass immutable data through
@Input(), -
prefer the
asyncpipe over manualsubscribe()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.
storeFreezeis 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
createSelectorand colocated with their slice. -
Components select; they never reach into
statedirectly.
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 |
|---|---|
|
ESLint over |
|
Lint with autofix. |
|
Prettier over |
|
|
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 |
It is a singleton; a second import duplicates interceptors and providers. |
Edit |
It is generated by |
Drop |
The Angular 12 build runs out of heap without it. |
Mutate NgRX state |
|
Add a gated feature without a |
The permission tree in |
Hardcode a route string |
|