Centric connect.engage.succeed

Angular App Architecture

Geschreven door Renzo Veldkamp - 18 januari 2019

Renzo Veldkamp
The fantastic JavaScript framework Angular enables you to write comprehensive apps. Usually such an app starts out small and gets bigger over time. To make sure your app stays easy to extend, test and maintain, you need some sort of architecture. Allow me to guide you through the approach I would take when defining LinkedIn’s web app architecture using Angular.

Outline

If you search for ‘Angular architecture’, you will find many articles on the architecture of the Angular framework itself. However, if you search for articles on the architecture of the app you are writing using Angular, you will find much less information. Many of these articles describe an app architecture and how to write code in line with that architecture. They are all great articles, yet they leave me with the question “how do you create that sort of architecture?”. Or “what function/action/code do I need to put into which component/service/directive/pipe?”

I would like to show you some of my personal approaches. My colleague Martijn Kloosterman wrote “How to talk to your children and listen to what they have to say” part 1 and part 2 where he explains parent-child communication between components. This article is actually the next step, because some basic Angular knowledge is required. If you are not sure, take a look at Martijn’s blog posts first.

Start

We are lucky enough to be able to start building the LinkedIn Angular app from scratch, so let’s use the convenience of Angular’s command line interface (CLI) and start with

 ng new linkedin

After switching to the linkedin folder and entering

 ng serve

we can go to our fully-functioning, testable Angular app:

When we run our tests:

 ng test

we see a browser window pop up that looks like this:

In the end, we want to turn our app into something like this:

This is basically the specification of the app we need to design.

Decomposition

When we look at this image, two things immediately attract our attention:

  • the navigation bar:

  • and the content of the homepage:

The items on the navigation bar typically represent separate components (I will only mention a few of the items):

  • HomeComponent (the LinkedIn logo and the Home icon)
  • NetworkComponent
  • JobsComponent

The home page has separate blocks of information, which I have highlighted with red borders. Each one of these blocks is a parent component and may contain many child components that each have a single function.

An example 

The top left component shows some profile info, like a customisable background, the profile picture, name, job title, company, a few counters and an ad at the bottom. The lines in the block show the different sections: 

  • personal information
  • view counters
  • advertisement

And because these sections might need to be hidden or made visible on request, let’s put them in separate child or dumb (i.e. presentation-only) components.

This gives us a ProfileSummaryComponent with the following view:

<app-personal-info>

<app-viewcounters>

<app-profile-block-ad>

 I won’t go into the details for these components: this is enough to describe the decomposition process.

Building the navigation bar

Designing the navigation bar consists of roughly 2 steps:

1. Define the routes

2. Define the component for each route

Our routing table (in src/app/app-routing.module.ts) will look like this:

const routes: Routes = [
  { path: '', pathMatch: 'full', redirectTo: '/home' },
  { path: 'home', component: HomeComponent },
  { path: 'network', component: NetworkComponent },
  { path: 'jobs', component: JobsComponent },
  { path: '**', redirectTo: '/home' }
];


We want to keep each of the components in the navigation bar as small as possible, because their sole responsibility is to instantiate the proper component when the selected (“activated”) route changes. If we need to update the network page functionality, we want to ensure that it does not affect our routing.

Bearing this in mind, I would create 2 components for each route, for example, a NetworkComponent and a NetworkPageComponent. The NetworkComponent is instantiated when the user clicks “My Network” and activates the network route. The NetworkComponent view consists of the NetworkPageComponent selector/tag only. The NetworkPageComponent contains the actual networking information.

Using the CLI, we start creating the components:

ng generate component network
ng g c network-page

(we got lazy and abbreviated the arguments of the ng-command)

As mentioned before, the only content in the NetworkComponent view is the HTML tag: <app-network-page></ app-network-page>. This tells us that the NetworkComponent has one task, i.e. to instantiate the NetworkPageComponent.

Now we’ve got our routing set up and we have PageComponents to instantiate when activating a route, let’s take a look at our navigation bar. As you might have guessed, this is also a component (created by ng g c navbar) and we reference it in our app.component.html, which only needs to contain these HTML elements:

<app-navbar></app-navbar>
<router-outlet></router-outlet>

The router-outlet element is replaced by the component of the selected (activated) route. The Angular router module takes care of this. Our NavbarComponent’s view could essentially contain some hyperlinks, but we need to add some images as well, so we define a NavItemComponent that contains an image and a hyperlink:

The view:

<div>
  <a routerLink="{{linkUri}}" routerLinkActive="active">
    <img class="nav-image" alt="{{linkUri}}" src="{{imageUrl}}" />
    <p>{{caption}}</p>
  </a>
</div>

And the component (the relevant parts):

export class NavItemComponent {
  @Input() public linkUri: string;
  @Input() public caption: string;
  @Input() public imageName: string;

  public get imageUrl(): string {
    return `assets/images/${this.imageName}`;
  }
}

This gives us the following navbar definition:

<nav>
  <app-nav-item imageName="home.svg" linkUri="home" caption="Home"></app-nav-item>
  <app-nav-item imageName="network.svg" linkUri="network" caption="Network"></app-nav-item>
  <app-nav-item imageName="jobs.svg" linkUri="jobs" caption="Jobs"></app-nav-item>
….
</nav>

which is a good start for our version of the LinkedIn app.

Fixing the unit tests

It may seem tedious, but we still have one inevitable job to do: fix our unit tests. All our editing caused many unit tests to fail, simply because we added components. Fixing this issue boils down to adding component refs in the failing specs.

For example, in our jobs.component.spec.ts, the declaration of the JobsPageComponent in the TestBed is missing:

declarations: [ JobsComponent, JobsPageComponent ]

This is a big step forward, but our specs for the navbar component and the nav-item component will fail, because they don’t have the import of the RouterTestingModule. This means that we need to import the reference and add it to the TestBed imports array:

import { RouterTestingModule } from '@angular/router/testing';
    TestBed.configureTestingModule({
      declarations: [NavItemComponent],
      imports: [RouterTestingModule]
    })

Once we have added all the missing declarations, all our existing unit tests will be passed:

Finally…

I’ve put the code that I mentioned before in my Git repo, so you can take a look: https://github.com/RenzoVeldkamp/AngularAppArchitecture.git

Enjoy the tips and use them if you like. And don't stop thinking!

References

  1. https://medium.com/@cyrilletuzi/architecture-in-angular-projects-242606567e40 by Cyrille Tuzi; discusses the modularity of your app
  2. https://bulldogjob.pl/articles/539-scalable-angular-application-architecture by Damian Sosnowski; describes smart and dumb components, as well as state management using Redux.
  3. https://netmedia.io/dev/angular-architecture-patterns-high-level-project-architecture_5589 by Ante Burazer; describes smart and dumb components, as well as state management using NgRx.
  4. https://medium.com/@bojzi/anatomy-of-a-large-angular-application-f098e5e36994 by Kristian Poslek; a great article that describes the design principles I encourage and mainly focuses on how to design data retrieval functions (he uses some AngularJS terminology, but the essence of his article hasn’t changed since the release of the next generation of Angular)

About Renzo

Craft Expert Renzo Veldkamp is part of the .NET team within Craft, the development programme for IT professionals (powered by Centric). If you would like to follow his blog, sign up for the Craft updates

Want to know more about Craft, the development programme for IT professionals? Check out the website.

Tags:.NET

       
Reacties
  • Centric
    Natasha Latham
    24 januari 2019
    It's interesting that you would make two components in order to use one solely for routing. It kind of feels like an unnecesarry addition at first, but I can see how it can be useful for when you would want to make changes. It's a way of keeping the routing consistent, but the content to which it routes to stays very flexible. I'll try it out! Thanks for the tip. :D
Schrijf een reactie
  • Captcha image
  • Verzenden