Angular requires you to declare a component in one and only one NgModule.
Find the right one in the following order:
Your Component is used only on one page? - Add it to the declarations of the corresponding page.module.
Your Component is used among multiple pages? - Declare it in the shared.module and also export it there.
Your Component is used in the application shell (and maybe again on certain pages)? - Declare it in the shell.module and also export it there.
(advanced) Your component relates to a specific B2B extension? - Declare it in that extension module and add it as an entryComponent, add a lazy-loaded component and add that to the extension exports, which are then im-/exported in the shared.module.
When using ng generate
, the right module should be found automatically.
Using NgRx or Services directly in components violates our model of abstraction.
Only facades should be used in components, as they provide the simplest access to the business logic.
There should not be any string or URL manipulation, routing mapping or REST endpoint string handling within components.
This is supposed to be handled by methods of services.
See also Angular Style Guide.
constructor
as Possible - Use ngOnInit
See The essential difference between Constructor and ngOnInit in Angular and Angular constructor versus ngOnInit.
See Explanation of the difference between an HTML attribute and a DOM property.
There are often two ways to bind values dynamically to attributes or properties: interpolation or property binding.
In the PWA we prefer using property binding since this covers more cases in the same way.
So the code will be more consistent.
There is an exception for direct string value bindings where we use for example routerLink="/logout"
instead of [routerLink]="'/logout'"
.
<div attr.data-testing-id="category-{{category.id}}">
<img src="{{base_url + category.images[0].effectiveUrl}}" />
</div>
✔️ Correct pattern
<div [attr.data-testing-id]="'category-' + category.id">
<img [src]="base_url + category.images[0].effectiveUrl" />
</div>
Also for consistency reasons, we want to establish the following pattern for conditions in component templates:
<ng-container *ngIf="show; else elseBlock">
... (template code for if-branch)
</ng-container>
<ng-template #elseBlock>
... (template code for else-branch)
</ng-template>
This pattern provides the needed flexibility if used together with handling observables with *ngIf
and the async
pipe.
In this case the condition should look like this:
<ng-container *ngIf="observable$ | async as synchronized; else loading">
Looping through an array in a component may sometimes be accompanied by side effects if the data within the array are changing.
<ng-container *ngFor="let element of array$ | async">
<another-component [element]="element"></another-component>
</ng-container>
In case the values of the array$ observable are varying somehow during the lifetime of the component (reordering elements, add/ delete elements, changing properties), all children DOM elements are destroyed and re-initialized with the new data.
To avoid these many and expensive DOM manipulations and persist the children DOM elements the loop elements has to be uniquely identified in the NgFor
directive.
This can be achieved by using custom trackBy
functions within the ngFor
directive.
@Component({
...
template: `
<ng-container *ngFor="let element of array$ | async; trackBy: customTrackByFn">
<another-component [element]="element"></another-component>
</ng-container>`
})
export class AnyComponent implements OnInit, OnDestroy {
...
customTrackByFn(index, element) {
return element.id;
}
}
The custom trackBy function needs to return unique values for all unique inputs.
Following the ideas of the article takeUntilDestroyed in Angular v16, the following pattern is used for ending subscriptions to observables that are not handled via async pipe in the templates.
✔️ 'unsubscribe' via takeUntilDestroyed
export class AnyComponent implements OnInit {
...
ngOnInit() {
...
observable$.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(/* ... */);
}
}
The ESLint rule rxjs-angular/prefer-takeuntil
enforces the usage of takeUntilDestroyed
when subscribing in an Angular artifact.
OnPush
Change Detection if PossibleTo reduce the number of ChangeDetection computation cycles, all components should have their Component
decorator property changeDetection
set to ChangeDetectionStrategy.OnPush
.
When using Angular, avoid to access or change the DOM directly because this may lead to several issues:
However, if you need to manipulate the DOM use the multiple DOM manipulation methods of the Angular Renderer2 API or the pwa DomService or other angular techniques.
Consider splitting one into multiple components when:
Size: Component code becomes too complex to be easily understandable
Separation of concerns: A component serves different concerns that should be separated
Reusability: A component should be reused in different contexts. This can introduce a shared component which could be placed in a shared module.
Async data: Component relies on async data from the store which makes the component code unnecessarily complex. Use a container component then which resolves the observables at the outside of the child component and passes data in via property bindings. Do not do this for simple cases.
Single-use dumb components are always okay if it improves readability.
Angular Artifacts like Components, Directives and Pipes should solely depend on facades to interact with the State Management.
This is enforced with the ESLint rule no-intelligence-in-artifacts
which rejects every usage of REST API Services and NgRx Artifacts.
Use ts-mockito for creating and managing these mocks.
Providers for Facades can easily be added by using the VSCode snippet ish-provider-ts-mockito
: