The Angular MonoRepo (II): folders
WARNING (20200213): this is a legacy article written for Angular 7.
Welcome to the second article of The Angular Monorepo. In the previous one we took a step by step journey through the base configurations on a project of these characteristics. This time we are going to get deeper into it by talking about the folder structure.
But what have we got so far? Essentially a number of configuration files and a projects
folder that provides a common place for our apps and libraries.
Of course the way we structure a library may vary depending on its type and purpose. However an Angular app, no matter how it is driven, will always follow certain patterns. Let us check on what we know for sure:
Similarly to the root folder, our app project folder will also contain configuration files and a code folder:
src
.Within it the Angular CLI has already created for us some base files (such as
index.html
andmain.ts
) and 3 folders:app
,assets
andenvironments
.
Since later on we will need to scale our styles we could not do better than creating a fourth folder called scss
for our Sass partials. We can even create a variables.scss
file into it:
core and shared
If you have read the Angular Style Guide you probably have a good knowledge on the Overall structural guidelines. An Angular application must have a separation of concerns and that starts by getting the big picture of it and splitting that into different features.
The first 2 features that come into play in every app are the core and the shared ones.
The 6.0.0 release of Angular did not only include library support (without which this article could not be written) but also something that may change our structure: tree shakable providers.
Before v6 all our singleton services would be put in the CoreModule
but now we are not bounded to do this anymore. We can simply add providedIn: ‘root’
into our injectable metadata and that is it.
Do we still need the core feature module though? The answer is yes, but we will only use it to provide injection tokens such as APP_INITIALIZER
or HTTP_INTERCEPTORS
.
As per the shared feature nothing changes: it will still contain components, directives, and pipes that are meant to be re-used in other feature modules. But since both core and shared are so particular and not related to the specific traits of our product we should do something in order differentiate them from the rest.
Just create a _core
and a _shared
folders into app
with their corresponding module files. The underscore is usually something to avoid when naming folders (and files) but in this case it will re-arrange them in the most convenient way and at the same time it will make them look as “private”.
app feature
Believe it or not there will be many elements in your application that will not belong to a specific feature but rather to a more generic app-wide one. This is why we need to create an “app into app” folder. Both parent and child folders may have the same name but their meaning is completely different. The former is a container of features whereas the latter is the app feature. We will move all the app files into it (do not forget to change the import in main.ts
).
The folder structure now looks like this:
Feature members
At this point we need to create a last level of granularity that can be applied not only to the app feature but also to any of the further ones (e.g. heroes and villains or pizzas and toppings).
We can start by creating folders for each feature member type:
components
containers
enums
mixins
models
services
store
The last one is dedicated to the implementation of NGRX and will be not be treated in this article. Nevertheless we can already mention that the store
has also its own member types:
actions
effects
reducers
selectors
Each member folder should have a barrel index.ts
file in order to re-export its Typescript modules. We will realise how good this practice is as our project grows.
Do you remember all those singleton services that we used to include in the core module? Now they belong to the services
of the app
feature. Let’s imagine that our app needs a global ApiBaseService
. We would export it in app/services/index.ts
like this:
export * from '@first-app/services/api-base.service';
This mapped path is possible thanks to the configurations that we previously wrote in the tsconfig files.
Then we would be able to import this and any other root service this way:
import * as fromRootServices from '@first-app/services';
private apiBaseService: fromRootServices.ApiBaseService;
containers and components
Even if we do not employ NGRX in our app we can always use the benefits of a one way data flow: from stateful (smart) components to stateless (dummy) components. The smart ones are the containers and they communicate with the exterior world (e.g. making HTTP requests). They take stream properties in the shape of observables and pass them down to their children.
<fst-container [entities]="entities$ | async">
Angular’s AsyncPipe
takes care of the observable by subscribing and unsubscribing automatically but, most importantly, it allows us to set our change detection strategy to OnPush
. This applies not only to the containers but any other @Component
declaration of our app. As you can imagine this kind of design increases our performance dramatically.
The so called components are merely presentational. They take the subscribed properties from their containers as @Input
and emit data to them when necessary as @Output
.
In some special cases containers need to subscribe to observables in the Typescript class. It is an extended convention to add a dollar sign suffix to end of the observable property name. However you if you need to assign a property to a subscription you can go a little further by adding two:
entities$: Observable;
entities$$: Subscription;
So what kind of component is app.component.ts
? You got it: a container. That is why we need to move it to the containers
folder of our app
feature. Moreover we need to create the third app
folder for the app component since it will most likely involve at least 4 files: *.component.html
, *.component.scss
, *.component.spec.ts
and .component.ts
.
We are still not adding specs but we can already move the other 3 files to that folder:
Now, for a barrel file on containers and components we will follow a slightly different re-export pattern:
import { AppComponent } from '@first-app/containers/app/app.component';
export const containers: any[] = [AppComponent];
export { AppComponent } from '@first-app/containers/app/app.component';
This way we wan use the spread operator on the containers
array to import our declarations in AppModule
:
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppRoutingModule } from '@first-app/app-routing.module';
import * as fromRootContainers from '@first-app/containers';
@NgModule({
declarations: [...fromRootContainers.containers],
imports: [BrowserModule, AppRoutingModule],
providers: [],
bootstrap: [fromRootContainers.AppComponent]
}
export class AppModule {}
And once we have implemented app feature components we can also add ...fromRootComponents.components
to our declarations array. Nice and clean.
models and enums
Before even start writing any implementation we should first be aware of our data model.
In most of the cases we will express models as interfaces since they are not compiled in the final bundle. We should use them to type our app as strongly as possible. For example:
import { NavigationExtras, Params } from '@angular/router';
export interface NavRoute<P> {
path: ['/', ...P[]];
query?: Params;
extras?: NavigationExtras;
callback?: (value?: any) => any;
}
export interface NavLink<P> extends NavRoute<P> {
text: string;
icon?: string;
}
If you are on a project that tests everything but routing or if you are just a methodic person and do not want to include plain arbitrary strings in your RouterLink
directives you will definitely want to have all your links centralised and perfectly typed.
Do not focus on the NavRoute
interface. It is just the kind of object that we would pass to a higher level navigate
method in our router.effect.ts
if we implement NGRX. If you are curious about that take a look here.
As you can see, a NavLink
is just the same as a NavRoute
but with a text and an optional icon text (e.g. if we use Material Icons). This is extremely useful when we need to create a bunch of navigation links in our template. By creating this kind of data you can iterate over a *ngFor=”let link of links”
and then just assign [routerLink]="link.path"
in the link tag attributes and {{link.text}}
in the link tag content.
Now notice that we have got a generic type <P>
for the paths. That means that in our application we will always specify all the paths as an array whose first element is the root path string /
whereas the other elements will belong to a certain type that will be given on a case basis.
To achieve that kind of precision we need to provide our specific app path strings as well as the strings for the corresponding texts:
export enum NavBase {
empty = '',
root = '/',
wildcard = '**'
}
export enum NavPath {
path1 = 'path1',
path2 = 'path2',
path3 = 'path3'
}
export type NavPathET = keyof typeof NavPath;
export enum NavText {
text1 = 'text1',
text2 = 'text2',
text3 = 'text3'
}
export type NavTextET = keyof typeof NavText;
export enum NavParams {
param1,
param2,
param3,
}
export type NavParamsET = keyof typeof NavParams;
That means that we can type our routes and links like this:
import * as fromRootEnums from '@first-app/enums';
NavRoute<fromRootEnums.NavPathET>
NavLink<fromRootEnums.NavPathET>
If you ever worked with AngularJS you might remember something called constants. They were simply fixed values that we only wanted to write once in our app in order to stay DRY (Don’t Repeat Yourself). Despite the fact that this concept has become somehow obsolete it really comes in handy and many people declare const
variables to reach the same goal.
There is a better solution: use enums. They are not only valid for simple enumerations but for a better and more elegant constant declaration.
Enum members can have the same name as value or not. NavBase
is used for the routing modules and clearly has a different value than the expressed name on each member (empty, root or wildcard). In the case of NavPath
and NavText
we are not entirely sure if our manager or our client will want us to change the value so we need to apply assignation here too. However the definition of NavParams
for both required and optional route params is totally up to us so we just assign a name that will be the same as the string value. This is also the reason why we are defying the convention of setting the names in upper camel case by using lower camel case instead, to make this pattern blend better into our app. This will cause no problem whatsoever.
Let’s take a look at how we would retrieve these values:
import * as fromRootEnums from '@first-app/enums';
const path1: fromRootEnums.NavPathET = fromRootEnums.NavPath.path1;
const param1: fromRootEnums.NavParamsET = fromRootEnums.NavParams[fromRootEnums.NavParams.param1];
Getting path1 is self-explanatory. That would give us “path1” string. But how we got “param1” in the second case is a little more complicated to understand. For that we need to take a look at what the NavParams
enum compiles to:
var NavParams;
(function (NavParams) {
NavParams[NavParams["param1"] = 0] = "param1";
NavParams[NavParams["param2"] = 1] = "param2";
NavParams[NavParams["param3"] = 2] = "param3";
})(NavParams = exports.NavParams || (exports.NavParams = {}));
As we can see if we would have tried to retrieve the value in the same way as with the NavPath
enum we would have got the value 0
.
You must have also noticed that we can get the type of the enum (so the type of our constants) by applying keyof typeof
. As a naming convention we apply the ET (Enum Type) suffix.
And that is it. With our models and enums we have almost everything under control. But there is more…
mixins
Interfaces are fine to model simple classes or properties but when it comes to more complex elements such as components that follow a certain patterns we need something more powerful: mixin classes.
Have you ever thought of component classes that contain subscriptions? They need to unsubscribe from the observables in the OnDestroy
lifecycle hook. This kind of cleanup is most recurrent in Angular and requires an amount of boilerplate that can be avoided like this:
import { OnDestroy } from '@angular/core';
import { Constructor } from '@my-scope/my-ng-lib/constructor';
import { Subject } from 'rxjs';
// WARNING: THIS DOES NOT WORK IN ANGULAR 8 WITH AOT!
// HOWEVER, IT DOES WORK IN ANGULAR 9!
/**
* Mixin class to automatically unsubscribe in component classes.
*
* // constructor.ts
* export type Constructor<T = {}> = new (...args: any[]) => T;
*/
export const subscribedContainerMixin = <T extends Constructor>(base: T = class {} as T) =>
class extends base implements OnDestroy {
destroyed$ = new Subject<void>();
/**
* DO NOT this.destroyed$.complete();
* It is not necessary:
* https://stackoverflow.com/questions/44289859/do-i-need-to-complete-a-subject-for-it-to-be-garbage-collected
*/
ngOnDestroy(): void {
this.destroyed$.next();
}
};
Check out this article if you want to dig in this interesting topic.
The next article will talk about libraries and how they can empower us even more when scaling our project.
Stay tuned!