This guide should be a living document describing the core principles of writing a modern, scalable angular application that is aligned with best practices in the angular style guide and the angular community.
The constant theme of this guide is the clear and explicit separation of boundaries of the application.
These boundaries can take the variety of forms, some of them will be discussed in the next sections.
Angular architecture guide strongly suggests the use of feature modules that help with clear separation of the domain features and workflows.
This means that the application starts off at the root module, that acts as a top-level glue for the nested branches of other modules. These modules, in turn, may have other modules if the feature separation drives that.
In theory, it may look like this:
Root Module (includes FeatureA Module)
-> FeatureA Module(includes SubFeatureA module)
-> SubFeatureA Module
In practice using the Twitter app:
AppModule (includes Home module, Moments module, Notifications module, Messages module)
-> Home module(includes Feed module, Profile Module, Trends Module etc)
-> Feed module
etc
etc
See https://angular.io/guide/styleguide#style-04-09 and onwards for additional reasoning about feature modules.
Also, this medium article is a great follow-up reading material that explores modules in more details.
Routing is done by a separate routing module on a per feature module basis. If the navigation is handed to a feature module, this feature module is lazy loaded.
const topLevelRoutes: Routes = [
{path: '', redirectTo: '/home', pathMatch: 'full'},
{path: 'home', loadChildren: './home/home.module#HomeModule'}
];
@NgModule({
imports: [RouterModule.forRoot(topLevelRoutes, {enableTracing: true})],
exports: [RouterModule]
})
export class TopLevelRouterModule {
}
And in the HomeRouterModule:
const homeRoutes: Routes = [
{path: '', component: TwitterFeedComponent, pathMatch: 'full'},
{path: 'profile', component: ProfileComponent}
];
@NgModule({
declarations: [],
imports: [
RouterModule.forChild(homeRoutes, {enableTracing: true}),
CommonModule
],
exports: [RouterModule]
})
export class HomeRouterModule {
}
Notice that the empty path in the HomeRouterModule is in fact a scheme://hostname/home URL in the address bar.
Also, pay attention to the use of forChild in the lazy-loaded module.
Typescript offers a powerful type support. Types should be leveraged at all times. Any declared method should return an appropriate type:
Good(returns Observable<Tweet[]>)
getFeed = httpClient.get('https://api.twitter.com/twitterFeed')
.pipe(map((response: Tweet[]) => response));
Not So Good(returns Observable<Object>)
getFeed = this.httpClient.get('https://api.twitter.com/twitterFeed');
Boundaries should be separated with the use of Interfaces, that make mapping extremely easy:
interface NetworkTweet {
body: string;
twitter: NetworkUser;
}
interface DomainTweet {
body: string;
twitter: DomainUser;
hasBeenFavourited: boolean;
hasBeenLikedNumTimes: number;
}
interface PresentationTweet {
formattedBody: string;
author: string;
authorImageUrl: string
isFavourited: boolean;
numberOfLikes: number;
}
Typically, the API exposes the NetworkType, the service exposes the DomainType and the Component exposes the PresentationType.
Mapping is performed by the API class, service class, and component class respectively.
tweets: Observable<PresentationTweet[]> = this.feedService.allTweets // gets DomainTweet[]
.pipe(
map(tweets => tweets.map(tweet => mapToPresentation(tweet)))
);
This section in the TypeScript explores additional types that offer great power to the developers.
The reasons for why Angular decided to use Observable over JS Promise is discussed at large in this thread: angular/angular#5876
Angular 2 and above is built on top of RxJS framework and observable type should be a common occurrence.
Most often it will originate in the HttpClient, that exposes Observable type for all network calls.
Observable type should always be propagated up to the template, mapped to the presentable type if necessary. At that point, the template will subscribe to the chain using the Async pipe.
If observable being subscribed represents a cold observable, the chain will do the work on each subscription. If this behavior is undesirable the use of multicasting operators should be considered.
Service acts as a boundary between the data type and the domain type.
This is often a synonym of the Repository Pattern, where the source of data is hidden behind the Repository class (service in this case).
Services typically expose the observables of some domain type(fetchItems, loadData etc), or perform one-off side effect operations (such as save, insert, refresh etc) and return void;
A component is a final stop between presenting the data in the template and answers the question of "What to present on the screen?"
Components map the domain types to the presentable versions of these objects.
Components expose properties for non-async data results and observables where the async operation is involved.
Observable types are typically not subscribed on in the components, but instead exposed as properties.
There are two types of components - Container component and Presentation component
Container component is the one that has the services injected and generally does the data manipulation and transformation.
Presentation component, on the other hand, is responsible for displaying the data via the Input properties:
<app-tweet-count-header [numberOfTweets]={{totalNumberOfTweets | async}}></app-tweet-count-header >
If the component needs to observe the data from the service to perform a side-effect, then subscription should be manually cleaned-up to prevent memory leaks. \
see such use case described here: https://angular.io/guide/component-interaction#!%23bidirectional-service \
It has to be mentioned that there are multiple ways of handling of the subscription in the component. For this reason, use async pipe as much as possible. Observables should be combined to have no more than one subscription that is managed at a time.
A template is a View in a traditional architecture sense. A view has the answer to "How to present the given data on the screen?"
Template binds properties exposed in the Component to the DOM elements.
The difference can be illustrated by the following example:
What - "Bonjour le monde"
How - "On top of the screen with 50px margin"
A template should not be interpreting the data in any way or make any logical decision about the data. The template just presents what it is given or sends the data into the component that can present it.
Example of what NOT to do:
<h2>Total number of tweets: {{(allTweets | async)?.length}}</h2>
Do this instead:
<h2>Total number of tweets: {{totalNumberOfTweets | async}}</h2>
Remember that each class has only one purpose. TwitterFeedService only deals with the twitter feed and should not include any profile information for example.
If this additional functionality needs to be included, for example, to display the short author summary next to each tweet, the use of composition or delegation should be considered.
Typescript also offers strong support for Intersection types .
For the reasons described above names such as Manager, Coordinator and such should not be used, as they offer an invitation for bloat and dilute the separation of concerns.
- Architecture in Angular projects
- [Organizing angular applications] (https://medium.com/@michelestieven/organizing-angular-applications-f0510761d65a)