Skip to main content
cngx-src documentation

CngxTabsRouteSync

DirectivePrimaryv0.1.0WCAG AA

projects/common/tabs/route-sync.directive.ts

Import#

import { CngxTabsRouteSync } from '@cngx/common/tabs'

Description#

Router-outlet integration for tab groups.
Opt-in via [cngxTabsRouteSync] on the presenter element. Each tab is an Angular child route whose component renders into a <router-outlet>; switching tabs navigates, and a CanDeactivate guard can gate the leave.

Supplies the presenter's commit-action through the CNGX_TABS_COMMIT_ACTION DI fallback (no consumer [commitAction] hand-binding) and pins pessimistic mode so the active tab follows the resolved route - a guard-cancelled switch keeps the old tab. The routing gate reuses the presenter's commit lifecycle verbatim; the router navigation is the async op.

Seeds the active tab from the current URL at content-init - before the group renders its panels, so a deep link under panelMode="lazy" mounts only the target panel - and reflects external navigations (back/forward, direct URL) into activeIndex without re-navigating - those writes bypass the commit-action, since the router is already at the resolved route.

Two URL-matching modes, defaulted from the host flavour. On a <cngx-tab-group> tablist each tab is a leaf route, so the default is 'suffix': a tab owns the trailing segment(s) of the URL and nothing else. On a <cngx-tab-nav> (or [cngxMatTabNav]) each link is an application section that owns a whole subtree, so the default flips to 'prefix' and the section stays current for every URL beneath it - the non-exact flavour of routerLinkActive. The flavour is read from the CNGX_TAB_NAV_HOST marker the nav organisms provide, so neither default needs a consumer flag; [match] overrides both for the mixed case.

Router is optional - without it the directive logs a single dev warning via afterNextRender, exposes a null action, and becomes a no-op. Mirrors CngxTabsFragmentSync; every activeIndex write inside an effect sits in untracked().

The mental model. It turns the tab group into the router's view switcher: the tabs choose which child route renders in the <router-outlet>, not which inline template shows.
Concretely, a Settings page with Profile / Security / Billing tabs then gets, for free: each tab its own URL (/settings/profile) so a refresh or a shared link stays on the tab; browser back/forward between tabs; Billing lazy-loading its chunk only when first opened; and Profile blocking the switch via a CanDeactivate guard while its form is dirty - all while keeping the role="tab" keyboard and ARIA model.

Use it when each tab's content is a real Angular route - a lazy chunk, a resolver, or a CanActivate / CanDeactivate guard - rendered into a <router-outlet>, and you want browser back/forward and deep links to move between tabs while the route's own guard gates the switch (e.g. an unsaved-changes prompt on leave).
If the tabs are purely visual, use a plain <cngx-tab-group> with no sync directive; if you only want the active tab reflected in the URL while content stays inline, use CngxTabsFragmentSync.

What this is not.
Not a per-tab routerLink:
the tabs stay role="tab" buttons and the directive drives router.navigate centrally through the commit lifecycle, so there are no anchors, no href, and no native open-in-new-tab / middle-click / copy-link.
For real link semantics (right-click, ctrl-click) reach for the CNGX <cngx-tab-nav> and <a cngx-tab-link routerLink> variant instead.
Not URL deep-linking either - that is CngxTabsFragmentSync (a #tab= fragment or ?tab= query-param); this directive renders child route components into a <router-outlet>.
And not optimistic: the active tab moves only once the route resolves, so a CanDeactivate guard can cancel the switch.

// Each tab is a child route; the editor guards its own exit.
const ROUTES: Routes = [
  { path: '', pathMatch: 'full', redirectTo: 'editor' },
  { path: 'editor', component: EditorPage, canDeactivate: [unsavedChangesGuard] },
  { path: 'preview', component: PreviewPage },
  { path: 'settings', component: SettingsPage },
];
<!-- Tab ids match the child segments (default routeFor = (h) => [h.id]).
     Switching a tab navigates; the guard gates leaving the editor;
     back/forward and /account/settings deep links move the active tab. -->
<cngx-tab-group cngxTabsRouteSync aria-label="Account">
  <div cngxTab id="editor" label="Editor"></div>
  <div cngxTab id="preview" label="Preview"></div>
  <div cngxTab id="settings" label="Settings"></div>
</cngx-tab-group>

<!-- The active tab's routed component renders here. -->
<router-outlet></router-outlet>

Metadata#

Providers#

CNGX_TABS_COMMIT_ACTION
useExisting CngxTabsRouteSync

Relationships

Index#

Properties

Derived State

Inputs#

input()

How a tab's route is matched against the current URL.

  • 'suffix' - the tab owns the trailing segment(s). A leaf tab in a tablist: /settings/billing activates the billing tab, and /billing/invoices/42 activates nothing.
  • 'prefix' - the tab owns a subtree. A section link in a nav: /rounds/explorer activates the rounds link. Matching is a UrlTree subset test anchored at the host's own ActivatedRoute, so a nav mounted under a base path still resolves. When several tabs match, the one with the most path segments wins.

Cascade: this input -> CngxTabsConfig.routeMatch (withTabsRouteMatch) -> the host flavour, which is 'prefix' when CNGX_TAB_NAV_HOST is present and 'suffix' otherwise.

This picks which policy runs. CNGX_TAB_URL_MATCH_STRATEGY replaces the policies themselves.

Maps a tab handle to its router command array. Default (handle) => [handle.id] - the tab id is the child segment. Override for real route paths.

default cngxDefaultTabRoute

Instance Properties#

signal()Readonly

Pessimistic-only - not exposed as an input. The route-sync owns the mode so optimistic (show-then-revert) can never flash the target tab before a CanDeactivate guard decides.

signal('pessimistic')

Methods#

ngAfterContentInit#void

Seed from the URL before the group renders its panels. CngxTab registers in ngOnInit, so the registry is populated by the time this runs, and the group's own view has not rendered yet - which is what keeps panelMode="lazy" from counting the default tab's first render as its first activation on a deep link.

Its own router guard: the constructor's early return does not stop lifecycle hooks from running.