Angular App Architecture

A picture

Auteur

Craft Redactie

Datum

18-1-2019

Leestijd

6 minuten

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:

A picture

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

A picture

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:
A picture

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!

Read my second article about the way I would design LinkedIn’s web app architecture using Angular.