NgMd
On this page ▾
A guided walkthrough of NgMd's authoring components, framed as the kind of tutorial you'd actually publish.

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.

Every component below is rendered from a plain .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.

If your team is still on Node 20, upgrade before continuing. Angular 22 requires Node 22.22.3 or 24.15.0 and newer.

What you'll build

Form that takes an email and password, posts to your backend, stores the returned JWT. Route guard that blocks unauthenticated users from protected pages. HTTP interceptor that attaches the JWT to outbound requests.

The flow

Add @angular/forms for reactive forms and a JWT helper of your choice. The form layer is the only required piece. Standalone component under src/app/pages/login.page.ts. Reactive form with email and password controls, plus a submit handler that calls your auth service. Service that posts to your API, stores the token in localStorage (or a cookie for SSR projects), and exposes an isAuthenticated signal. Functional guard reading isAuthenticated. Apply it to every route that should require login via the canActivate property. Sign in, navigate to a guarded route, refresh the page, sign out. Each transition should behave correctly.

Step 1: Install

Match @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

Storing JWTs in 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

Submit valid credentials, get redirected to /dashboard, refresh the page, stay logged in. If the refresh logs you out, your token isn't being read on startup. Submit bad credentials, see the form's error state, the route does not change. If the route changes anyway your onSubmit isn't awaiting the service call. Wait for the JWT to expire (or set a short 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. Call 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 Stable, an experimental one is Beta, and a removed option is Deprecated. New helpers ship with a New tag.

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 &lt;ngmd-tab&gt; children (real components, not &lt;ng-template&gt; directives):

Stateless tokens signed by the server. Read on every request from Authorization: Bearer. Easy to scale horizontally; revocation needs a denylist. Server stores the session, client carries an opaque ID. Built-in revocation via session delete. Sticky to one origin. Delegate sign-in to a provider (Google, GitHub, Auth0). You get back a token + identity claims. Best for B2C and "sign in with..." flows.

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-docs
npm create ngmd@latest my-docs
yarn create ngmd my-docs
bun create ngmd my-docs

Per-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:

No class on this callout. Default 1.5rem above and below. 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.