18 | }
19 | `,
20 | })
21 | export class SomeComponent {
22 | users: signal([]);
23 | }
24 | ```
25 |
26 | # Solution
27 |
28 | Let's refactor this by extracting the logic into the component's class. This will make the template more readable and the logic easier to test.
29 | To do so, we use `computed`, introduced in Angular 16, to create a computed property that will be used in the template.
30 |
31 | ```ts
32 | @Component({
33 | template: `
34 | @if(noCriteriaMet()) {
35 |
No items meet the criteria.
36 | }
37 | `,
38 | })
39 | export class SomeComponent {
40 | users: signal([]);
41 |
42 | noCriteriaMet = computed(() => this.users().length && !this.users().some(user => user.criteriaMet));
43 | }
44 | ```
45 |
46 | ### Best Practices
47 |
48 | Be careful when the `ChangeDetectionStrategy` is set to `Default`, as it'd cause the functions bound in the template to be called each time the `Change Detection Cycle` runs.
49 | You can optimize this by turning on the `OnPush` change detection strategy and leverage the `async` pipe in combination with `Observables` that return the desired value.
50 |
--------------------------------------------------------------------------------
/content/components/only-manipulate-the-dom-via-the-renderer.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: only manipulate the DOM via the Renderer
3 | author:
4 | name: Billy Lando
5 | url: https://github.com/billyjov
6 | ---
7 |
8 | # Problem
9 |
10 | According to the Angular documentation, relying on direct DOM access creates tight coupling between your application and rendering layers which will make it impossible to separate the two and deploy your application into a web worker.
11 |
12 | Consequently, using jQuery , `document` object, or `ElementRef.nativeElement` is not recommended as it's not available on other platforms such as server (for server-side rendering) or web worker.
13 |
14 | In addition, permitting direct access to the DOM can make your application more vulnerable to **XSS** attacks.
15 |
16 | # Solution
17 |
18 | Always try to prefer the `Renderer2` for DOM manipulations. It provides an API that can safely be used even when direct access to native elements is not supported.
19 |
20 | - **Bad practice**
21 | ```ts
22 | @Component({
23 | ...
24 | template: `
25 |
26 |
27 | `
28 | })
29 | export class SomeComponent implements OnInit {
30 |
31 | constructor(private elementRef: ElementRef) {}
32 |
33 | ngOnInit() {
34 | this.elementRef.nativeElement.style.backgroundColor = '#fff';
35 | this.elementRef.nativeElement.style.display = 'inline';
36 | const textareaElement = document.querySelector('textarea');
37 | const myChildComponent = $('my-child-component');
38 | }
39 | }
40 | ```
41 |
42 | We can refactor this by using a combination of `ElementRef` and `Renderer2`.
43 |
44 | - **Good practice**
45 | ```ts
46 | import { MyChildComponent } from './my-child.component';
47 |
48 | @Component({
49 | ...
50 | template: `
51 |
52 |
53 | `
54 | })
55 | export class SomeComponent implements OnInit {
56 |
57 | @ViewChild('textareaRef') myTextAreaRef: ElementRef;
58 | @ViewChild(MyChildComponent) myChildComponentRef: MyChildComponent;
59 |
60 | constructor(private elementRef: ElementRef, private renderer: Renderer2) {}
61 |
62 | ngOnInit() {
63 | this.renderer.setStyle(this.elementRef.nativeElement, 'backgroundColor', '#fff');
64 | this.renderer.setStyle(this.elementRef.nativeElement, 'display', 'inline');
65 | const textareaElement = this.myTextAreaRef.nativeElement;
66 | const myComponent = this.myChildComponent;
67 | }
68 | }
69 | ```
70 |
71 | # Resources
72 |
73 | - [Angular Documentation for ElementRef](https://angular.io/api/core/ElementRef#description)
74 | - [Exploring Angular DOM manipulation techniques using ViewContainerRef](https://blog.angularindepth.com/exploring-angular-dom-abstractions-80b3ebcfc02) by Max Koretskyi
75 |
--------------------------------------------------------------------------------
/content/forms/.category:
--------------------------------------------------------------------------------
1 | ---
2 | title: Forms
3 | summary: This category summarizes best practices regarding Forms.
4 | ---
5 |
--------------------------------------------------------------------------------
/content/forms/do-not-mix-form-apis.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Do not mix Angular Forms API
3 | ---
4 |
5 | # Problem
6 |
7 | Angular provides two APIs for building forms: the `Template-driven` and the `Reactive` forms API.
8 | Both APIs can be used to build forms in Angular, but they are not meant to be mixed.
9 | Mixing these APIs can lead to confusion and make the code harder to maintain.
10 |
11 | Here is an example of mixing the two APIs:
12 |
13 | ```ts
14 | import { FormControl } from '@angular/forms';
15 |
16 | @Component({
17 | template: `
18 |
22 | `
23 | })
24 | export class SomeComponent {
25 | name: string = 'Gerome';
26 |
27 | form = new FormGroup({
28 | name: new FormControl('Gerome', {Validators.required})
29 | });
30 | }
31 | ```
32 |
33 | In this example, we are using both `formControl` and `ngModel` to bind the input field to the `name` property.
34 |
35 | # Solution
36 |
37 | Choose one API and stick to it. If you are using the `Reactive` forms API, then use it consistently throughout your application.
38 |
39 | Here is the same example using the `Reactive` forms API:
40 |
41 | ```ts
42 | import { FormControl } from '@angular/forms';
43 |
44 | @Component({
45 | template: `
46 |
50 | `
51 | })
52 | export class SomeComponent {
53 | form = new FormGroup({
54 | name: new FormControl('Gerome', {Validators.required})
55 | });
56 | }
57 | ```
58 |
--------------------------------------------------------------------------------
/content/general/.category:
--------------------------------------------------------------------------------
1 | ---
2 | title: General
3 | summary: This category summarizes best practices regarding general topics.
4 | ---
5 |
--------------------------------------------------------------------------------
/content/general/use-latest-version-of-everything.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: use latest version of everything
3 | author:
4 | name: Billy Lando
5 | url: https://github.com/billyjov
6 | ---
7 |
8 | # Problem
9 |
10 | The Angular team and community are continually improving the ecosystem to make it easier to build applications. Both the performance and the compiler (e.g., Ivy Renderer) are constantly being improved for better web applications.
11 |
12 | Angular uses semantic versioning (semver), which means they use a regular schedule of releases. This includes a major release every six months, 1-3 minor releases for each major release, and a patch release almost every week. It’s important to keep up with major releases, as they contain significant new features. The longer we wait to update our application, the more expensive a future update can be. **Be aware** that major releases may contain breaking changes.
13 |
14 | In addition, when APIs are deprecated, they remain present in the next two major releases until they are removed. Again, if we wait too long, it’s likely that an update will require much more work. You can read more about deprecations in the changelog.
15 |
16 | **Note:** In the case of significant refactors, the Angular team may create schematics that can help update your app for you. At present, there are [schematics](https://angular.dev/reference/migrations) that convert to standalone components, the new control flow syntax, and more.
17 |
18 | # Solution
19 |
20 | You can follow this steps using Angular CLI:
21 |
22 | - **Step 1:** Create a new feature branch
23 | - **Step 2:** Run `ng update @angular/core @angular/cli` inside your project directory
24 | - **Step 3:** Run `ng serve`, `ng test`, `ng build --prod` and make sure your app works as expected
25 | - **Step 4:** Fix update deprecations, issues, styling issues in case of Angular Material and run the previous step again
26 | - **Step 5:** merge or rebase your changes on top of the main branch
27 |
28 | For more information, check out the [official update guide](https://update.angular.io/) on how to update from different versions.
29 |
30 | # Resources
31 |
32 | - [Keeping your Angular Projects Up-to-Date](https://angular.io/guide/updating)
33 | - [Don’t be afraid and just `ng update`!](https://itnext.io/dont-be-afraid-and-just-ng-update-1ad096147640) by Bram Borggreve
34 |
--------------------------------------------------------------------------------
/content/http/.category:
--------------------------------------------------------------------------------
1 | ---
2 | title: HTTP
3 | summary: This category summarizes best practices regarding HTTP interactions and modules.
4 | ---
5 |
--------------------------------------------------------------------------------
/content/ngrx/.category:
--------------------------------------------------------------------------------
1 | ---
2 | title: NgRx
3 | summary: This category summarizes best practices regarding NgRx.
4 | ---
5 |
--------------------------------------------------------------------------------
/content/ngrx/action-hygiene.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: capture events with actions, not commands
3 | ---
4 |
5 | # Problem
6 |
7 | When using NgRx, we are constantly dispatching actions to the store. These can be dispatched from different places such as components and effects. It can become really hard to figure out where all these actions originated from, why they were sent and how they are impacting the state.
8 |
9 | # Solution
10 |
11 | By changing the way we name our actions, we can more easily see where actions are being dispatched from. It allows us, by just looking at the action history, what the user was doing and the order he was doing it in. So instead of having something like this as action log:
12 |
13 | - [Users] Add User
14 | - [Users] Remove User
15 | - [Users] Update User
16 |
17 | We can have something like:
18 |
19 | - [Users Overview Page] Add User
20 | - [Users Overview Page] Remove User
21 | - [Users Detail Page] Update User
22 |
23 | This also implies that actions should not be reused. This might seem like an overkill to create a second action that will have the same result. But we have to keep in mind that at some point in time, we might need to update this code later on. This explicitness will help us in the future.
24 |
25 | This is considered good _action hygiene_. The format for action names should be `[${source}] ${event}`.
26 |
27 | # Resources
28 |
29 | - [Good action hygiene](https://www.youtube.com/watch?v=JmnsEvoy-gY) by Mike Ryan
30 |
--------------------------------------------------------------------------------
/content/ngrx/actions-are-defined-as-classes.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: define actions as classes
3 | ---
4 |
5 | # Problem
6 |
7 | When we send an action to the store, we need to send an object that has a type property and optional metadata (often added as a payload property). We could recreate an object every time we want to send that but we would violate the DRY principle.
8 |
9 | One of the promises in NgRx is that it provides extreme type safety. This is something that cannot be achieved with plain objects.
10 |
11 | # Solution
12 |
13 | We want to define our actions as classes. When we use classes to define our actions, we can define them once in a separate file and reuse them everywhere.
14 |
15 | Here's an example on how to define an action.
16 |
17 | ```ts
18 | import { Action } from '@ngrx/store';
19 |
20 | export enum AppActionTypes {
21 | APP_PAGE_LOAD_USERS = '[App Page] Load Users'
22 | }
23 |
24 | export class AppPageLoadUsers implements Action {
25 | readonly type = AppActionTypes.APP_PAGE_LOAD_USERS;
26 | }
27 |
28 | export type AppActions = AppPageLoadUsers;
29 | ```
30 |
31 | Now, we can use the `AppPageLoadUsers` class to send this action to the store which is then passed to our reducers.
32 |
33 | **Note:** Because of the way the action is being defined, using features like string literals and union types, we can leverage discriminated unions in our reducers to have extreme type safety when it comes to typing the action's payload. See the additional resources for more info.
34 |
35 | # Resources
36 |
37 | - [Type safe actions in reducers](https://blog.strongbrew.io/type-safe-actions-in-reducers/) by Kwinten Pisman
38 |
--------------------------------------------------------------------------------
/content/ngrx/do-not-put-everything-in-the-store.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: don't put everything in the store
3 | ---
4 |
5 | # Problem
6 |
7 | `@ngrx/store` (or Redux in general) provides us with a lot of great features and can be used in a lot of use cases. But sometimes this pattern can be an overkill. Implementing it means we get the downside of using Redux (a lot of extra code and complexity) without benefiting of the upsides (predictable state container and unidirectional data flow).
8 |
9 | # Solution
10 |
11 | The NgRx core team has come up with a principle called **SHARI**, that can be used as a rule of thumb on data that needs to be added to the store.
12 |
13 | - Shared: State that is shared between many components and services
14 | - Hydrated: State that needs to be persisted and hydrated across page reloads
15 | - Available: State that needs to be available when re-entering routes
16 | - Retrieved: State that needs to be retrieved with a side effect, e.g. an HTTP request
17 | - Impacted: State that is impacted by other components
18 |
19 | Try not to over-engineer your state management layer. Data is often fetched via XHR requests or is being sent over a WebSocket, and therefore is handled on the server side. Always ask yourself **when** and **why** to put some data in a client side store and keep alternatives in mind. For example, use routes to reflect applied filters on a list or use a `BehaviorSubject` in a service if you need to store some simple data, such as settings. Mike Ryan gave a very good talk on this topic: [You might not need NgRx](https://youtu.be/omnwu_etHTY)
20 |
21 | # Resources
22 |
23 | - [Reducing the Boilerplate with NgRx](https://www.youtube.com/watch?v=t3jx0EC-Y3c) by Mike Ryan and Brandon Roberts
24 | - [Do we really need @ngrx/store](https://blog.strongbrew.io/do-we-really-need-redux/) by Brecht Billiet
25 | - [Simple State Management with RxJS’s scan operator](https://juristr.com/blog/2018/10/simple-state-management-with-scan/) by Juri Strumpflohner
26 |
--------------------------------------------------------------------------------
/content/ngrx/dont-store-state-that-can-be-derived.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: don't store state that can be derived
3 | ---
4 |
5 | # Problem
6 |
7 | We can use `@ngrx/store` to store data. When we store duplicate data, we are making our reducer logic way more difficult. Take a look at the following type definition for a potential state object:
8 |
9 | ```ts
10 | export interface ApplicationState {
11 | users: Array;
12 | selectedUserId: number;
13 | selectedUser: User;
14 | }
15 | ```
16 |
17 | In this scenario, we are both storing the id of the `selectedUser` and the object of the `selectedUser`. This poses a lot of problems. First of all, when we change the selected user, we need to remember to update both references. But even worse, what if we update the user that is currently selected. Then we need to update both the reference in the `users` array and the `selectedUser`. This is easily overlooked and makes the implementation much more difficult and verbose.
18 |
19 | # Solution
20 |
21 | To fix this, we **shouldn't store state that can be derived**. If we store the `users` and the `selectedUserId`, we can easily derive which user is selected. This is logic that we can put in a selector or most probably in a composed selector. As a solution, we can define the state object as follows:
22 |
23 | ```ts
24 | export interface ApplicationState {
25 | users: Array;
26 | selectedUserId: number;
27 | }
28 | ```
29 |
30 | Now, when we update a user, we only need to update the reference in the `users` array.
31 |
--------------------------------------------------------------------------------
/content/ngrx/reducers-are-pure-functions.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: reducers are pure functions
3 | ---
4 |
5 | # Problem
6 |
7 | Reducers are responsible for updating the state in our application based on actions. It is extremely important that these are pure making them deterministic, so that every action, given the same input, will always have the same result. If they are not pure, we can no longer trust them to manage our state.
8 |
9 | # Solution
10 |
11 | By writing our reducers as pure functions, we are 100% sure that the reducer is deterministic and can be used to manage our state. A pure function has the following properties:
12 |
13 | * it does not depend on external state
14 | * it does not produce any side-effects
15 | * it does not mutate any of its inputs
16 | * if you call it over and over again, with the same arguments, you always get back the same results
17 |
18 | These properties are exactly what we need for our reducers to be deterministic and to comply with the key concepts of Redux.
19 |
20 | In addition, pure functions are very easy to test.
21 |
22 | Example of an **impure** function:
23 |
24 | ```ts
25 | const state = 1;
26 |
27 | function impureFunction(value: number) {
28 | return value + state;
29 | }
30 |
31 | // Returns 2
32 | impureFunction(1);
33 | ```
34 |
35 | The `impureFunction` relies on external state making it non-deterministic. We have no control of the state defined outside of the function as it is visible to many other functions.
36 |
37 | Instead, we can make this function **pure** by passing in the data it needs:
38 |
39 | ```ts
40 | const state = 1;
41 |
42 | function pureFunction(value: number, otherValue: number) {
43 | return value + otherValue;
44 | }
45 |
46 | // Returns 2
47 | pureFunction(1, state);
48 | ```
49 |
50 | Now, `pureFunction` only relies on its parameters, does not mutate its arguments and has no side-effects.
51 |
52 | The same is true for reducers. They have the following signature `(state, action) => state`. They do not rely on external state and shouldn't update its inputs.
53 |
--------------------------------------------------------------------------------
/content/ngrx/use-entity-pattern.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: use the entity pattern for large collections
3 | ---
4 |
5 | # Problem
6 |
7 | In our applications, we use a lot of arrays to store our data. When we fetch a list of users and we want to show them in the view, we can loop over them really easily using the `*ngFor` directive. We can put that data in our store so that we, for example, don't have to fetch it again later, or if the list is impacted by other components.
8 |
9 | But arrays are not the most performant solution when we want to update, delete, or get a single element out of the list. All these operations have a linear time complexity of O(n). For large collections, this can have a huge impact on the performance.
10 |
11 | # Solution
12 |
13 | To make the CRUD operations more efficient we can adopt the entity pattern. This means that we will no longer store the data as an array but transform it to an object where the key is the unique identifier of the element and the value is the actual element. This is also called state normalization.
14 |
15 | Here's an example.
16 |
17 | ```ts
18 | const contacts = [
19 | { id: 1, name: 'Dominic Elm' },
20 | { id: 2, name: 'Kwinten Pisman' }
21 | ];
22 | ```
23 |
24 | We can normalize this into the following:
25 |
26 | ```ts
27 | const entities = {
28 | 1: { id: 1, name: 'Dominic Elm' },
29 | 2: { id: 2, name: 'Kwinten Pisman' }
30 | };
31 | ```
32 |
33 | Now, finding, deleting, or updating an element all have a complexity of O(1).
34 |
35 | **Note:** As this is a common pattern in NgRx, there is a separate package that will help us to implement the entity pattern called `@ngrx/entity`.
36 |
37 | # Resources
38 |
39 | - [@ngrx/entity](https://github.com/ngrx/platform/tree/master/docs/entity)
40 |
--------------------------------------------------------------------------------
/content/ngrx/use-selectors.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: use selectors to select data from the store
3 | ---
4 |
5 | # Problem
6 |
7 | When we want to fetch data from the store, we can use queries to get the data out. These queries are functions that have the following signature `(state: T) => K`.
8 |
9 | While retrieving state from the store, we can execute some pretty complex and potentially inefficient or blocking logic. Every time the state changes, this logic will be re-executed.
10 |
11 | Also, the plain queries we define cannot be used to compose new ones. This means that we have to define the same queries in multiple locations violating the DRY principle.
12 |
13 | # Solution
14 |
15 | `@ngrx/store` provides us with the concept of selectors. A selector helps us to build up queries that have a type signature of `(state: T): K`. The great benefit of these selectors is that they are composable.
16 |
17 | `@ngrx/store` exposes a `createSelector` function that accepts other selectors to create new ones based on these. This means that we only have to define every selector just once and reuse them in multiple places.
18 |
19 | Let's look at a simple example:
20 |
21 | ```ts
22 | // Plain Selector
23 | export const selectFeature = (state: AppState) => state.feature;
24 |
25 | // Composed Selector
26 | export const selectFeatureCount = createSelector(
27 | selectFeature,
28 | (state: FeatureState) => state.counter
29 | );
30 | ```
31 |
32 | Another benefit of composed selectors is that they use an optimization technique called memoization. This means that the selector logic will **not** be re-executed if the source selectors did not update. As a result, the complex logic we might execute to get data from the store is only executed when it is actually needed.
33 |
34 | # Resources
35 |
36 | * [Selectors in Ngrx](https://github.com/ngrx/platform/blob/master/docs/store/selectors.md)
37 | * [NgRx: Parameterized selectors](https://blog.angularindepth.com/ngrx-parameterized-selector-e3f610529f8) by Tim Deschryver
38 | * [Memoization](https://en.wikipedia.org/wiki/Memoization)
39 |
--------------------------------------------------------------------------------
/content/performance/.category:
--------------------------------------------------------------------------------
1 | ---
2 | title: Performance
3 | summary: This category contains a list of practices which will help us boost the performance of our Angular applications. It covers different topics - from server-side pre-rendering and bundling of our applications, to runtime performance and optimization of the change detection performed by the framework.
4 | ---
--------------------------------------------------------------------------------
/content/performance/lazy-load-animations.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: lazy load animation providers
3 | ---
4 |
5 | # Problem
6 |
7 | Angular uses the [animations package](https://angular.dev/guide/animations) to add motion to your application.
8 | To enable animations, add the `provideAnimations()` call to your `app.config.ts` file:
9 |
10 | ```typescript
11 | import { provideAnimations } from '@angular/platform-browser/animations';
12 |
13 | export const appConfig: ApplicationConfig = {
14 | providers: [
15 | provideRouter(routes),
16 | provideAnimations()
17 | ]
18 | };
19 | ```
20 |
21 | However, this will eagerly load the animations package with the main bundle, which can slow down the initial load time of your application.
22 | The unminified size of the animations package is around **65kb**, which is not a lot, but it can add up with other packages.
23 |
24 | # Solution
25 |
26 | Starting with Angular **17.0.0**, you can now lazy load the animation package. You can change the way you provide the animations package in favor of `provideAnimationsAsync()`:
27 |
28 | ```typescript
29 | import { provideAnimationsAsync } from '@angular/platform-browser/animations';
30 |
31 | export const appConfig: ApplicationConfig = {
32 | providers: [
33 | provideRouter(routes),
34 | provideAnimationsAsync()
35 | ]
36 | };
37 | ```
38 |
39 | **Note:** This behavior will only work if you use the animations in lazy loaded components. Otherwise, the animations will be eagerly loaded with the main bundle.
40 |
41 | # Resources
42 |
43 | - [Lazy-loading Angular's animation module](https://riegler.fr/blog/2023-10-04-animations-async) by [Matthieu Riegler](https://twitter.com/Jean__Meche)
44 |
--------------------------------------------------------------------------------
/content/performance/track-by-option-on-ng-for.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: use trackBy option on *ngFor
3 | source: https://github.com/mgechev/angular-performance-checklist#use-trackby-option-for-ngfor-directive
4 | author:
5 | name: Minko Gechev
6 | url: https://twitter.com/mgechev
7 | ---
8 |
9 | # Problem
10 |
11 | The `*ngFor` directive is used for rendering a collection. By default `*ngFor` identifies object uniqueness by reference.
12 |
13 | Which means when developer breaks reference to object during updating item's content Angular treats it as removal of the old object and addition of the new object. This effects in destroying old DOM node in the list and adding new DOM node on its place.
14 |
15 | # Solution
16 |
17 | We can provide a hint for angular how to identify object uniqueness: custom tracking function as the `trackBy` option for the `*ngFor` directive. Tracking function takes two arguments: index and item. Angular uses the value returned from tracking function to track items identity. It is very common to use ID of the particular record as the unique key.
18 |
19 | ```ts
20 | @Component({
21 | selector: 'yt-feed',
22 | template: `
23 |
Your video feed
24 |
25 | `
26 | })
27 | export class YtFeedComponent {
28 | feed = [
29 | {
30 | id: 3849, // note "id" field, we refer to it in "trackById" function
31 | title: 'Angular in 60 minutes',
32 | url: 'http://youtube.com/ng2-in-60-min',
33 | likes: '29345'
34 | }
35 | // ...
36 | ];
37 |
38 | trackById(index, item) {
39 | return item.id;
40 | }
41 | }
42 | ```
43 |
44 | # Resources
45 |
46 | - ["NgFor directive"](https://angular.io/docs/ts/latest/api/common/index/NgFor-directive.html) - Official documentation for `*ngFor`
47 | - ["Angular — Improve performance with trackBy"](https://netbasal.com/angular-2-improve-performance-with-trackby-cc147b5104e5) - By Netanel Basal
48 |
--------------------------------------------------------------------------------
/content/performance/use-angular-new-cache.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: use persistent disk cache
3 | ---
4 |
5 | ## Problem
6 |
7 | The build process for large Angular applications can take some time to complete, and having to rebuild the application frequently will increase the time you spend waiting for it to finish.
8 |
9 | ## Solution
10 |
11 | **This only applies to Angular v13+**
12 |
13 | Use the new persistent build cache. This results in up to 68% improvement in build speed and more ergonomic options.
14 |
15 | New projects using Angular CLI v13+ have the persistent disk cache already enabled by default but, if you're updating your app from previous versions, you need to add the following to you `angular.json` file:
16 |
17 | ```json
18 | {
19 | "$schema": "...",
20 | "cli": {
21 | "cache": {
22 | "enabled": true,
23 | "path": ".cache",
24 | "environment": "all"
25 | }
26 | }
27 | ...
28 | }
29 | ```
30 |
31 |
32 | # Resources
33 |
34 | - [Official documentation for Angular's persistent disk cache](https://angular.io/cli/cache)
35 | - [Angular v13 is now Available](https://blog.angular.io/angular-v13-is-now-available-cce66f7bc296)
36 |
--------------------------------------------------------------------------------
/content/performance/use-aot-compilation.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: use AOT compilation for prod builds
3 | source: https://github.com/mgechev/angular-performance-checklist
4 | author:
5 | name: Minko Gechev
6 | url: https://twitter.com/mgechev
7 | ---
8 |
9 | **Note**: Starting from Angular v9, the AOT compiler is the default compiler, so if you're using Angular v9+ you don't need to take any action.
10 |
11 | # Problem
12 |
13 | The biggest part of the code that we ship to the browser when we use Angular is the compiler. The compiler is needed to transform our HTML-like templates to Javascript. This is doesn't only has a negative impact on the bundle size but also on the performance as this process is computationally expensive.
14 |
15 | # Solution
16 |
17 | We can avoid shipping the compiler by performing the compile step as part of the build step. We can achieve this by using AOT.
18 |
19 | AoT can be helpful not only for achieving more efficient bundling by performing tree-shaking, but also for improving the runtime performance of our applications. The alternative of AoT is Just-in-Time compilation (JiT) which is performed runtime, therefore we can reduce the amount of computations required for rendering of our application by performing the compilation as part of our build process.
20 |
21 | # Tooling
22 |
23 | * [@angular/compiler-cli](https://github.com/angular/angular/tree/master/packages/compiler-cli) - a drop-in replacement for [tsc](https://www.npmjs.com/package/typescript) which statically analyzes our application and emits TypeScript/JavaScript for the component's templates.
24 | * [angular2-seed](https://github.com/mgechev/angular-seed) - a starter project which includes support for AoT compilation.
25 | * [Angular CLI](https://cli.angular.io/) Using the ng serve --prod
26 |
27 | # Resources
28 |
29 | * [Ahead-of-Time Compilation in Angular](http://blog.mgechev.com/2016/08/14/ahead-of-time-compilation-angular-offline-precompilation/) by Minko Gechev
30 |
--------------------------------------------------------------------------------
/content/performance/use-on-push-cd-strategy.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: use onPush CD strategy on dumb components
3 | ---
4 |
5 | # Problem
6 |
7 | Change detection (CD) in Angular is performed from top to bottom. This means that everything is only checked once. This is a huge difference compared to AngularJS where change detection was performed in cycles until everything was considered stable.
8 |
9 | However, it still means that everything is checked every time CD is triggered, even things that we know for sure have not changed.
10 |
11 | # Solution
12 |
13 | Angular components can use different strategies for change detection. They can either use `Default` or `OnPush`.
14 |
15 | The default strategy means that the component will be checked during every CD cycle.
16 |
17 | With the `OnPush` strategy, the component (and all of its children!) will only be checked if one of its `@Input`s have changed (reference check) **or** if an event was triggered within the component.
18 |
19 | This means that we can easily tell Angular to not run CD for huge parts of our component tree, speeding up CD a lot! We can enable the `OnPush` strategy like this:
20 |
21 | ```ts
22 | @Component({
23 | ...
24 | changeDetection: ChangeDetectionStrategy.OnPush
25 | })
26 | ```
27 |
28 | **Note 1:** This also implies that we should always try to work immutable. Let's say that we add an element to an array by mutating the array and we pass the array to a component to visualise it. If we apply the `OnPush` strategy for this component, we wouldn't see the changes in the UI. Angular will not check if the array's content has changed. It will only check the reference. As the reference has not changed, it means that CD will not run for that component and the view will not be updated.
29 |
30 | **Note 2:** This also means that, every component we apply this strategy to, has to be dumb. If the component fetches its own data, we cannot have the `OnPush` strategy. Because in that case, the component's `@Input`s wouldn't be the only reason to run CD, but also data being fetched.
31 |
32 | **Note 3:** When using the `async` pipe, it will automatically call `markForCheck` under the hood. This marks the path to that component as "to be checked". When the next CD cycle kicks in, the path to that component is not disabled and the view will be updated.
33 |
34 | # Resources
35 |
36 | - [Angular change detection explained](https://blog.thoughtram.io/angular/2016/02/22/angular-2-change-detection-explained.html) by Pascal Precht
37 | - [Everything you need to know about change detection in Angular](https://blog.angularindepth.com/everything-you-need-to-know-about-change-detection-in-angular-8006c51d206f) by Maxim Koretskyi
38 |
--------------------------------------------------------------------------------
/content/router/.category:
--------------------------------------------------------------------------------
1 | ---
2 | title: Router
3 | summary: This category summarizes best practices regarding routing.
4 | ---
5 |
--------------------------------------------------------------------------------
/content/router/add-404-route.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: add 404 fallback route
3 | ---
4 |
5 | # Problem
6 |
7 | There are multiple reasons why we need to make sure that we have a fallback for when a page is not found.
8 |
9 | - Our users are humans. Humans are quite error-prone. This means that they are likely to mistype a url at some point.
10 | - Over time, our applications will change. Users might bookmark urls for pages which are not supported anymore.
11 |
12 | # Solution
13 |
14 | Every application should define a 404 route. This is a route to be shown whenever the user tries to go to a non existing route.
15 |
16 | ```ts
17 | [
18 | ...,
19 | { path: '404', component: NotFoundComponent },
20 | { path: '**', redirectTo: '/404' },
21 | ]
22 | ```
23 |
24 | The last route definition uses a wildcard as a path. Since the Angular router will render the first definitions that matches, be sure to always put this route definition last!
25 |
--------------------------------------------------------------------------------
/content/router/default-route.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: make sure default route is defined
3 | ---
4 |
5 | # Problem
6 |
7 | When users type in the url for your application, they do not know all the routes of our application. We need to make sure that we always have a landing page or a redirect set up.
8 |
9 | # Solution
10 |
11 | Every application should define a default route. This is the route that will be used whenever the user goes to `/`.
12 |
13 | ```ts
14 | [
15 | { path: '', redirectTo: '/heroes', pathMatch: 'full' },
16 | ...
17 | ]
18 | ```
19 |
20 | Note that `pathMatch: full` should be used to make sure that this route definitions is only triggered when the user is going to `/`.
21 |
--------------------------------------------------------------------------------
/content/router/lazy-load-feature-modules.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: lazy load feature modules
3 | ---
4 |
5 | # Problem
6 |
7 | When working with SPAs, we need to ship an entire application to the client. The more bytes we need to ship, the slower it will be to load but also to parse. This will greatly influence the TTI (Time to Interactive) of our application.
8 |
9 | We are shipping way too much JavaScript to the client.
10 |
11 | # Solution
12 |
13 | Angular provides us with a module system. When we break up our application in feature modules, we can leverage this to only load the modules that are needed for the first page render. The other modules can be lazily loaded only when they are needed. We can do this, when the user requests them or via a more sophisticated preloading strategy.
14 |
15 | The following module is **not** using lazy loading to load the `UsersModule`.
16 |
17 | ```ts
18 | // app.routing.ts
19 | const routes: Routes = [
20 | ...
21 | {path: 'users', component: UsersComponent}
22 | ...
23 | ];
24 |
25 | // app.module.ts
26 | @NgModule({
27 | declarations: [AppComponent],
28 | imports: [
29 | ...
30 | UsersModule,
31 | RouterModule.forRoot(routes),
32 | ],
33 | bootstrap: [AppComponent]
34 | })
35 | export class AppModule {}
36 | ```
37 |
38 | This means that the `UsersModule` will be added to the main bundle. The main bundle contains all the code that is needed for the first page load. As the `UsersModule` is only needed when the user specifically navigates to the `/users` page, it doesn't make sense to load it up front. Let's leverage lazy loading to fix this.
39 |
40 | ```ts
41 | // app.routing.ts
42 | const routes: Routes = [
43 | ...
44 | {
45 | path: 'users',
46 | loadChildren: () => import('../users/usersModule').then(m => m.UsersModule)
47 | }
48 | ...
49 | ];
50 |
51 | // app.module.ts
52 | @NgModule({
53 | declarations: [AppComponent],
54 | imports: [
55 | ...
56 | RouterModule.forRoot(routes),
57 | ],
58 | bootstrap: [AppComponent]
59 | })
60 | export class AppModule {}
61 | ```
62 |
63 | We updated the `/users` route to use the `loadChildren` property. This uses the standard dynamic import syntax.
64 | Called as a function, the import returns a promise which loads the module.
65 |
66 | Also note that we no longer add the `UsersModule` to the imports of the `AppModule`. This is important because otherwise lazy loading wouldn't work as expected. If the `UsersModule` was referenced by the `AppModule` the code for that module would be added to the main bundle.
67 |
68 | By using `loadChildren` and removing the module import from the `AppModule`, the `UsersModule` will be packaged in its own bundle and will only be loaded when the user navigates to `/users`.
69 |
70 |
71 | # Resources
72 |
73 | [The cost of JavaScript](https://medium.com/@addyosmani/the-cost-of-javascript-in-2018-7d8950fbb5d4) by Addy Osmani
74 |
--------------------------------------------------------------------------------
/content/router/protect-restricted-pages-with-guards.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: protect restricted pages with guards
3 | ---
4 |
5 | # Problem
6 |
7 | Users should not be able to access pages that they don't have access to. We could hide the menu item so they could not navigate to it by clicking on that menu item but this means they can still manually type in the url to go to that page. We need some way to protect certain routes.
8 |
9 | # Solution
10 |
11 | We can use guards to allow or deny route changes. Every part of your application that should be limited to users with certain roles should be protected with guards.
12 |
13 | We can create a guard by creating a service that implements the `CanActivate` interface to avoid users going to a certain component or a `canLoad` interface to avoid entire modules to be loaded.
14 |
15 | The following example shows how to use a `canActivate` guard.
16 |
17 | ```ts
18 | @Injectable()
19 | export class UserHasRoleGuard implements CanActivate {
20 | constructor(private activatedRoute) {}
21 |
22 | canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
23 | // return an Observable | Promise | boolean;
24 | }
25 | }
26 | ```
27 |
28 | We can now use it in our route definitions:
29 |
30 | ```ts
31 | [
32 | ...,
33 | { path: 'users', component: UsersComponent, canActivate: [UserHasRoleGuard] },
34 | ]
35 | ```
36 |
37 | You can see that the `canActivate` property on the route definition takes an array. This means we can add multiple guards which will be called chronologically in the order they are defined.
38 |
--------------------------------------------------------------------------------
/content/router/use-preloading-strategy.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: use preloading strategy
3 | ---
4 |
5 | # Problem
6 |
7 | When we use lazy loading, we are only loading the code that is needed for the first page render. Modules that are not yet needed are not loaded.
8 |
9 | By default, the next modules will be loaded whenever the user requests them. This is not ideal in every scenario because it means that whenever a user requests a url, they have to wait until the module is loaded and parsed.
10 |
11 | # Solution
12 |
13 | Depending on the application you are building and whether you have to deal with low bandwidth, it might be better to use a different strategy other than loading modules on request.
14 |
15 | When working on an application that will be used only on a steady WiFi connection, it makes sense to preload all of the modules when the CPU is idle. If our application will be used mainly on a slow 3G connection, we should only load the modules that are most likely used.
16 |
17 | ## Load all modules after first page render
18 |
19 | One strategy provided by the Angular team is to preload all modules when the CPU becomes idle. This means that, after the first page render, the modules will all be loaded in the background.
20 |
21 | ```ts
22 | @NgModule({
23 | imports: [
24 | ...modules,
25 | RouterModule.forRoot(routes, {
26 | preloadingStrategy: PreloadAllModules
27 | })
28 | ],
29 | ...
30 | })
31 | export class AppModule {}
32 | ```
33 |
34 | ## Defining a custom preloading strategy
35 |
36 | If our users can be both on mobile and on WiFi, it might make sense to only preload the modules if they are on WiFi. To do this, we can implement a custom preloading strategy.
37 |
38 | A custom preloading strategy is implemented as a class and implements the `PreloadingStrategy` interface.
39 |
40 | ```ts
41 | // custom.preloading-strategy.ts
42 | export class MyCustomPreloadingStrategy implements PreloadingStrategy {
43 | preload(route: Route, load: Function): Observable {
44 | // Implement your strategy here
45 | }
46 | }
47 |
48 | // app.module.ts
49 | @NgModule({
50 | imports: [
51 | ...modules,
52 | // Custom Preloading Strategy
53 | RouterModule.forRoot(routes, { preloadingStrategy: MyCustomPreloadingStrategy });
54 | ],
55 | ...
56 | })
57 | export class AppModule {}
58 | ```
59 |
60 | ## Data-driven bundling
61 |
62 | Another way is to use [Guess.js](https://github.com/guess-js/guess), a data-driven bundling approach. The goal with Guess.js is to minimize the bundle layout configuration, make it data-driven, and much more accurate. Guess.js will figure out which bundles to be combined together and what pre-fetching mechanism to be used.
63 |
64 | Guess.js can also be used with the Angular CLI. Here's an [example](https://github.com/mgechev/guess-js-angular-demo).
65 |
66 | # Resources
67 |
68 | - [Angular Router: Preloading Modules](https://vsavkin.com/angular-router-preloading-modules-ba3c75e424cb) by Victor Savkin
69 | - [Introducing Guess.js - a toolkit for enabling data-driven user-experiences on the Web](https://blog.mgechev.com/2018/05/09/introducing-guess-js-data-driven-user-experiences-web/) by Minko Gechev
70 |
--------------------------------------------------------------------------------
/content/rxjs/.category:
--------------------------------------------------------------------------------
1 | ---
2 | title: RxJS
3 | summary: This category summarizes best practices regarding RxJS.
4 | ---
--------------------------------------------------------------------------------
/content/rxjs/avoid-nested-subscriptions.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: avoid nested subscriptions
3 | ---
4 |
5 | # Problem
6 |
7 | Sometimes we need to aggregate values from multiple observables or deal with nested observables to perform an action. In that case, you could subscribe to an observable in the subscribe block of another observable. This makes handling subscriptions way more difficult and feels like callback hell all over again.
8 |
9 | # Solution
10 |
11 | For aggregating values or dealing with nested observables we can use one of the combination or flattening operators.
12 |
13 | Let's consider the following example: In an e-commerce system we are fetching a product and based on that product we want to fetch similar ones.
14 |
15 | A naive solution could look like this:
16 |
17 | ```ts
18 | fetchProduct(1).subscribe(product => {
19 | fetchSimilarProducts(product).subscribe(similarProducts => {
20 | ...
21 | });
22 | });
23 | ```
24 |
25 | We first fetch the product and once the request is resolved we fetch similar products inside the subscribe block of the first, most outer observable.
26 |
27 | This is considered to be an anti-pattern or code smell for the following reasons:
28 | - 👹 it brings us back to callback hell,
29 | - 💔 it breaks Reactive Programming,
30 | - 🐢 it breaks observables laziness,
31 | - 💥 it doesn’t help with subscription management,
32 | - 🤢 it's ugly anyway.
33 |
34 | Instead we can use one of the flattening operators to get rid of this code smell and solve it more elegantly:
35 |
36 | ```ts
37 | fetchProduct(1).pipe(
38 | switchMap(product => fetchSimilarProducts(product))
39 | ).subscribe(...)
40 | ```
41 |
42 | Here's another example: A simple list view where the user can filter and paginate the list. Whenever the user goes to the next page we also need to take into account the filter:
43 |
44 | Naive solution:
45 |
46 | ```ts
47 | nextPage$.subscribe(page => {
48 | filter$.pipe(take(1)).subscribe(filter => {
49 | fetchData(page, filter).subscribe(items => {
50 | this.items = items;
51 | });
52 | });
53 | });
54 | ```
55 |
56 | That's again not the most idiomatic solution because we have introduced several nested subscriptions.
57 |
58 | Let's fix this with a combination and flattening operator:
59 |
60 | ```ts
61 | nextPage$
62 | .pipe(
63 | withLatestFrom(filter$),
64 | switchMap(([page, filter]) => fetchData(page, filter))
65 | )
66 | .subscribe(items => {
67 | this.items = items;
68 | });
69 | ```
70 |
71 | Or when we want to listen for changes in both the `nextPage$` and the `filter$` we could use `combineLatest`:
72 |
73 | ```ts
74 | combineLatest(nextPage$, filter$)
75 | .pipe(switchMap(([page, filter]) => fetchData(page, filter)))
76 | .subscribe(items => {
77 | this.items = items;
78 | });
79 | ```
80 |
81 | Both solutions are much more readable and they also reduces the complexity of our code.
82 |
83 | Here are some very common combination and flattening operators:
84 |
85 | **Combination Operators**:
86 |
87 | - `combineLatest`
88 | - `withLatestFrom`
89 | - `merge`
90 | - `concat`
91 | - `zip`
92 | - `forkJoin`
93 | - `pairwise`
94 | - `startWith`
95 |
96 | **Flattening Operators**:
97 |
98 | - `switchMap`
99 | - `mergeMap`
100 | - `concatMap`
101 | - `exhaustMap`
102 |
--------------------------------------------------------------------------------
/content/rxjs/pipeable-operators.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: use pipeable operators
3 | ---
4 |
5 | # Problem
6 |
7 | Since the release of RxJS 6, patch operators have been removed. This means that we can no longer use them.
8 |
9 | This means the following is no longer possible:
10 |
11 | ```ts
12 | import 'rxjs/add/observable/interval';
13 | import 'rxjs/add/operator/map';
14 | import 'rxjs/add/operator/filter';
15 | import 'rxjs/add/operator/switchMap';
16 |
17 | Observable.interval(1000)
18 | .filter(x => x % 2 === 0)
19 | .map(x => x*2)
20 | .switchMap(x => mapToObservable(x))
21 | ```
22 |
23 | # Solution
24 |
25 | Instead, we should be using pipeable operators.
26 |
27 | ```ts
28 | import { interval } from 'rxjs';
29 | import { filter, map, switchMap } from 'rxjs/operators';
30 |
31 | Observable.interval(1000)
32 | .pipe(
33 | filter(x => x % 2 === 0),
34 | map(x => x*2),
35 | switchMap(x => mapToObservable(x)),
36 | );
37 | ```
38 |
39 | Even if you are using the older versions of RxJS, all new code should be written using pipeable operators.
40 |
41 | ## Upgrading
42 |
43 | If you have a lot of code written using patch operators, you can use a script released written by Google engineers to do this upgrade automatically for you. You can find the script and how to use it in the [rxjs-tslint](https://github.com/ReactiveX/rxjs-tslint#migration-to-rxjs-6) package.
44 |
--------------------------------------------------------------------------------
/content/rxjs/takeuntil-operator.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: don't manage subscriptions imperatively
3 | ---
4 |
5 | # Problem
6 |
7 | When we subscribe to an Observable, we also need to unsubscribe to clean up its resources. Unsubscribing can be done like this:
8 |
9 | ```ts
10 | // hold a reference to the subscription object
11 | const subscription = interval(1000).subscribe(console.log);
12 |
13 | // use the subscription object to kill the subscription
14 | subscription.unsubscribe();
15 | ```
16 |
17 | But if we have multiple subscriptions, we need to manage all of them. We could do this in an array but this gets extremely verbose. We want to avoid having to do this imperatively.
18 |
19 | # Solution
20 |
21 | RxJS provides us with the `takeUntil` operator, and a few other conditional operators. This operator will mirror the source observable until a certain event happens. In most cases, we want to stop listening to Observables when the component gets destroyed. This allows us to write something like this:
22 |
23 | ```ts
24 | @Component({...})
25 | export class SomeComponent implements OnInit, OnDestroy {
26 | private destroy$ = new Subject();
27 | users: Array;
28 |
29 | constructor(private usersService: UsersService) {}
30 |
31 | ngOnInit() {
32 | // long-living stream of users
33 | this.usersService.getUsers()
34 | .pipe(
35 | takeUntil(this.destroy$)
36 | )
37 | .subscribe(
38 | users => this.users = users;
39 | );
40 | }
41 |
42 | ngOnDestroy() {
43 | this.destroy$.next();
44 | }
45 | }
46 | ```
47 |
48 | We create a `Subject` called `destroy$` and when the `ngOnDestroy` hook is called, we `next` a value onto the subject.
49 |
50 | The manual subscribe we defined in the `ngOnInit` hook uses the `takeUntil` operator in combination with our subject. This means that the subscription will remain active **until** `destroy$` emits a value. After that, it will unsubscribe from the source stream and complete it.
51 |
52 | This is a lot better than imperatively handling the subscriptions.
53 |
54 | **Note:** Using the `async` pipe is even better as we don't have to think about this at all. It will hook into the destroy lifecycle hook and unsubscribe for us.
55 |
56 | # Resources
57 |
58 | * [RxJS: don't unsubscribe](https://medium.com/@benlesh/rxjs-dont-unsubscribe-6753ed4fda87) by Ben Lesh
59 | * [RxJS: Avoiding takeUntil leaks](https://blog.angularindepth.com/rxjs-avoiding-takeuntil-leaks-fb5182d047ef) by Nicholas Jamieson
60 |
--------------------------------------------------------------------------------
/content/rxjs/use-async-pipe.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: use the async pipe
3 | ---
4 |
5 | # Problem
6 |
7 | In Angular, everything async is handled by Observables and they are triggered by subscribing to them. Whenever we do so, it is very important to also unsubscribe. Unsubscribing will clean up the resources being used by this stream. If we neglect to do this, we might introduce memory leaks.
8 |
9 | If we manually subscribe, it also means that we have to manually unsubscribe. This is something that is easily forgotten.
10 |
11 | # Solution
12 |
13 | Instead of manually subscribing, we can use the `async` pipe provided by Angular.
14 |
15 | The async pipe will:
16 |
17 | - subscribe to an Observable
18 | - unsubscribe from the Observable when the component is destroyed by hooking into the `onDestroy` hook
19 | - mark this component as "to be checked" for the next change detection cycle
20 |
21 | Using the `async` pipe as much as possible will make sure all the resources are cleaned up properly and reduce the likelihood of memory leaks.
22 |
23 | Here's an example:
24 |
25 | ```ts
26 | @Component({
27 | template: `{{data$ | async}}`,
28 | ...
29 | })
30 | export class SomeComponent {
31 | data$ = interval(1000);
32 | }
33 | ```
34 |
35 | Here, we set up an `interval` that emits a value every second. This is a long-living Observable and because we are using the `async` pipe, the resource (subscription) is cleaned up when the component is destroyed.
36 |
37 | # Resources
38 |
39 | [Three things you didn't know about the async pipe](https://blog.thoughtram.io/angular/2017/02/27/three-things-you-didnt-know-about-the-async-pipe.html) by Christoph Burgdorf
40 |
--------------------------------------------------------------------------------
/content/rxjs/use-ngifas.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: use ngIfAs to subscribe only once
3 | ---
4 |
5 | # Problem
6 |
7 | An Observable is lazy and unicast by default. This means that for every subscription, the Observable is executed. If the Observable is triggering a backend call when subscribed to, the following code will trigger two backend calls.
8 |
9 | ```ts
10 | @Component({
11 |
12 |
13 | })
14 | export class SomeComponent implements OnInit, OnDestroy {
15 | data$;
16 | ...
17 | }
18 | ```
19 |
20 | This is not the intended behavior. We want to fetch the data only once.
21 |
22 | # Solution
23 |
24 | We can fix this problem in multiple ways, either with the `ngIfAs` syntax, or by making our Observable hot.
25 |
26 | ## ngIfAs syntax
27 |
28 | We can use an `*ngIf` to hide an element. We can also leverage it to _unpack_ an observable and bind the value to a variable. We can then use that variable inside of the template.
29 |
30 | ```ts
31 | @Component({
32 |
33 |
34 |
35 |
36 | })
37 | export class SomeComponent implements OnInit, OnDestroy {
38 | data$;
39 | ...
40 | }
41 | ```
42 |
43 | By wrapping the components with a div that hides the element if no data is present, we were able to reduce the number of subscriptions from 2 to 1. This means that we only have a single subscription. Using the `as` syntax, we can also _catch_ the event from that observable and bind it to a variable and use that variable to pass it to our components.
44 |
45 | Better yet, if we don't want to introduce another level of nesting, we can use the `` element. This elements lets us group sibling elements under an invisible container element that is not rendered.
46 |
47 | Here's what the code from above looks like using ``:
48 |
49 | ```ts
50 | @Component({
51 |
52 |
53 |
54 |
55 | })
56 | export class SomeComponent implements OnInit, OnDestroy {
57 | data$;
58 | ...
59 | }
60 | ```
61 |
62 | Now, the template will be rendered as:
63 |
64 | ```html
65 |
66 |
67 | ```
68 |
69 | ## Make the Observable hot
70 |
71 | We can also make our Observable hot so that the Observable will no longer trigger a backend call with every subscription. A hot Observable will share the underlying subscription so the source Observable is only executed once.
72 |
73 | This fixes our problem because it means it doesn't matter anymore if we have multiple subscriptions.
74 |
75 | To do this, we can use for example the `shareReplay` operator.
76 |
77 | ```ts
78 | @Component({
79 |
80 |
81 | })
82 | export class SomeComponent implements OnInit, OnDestroy {
83 | sharedData$ = data$.pipe(
84 | shareReplay({ bufferSize: 1, refCount: true })
85 | );
86 | ...
87 | }
88 | ```
89 |
90 | > Note: we should specify `refCount: true` to prevent possible memory leaks.
91 |
92 | # Resources
93 |
94 | - [Multicasting operators in RxJS](https://blog.strongbrew.io/multicasting-operators-in-rxjs/) by Kwinten Pisman
95 |
--------------------------------------------------------------------------------
/content/rxjs/use-switchMap-only-when-you-need-cancellation.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: use switchMap only when you need cancellation
3 | ---
4 |
5 | # Problem
6 |
7 | In certain scenarios, using the wrong flattening operators from RxJS can result in unwanted behavior and race conditions.
8 |
9 | # Solution
10 |
11 | For example, in an e-commerce application users can add and remove items from their shopping cart. The logic for removing an item could look like this:
12 |
13 | ```ts
14 | removeItemButtonClick.pipe(
15 | switchMap(item => this.backend.removeFromCart(item.id))
16 | )
17 | ```
18 |
19 | Whenever the user clicks on the button to remove a certain item from the shopping cart, this action is forwarded to the application's backend. Most of the times this works as expected. However, the behavior depends on how rapidly items are removed from the cart. For example, either all items could be removed, or only some of them.
20 |
21 | In this example, `switchMap` is not the right operator because for every new action it will abort / cancel the previous action. This behavior makes `switchMap` unsafe for create, update and delete actions.
22 |
23 | There are several other flattening operators that may be more appropriate:
24 |
25 | - `mergeMap`: concurrently handle all emissions
26 | - `concatMap`: handle emissions one after the other
27 | - `exhaustMap`: when you want to cancel new emissions while processing a previous emission
28 |
29 | So we could fix the problem from above by `mergeMap`:
30 |
31 | ```ts
32 | removeItemButtonClick.pipe(
33 | mergeMap(item => this.backend.removeFromCart(item.id))
34 | )
35 | ```
36 |
37 | If the order is important we could use `concatMap`.
38 |
39 | For more information see the article from [Nicholas Jamieson](https://twitter.com/ncjamieson) listed below.
40 |
41 | # Resources
42 |
43 | - [RxJS: Avoiding switchMap-Related Bugs](https://blog.angularindepth.com/switchmap-bugs-b6de69155524) by Nicholas Jamieson
44 |
--------------------------------------------------------------------------------
/content/tooling/.category:
--------------------------------------------------------------------------------
1 | ---
2 | title: Tooling
3 | summary: This category summarizes best practices regarding tooling.
4 | ---
5 |
--------------------------------------------------------------------------------
/content/tooling/compodoc.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: use compodoc for documentation
3 | optional: true
4 | ---
5 |
6 | # Problem
7 |
8 | On boarding of new developers in your project can be quite difficult. Especially if the applications are getting bigger and bigger.
9 |
10 | When the project becomes really big, even for developers that have been working on it for a long time, keeping an overview is not that easy.
11 |
12 | # Solution
13 |
14 | Documentation for our code is the solution to this problem. Of course, everyone knows that writing documentation is hard, boring and the documentation itself gets out of date quite quickly.
15 |
16 | To fix this, we can use compodoc to generate documentation from our code. This means that it doesn't take any time to write and it can never get out of date as it is generated from the existing code at all times.
17 |
18 | # Resources
19 |
20 | * [Compodoc](https://compodoc.app/)
21 |
--------------------------------------------------------------------------------
/content/tooling/use-angular-cli.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: use @angular/cli
3 | optional: true
4 | ---
5 |
6 | # Problem
7 |
8 | When we ship our code to the browsers, our code needs to be optimised, bundled, minified, uglified and much more. There are also other steps involved in a proper build process. This can be quite a difficult and cumbersome task to do and especially to maintain.
9 |
10 | # Solution
11 |
12 | To fix this, we should use the `@angular/cli` to take over the build process. The Angular CLI simplifies the development of your Angular applications drastically. Aside from the build process, the CLI also provides you with code scaffolding which you can use to easily generate entire projects, components and much more.
13 |
14 | The CLI abstracts everything for us. This also means that when there are better solutions available to for example perform the build process, and if they integrate this, we get this update without putting any effort in. Since version 6, it also possible to hook into the entire build process via builders.
15 |
16 | # Resources
17 |
18 | - [Angular CLI](https://cli.angular.io/)
19 | - [Angular CLI under the hood - builders demystified](https://medium.com/dailyjs/angular-cli-6-under-the-hood-builders-demystified-f0690ebcf01) by Evgeny Barabanov
20 |
--------------------------------------------------------------------------------
/content/tooling/use-angular-dev-tools.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: use Angular DevTools
3 | author:
4 | name: Maciej Wójcik
5 | url: https://twitter.com/maciej_wwojcik
6 | ---
7 |
8 | # Problem
9 |
10 | Understanding complex and nested component trees can be challenging, especially if you are interested in the DOM view, the logic, and data layer. In addition, it's not easy to debug Angular components using standard debugging tools because they are not adapted very well for Angular-specific features.
11 |
12 | # Solution
13 |
14 | Angular DevTools (available in Chrome) is an extension to Chrome DevTools. It provides a set of helpful features to debug your Angular application.
15 |
16 | You can:
17 |
18 | - explore component or directive structure, check its current state or even edit it
19 | - profile your application to check for performance bottlenecks, including change detection execution information
20 |
21 | # Resources
22 |
23 | - [Angular DevTools for Chrome](https://chrome.google.com/webstore/detail/angular-devtools/ienfalfjdbdpebioblfackkekamfmbnh)
24 | - [Introduction to Angular DevTools](https://blog.angular.io/introducing-angular-devtools-2d59ff4cf62f) by Minko Gechev
25 |
--------------------------------------------------------------------------------
/content/tooling/use-prettier.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: use prettier for code formatting
3 | ---
4 |
5 | # Problem
6 |
7 | Whenever we write code, we want this code to be formatted in a standardised way. This poses two problems.
8 |
9 | - We need to align everyone in the team to agree with the same standards.
10 | - We need to get all of their IDE's/editors aligned as well. This can be extremely difficult.
11 |
12 | # Solution
13 |
14 | Prettier is an opinionated code formatter that can fix both of these problems. It imposes a standard way of formatting and it has a CLI that makes sure the formatting happens the same way on all environments. Adding Prettier and running it as a pre-commit hook will make sure only formatted code can be checked in.
15 |
16 | # Resources
17 |
18 | - [Prettier](https://prettier.io/)
19 | - [Add prettier to Angular CLI schematic](https://github.com/schuchard/prettier-schematic)
20 |
--------------------------------------------------------------------------------
/content/typescript/.category:
--------------------------------------------------------------------------------
1 | ---
2 | title: Typescript
3 | summary: This category summarizes best practices regarding Typescript.
4 | ---
5 |
--------------------------------------------------------------------------------
/content/typescript/avoid-using-any.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: avoid using any
3 | ---
4 |
5 | # Problem
6 |
7 | TypeScript allows us to write code that is statically type checked. This provides huge benefits. It helps us during development with auto completion, it simplifies working with third party libraries, helps us to refactor our code, spots errors during development that would have otherwise been runtime errors and much more.
8 |
9 | If we start using the `any` type, we lose all these benefits.
10 |
11 | # Solution
12 |
13 | The solution is to avoid the `any` type wherever possible in our code and we should define proper types instead.
14 |
15 | Here's a classic example:
16 |
17 | ```ts
18 | var x: number = 10;
19 | var y: any = "a";
20 | x = y;
21 | ```
22 |
23 | See how we assign a string to `x` although `x` is defined as a `number`? That's a nightmare.
24 |
25 | Let's look at another example:
26 |
27 | ```ts
28 | const x: number = 10;
29 | const y: any = 'a';
30 | const z = x + y;
31 |
32 | // Prints out 10a
33 | console.log(z);
34 | ```
35 |
36 | In the last example we add `x` and `y` together, and typing `y` as `any`, TypeScript cannot really help us and avoid this bug at development time. Basically, we end up with a concatenation and we’re essentially back in JavaScriptLand.
37 |
38 | ## Compiler Options
39 |
40 | Set the compiler `–noImplicitAny` flag. With this flag enabled the compiler will complain if anything has an implicit type of `any`.
41 |
42 | ## 3rd party libraries
43 |
44 | When working with 3rd party libraries that are written in vanilla JavaScript, we most likely don't have type information available. Luckily there is an initiative to create type definitions for those libraries. If it exists, you can find it by installing the type package via `yarn add --dev @types/${library-name}`.
45 |
46 | If this does not exist yet, you can create one yourself. Contributions are always welcome and appreciated.
47 |
48 | # Best Practices
49 |
50 | Using types is not just about enhancing your coding experience.
51 | Starting a feature by defining the types of the data you are going to work with can help you to better understand it and might even lead to a better design.
52 |
--------------------------------------------------------------------------------
/content/typescript/define-interfaces-for-models.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: define interfaces for models
3 | ---
4 |
5 | # Problem
6 |
7 | TypeScript helps us to create type safe code. When working with REST APIs, we will get back data (a DTO) at runtime that has a specific format. In case we don't define types in our code for the objects we expect to get back, we lose the benefit of TypeScript.
8 |
9 | # Solution
10 |
11 | We should define our models or DTOs (Data Transfer Objects) as interfaces instead of classes. Interfaces are virtual structures that only exist within the context of TypeScript. This means an interface does not generate code whereas a class is primarily syntactical sugar over JavaScript's existing prototype-based inheritance. Consequently, a class generates code when it's compiled to JavaScript.
12 |
13 | For example, if we make a backend request that will return an a user object with the properties `userName` and `password`, both strings, we can define an interface `User` that describes the shape of the response:
14 |
15 | ```ts
16 | export interface User {
17 | userName: string;
18 | password: string;
19 | }
20 | ```
21 |
--------------------------------------------------------------------------------
/content/typescript/define-types-at-the-non-typed-boundaries.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: define types at the non-typed boundaries
3 | ---
4 |
5 | # Problem
6 |
7 | All our JavaScript code is written in TypeScript. This means that we can leverage types. However, our codes interacts with different non-typed boundaries such as the HTML layer (think of events) and backend requests. Interacting with these boundaries influences the type safety of our code.
8 |
9 | # Solution
10 |
11 | When interacting with these boundaries, it is important to add type information so TypeScript knows the structure of the objects we are dealing with. By providing the type right at the boundary, TypeScript is able to infer it everywhere else where that variable is being used.
12 |
13 | For example when working with custom events:
14 |
15 | ```ts
16 | @Component({
17 | template: ``
18 | })
19 | export class SomeComponent {
20 | someEventHandler(event: TypeForThisEvent) {
21 | ...
22 | }
23 | }
24 | ```
25 |
26 | `TypeForThisEvent` will make sure that the non-typed HTML event is typed inside of our TypeScript code.
27 |
--------------------------------------------------------------------------------
/content/typescript/move-common-types-to-interfaces.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: move common types to interfaces
3 | ---
4 |
5 | # Problem
6 |
7 | With Typescript, we can easily add types to our code like this:
8 |
9 | ```ts
10 | let user: { userName: string; password: string };
11 | ```
12 |
13 | In this case, we defined the type of our user _inline_. While this is a valid option, it also means that it's not reusable. We could define it in multiple places. The downside here is that, when it is updated, we have to update multiple places.
14 |
15 | # Solution
16 |
17 | Whenever a type is reused in multiple places, it is recommended to move it into a separate interface.
18 |
19 | For example, we could define an interface `User`:
20 |
21 | ```ts
22 | export interface User {
23 | userName: string;
24 | password: string;
25 | }
26 | ```
27 |
--------------------------------------------------------------------------------
/content/typescript/use-type-inference.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: use type inference
3 | ---
4 |
5 | # Problem
6 |
7 | Typescript is really good at inferring the types in our code. Whenever it can do that, we don't have to add the types ourselves.
8 |
9 | If we do add them everywhere, it doesn't only take a lot of time, but it also means that we have to update them everywhere whenever anything changes.
10 |
11 | # Solution
12 |
13 | In TypeScript, we want to take advantage of type inference as much as possible. TypeScript uses this to to provide type information when there is no explicit type annotation.
14 |
15 | Here's an example:
16 |
17 | ```ts
18 | const x: number = 3;
19 | const y: string = 'typescript will automatically infer the string type';
20 | ```
21 |
22 | In both cases, the type is inferred when initializing the variables.
23 |
24 | To keep this code clean, we can omit the type information and use the type inference to automatically provide type information.
25 |
26 | ```ts
27 | const x = 3;
28 | const y = 'typescript will automatically infer the string type';
29 | ```
30 |
31 | Type inference does not only take place when initializing variables but also when initializing class members, setting parameter default values, and determining function return types.
32 |
--------------------------------------------------------------------------------
/cypress.json:
--------------------------------------------------------------------------------
1 | {
2 | "baseUrl": "http://localhost:4200"
3 | }
4 |
--------------------------------------------------------------------------------
/cypress/plugins/cy-ts-preprocessor.js:
--------------------------------------------------------------------------------
1 | const wp = require('@cypress/webpack-preprocessor');
2 |
3 | const webpackOptions = {
4 | resolve: {
5 | extensions: ['.ts', '.js']
6 | },
7 | module: {
8 | rules: [
9 | {
10 | test: /\.ts$/,
11 | exclude: [/node_modules/],
12 | use: [
13 | {
14 | loader: 'ts-loader'
15 | }
16 | ]
17 | }
18 | ]
19 | }
20 | };
21 |
22 | const options = {
23 | webpackOptions
24 | };
25 |
26 | module.exports = wp(options);
27 |
--------------------------------------------------------------------------------
/cypress/plugins/index.js:
--------------------------------------------------------------------------------
1 | const cypressTypeScriptPreprocessor = require('./cy-ts-preprocessor');
2 |
3 | module.exports = on => {
4 | on('file:preprocessor', cypressTypeScriptPreprocessor);
5 | };
6 |
--------------------------------------------------------------------------------
/cypress/support/commands.js:
--------------------------------------------------------------------------------
1 | // ***********************************************
2 | // This example commands.js shows you how to
3 | // create various custom commands and overwrite
4 | // existing commands.
5 | //
6 | // For more comprehensive examples of custom
7 | // commands please read more here:
8 | // https://on.cypress.io/custom-commands
9 | // ***********************************************
10 | //
11 | //
12 | // -- This is a parent command --
13 | // Cypress.Commands.add("login", (email, password) => { ... })
14 | //
15 | //
16 | // -- This is a child command --
17 | // Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... })
18 | //
19 | //
20 | // -- This is a dual command --
21 | // Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... })
22 | //
23 | //
24 | // -- This is will overwrite an existing command --
25 | // Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... })
26 |
--------------------------------------------------------------------------------
/cypress/support/index.js:
--------------------------------------------------------------------------------
1 | // ***********************************************************
2 | // This example support/index.js is processed and
3 | // loaded automatically before your test files.
4 | //
5 | // This is a great place to put global configuration and
6 | // behavior that modifies Cypress.
7 | //
8 | // You can change the location of this file or turn off
9 | // automatically serving support files with the
10 | // 'supportFile' configuration option.
11 | //
12 | // You can read more here:
13 | // https://on.cypress.io/configuration
14 | // ***********************************************************
15 |
16 | // Import commands.js using ES2015 syntax:
17 | import './commands';
18 |
19 | // Alternatively you can use CommonJS syntax:
20 | // require('./commands')
21 |
--------------------------------------------------------------------------------
/cypress/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "strict": true,
4 | "baseUrl": "../node_modules",
5 | "target": "es5",
6 | "experimentalDecorators": true,
7 | "skipLibCheck": true,
8 | "noImplicitAny": false,
9 | "lib": ["es6", "dom"],
10 | "types": ["cypress"]
11 | },
12 | "include": ["**/*.ts"]
13 | }
14 |
--------------------------------------------------------------------------------
/karma.conf.js:
--------------------------------------------------------------------------------
1 | // Karma configuration file, see link for more information
2 | // https://karma-runner.github.io/1.0/config/configuration-file.html
3 |
4 | module.exports = function(config) {
5 | config.set({
6 | basePath: '',
7 | frameworks: ['jasmine', '@angular-devkit/build-angular'],
8 | plugins: [
9 | require('karma-jasmine'),
10 | require('karma-chrome-launcher'),
11 | require('karma-jasmine-html-reporter'),
12 | require('karma-coverage'),
13 | require('@angular-devkit/build-angular/plugins/karma')
14 | ],
15 | client: {
16 | clearContext: false // leave Jasmine Spec Runner output visible in browser
17 | },
18 | jasmineHtmlReporter: {
19 | suppressAll: true // removes the duplicated traces
20 | },
21 | coverageReporter: {
22 | dir: require('path').join(__dirname, './coverage/angular-checklist'),
23 | subdir: '.',
24 | reporters: [{ type: 'html' }, { type: 'text-summary' }]
25 | },
26 | reporters: ['progress', 'kjhtml'],
27 | port: 9876,
28 | colors: true,
29 | logLevel: config.LOG_INFO,
30 | autoWatch: true,
31 | browsers: ['Chrome'],
32 | singleRun: false,
33 | restartOnFileChange: true
34 | });
35 | };
36 |
--------------------------------------------------------------------------------
/nodemon.json:
--------------------------------------------------------------------------------
1 | {
2 | "ignore": [".git", "node_modules"],
3 | "watch": ["content/**/*"],
4 | "exec": "yarn build-content",
5 | "ext": "md"
6 | }
7 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "angular-checklist",
3 | "version": "0.0.0",
4 | "scripts": {
5 | "ng": "ng",
6 | "start": "concurrently \"ng serve\" \"yarn build-content:watch\"",
7 | "build": "ng build",
8 | "test": "ng test",
9 | "lint": "ng lint",
10 | "e2e": "cypress open",
11 | "build-content": "tsx ./tools/build-checklist.ts",
12 | "build-content:watch": "nodemon",
13 | "ci": "npm run build-content && ng build",
14 | "format:base": "prettier \"{src,tools,cypress}/**/*{.ts,.js,.json,.scss,.html}\"",
15 | "format:check": "npm run format:base -- --list-different",
16 | "format:fix": "npm run format:base -- --write"
17 | },
18 | "packageManager": "yarn@1.22.19",
19 | "engines": {
20 | "node": "18 || 20"
21 | },
22 | "husky": {
23 | "hooks": {
24 | "commit-msg": "commitlint --env HUSKY_GIT_PARAMS",
25 | "pre-commit": "lint-staged"
26 | }
27 | },
28 | "lint-staged": {
29 | "{src,tools}/**/*{.ts,.js,.json,.html}": [
30 | "prettier --write",
31 | "git add"
32 | ]
33 | },
34 | "private": true,
35 | "dependencies": {
36 | "@angular/animations": "^17.1.1",
37 | "@angular/cdk": "^17.1.1",
38 | "@angular/common": "^17.1.1",
39 | "@angular/compiler": "^17.1.1",
40 | "@angular/core": "^17.1.1",
41 | "@angular/forms": "^17.1.1",
42 | "@angular/material": "^17.1.1",
43 | "@angular/platform-browser": "^17.1.1",
44 | "@angular/platform-browser-dynamic": "^17.1.1",
45 | "@angular/platform-server": "^17.1.1",
46 | "@angular/router": "^17.1.1",
47 | "@fortawesome/angular-fontawesome": "^0.11.1",
48 | "@fortawesome/fontawesome-svg-core": "^6.2.0",
49 | "@fortawesome/free-brands-svg-icons": "^6.2.0",
50 | "@fortawesome/free-solid-svg-icons": "^6.2.0",
51 | "@ngrx/router-store": "^17.1.0",
52 | "@ngrx/store": "^17.1.0",
53 | "@ngrx/store-devtools": "^17.1.0",
54 | "fuzzysort": "^1.1.4",
55 | "hammerjs": "^2.0.8",
56 | "highlight.js": "^11.3.1",
57 | "lodash.groupby": "^4.6.0",
58 | "ngrx-store-freeze": "^0.2.4",
59 | "ngrx-store-localstorage": "^16.1.0",
60 | "normalize.css": "^8.0.0",
61 | "rxjs": "~7.5.5",
62 | "tslib": "^2.3.1",
63 | "zone.js": "~0.14.3"
64 | },
65 | "devDependencies": {
66 | "@angular-devkit/build-angular": "^17.1.1",
67 | "@angular-devkit/schematics": "17.1.1",
68 | "@angular-eslint/builder": "17.2.1",
69 | "@angular-eslint/eslint-plugin": "17.2.1",
70 | "@angular-eslint/eslint-plugin-template": "17.2.1",
71 | "@angular-eslint/schematics": "17.2.1",
72 | "@angular-eslint/template-parser": "17.2.1",
73 | "@angular/cli": "^17.1.1",
74 | "@angular/compiler-cli": "^17.1.1",
75 | "@angular/language-service": "^17.1.1",
76 | "@commitlint/cli": "^7.2.1",
77 | "@commitlint/config-angular": "^7.1.2",
78 | "@commitlint/travis-cli": "^7.2.1",
79 | "@cypress/webpack-preprocessor": "^4.0.2",
80 | "@types/express": "^4.17.0",
81 | "@types/jasmine": "~3.10.0",
82 | "@types/lodash": "^4.14.202",
83 | "@types/markdown-it": "^12.2.3",
84 | "@types/node": "^20.11.16",
85 | "@typescript-eslint/eslint-plugin": "^6.10.0",
86 | "@typescript-eslint/parser": "^6.10.0",
87 | "browser-sync": "^3.0.0",
88 | "chalk": "^2.4.1",
89 | "concurrently": "^4.1.0",
90 | "cypress": "^3.1.3",
91 | "eslint": "^8.53.0",
92 | "gray-matter": "^4.0.1",
93 | "http-server": "^0.11.1",
94 | "husky": "^1.2.0",
95 | "jasmine-core": "~3.10.0",
96 | "karma": "~6.3.0",
97 | "karma-chrome-launcher": "~3.1.0",
98 | "karma-coverage": "~2.0.3",
99 | "karma-jasmine": "~4.0.0",
100 | "karma-jasmine-html-reporter": "~1.7.0",
101 | "lint-staged": "^8.1.0",
102 | "markdown-it": "^12.2.0",
103 | "nodemon": "^1.18.7",
104 | "prettier": "^1.15.2",
105 | "shorthash": "^0.0.2",
106 | "ts-loader": "^5.3.1",
107 | "tsx": "^4.15.6",
108 | "typescript": "~5.3.3"
109 | }
110 | }
111 |
--------------------------------------------------------------------------------
/src/app/app.component.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/src/app/app.component.scss:
--------------------------------------------------------------------------------
1 | mat-progress-bar {
2 | position: fixed;
3 | }
4 |
--------------------------------------------------------------------------------
/src/app/app.component.ts:
--------------------------------------------------------------------------------
1 | import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
2 | import { NavigationEnd, NavigationStart, Router, RouterOutlet } from '@angular/router';
3 | import { merge, Observable } from 'rxjs';
4 | import { filter, mapTo } from 'rxjs/operators';
5 | import { MatProgressBar } from '@angular/material/progress-bar';
6 | import { AsyncPipe, NgIf } from '@angular/common';
7 |
8 | @Component({
9 | standalone: true,
10 | selector: 'ac-root',
11 | templateUrl: './app.component.html',
12 | styleUrls: ['./app.component.scss'],
13 | changeDetection: ChangeDetectionStrategy.OnPush,
14 | imports: [RouterOutlet, AsyncPipe, NgIf, MatProgressBar]
15 | })
16 | export class AppComponent implements OnInit {
17 | loading$: Observable;
18 |
19 | constructor(private router: Router) {}
20 |
21 | ngOnInit() {
22 | const navigationStart$ = this.router.events.pipe(
23 | filter(event => event instanceof NavigationStart),
24 | mapTo(true)
25 | );
26 |
27 | const navigationEnd$ = this.router.events.pipe(
28 | filter(event => event instanceof NavigationEnd),
29 | mapTo(false)
30 | );
31 |
32 | this.loading$ = merge(navigationStart$, navigationEnd$);
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/app/app.config.ts:
--------------------------------------------------------------------------------
1 | import { ApplicationConfig } from '@angular/core';
2 | import { provideState, provideStore } from '@ngrx/store';
3 | import { provideStoreDevtools } from '@ngrx/store-devtools';
4 | import { environment } from '../environments/environment';
5 | import { ROOT_REDUCER, USER_PROVIDED_META_REDUCERS } from './state/app.state';
6 | import { PreloadAllModules, provideRouter, withPreloading } from '@angular/router';
7 | import { APP_ROUTES } from './app.routes';
8 | import { provideRouterStore } from '@ngrx/router-store';
9 | import { provideAnimations } from '@angular/platform-browser/animations';
10 | import { checklistReducer } from './checklist/state/checklist.reducer';
11 | import { projectsReducer } from './projects/state/projects.reducer';
12 | import { MAT_CHECKBOX_DEFAULT_OPTIONS, MatCheckboxDefaultOptions } from '@angular/material/checkbox';
13 |
14 | export const appConfig: ApplicationConfig = {
15 | providers: [
16 | provideAnimations(),
17 | provideRouter(APP_ROUTES, withPreloading(PreloadAllModules)),
18 | provideStore(ROOT_REDUCER, {
19 | metaReducers: USER_PROVIDED_META_REDUCERS
20 | }),
21 | provideState('checklist', checklistReducer),
22 | provideState('projects', projectsReducer),
23 | provideStoreDevtools({
24 | maxAge: 25,
25 | logOnly: environment.production,
26 | connectInZone: true
27 | }),
28 | provideRouterStore(),
29 |
30 | // material design
31 | {
32 | provide: MAT_CHECKBOX_DEFAULT_OPTIONS,
33 | useValue: { clickAction: 'noop' } as MatCheckboxDefaultOptions
34 | }
35 | ]
36 | };
37 |
--------------------------------------------------------------------------------
/src/app/app.routes.ts:
--------------------------------------------------------------------------------
1 | import { Routes } from '@angular/router';
2 |
3 | export const APP_ROUTES: Routes = [
4 | { path: 'projects', loadChildren: () => import('./projects/projects.routes').then(m => m.PROJECTS_ROUTES) },
5 | {
6 | path: ':project/checklist',
7 | loadChildren: () => import('./checklist/checklist.routes').then(m => m.CHECKLIST_ROUTES)
8 | },
9 | { path: '', redirectTo: 'projects', pathMatch: 'full' }
10 | ];
11 |
--------------------------------------------------------------------------------
/src/app/checklist/checklist-cta-bar/checklist-cta-bar.component.html:
--------------------------------------------------------------------------------
1 |
4 | Angular Checklist is a curated list of best practices that we believe every application should follow in order to
5 | avoid some common pitfalls.
6 |
7 |
8 | The idea is that for all your projects, you can go over the checklist and see which items your projects already
9 | comply with and which you still have to put in some more effort!
10 |
11 |
If you follow the items in this list, your project will definitely be on track to success 🏆!