A login page is usually the first thing in an Angular app. Here's the complete implementation — reactive form, validation, password toggle, loading spinner and the auth service pattern.

Login Component

// pages/login/login.component.ts
import { Component, signal } from '@angular/core'
import { FormBuilder, FormGroup, Validators, ReactiveFormsModule } from '@angular/forms'
import { Router, ActivatedRoute, RouterLink } from '@angular/router'
import { CommonModule } from '@angular/common'
import { AuthService } from '../../services/auth.service'

@Component({
  selector: 'app-login',
  standalone: true,
  imports: [ReactiveFormsModule, CommonModule, RouterLink],
  templateUrl: './login.component.html'
})
export class LoginComponent {
  loginForm: FormGroup
  isLoading = signal(false)
  showPassword = signal(false)
  errorMessage = signal<string | null>(null)

  private returnUrl: string

  constructor(
    private fb: FormBuilder,
    private auth: AuthService,
    private router: Router,
    private route: ActivatedRoute
  ) {
    this.loginForm = this.fb.group({
      email: ['', [Validators.required, Validators.email]],
      password: ['', [Validators.required, Validators.minLength(6)]],
      remember: [false]
    })

    this.returnUrl = this.route.snapshot.queryParams['returnUrl'] || '/dashboard'
  }

  get email() { return this.loginForm.get('email')! }
  get password() { return this.loginForm.get('password')! }

  togglePassword() {
    this.showPassword.update(v => !v)
  }

  async onSubmit() {
    if (this.loginForm.invalid) {
      this.loginForm.markAllAsTouched()
      return
    }

    this.isLoading.set(true)
    this.errorMessage.set(null)

    try {
      await this.auth.login(
        this.email.value,
        this.password.value,
        this.loginForm.get('remember')!.value
      )
      this.router.navigate([this.returnUrl])
    } catch (err: any) {
      this.errorMessage.set(err.message || 'Invalid email or password.')
    } finally {
      this.isLoading.set(false)
    }
  }
}

Template

<!-- login.component.html -->
<div class="min-vh-100 d-flex align-items-center justify-content-center py-5"
  style="background:#f8f9fa;">
  <div style="width:100%;max-width:420px;padding:0 16px;">

    <!-- Brand -->
    <div class="text-center mb-4">
      <a routerLink="/" class="text-decoration-none fw-bold fs-4" style="color:#fd4766;">
        MyApp
      </a>
      <p class="text-muted small mt-1">Sign in to continue</p>
    </div>

    <!-- Card -->
    <div class="card border-0 shadow-sm">
      <div class="card-body p-4">

        <!-- Error alert -->
        @if (errorMessage()) {
          <div class="alert alert-danger d-flex align-items-center gap-2 py-2 mb-3" role="alert">
            <span>⚠️</span>
            <span class="small">{{ errorMessage() }}</span>
          </div>
        }

        <form [formGroup]="loginForm" (ngSubmit)="onSubmit()" novalidate>

          <!-- Email -->
          <div class="mb-3">
            <label for="email" class="form-label fw-semibold small">Email</label>
            <input type="email" class="form-control" id="email"
              formControlName="email"
              placeholder="you@example.com"
              autocomplete="email"
              [class.is-invalid]="email.invalid && email.touched"
              [class.is-valid]="email.valid && email.touched">
            <div class="invalid-feedback">
              @if (email.errors?.['required']) { Email is required. }
              @else if (email.errors?.['email']) { Enter a valid email address. }
            </div>
          </div>

          <!-- Password -->
          <div class="mb-3">
            <div class="d-flex justify-content-between mb-1">
              <label for="password" class="form-label fw-semibold small mb-0">Password</label>
              <a routerLink="/forgot-password" class="text-muted text-decoration-none"
                style="font-size:0.78rem;">Forgot password?</a>
            </div>
            <div class="input-group">
              <input [type]="showPassword() ? 'text' : 'password'"
                class="form-control border-end-0"
                id="password"
                formControlName="password"
                placeholder="••••••••"
                autocomplete="current-password"
                [class.is-invalid]="password.invalid && password.touched">
              <button type="button"
                class="btn btn-outline-secondary border-start-0"
                (click)="togglePassword()">
                {{ showPassword() ? '🙈' : '👁' }}
              </button>
              <div class="invalid-feedback">
                @if (password.errors?.['required']) { Password is required. }
                @else if (password.errors?.['minlength']) { At least 6 characters. }
              </div>
            </div>
          </div>

          <!-- Remember me -->
          <div class="mb-3 form-check">
            <input type="checkbox" class="form-check-input" id="remember"
              formControlName="remember">
            <label class="form-check-label small" for="remember">Keep me signed in</label>
          </div>

          <!-- Submit -->
          <button type="submit" class="btn w-100 py-2 fw-semibold text-white"
            style="background:#fd4766;"
            [disabled]="isLoading()">
            @if (isLoading()) {
              <span class="spinner-border spinner-border-sm me-2"></span>
              Signing in...
            } @else {
              Sign In →
            }
          </button>

        </form>

      </div>
    </div>

    <!-- Register link -->
    <p class="text-center text-muted small mt-3">
      No account?
      <a routerLink="/register" style="color:#fd4766;font-weight:600;">
        Create one free →
      </a>
    </p>

  </div>
</div>

Auth Service Skeleton

// services/auth.service.ts
import { Injectable } from '@angular/core'
import { HttpClient } from '@angular/common/http'
import { firstValueFrom } from 'rxjs'

@Injectable({ providedIn: 'root' })
export class AuthService {
  private apiUrl = 'https://api.example.com'

  constructor(private http: HttpClient) {}

  async login(email: string, password: string, remember: boolean) {
    const response = await firstValueFrom(
      this.http.post<{ token: string; user: any }>(`${this.apiUrl}/auth/login`, { email, password })
    )

    // Store token
    const storage = remember ? localStorage : sessionStorage
    storage.setItem('token', response.token)

    return response
  }

  logout() {
    localStorage.removeItem('token')
    sessionStorage.removeItem('token')
  }

  getToken(): string | null {
    return localStorage.getItem('token') || sessionStorage.getItem('token')
  }

  isLoggedIn(): boolean {
    return !!this.getToken()
  }
}

The remember checkbox controls whether the token goes to localStorage (persists after browser close) or sessionStorage (cleared when tab closes). A small detail that significantly improves the UX.

Frequently Asked Questions

Create a FormGroup with email and password FormControls. Add Validators.required and Validators.email to the email field. On submit check form.valid before calling the auth service. Use form.markAllAsTouched() to show all validation errors if the user submits without filling in fields.
Inject Router and call this.router.navigate(['/dashboard']) after successful authentication. To redirect to the page the user was trying to access before login, read the returnUrl from route query params: const returnUrl = this.route.snapshot.queryParams['returnUrl'] || '/dashboard'.
Store in localStorage for persistence across tabs and browser restarts: localStorage.setItem('token', token). Read with localStorage.getItem('token'). For security-sensitive apps use httpOnly cookies via the backend instead — localStorage is vulnerable to XSS.

Related

Need a Full Angular + Bootstrap Admin Dashboard?

Marvel Angular Dashboard — Angular 21 + Bootstrap 5 with 50+ components and dark mode.

Browse Templates →

Use code FIRST30 for 30% off.