ui-shell
@mmm/ui-shell
@mmm/ui-shell ist eine Angular-Shell fuer Backoffice-/Portal-Anwendungen. Das Paket liefert ein wiederverwendbares Layout mit:
- Top-Navigation
- Header mit Branding
- optionaler Seitenleiste
- dynamischen Header-/Toolbar-Slots
- Footer
- Partner-Carousel
- Guard fuer geschuetzte Shell-Routen
Die Fachanwendung rendert ihren eigentlichen Inhalt ueber Child-Routes im router-outlet der Shell.
Was die Shell tut
Die Shell liest eine zentrale Konfiguration vom Injection-Token MMM_SHELL_OPTIONS. Aus dieser Konfiguration werden zur Laufzeit unter anderem folgende Bereiche aufgebaut:
navItems: linke Seiten-Navigationregions.mainNav: obere Hauptnavigationregions.toolbar: rechte Toolbar in der Top-Navigationregions.header: Komponentenbereich im Headerregions.footerLinks: Footer-Linksregions.partnerCarousel: Partnerlogos unter dem Contentbranding: Logo und Alt-TextloginUrlundreturnUrlParam: Verhalten des Guards bei nicht angemeldeten Nutzern
Zusätzlich wertet die Shell Access-Regeln aus. Damit lassen sich einzelne Nav-Items, Region-Komponenten, Footer-Links oder Partnerlogos anhand von Auth-Status, Rollen oder Berechtigungen sichtbar machen.
Voraussetzungen im Host-Frontend
Das Host-Frontend muss Folgendes bereitstellen:
- Angular Router
- eine Auth-Store-Implementierung, die per DI verfuegbar ist
- ein
AuthStore-Objekt mit mindestens:
type AuthSnapshot = { status: "unknown" | "anonymous" | "authenticated"; user?: { permissions?: string[]; roles?: string[]; } | null;};Die Shell erwartet, dass der Store eine snapshot-Property und optional user bereitstellt, damit Sichtbarkeiten und Guard-Entscheidungen funktionieren.
Einbau im Angular-Frontend
1. Paket einbinden
Die Shell wird als Provider konfiguriert. Aktuell ist der einfachste Weg in einer Standalone-Applikation:
import { ApplicationConfig, importProvidersFrom } from "@angular/core";import { provideRouter } from "@angular/router";import { UiMmmShellModule } from "@mmm/ui-shell";import { routes } from "./app.routes";import { shellConfig } from "./shell.config";
export const appConfig: ApplicationConfig = { providers: [ provideRouter(routes), importProvidersFrom(UiMmmShellModule.forRoot(shellConfig)), ],};Alternativ kann dieselbe Konfiguration auch direkt ueber den Token registriert werden:
import { ApplicationConfig } from "@angular/core";import { provideRouter } from "@angular/router";import { MMM_SHELL_OPTIONS, MmmShellGuard,} from "@mmm/ui-shell";import { routes } from "./app.routes";import { shellConfig } from "./shell.config";
export const appConfig: ApplicationConfig = { providers: [ provideRouter(routes), { provide: MMM_SHELL_OPTIONS, useValue: shellConfig }, MmmShellGuard, ],};Shell-Konfiguration
Die Konfiguration hat den Typ MmmShellOptions.
Beispiel fuer shell.config.ts
import { MmmShellOptions } from "@mmm/ui-shell";import { UserMenuComponent } from "./shell/user-menu.component";import { TenantSwitcherComponent } from "./shell/tenant-switcher.component";
export const shellConfig: MmmShellOptions = { loginUrl: "/login", returnUrlParam: "returnUrl", branding: { logoUrl: "/assets/brand/logo.svg", logoAlt: "MMM Portal", }, privacyLinks: { label: "Cookie-Einstellungen", target: "/datenschutz", }, navItems: [ { label: "Dashboard", route: "/dashboard", icon: "dashboard_24", access: { when: "authenticated" }, }, { label: "Benutzer", route: "/users", icon: "person_24", access: { when: "permission", anyOf: ["users.read"] }, }, ], regions: { mainNav: [ { label: "Start", route: "/dashboard" }, { label: "Berichte", route: "/reports", access: { when: "role", anyOf: ["admin"] } }, ], toolbar: [ { id: "tenant-switcher", component: TenantSwitcherComponent, access: { when: "authenticated" }, }, ], header: [ { id: "user-menu", component: UserMenuComponent, inputs: { showAvatar: true, }, access: { when: "authenticated" }, }, ], footerLinks: [ { label: "Impressum", href: "/impressum" }, { label: "Hilfe", href: "https://example.org/help" }, ], partnerCarousel: [ { imageUrl: "/assets/partners/partner-a.svg", altText: "Partner A", url: "https://partner-a.example", sortIndex: 10, }, ], },};Bedeutung der Config-Felder
type MmmShellOptions = { iconComponent?: Type<unknown>; navItems?: AdminNavItem[]; loginUrl?: string; returnUrlParam?: string; branding?: { logoUrl?: string; logoAlt?: string; }; loginTeaser?: { src?: string; headline?: string; description?: string; }; privacyLinks?: { label?: string; target?: string; }; regions?: { mainNav?: MainNavItem[]; toolbar?: RegionItem[]; header?: RegionItem[]; footerLinks?: Array<{ label: string; href: string; access?: AccessRule; }>; partnerCarousel?: Array<{ imageUrl: string; altText?: string; url?: string; sortIndex?: number; access?: AccessRule; }>; };};Zugriffregeln
Folgende Access-Regeln koennen verwendet werden:
type AccessRule = | { when: "always" } | { when: "authenticated" } | { when: "anonymous" } | { when: "permission"; anyOf: string[] } | { when: "role"; anyOf: string[] };Sie steuern nur die Sichtbarkeit in der Shell. Die eigentliche Routenabsicherung passiert ueber MmmShellGuard und ggf. weitere fachliche Guards im Host-Frontend.
Routing: Shell als Layout-Route
Die Shell wird als Layout-Komponente in die Routen eingehangen. Die eigentlichen Fachseiten liegen darunter als children.
Beispiel app.routes.ts
import { Routes } from "@angular/router";import { MmmLayoutComponent, MmmShellGuard } from "@mmm/ui-shell";import { DashboardPageComponent } from "./pages/dashboard-page.component";import { UsersPageComponent } from "./pages/users-page.component";import { LoginPageComponent } from "./pages/login-page.component";
export const routes: Routes = [ { path: "", component: MmmLayoutComponent, canActivate: [MmmShellGuard], children: [ { path: "", pathMatch: "full", redirectTo: "dashboard", }, { path: "dashboard", component: DashboardPageComponent, }, { path: "users", component: UsersPageComponent, }, ], }, { path: "login", component: LoginPageComponent, data: { public: true }, },];Was der Guard macht
MmmShellGuard prueft beim Betreten der Shell:
authenticated: Zugriff erlaubtanonymous: Redirect aufloginUrlunknown: Zugriff wird blockiertdata.public === trueauf dem Leaf-Route-Snapshot: Zugriff erlaubt
Beim Redirect zur Login-Seite wird die aktuelle URL als Query-Parameter mitgegeben. Standard ist returnUrl.
Beispiel:
/login?returnUrl=%2FusersDynamische Regionen
toolbar und header koennen Komponenten direkt aus dem Host-Frontend rendern. Dafuer wird ngComponentOutlet verwendet.
Ein RegionItem sieht so aus:
type RegionItem = { id: string; component: Type<unknown>; inputs?: Record<string, unknown>; access?: AccessRule;};Damit lassen sich Shell-nahe UI-Bausteine wie Benutzer-Menues, Tenant-Switcher, Kontextaktionen oder Filterleisten einhaengen, ohne die Shell selbst aendern zu muessen.
Laufzeit-Update der Shell-Konfiguration
Die Konfiguration kann auch zur Laufzeit angepasst werden. Dafuer gibt es MmmShellRuntimeService.
import { inject, Injectable } from "@angular/core";import { MmmShellRuntimeService } from "@mmm/ui-shell";
@Injectable({ providedIn: "root" })export class TenantBrandingService { private readonly shellRuntime = inject(MmmShellRuntimeService);
updateLogo(logoUrl: string, logoAlt: string) { this.shellRuntime.setConfig({ branding: { logoUrl, logoAlt, }, }); }}setConfig(...) merged Objekte tief, Arrays werden dabei ersetzt.
Technische Zusammenfassung
Das Paket ist kein Router-Ersatz und keine komplette Auth-Loesung. Es ist eine Layout- und Navigationsschicht fuer Angular-Anwendungen.
Die Verantwortung ist wie folgt getrennt:
@mmm/ui-shell: Layout, Navigation, Regionen, Sichtbarkeitsregeln, Shell-Guard- Host-Frontend: konkrete Seiten, konkrete Region-Komponenten, Auth-Store, Fachlogik, zusaetzliche Guards
Wenn das Host-Frontend die Shell konfiguriert und als Layout-Route verwendet, entsteht eine zentrale Rahmenanwendung, in die Fachseiten konsistent eingehangen werden koennen.
Oeffentliche API
Exportiert ueber src/public-api.ts:
UiMmmShellModuleMMM_SHELL_OPTIONSMmmShellOptionsMmmLayoutComponentMmmShellGuardRegionOutletComponentcanAccessMmmShellRuntimeService