Angular 17 Project from Scratch

Last updated: 3/1/2024

This is a writeup for building a very rudimentary, yet clean, web UI using Angular and Bootstrap. As I add more relevent pieces and as the frameworks upgrade, I’ll do my best to keep it up to date.

Based on the last updated timestamp at the top, here’s the result of the ng version command with the current versions of the tools for this project.

ng version
---
Angular CLI: 17.2.2
Node: 18.19.1
Package Manager: npm 10.2.4
---

Easy Button

If you just want to clone the project and use it as a starting point, just do a clone and replace some string values in the relevent project json files. I use this to quickly get prototypes up and running.

git clone git@github.com:stephennimmo/angular-starter.git <project-name>
find <project-name> -type f -name '*.json' -exec sed -i '' s/angular-starter/<project-name>/g {} +

Building It Yourself

Before starting a project, get updated.

brew update
brew install node angular-cli

Project Initialization

First, we need to create our angular application using the cli. Here’s the command I use.

ng new --routing --style scss <your-project-name-here>

I want to add routing to the project so I add the --routing flag. I also want to use SCSS for all my stylesheets, so I set that using the --style scss flag. The naming is a bit subjective for me. Sometimes I want the project name to end with ‘-web’ to denote it’s a web app. Other times I want it to end with ‘-ng’ to denote it’s an Angular app.

Once it’s ready, open the project in your favorite IDE.

Core Module

The core module is the entry point for the “backend”. This module will contain all of the services and domain objects related to API calls.

ng g module core

We only want the core module to be loaded once in the AppModule. We are going to add a constructor check to enforce this. Here’s the full CoreModule class at this point.

import { NgModule, Optional, SkipSelf } from '@angular/core';
import { CommonModule } from '@angular/common';


@NgModule({
  declarations: [],
  imports: [
    CommonModule
  ]
})
export class CoreModule {
  constructor(@Optional() @SkipSelf() parentModule: CoreModule) {
    if (parentModule) {
      throw new Error('CoreModule has already been loaded. You should only import Core modules in the AppModule only.');
    }
  }
}

Shared Module

The shared module is where all the shared component stuff goes. Think navbar, error-headers, common elements.

ng g module shared

With the core and shared modules created, update the AppModule to import them.

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { CoreModule } from './core/core.module';
import { SharedModule } from './shared/shared.module';


@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    AppRoutingModule,
    CoreModule,
    SharedModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule {
}

Feature Modules

Now let’s get to the feature modules. This is where the setup gets a bit more subjective which creates optionality. To me, the driver on the modules is the logical organization of the site based on URL paths. The modules add paths in the routing.

If you are simply building a public website with no protected parts, then you could proceed two different ways.

  1. If it’s super small, with just a handful of pages, just drop the components in the app folder and put them in the AppModule. Each page can be a path off the root.
    • https://domain.com/component1, https://domain.com/component2
  2. If you have more components that can be broken into groups, then create a different module for each group.
    • This will add paths to the URL – https://domain.com/group1/component and https://domain.com/group2/component

If you are building a site with public components and protected-by-auth components, there are more decisions to be made.

If it’s super small, create a module called “admin” or “protected” or whatever you want in the URL to denote that these are not public and put the restricted components in that module. The public components can then stay on the root of the app or you can create a public module and place the components in there. If you do it this way, you can still route the public module to “/” and the components won’t have to have an ugly extra path on them.

ng g module public --routing
ng g module protected --routing

For all modules, add the import for the SharedModule. Here’s what the PublicModule looks like.

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { PublicRoutingModule } from './public-routing.module';
import { SharedModule } from '../shared/shared.module';


@NgModule({
  declarations: [],
  imports: [
    CommonModule,
    PublicRoutingModule,
    SharedModule
  ]
})
export class PublicModule {
}

Create the Index Component and setting up Routing

Let’s create our Index page for the public-facing part of the site. At the root of the project, run the following command.

ng g component public/index

This will generate all the boilerplate code (ts, html, scss, and the test) in the public module in a folder named index.

Next, we will update the public-routing.module.ts which was generated when we added the --routing flag to the module generation command. Here we will add the route for the IndexComponent to the routes var located in the file.

const routes: Routes = [
  { path: '', component: IndexComponent }
];

Now that the route is setup within the public module, we will need to plug the public module and it’s routing into the main app-routing.module.ts. We are going to use lazy-loading of the modules.

const routes: Routes = [
  {
    path: '', loadChildren: () => import('./public/public.module').then(m => m.PublicModule)
  }
];

Once these are plugged together, then we can rip out the boilerplate html in the app.component.html file to just include the router-outlet tag.

<router-outlet></router-outlet>

Now, start the test server (ng serve --open) and you can check out that everything is running as expected – which should just be index works!

Adding a Navbar and setting up the module imports

The index works. Our next step is to start creating the overall structure of the UI and with that, we start with the navbar. We are going to be using ng-bootstrap for our UI components. It’s all pure Angular components with Bootstrap’s styles, meaning we won’t be using any of the bootstrap js components. Let’s add that library to the project.

ng add @ng-bootstrap/ng-bootstrap --skip-confirmation

This will automatically add the NgbModule into the imports section of the AppModule. However, we will end up needing this module to be imported in all the feature modules and the shared module. Instead of littering all the modules, we can bundle the module imports by importing the NgbModule into the SharedModule and then exporting the NgbModule module out, which will then allow all the feature modules that import the SharedModule to automatically get the NgbModule as well. Here’s what the shared.module.ts looks like.

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';


@NgModule({
  declarations: [],
  imports: [
    CommonModule,
    NgbModule
  ],
  exports: [
    NgbModule
  ]
})
export class SharedModule { }

The way you know this works is to go look at the index page again. The font should have changed (to the bootstrap default) and the placement of the text should be in the far top-left now with no margins. Now, go back to all the modules, including the AppModule, and make sure they import the SharedModule.

Note: As a general practice for importing, I always import my application modules last. I’m not sure if there is any technical reason to do so, but I like keeping things organized.

Now we can add the navbar component to the SharedModule.

ng g component shared/navbar

You will also need to export this component (and all other components you want to use in other modules) from the SharedModule. Additionally, we will be introducing angular routing into the mix so we need to import the RouterModule. Here’s the full SharedModule at this point.

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RouterModule } from '@angular/router';
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
import { NavbarComponent } from './navbar/navbar.component';

@NgModule({
  declarations: [
    NavbarComponent
  ],
  imports: [
    CommonModule,
    RouterModule,
    NgbModule
  ],
  exports: [
    RouterModule,
    NgbModule,
    NavbarComponent
  ]
})
export class SharedModule { }

We then need to go back to the app.component.html and add the tag for the navbar so that it’s included in all the views. We are also going to be using the container class from Bootstrap to keep the width standard.

<app-navbar></app-navbar>
<div class="container">
    <router-outlet></router-outlet>
</div>

You should now see that the navbar works! (ignore it’s left justification for now) and the index works! (which should be formatted in the container.

Updating the Navbar Code

One of the weird things about the ng-bootstrap documentation is that the navbar example is buried in the collapse example. Taking that as a starting point, here’s what the navbar html looks like.

  • Notice the addition of the <div class="container"> tag. This allows the nav to be fluid across the entire view, but the contents of the nav will be in the same container the content of the page is in. Everything will be aligned now.
<nav class="navbar navbar-expand-lg navbar-light bg-light mb-3 border-bottom">
    <div class="container">
        <a class="navbar-brand" routerLink="/">App Name</a>
        <button class="navbar-toggler" type="button" (click)="isCollapsed = !isCollapsed">
            &#9776;
        </button>
        <div [ngbCollapse]="isCollapsed" class="collapse navbar-collapse">
            <ul class="navbar-nav">
                <li class="nav-item" [routerLinkActive]="['active']">
                    <a class="nav-link" routerLink="about" (click)="isCollapsed = true">About</a>
                </li>
            </ul>
        </div>
    </div>
</nav>

In the NavbarComponent class, we need to add the public var for isCollapsed.

@Component({
  selector: 'app-navbar',
  templateUrl: './navbar.component.html',
  styleUrls: ['./navbar.component.scss']
})
export class NavbarComponent implements OnInit {

  isCollapsed: boolean = true;

  constructor() { }

  ngOnInit(): void {
  }

}

At this point, the navbar should be fully functional and will route you between the index and about pages without a page refresh.

Login Page Generation and Routing

We’ve built three modules for public, protected, and admin. Since we have some components that we want only accessible to authorized users, we will need to add a login page. The login page is public facing, so we will place it in the public module.

ng g component public/login

Note: There is another opportunity to modularize the components associated with login and it’d depend on the features you’ll be exposing. If you are going to build multiple components related to auth such as resetting passwords or creating a new account, you could create a module called auth and put that inside the public module. ex: ng g module public/auth --routing

Every time you add a component, you’ll need to go add the routing path as well. Here’s what the routes constant now looks like with the additional route in public-routing.module.ts.

const routes: Routes = [
  { path: '', component: IndexComponent },
  { path: 'login', component: LoginComponent }
];

We also need to add this to our navbar.component.html page. We are also going to right-align the login link. Here’s what it looks like now. A great reference for navbar alignment tricks is at https://stackoverflow.com/a/20362024

<nav class="navbar navbar-expand-lg navbar-light bg-light mb-3 border-bottom">
  <div class="container">
    <a class="navbar-brand" routerLink="/">angular-starter</a>
    <button class="navbar-toggler" type="button" (click)="isCollapsed = !isCollapsed">
      &#9776;
    </button>
    <div [ngbCollapse]="isCollapsed" class="collapse navbar-collapse">
      <ul class="navbar-nav me-auto">
        <li class="nav-item" [routerLinkActive]="['active']">
          <a class="nav-link" routerLink="about" (click)="isCollapsed = true">About</a>
        </li>
      </ul>
      <ul class="navbar-nav ms-auto">
        <li class="nav-item" [routerLinkActive]="['active']">
          <a class="nav-link" routerLink="login" (click)="isCollapsed = true">Login</a>
        </li>
      </ul>
    </div>
  </div>
</nav>

Form Infrastructure Setup

We are going to use Angular reactive forms. The first form is a bit of a pain because there is a ton of infrastructure-related code that needs to get set up for use in the project. I’m going to try and pack a bunch of helpful abstractions into this first form for us to reuse on other components.

First, let’s add the necessary modules. Remember we are using the “Shared module has everything” pattern, so add the modules there.

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RouterModule } from '@angular/router';
import { ReactiveFormsModule } from '@angular/forms';
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
import { NavbarComponent } from './navbar/navbar.component';

@NgModule({
  declarations: [
    NavbarComponent
  ],
  imports: [
    CommonModule,
    RouterModule,
    ReactiveFormsModule,
    NgbModule
  ],
  exports: [
    RouterModule,
    ReactiveFormsModule,
    NgbModule,
    NavbarComponent
  ]
})
export class SharedModule { }

Note: The details in the ordering. Angular modules first, then third-party modules, and finally shared components. I also keep the import and export order synchronized. This really helps me with readability.

To reduce boilerplate code, we are going to create a BaseComponent. This is an abstract class that will contain the common functionality needed on other components.

ng g class shared/BaseComponent --skip-tests

Replace the generated code.

import { Component, OnDestroy, OnInit } from "@angular/core";

@Component({ template: '' })
export abstract class BaseComponent implements OnInit, OnDestroy {

  errors?: Error[];

  abstract init(): void;

  abstract destroy(): void;

  ngOnInit(): void {
    this.init();
  }

  ngOnDestroy(): void {
    this.destroy();
  }

}
  • The errors array is for page-level errors. This is for scenarios like request validation errors coming back from an API. Almost every component will need this.
  • We also abstracted the angular core init and destroy methods and will force the implementer to manage these. The destroy will be a good reminder to make sure the developers are handling cleanup such as dangling subscriptions.

The page-level errors display will end up being a shared component for consistency across all pages. Let’s add a new component to the SharedModule for this.

ng g component shared/PageErrors --skip-tests

Remember to export the PageErrorsComponent from the SharedModule.

The PageErrorsComponent uses the @Input decorator to allow the errors array to be passed in to the component.

import { Component, Input } from '@angular/core';

@Component({
  selector: 'app-page-errors',
  templateUrl: './page-errors.component.html',
  styleUrls: ['./page-errors.component.scss']
})
export class PageErrorsComponent {

  @Input()
  errors?: Error[]

}

To display the errors array, here’s the html.

<div *ngIf="errors" class="alert alert-danger" role="alert">
  <div>The following errors have occurred:</div>
  <ul class="mb-0">
    <li *ngFor="let error of errors">{{error.message}}</li>
  </ul>
</div>

Don’t forget to go add the PageErrorsComponent to the exports of the shared module:

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
import { NavbarComponent } from './navbar/navbar.component';
import { RouterModule } from '@angular/router';
import { ReactiveFormsModule } from '@angular/forms';
import { PageErrorsComponent } from './page-errors/page-errors.component';


@NgModule({
  declarations: [
    NavbarComponent,
    PageErrorsComponent
  ],
  imports: [
    CommonModule,
    RouterModule,
    ReactiveFormsModule,
    NgbModule
  ],
  exports: [
    RouterModule,
    ReactiveFormsModule,
    NgbModule,
    NavbarComponent,
    PageErrorsComponent
  ]
})
export class SharedModule { }

The Login Component

Now on to the login.component.ts file.

import { Component } from '@angular/core';
import { AbstractControl, FormBuilder, Validators } from "@angular/forms";
import { Router } from "@angular/router";
import { BaseComponent } from "../../shared/base-component";

@Component({
  selector: 'app-login',
  templateUrl: './login.component.html',
  styleUrls: ['./login.component.scss']
})
export class LoginComponent extends BaseComponent {

  loginForm = this.formBuilder.group({
    email: ['', [Validators.email, Validators.required]],
    password: ['', [Validators.required]]
  });

  constructor(private router: Router, private formBuilder: FormBuilder) {
    super();
  }

  init(): void {
  }

  destroy(): void {
  }

  onSubmit() {
    if (this.loginForm.invalid) {
      return;
    }
  }

  get controls(): { [p: string]: AbstractControl } {
    return this.loginForm.controls;
  }

}

Take notice of the way the controls are accessed. There is a setting that needs to be changed to avoid the errors that look like this.

error TS4111: Property 'email' comes from an index signature, so it must be accessed with ['email'].

This is a noPropertyAccessFromIndexSignature setting in the tsconfig.json file. Go change that to false and then restart.

Here’s the form in the login.component.html.

<form #form="ngForm" [formGroup]="loginForm" (ngSubmit)="onSubmit()" class="form">
  <div class="row justify-content-center mt-5">
    <div class="col-md-6">
      <div class="card">
        <h3 class="card-header">Login</h3>
        <div class="card-body">
          <app-page-errors [errors]="errors"></app-page-errors>
          <div class="mb-3">
            <label for="email">Email</label>
            <input id="email" type="email" formControlName="email" class="form-control"
                   [ngClass]="{ 'is-invalid': form.submitted && controls.email.invalid }">
            <div *ngIf="form.submitted && controls.email.invalid" class="text-danger">
              <small *ngIf="controls.email.errors?.required">Email is required.</small>
              <small *ngIf="controls.email.errors?.email">Email is invalid.</small>
            </div>
          </div>
          <div class="mb-3">
            <label for="password">Password</label>
            <input id="password" type="password" formControlName="password" class="form-control"
                   [ngClass]="{ 'is-invalid': form.submitted && controls.password.invalid }">
            <div *ngIf="form.submitted && controls.password.invalid" class="text-danger">
              <small *ngIf="controls.password.errors?.required">Password is required.</small>
            </div>
          </div>
          <div class="form-group mb-3">
            <button type="submit" class="btn btn-primary float-right">Login</button>
          </div>
        </div>
      </div>
    </div>
  </div>
</form>

The login form should work, as it relates to the form errors on submission. Now you just need to get started on the app.

Auth Infrastructure

For authentication and authorization (auth, in general), we will be using JSON Web Tokens (JWT). There is an existing javascript library for helping with JWTs so let’s get that installed.

npm install --save @auth0/angular-jwt

We will also need to import the module into the CoreModule like JwtModule.forRoot({}).

Here’s where we will create our first service, the AuthService. The service will go into the core module. We will also want to organize the core module with folders for the different types of core components including guards, services, and interceptors. You’ll notice the extra ‘auth’ in the command. This command creates two files: the service class and the test. If you don’t add the extra folder, then the service folder can get cluttered with all the services and their tests.

ng g service core/service/auth/auth

The AuthService will be checking local storage for the presence of a JWT token and also checking if the token is expired.

import { Injectable } from '@angular/core';
import { JwtHelperService } from '@auth0/angular-jwt';

@Injectable({
  providedIn: 'root'
})
export class AuthService {

  constructor(public jwtHelper: JwtHelperService) { }

  isAuthenticated(): boolean {
    const token = localStorage.getItem('token');
    if (!token) {
      return false;
    }
    return !this.jwtHelper.isTokenExpired(token);
  }

}

The AuthGuard sets up authorization checks during Angular routing.

ng g guard core/guard/auth --implements CanActivate

The AuthGuard will use the AuthService to check if the user is authenticated and if not, it uses the Router to route the user back to the login page.

import { inject } from '@angular/core';
import { CanActivateFn, Router } from '@angular/router';
import { AuthService } from '../service/auth/auth.service';


export const authGuard: CanActivateFn = (route, state) => {
  const router: Router = inject(Router);
  const authService: AuthService = inject(AuthService);
  if (!authService.isAuthenticated()) {
    router.navigate(['login']);
    return false;
  }
  return true;
};

The last piece is to add the AuthGuard into the router definitions. Because we created modules containing the protected components, we can do so at the module level. Updating the app-routing.module.ts with the guard does the protections.

const routes: Routes = [
  {
    path: '', loadChildren: () => import('./public/public.module').then(m => m.PublicModule)
  }, {
    path: 'protected', loadChildren: () => import('./protected/protected.module').then(m => m.ProtectedModule), canActivate: [AuthGuard]
  }, {
    path: 'admin', loadChildren: () => import('./admin/admin.module').then(m => m.AdminModule), canActivate: [AuthGuard]
  }
];const routes: Routes = [
  {
    path: '', loadChildren: () => import('./public/public.module').then(m => m.PublicModule)
  },
  {
    path: 'app', loadChildren: () => import('./protected/protected.module').then(m => m.ProtectedModule), canActivate: [authGuard]
  }
];

We can test this by adding an IndexComponent to the protected module, adding the route to the protected-routing.module.ts, and then attempting to navigate to the component.

ng g component protected/dashboard
const routes: Routes = [
  { path: 'dashboard', component: DashboardComponent }
];

Add the JwtModule to the app.module.ts to register the provider for injection.

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { CoreModule } from './core/core.module';
import { SharedModule } from './shared/shared.module';
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
import { JwtModule } from '@auth0/angular-jwt';
import { AuthService } from './core/service/auth/auth.service';


@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    AppRoutingModule,
    CoreModule,
    SharedModule,
    NgbModule,
    JwtModule.forRoot({}),
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule {
}

Navigate to http://localhost:4200/app/dashboard and it should send you back to the login page.

Leave a Reply

Your email address will not be published. Required fields are marked *