Best practices for clean Angular project

Introduction :

Hello Dear friends, this is my first blog in the 2021 series and I will start by writing about Angular technology best practices,

Let’ start :

Project Structure : 📓 📕

Before starting a new Angular project, you should always care of your folder structure and follow the community recommendation that should be set up

|-- app
|-- modules
|-- home
|-- [+] components
|-- [+] pages
|-- home-routing.module.ts
|-- home.module.ts
|-- core
|-- [+] authentication
|-- [+] footer
|-- [+] guards
|-- [+] http
|-- [+] interceptors
|-- [+] mocks
|-- [+] services
|-- [+] header
|-- core.module.ts
|-- ensureModuleLoadedOnceGuard.ts
|-- logger.service.ts
|
|-- shared
|-- [+] components
|-- [+] directives
|-- [+] pipes
|-- [+] models
|
|-- [+] configs
|-- assets
|-- scss
|-- [+] partials
|-- _base.scss
|-- styles.scss
|-- shared
|-- [+] components
|-- [+] directives
|-- [+] pipes
|-- [+] models
import { Component, OnInit, Input } from '@angular/core';

@Component({
selector: 'app-button',
templateUrl: './button.component.html',
styleUrls: ['./button.component.css']
})
export class ButtonComponent implements OnInit {
@Input() label:string;
@Input() functionCall:string;

constructor() { }

@Input() label: string;
@Output() onClick = new EventEmitter<any>();

onClickButton(event) {
this.onClick.emit(event);
}
}
<div id = "button">
<button type="button" class= "btn" (click)="onClickbutton($event)" >{{label}}</button>
</div>
  • models: all the models across the application that can be divided into sub-folders
|-- models
|-- [+] crud
|-- [+] common
|-- [+] filter
|-- [+] helpers
|-- core
|-- [+] guards
|-- [+] http
|-- [+] interceptors
|-- [+] mocks
|-- [+] services
|-- core.module.ts
|-- ensureModuleLoadedOnceGuard.ts
|-- logger.service.ts

Short Paths With Path Aliases:➰ ✔️

../../../../../../../../../app/config/config.ts
Or
import { User} from '../../../models/crud/User';
"paths": {"@core/*": ["src/app/core/*"],"@modules/common/*": ["src/app/common/*"],"@shared/*": ["src/app/shared/*"],"@configs/*": ["src/configs/*"],
"@angular/core/src/metadata/*": ["./node_modules/@angular/core"]
import {  Model1 , Model2 , Model3 } from '@shared/models';import { Service1 , Service2 , Service3 } from '@core/service';

Lazy Loading : 📓 📕

import { NotFoundComponent } from './modules/general/not-found/not-found.component';

const routes: Routes = [
{ path: '', component: HomeComponent, },
{
path: 'contact',
loadChildren: () => import('./modules/general/contact/contact.module')
.then(mod => mod.ContactModule)
},
{
path: 'about',
loadChildren: () => import('./modules/general/about/about.module')
.then(mod => mod.AboutModule)
},
{
path: 'signin',
loadChildren: () => import('./modules/general/signin/signin.module')
.then(mod => mod.SigninModule)
},
{ path: '**', component: NotFoundComponent }
];

Use Angular CLI for File generating 🔧 🔨:

Angular CLI
# Install Angular CLI

npm install -g @angular/cli

# Check Angular CLI version

ng new [project-name]
ng generate component [name] (generate component)
ng g s everything-service (generate service)
ng g pipe my-pipe (generate pipe)
ng g directive my-directive (Generate a directive)
ng g enum some-enum
ng g module fancy-module
ng g cl my-class (Generate a class)
ng g interface my-interface (Generate an interface:)

Clean TypeScript Code 🍦 🍰:

ngOnInit(): void {this.function1);this.function2();}
const canvas = chartInstance.chart;

//Example
public
isConnected =false;
import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormControl, FormGroup } from '@angular/forms';
import { ActivatedRoute, Router } from '@angular/router';
import { Subject, Subscription } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { TranslateService } from '@ngx-translate/core';
import { JiraService, MetricsService, NavigationDataService, SubTaskService } from '@core/service';import { Constants } from '@core/utils/Constants';
import { StringUtils } from '@core/utils/string-utils';
import { Responsible, StatusFilterDTO, SubTaskFilter, SubTask, ModelMetricType, NavigationElement } from '@shared/models'
import { SharedData } from '@shared/sharedData';

Use Interface instead of Class for type checking 🚩 🏳:

interface MyInterface{
a: number;
b: string;
}
interface MyObject{
a: string;
b: number;
}
interface MyObject{a: string;b: number;}let obj :MyObject;
const test = (obj: MyObject) =>
{obj.a=100;
//Error "Type 'number' is not assignable to type 'string'."
}

Stop Using ‘any’, There’s a Type For That

let a: any;
let b: Object;

a.nomethod(); // Transpiles just fine
b.nomethod(); // Error: Property 'nomethod' does not exist on type 'Object'.

Safe Operator :

<b>1. </b>
<p>{{personone?.name}} </p>
<p>{{personone?.email}} </p>

Using *ngFor without trackBy 🔧 🔨:

@Component({
selector: 'app-movies-list',
template: `
<ul>
<li *ngFor="let movie of moviesArr; trackBy: trackByElement">

</li>
</ul>
`
})
// in your typescript codetrackByElement = (index, item) => {return item.id;}

Use hidden over ngIf for showing/hiding elements 🔑:

Avoid this ❌ :

<div  *ngIf="isEpic">
<span> To be defined</span>
</div>
<div class="col" [hidden]="!isEpic">
<span> To be defined</span>
</div>

Avoid console.log()✂️ 📐 :

if (env === 'production') {
console.log = function () {};
}

Using pipes instead of methods in templates:

Don’t do this ❌ :

<h1>    Current time is {{ currentTime.substr(0, 2).toUpperCase() }}!  </h1>
<h1>    Current time is {{ currentTime | SubtPipe | uppercase}}!  </h1>

Avoid complex computations in the template:

<span  *ngIf="object.name" class="badgeAvatar badge-primary"> {{functionFilter(object.name)?.toUpperCase()}}</span>`https://gist.github.com/akhilanandbv003/fd4f3898bb7b9c36f7b9a8c198e01548```

Smart && Dumb Component 🌚 🌝:

  • Code readability 📕
  • Keep components as Dumb as possible.
  • Decide when a component should be Smart instead of Dumb.
import{ Component, Input, OnInit } from ‘@angular/core’;
@Component({
selector: ‘app-book-list’,
templateUrl: ‘./book-list.component.html’,
styleUrls: [‘./book-list.component.css’]
})
export class BookItemComponent implements OnInit {
@Input(‘book’) book: Book;
constructor() { }
ngOnInit() {
}
  • the business logic should be extracted from in-service methods and separated from the view
  • Business logic is usually easier to the unit test when extracted out to a service, and can be reused by any other components that need the same business logic applied.

Detection Optimizations (Onpush) 🔔 🔕:

AS we discussed in the previous part about the Presentational component which ide depends on the @Input Data can cause a performance issue if we dint choose the right Change Detection strategy for our components!

Generic Service Api Interaction (DRY ):

While defining our services in our Angular project, which are dedicated for fetching API resources and dealing with HTTP requests as we see there is a lot of duplicated code

Service-1-code :

@Injectable({providedIn: 'root'})export class Object1Service {constructor(private httpClient: HttpClient) { }public getUsers(page: number, size: number, filter: string): Observable<PageUser> {const url = filter != null ? `${config.serviceBaseUrl}/users/all?&page=${page}&size=${size}&search=${filter}` : `${config.serviceBaseUrl}/users/all?&page=${page}&size=${size}`;return this.httpClient.get<PageUser>(url)}

Service-2-code :

@Injectable({providedIn: 'root'})export class Object2Service {constructor(private httpClient: HttpClient) {  }public getUseCase(epicId: string|number, page: string, size: string, searchFilter: UseCaseSearchFilter): Observable<PageUseCase> {const url = `${config.serviceBaseUrl}/v3/use-cases/by-epic/${epicId}?&page=${page}&size=${size}`;return this.httpClient.post<any>(url, searchFilter);}
export class SubResourceService<T extends Resource> {
constructor(
private httpClient: HttpClient,
private url: string,
private parentEndpoint: string,
private endpoint: string,
private serializer: Serializer) { }
public create(item: T): Observable<T> {
return this.httpClient
.post<T>(`${this.url}/${this.parentEndpoint}/${item.parentId}/${this.endpoint}`,
this.serializer.fromJson(item))
.pipe(map((data: any) => this.serializer.fromJson(data) as T));
}

Clear Your Subscriptions 🚫 ❌:

Use appropriate RXJS operators ✅✅ :

import { Component } from '@angular/core';import { FormControl,FormGroup,FormBuilder } from '@angular/forms';import { SearchService } from './services/search.service';import 'rxjs/Rx';@Component({selector: 'app-root',template: `<form [formGroup]="coolForm"><input formControlName="search" placeholder="Search Spotify artist"></form><div *ngFor="let artist of result">{{artist.name}}</div>`})export class AppComponent {searchField: FormControl;coolForm: FormGroup;constructor(private searchService:SearchService, private fb:FormBuilder) {this.searchField = new FormControl();this.coolForm = fb.group({search: this.searchField});this.searchField.valueChanges.debounceTime(400).switchMap(term => this.searchService.search(term)).subscribe((result) => {this.result = result.artists.items});}}
this.userService.getCurrentUser()
.pipe(
tap((currentUser) => (this.userId = currentUser._id)),mergeMap((currentUser) =>this.blogsService.getBlogsByUser(currentUser._id)));
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import { forkJoin } from 'rxjs'; // RxJS 6 syntax
@Injectable()
export class DataService {
constructor(private http: HttpClient) { }public requestDataFromMultipleSources(): Observable<any[]> {
let response1 = this.http.get(requestUrl1);
let response2 = this.http.get(requestUrl2);
let response3 = this.http.get(requestUrl3);
// Observable.forkJoin (RxJS 5) changes to just forkJoin() in RxJS 6
return forkJoin([response1, response2, response3]);
}
}

Caching API Call ⚠️ 🚸:

Use CDK Virtual Scroll 📜 📃:

Problem 🚫: Modern web applications are complex. They have a lot of moving parts, complexity and oftentimes they have to deal with a huge amount of data.

$ npm install @angular/cdk// in your app.module.tsimport { ScrollingModule} from '@angular/cdk/scrolling';
imports: [
ScrollingModule
]

// Component
export class CdkVirtualScroll {
items = [];
constructor() {
this.users = Array.from({length: 100000}).map((_, i) => `scroll list ${i}`);
}
}
//Template

<ul>
<cdk-virtual-scroll-viewport itemSize="100">
<ng-container *cdkVirtualFor="let user of users">

<li> {{user}}</li>

</ng-container>
</cdk-virtual-scroll-viewport>
</ul>

State Management with Ngrx 🎴:

  • It provides centralized control for state transition
  • The code will be clean and more readable
  • Makes it easy to debug when something goes wrong
  • Dev tools are available for tracing and debugging state management libraries

Conclusion ❤️ 💛:

In conclusion, my advice for junior software developers with any technology to look always how to write a clean code and search about the best practices for it, not only make it functional just working, because this is the thing that will make you advance quickly in your career

<script>alert('try your best')</script>

<script>alert('try your best')</script>