Standalone Angular Project from Scratch

In this post, we will discuss the new standalone component concepts as well as the use of signals.

To get started, let’s create a new project. We want routing. I like SCSS over the other styling mechanisms. And we don’t want any server side rendering. We are going to use Bootstrap as well so let’s add that in.

ng new --routing --style scss --ssr false <project-name>
cd <project-name>
ng add @ng-bootstrap/ng-bootstrap --skip-confirmation
ng generate environments

Component Organization

Now we are going to use standalone components which accomplish two major things. First, it reduces the amount of importing into all of the components. Second, it will help reduce refactoring. The project structure is greatly simplified as well. We will have pages, services, guards, interceptors, etc.

Let’s create two pages to get us started: Index and Login. Notice the big change here is that we aren’t going through the whole process of creating modules and routing modules and doing all the lazy loading. Just create the pages and add the routes.

ng g c -s pages/index
ng g c -s pages/login

(The -s inlines the SCSS in the component)

And add the routes.

import { Routes } from '@angular/router';
import { IndexComponent } from "./pages/index/index.component";
import { LoginComponent } from "./pages/login/login.component";

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

Let’s create our first shared component: the navbar.

ng g c pages/shared/navbar

The navbar component html is fairly simple.

<nav class="navbar navbar-expand-lg navbar-light bg-light mb-3 border-bottom">
  <div class="container">
    <a class="navbar-brand" routerLink="/">{{ appTitle }}</a>
    <button class="navbar-toggler" type="button" (click)="isCollapsed = !isCollapsed">
      &#9776;
    </button>
    <div [ngbCollapse]="isCollapsed" class="collapse navbar-collapse">
      <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>

The main differences is in the component itself. It’s a standalone component so it only imports the absolutely necessary items.

import { Component } from '@angular/core';
import { RouterLink, RouterLinkActive } from "@angular/router";
import { NgbCollapse } from "@ng-bootstrap/ng-bootstrap";
import { environment } from "../../../../environments/environment";

@Component({
  selector: 'app-navbar',
  standalone: true,
  imports: [ RouterLinkActive, NgbCollapse, RouterLink ],
  templateUrl: './navbar.component.html',
  styles: ``
})
export class NavbarComponent {
  appTitle = environment.app.title;
  isCollapsed: boolean = false;
}

Then go add the app title into the environment files. Here’s the dev example.

export const environment = {
  production: false,
  app: {
    title: 'My App Title'
  }
};

Once we update the app component, then the basic app structure becomes visible.

<app-navbar></app-navbar>
<div class="container">
  <router-outlet/>
</div>
import { Component } from '@angular/core';
import { RouterOutlet } from '@angular/router';
import { NavbarComponent } from "./pages/shared/navbar/navbar.component";

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [RouterOutlet, NavbarComponent],
  templateUrl: './app.component.html',
  styleUrl: './app.component.scss'
})
export class AppComponent {
}

Login and Forms

When we build the login form, we will see a few more updates. We have utilized Angular’s new template control flow feature @if which replaces the traditional structural directive *ngIf .

<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">
          <div class="mb-3">
            <label for="email">Email</label>
            <input id="email" type="email" formControlName="email" class="form-control" required
                   [ngClass]="{ 'is-invalid': form['submitted'] && controls.email.invalid }">
            @if (form['submitted'] && controls.email.invalid) {
              <div class="text-danger">
                @if (controls.email.hasError('required')) {
                  <div class="small">Email is required.</div>
                }
                @if (controls.email.hasError('email')) {
                  <div class="small">Email is invalid.</div>
                }
              </div>
            }
          </div>
          <div class="mb-3">
            <label for="password">Password</label>
            <input id="password" type="password" formControlName="password" class="form-control" required
                   [ngClass]="{ 'is-invalid': form['submitted'] && controls.password.invalid }">
            @if (form['submitted'] && controls.password.invalid) {
              <div class="text-danger">
                @if (controls.password.hasError('required')) {
                  <div class="small">Password is required.</div>
                }
              </div>
            }
          </div>
          <div class="form-group mb-3">
            <button type="submit" class="btn btn-primary float-end">Login</button>
          </div>
        </div>
      </div>
    </div>
  </div>
</form>

In the component, we aren’t having to inject the FormBuilder anymore. Instead, just use the FormGroup and FormControl classes directly.

import { Component } from '@angular/core';
import { AbstractControl, FormControl, FormGroup, ReactiveFormsModule, Validators } from "@angular/forms";
import { NgClass } from "@angular/common";

@Component({
  selector: 'app-login',
  standalone: true,
  imports: [ReactiveFormsModule, NgClass],
  templateUrl: './login.component.html',
  styleUrl: './login.component.scss'
})
export class LoginComponent {

  loginForm: FormGroup = new FormGroup({
    email: new FormControl('', [Validators.required, Validators.email]),
    password: new FormControl('', [Validators.required])
  });

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

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

}

* Don’t forget to go and update the value “noPropertyAccessFromIndexSignature”: false in your tsconfig.json t be able to use the controls accessor.