On this page ▾
Setting up authentication
This guide walks through wiring an email + password authentication flow into a fresh Angular app, then verifying it end-to-end. It exists primarily to demo every NgmdUi component inline from markdown. Restart your dev server if you scaffolded ngmd before 2026-05.
.md file, not a hand-coded .page.ts. The catch-all route + Angular Elements wiring lets authoring components compile inside markdown bodies. Read the source at src/content/concepts/showcase.md.
Prerequisites Stable
You'll need Node 22.22.3+ (or 24.15.0+) and one of pnpm, npm, yarn, or bun. The flow below uses pnpm but the others work identically.
What you'll build
The flow
@angular/forms for reactive forms and a JWT helper of your choice. The form layer is the only required piece.
src/app/pages/login.page.ts. Reactive form with email and password controls, plus a submit handler that calls your auth service.
localStorage (or a cookie for SSR projects), and exposes an isAuthenticated signal.
isAuthenticated. Apply it to every route that should require login via the canActivate property.
Step 1: Install
@angular/forms to your @angular/core version exactly. Mismatched majors will compile but throw at runtime in subtle ways.
pnpm add @angular/forms
Step 2: The login component
import { Component, inject, signal } from '@angular/core';
import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';
import { Router } from '@angular/router';
import { AuthService } from './auth.service';
@Component({
selector: 'app-login',
imports: [ReactiveFormsModule],
template: `
<form [formGroup]="form" (ngSubmit)="onSubmit()">
<input formControlName="email" type="email" autocomplete="email" />
<input formControlName="password" type="password" />
<button type="submit" [disabled]="form.invalid || loading()">
Sign in
</button>
</form>
`,
})
export default class LoginPage {
private readonly fb = inject(FormBuilder);
private readonly auth = inject(AuthService);
private readonly router = inject(Router);
readonly loading = signal(false);
readonly form = this.fb.nonNullable.group({
email: ['', [Validators.required, Validators.email]],
password: ['', [Validators.required, Validators.minLength(8)]],
});
async onSubmit() {
if (this.form.invalid) return;
this.loading.set(true);
try {
await this.auth.signIn(this.form.getRawValue());
this.router.navigate(['/dashboard']);
} finally {
this.loading.set(false);
}
}
}
Step 3: The auth service
localStorage is the simplest path but exposes them to XSS. For production with stricter requirements, prefer an httpOnly cookie issued by the backend.
import { Injectable, computed, inject, signal } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { firstValueFrom } from 'rxjs';
interface SignInPayload {
email: string;
password: string;
}
@Injectable({ providedIn: 'root' })
export class AuthService {
private readonly http = inject(HttpClient);
private readonly tokenKey = 'auth.token';
readonly token = signal<string | null>(localStorage.getItem(this.tokenKey));
readonly isAuthenticated = computed(() => this.token() !== null);
async signIn(payload: SignInPayload): Promise<void> {
const { token } = await firstValueFrom(
this.http.post<{ token: string }>('/api/auth/sign-in', payload),
);
localStorage.setItem(this.tokenKey, token);
this.token.set(token);
}
signOut(): void {
localStorage.removeItem(this.tokenKey);
this.token.set(null);
}
}
Step 4: The route guard
import { inject } from '@angular/core';
import { CanActivateFn, Router } from '@angular/router';
import { AuthService } from './auth.service';
export const authGuard: CanActivateFn = () => {
const auth = inject(AuthService);
const router = inject(Router);
if (auth.isAuthenticated()) return true;
return router.parseUrl('/login');
};
Step 5: Verify
/dashboard, refresh the page, stay logged in. If the refresh logs you out, your token isn't being read on startup.
onSubmit isn't awaiting the service call.
exp claim in dev), then make a request. The interceptor should clear the token and redirect to /login. If it doesn't, your interceptor isn't catching 401s.
auth.signOut(), confirm localStorage is empty, navigate to a guarded route, end up on /login. If you stay on the guarded route your guard isn't recomputing.
Reference
A video to round it off
And an image, because docs
Say hi to my cats Angular and Excel 👋
Inline pieces
Status flags work inline. A settled API is
Auto-linked keywords resolve from ngmd.config.ts: this guide builds on Angular and AnalogJS, with Tailwind for the form styling and Shiki for the code blocks you see above.
Tabs in markdown
Tabs now work inline too, using <ngmd-tab> children (real components, not <ng-template> directives):
Authorization: Bearer. Easy to scale horizontally; revocation needs a denylist.
For command tabs that pre-render through Shiki at build time, the fenced-code syntax is still the lightest option:
pnpm create ngmd@latest my-docsnpm create ngmd@latest my-docsyarn create ngmd my-docsbun create ngmd my-docsPer-instance spacing
Every block authoring tag accepts a Tailwind margin class to tighten or loosen the gap around it. Default is margin: 1.5rem 0:
class="mt-10" bumps just the top to 2.5rem. Bottom stays default.
class="my-1" collapses both sides to 0.25rem.
That's every NgmdUi component working inline in markdown via the catch-all + Custom Elements path. Build pipeline, link guards, sitemap. All of these pages run through the same machinery.