Angular guidelines
Angular is a framework completely rewritten from the ground up, replacing the AngularJS framework (Angular 1.x).
More than just a framework, Angular should now be considered as a whole platform. It comes with a complete set of tools, like its own CLI, debug utilities and performance tools.
Angular has been around for some time now, but I still get the feeling that it’s not getting the love it deserved, probably because of other players in the field like React or VueJS. While the simplicity behind these frameworks can definitely be attractive, they lack in my opinion what is essential when making big, enterprise-grade apps: a solid frame to lead both experienced developers and beginners in the same direction and a rational convergence of tools, patterns and documentation. Yes, the Angular learning curve may seem a little steep, but it’s definitely worth it.
— maintainers of ngx rocket
Angular also has great detailed docs available at Angular docs.
Getting started
Newcomer
One of the big disadvantages of Angular is its steep learning curve. A newcomer can be overwhelmed by the number of new concepts. So, if you are not familiar with Angular yet, check out this progressive tutorial which will guide step by step in making your first Angular application.
AngularJS developer
If you come from AngularJS, you already may be familiar with a lot of concepts of the new Angular. Here is a quick reference guide to make the transition easier AngularJS quick reference.
Cheatsheet
Here is a great cheatsheet you can keep around, till you know Angular by heart.
Setting up an application
The best way for getting started with Angular app is to use the Angular CLI tool:
- Link to official Angular CLI setup;
- Add Nx as an extension of the CLI. It improves developer speed and brings true monorepo development to Angular. It also allows you how to add NestJS as a backend to the same repo and to some extent even reuse the code. See more about Nx at the end of the file;
- A great tool to install is Angular Console, which is basically a visual tool for working with Angular CLI. It also supports installed Nx semantics if you choose to install Nx.
Style guides
Angular has comprehensive style guides written at style guides. Do check them out.
File structure
The best way is to stick to the Angular file structure style guide.
Creating new files inside a project
For starters, a good way of adding new components/modules/directives/… is using Angular CLI generate tool. If you are not a big fan of CLI tools you can install Angular Console.
You can find full documentation on Ng generate docs.
Here is an example of creating a new component using Angular CLI:
Run ng g component header
inside root folder, to create a header component.
NOTE: Every component/directive/service should be added to the corresponding Module. Using Angular CLI, this is added automatically, to the nearest module.
You can also find Angular CLI plugins for different IDEs:
- VS Code -> Angular schematics extension;
- WebStorm -> Built-in;
Forms
There are two ways of creating/binding to form(choose one and stick to it):
- Reactive forms Reactive form docs;
- Template-driven forms Template-driven forms docs;
Linting
Typescript linting is enabled by default when creating an app with Angular CLI.
For style linting run:
npm install stylelint stylelint-scss stylelint-config-standard stylelint-config-recommended-scss
;- Create a file named
.stylelintrc
in the root folder; - Place the following content inside:
{ "extends": [ "stylelint-config-standard", "stylelint-config-recommended-scss" ] }
- To run style linter use
stylelint \"src/**/*.scss\" --syntax scss
;
For html linting run:
npm install htmlhint
;- Create file named
.htmlhintrc
in the root folder; - Here is a sample configuration you can place inside the file:
{ "tagname-lowercase": false, "attr-lowercase": false, "attr-value-double-quotes": true, "tag-pair": true, "spec-char-escape": true, "id-unique": true, "src-not-empty": true, "attr-no-duplication": true, "title-require": true, "tag-self-close": true, "head-script-disabled": true, "doctype-html5": true, "id-class-value": "dash", "style-disabled": true, "inline-style-disabled": true, "inline-script-disabled": true, "space-tab-mixed-disabled": "true", "id-class-ad-disabled": true, "attr-unsafe-chars": true }
- To run html linter use
htmlhint \"src\" --config .htmlhintrc
;
Routing
Since one of the biggest problems of Angular is its bundle size, it is advised to keep well-structured routing with Lazy loaded modules. Also, the best way is keeping each module routes, inside separate routing module.
Example:
Lazy loading DevicesModule
in core routing file app-routing.module.ts
:
const routes: Routes = [
{
path: 'devices',
loadChildren: () => import('./devices/devices.module').then(m => m.DevicesModule)
},
...
Devices module file devices/devices.module.ts
:
@NgModule({
imports: [
CommonModule,
DevicesRoutingModule
],
declarations: []
})
export class DevicesModule { }
Devices module routing file devices/devices-routing.module.ts
:
const routes: Routes = [
{
path: '',
component: DevicesPageComponent
}
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class DevicesRoutingModule { }
Styling
So there are four ways to set the component view encapsulation, see more details at View encapsulation docs.
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss'],
encapsulation: ViewEncapsulation.Emulated // <-- component view encapsulation
})
The Emulated view encapsulation is the default one. If you are not used to shadow DOM, you are going to run into some troubles. Sure you can set the view encapsulation to None, but is not advised. Now imagine inside of app-root
component you have some external component ngx-datepicker
. You can’t set the styles inside of app.component.scss
for ngx-datepicker
, because of view encapsulation. You can use ::ng-deep
css selector to cheat, but since it is going to be soon deprecated, it is not advised, you can read more at Ng deep selector.
The best way to get through this problem is:
- Make sure you use SCSS;
- Create file named
_app.component.scss
in the same folder asapp.component.scss
; - Place the following content inside
_app.component.scss
:@mixin app-root-global() { ngx-datepicker { /* Place your styles here */ } }
- Inside global styles file
styles.scss
import the file and call the mixin:@import '_app.component.scss'; @include app-root-global();
You could just place the styles inside
styles.scss
file. But this way code is way more structured. Mixin is advised, so we don’t reimportscss
files(in case we imported, let’s say_variables.scss
inside_app.component.scss
).
Managing data
There are a couple of ways to send/receive data in an Angular application.
- @Input, @Output - use this to send data from component to child component and back(If you need multiple-level component communication don’t use this method!);
- Services - services can be used to send data to multiple-level component structures. And their lifespan is usually limited to its Module. Service can also be manually created with the
new
keyword and can live inside the particular component (can also be used for global state management, but stick with reactive state management); - Reactive state management(NGRX, NGXS), NGXS is simpler library than NGRX, so it’s your choice which one to use. It is advised to use reactive state management for global state management.
Lifecycles hooks
You can find all the details at lifecycle hooks guide. Let’s just mention the three, that we find most important:
- onChanges - detects changes to input parameters;
- onInit - Inputs are available, also a good idea to start making a request here, not in the constructor;
- onDestroy - here comes the component/service clean up logic;
Change detection
In Angular, change detection is done for each component separately. By default ChangeDetectionStrategy.Default
change detection is enabled. I would encourage you to use ChangeDetectionStrategy.OnPush
on all components, since it can dramatically speed up your application. A lot less change detection cycles are made. You need to explicitly set it like this:
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush // Default is ChangeDetectionStrategy.Default
})
Now if you are using Mutables
and Observables
everything should in general work the same. Where things fail is with primitives(i.e. string, number) since they are Immutable
. Basically, in Angular zones
take care of the change detection. In Angular, there is a specific zone called ngZone
. To update the Immutable
object run the following:
constructor(private ngZone: NgZone,
private cdr: ChangeDetectorRef) {
this.ngZone.run(() => {
// Make changes to immutable objects here
this.cdr.markForCheck();
});
// You can also force change detection, but above is preferred.
this.cdr.detectChanges();
}
Using ChangeDetectionStrategy.OnPush
may be more time consuming, but increases the speed of the application. Also, it improves the developer’s understanding of the internal working of the Angular change detection system.
Reactive programming(Observables)
Angular uses a reactive system by design. You are not forced to use the reactive pattern, but they make up the core of the Angular. It’s highly advised to learn and use the reactive programming pattern.
If used correctly, observables are one of the best things in Angular. Convention for observables is to use $
at the end of the variable name. Here is the rxjs documentation.
There is some good news, observables can be turned into promises and vice versa.
// Create an observable
const observable$ = new Observable((observer: Subscriber<any>) => {
observer.next([]);
observer.complete();
});
// Convert observable to promise
const promise = observable$.toPromise().then(() => {});
// Convert promise back to observable
const fromPromise$ = from(promise);
fromPromise$.subscribe();
NOTE: This is not a good practice, but it can make the transition from promises to observables easier. But promises can emit only one value!
Where they shine? Microservices architecture. In the next example you can see 3 subsequent calls to API, directly inside .html
file, here we use async pipe
so that the observable get resolved:
<ng-container *ngIf="device$ | async as device">
<ng-container *ngIf="deviceToOwner(device) | async as deviceToOwner">
<ng-container *ngIf="owner(deviceToOwner) | async as owner">
</ng-container>
</ng-container>
</ng-container>
Observables are lazy, which means they will not execute till subscribed to. A good practice is to subscribe to observables with an async pipe
inside HTML file since the component automatically cancels the observable in case of component destroy itself. Otherwise, we need to do this ourselves.
How we cancel the observable inside *.component.ts
file? This is necessary every time, we subscribe inside the code. Because if in the meantime, the component destroys itself and observable is still loading, this
no longer points to the component class.
export class AppComponent implements OnDestroy {
private readonly unsubscribe$ = new Subject();
constructor() {
const observable$ = new Observable((observer: Subscriber<any>) => {
observer.next(null);
observer.complete();
});
observable$
.pipe(takeUntil(this.unsubscribe$))
.subscribe();
}
ngOnDestroy(): void {
this.unsubscribe$.next();
this.unsubscribe$.complete();
}
}
TIP: Great way to not write all the code above is to use a library like untilDestroyed, and can be used like this:
observable$
.pipe(untilDestroyed(this))
.subscribe();
Some combination/creation/operators we find useful (these just some that we used before, there are a lot more):
- combineLatest - emits values from all observables, every time one updates;
- forkJoin - promises equivalent to
Promise.all
; - empty - returns empty observable, great for conditions;
- from - turn an array, promise, or iterable into an observable;
- interval - fires every time, some time is passed;
- of - creates observable from an object for example;
- throwError - throws an error as observable;
- catchError - catch an error, change response if you like;
- share - share observable;
- debounce - when typing, i.e. delay one second after the last character typed;
- distinctUntilChanged - only emit when the value changes;
- filter, first;
- take, takeUntil, takeWhile;
- map, mapTo - change emitted value;
- switchMap - Really powerful, chaining observables;
- tap - the same as subscribe callback, but an operator;
- finalize - run on the observable closing;
- toPromise - observable to promise;
- BehaviorSubject, Subject - streams as Observables;
Learning material
- What is reactive programming?, explained nicely through a simple imaged story;
- The introduction to reactive programming you’ve been missing, the title says it all;
- Functional reactive programming for Angular 2 developers, see the functional reactive programming principles in practice with Angular;
- RxMarbles, a graphical representation of Rx operators that greatly help to understand their usage;
— maintainers of ngx rocket
Testing
Angular has a great test guide written. When creating an app through Angular CLI, both unit testing and e2e testing are already configured. It’s advised to create files with Angular CLI so unit test files are automatically created(*.spec.ts
files). You can find e2e tests inside the e2e
folder. *.po.ts
files represent helper classes, for example, login.po.ts
can contain some helper methods for login, so they are defined only once. *.e2e-spec.ts
files contain e2e tests.
AOT vs JIT
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. With AoT we can reduce the number of computations required for the rendering of our application by performing the compilation as part of our build process.
The only case we can think of using the JiT compiler is when creating dynamic components. Otherwise, always use AoT compiler.
Imports
Since the bundle size of Angular is already huge, we must do everything to keep it, as small as possible. Do, this, when you are already almost finished with developing the application, and the application goes to production. We can analyze the bundle size with the following steps:
npm i webpack-bundle-analyzer --save-dev
ng build --prod --stats-json
npx webpack-bundle-analyzer dist/stats.json
Now a local server starts up running on http://localhost:8888, navigate to the page. Now you can examine if there are any unnecessary modules being imported.
One bad example is the moment
library. When importing it, all the moment-locale
files are being loaded as well, increasing bundle size.
A second bad example is when importing from rxjs/operators
.
Bad:
import {takeUntil} from 'rxjs/operators';
Since rxjs/operators
point to the index file, that imports all of the rxjs
operators.
Good:
import {takeUntil} from 'rxjs/internal/operators/takeUntil';
The above code only imports takeUntil
operator and not 50 other operators, that are not needed. Be careful what you import!
Third bad example, import only the Modules you need. For example, if using some chart library, and using only PieChart
, make sure that only PieChartModule
is imported and not the whole library module, for example ChartsModule
.
Deploying
Make sure you first consult Angular deploy docs.
The basic deployment process looks like this:
- Run
ng build --prod
; - Copy files from
/dist
to the remote server;
Since Angular is a SPA, it does not need an actual web server. You could copy files to a static file server, saving you some money. Also make sure that the cache is set on the server, since the files can get quite big, and if the application is frequently used, so the files don’t have to download every time.
Service worker
Follow the service worker getting started guide. It takes only half an hour in order to set up a working service worker. It can be configured to prefetch lazy modules, on page load, creating a really fast user experience.
browserlist
To configure which polyfills. are being included, open browserlist
file at the root of the project. For more check out format and rule options.
This handles JS unsupported features in browsers, and also manage auto prefixing for CSS for configured browsers.
Resources
- Angular performance checklist is a good list to go through before creating production builds;
Proxy
The proxy can be used to bypass CORS browser restriction, so you don’t need to run a web server locally. Also, it can help in some other cases. Follow the using proxy guide.
Mocking
For mocking services, here is how we do it:
Create a common interface DeviceServiceInterface
that implements all functions:
export interface DeviceServiceInterface {
getDevices(): Observable<DeviceInterface[]>;
}
Now create two services. The http service:
@Injectable()
export class DeviceService implements DeviceServiceInterface {
constructor(private httpClient: HttpClient) { }
getDevices(): Observable<DeviceInterface[]> {
return this.httpClient.get('<ENDPOINT_URL>') as Observable<DeviceInterface[]>;
}
}
And the mock service(responses should be in .json files, here are inline, for simplicity):
@Injectable()
export class DeviceMockService implements DeviceServiceInterface {
constructor() {
}
getDevices(): Observable<DeviceInterface[]> {
return of([{id: 1, name: 'device-1'}, {id: 2, name: 'device-2'}] as DeviceInterface[])
.pipe(delay(1000)); // To simulate delay from http response
}
}
In the corresponding module provide this:
{
...
providers: [
{
provide: DeviceService,
useClass: environment.production ? DeviceService : DeviceMockService
}
],
...
}
Now you can use DeviceService
, locally mocked and on production, the actual http server.
Server side rendering - SEO
The Angular Universal is a technology that renders Angular application on the server.
Here are three main reasons for using Angular Universal:
- Web crawlers can parse content for SEO;
- Improved performance on mobile and other low-powered devices;
- The first-page load is quicker;
Nx
Nx is a set of extensible dev tools for monorepos, which helps you develop like Google, Facebook, and Microsoft. Nx does not replace Angular CLI but makes it more powerful.
Why would you use Nx?
- Building frontend/backend application in a single repo and sharing code(i.e., sharing Types);
- Sharing libraries/components between multiple Angular application in a single repo;
- Testing and building becomes easier and faster, because Nx tests/builds only affected projects;
Resources
Upgrade Angular
An Angular update website has a great guide for upgrading. Upgrading to a newer version of Angular is pretty simple with ng update /*angular packages here*/
, since the command does most of the migration steps all already for us. We just need to update the code, if there are any breaking changes.
Analytics
You may want to consider Angulartics2. It has many supported providers, like Google Analytics
, Mixpanel
, Facebook Pixel
, Application Insights
, …
Documentation
Compodoc automatically generates documentation for Angular projects.