Page

The <smart-page> component provides a flexible page-header/layout wrapper with a map-based variant dispatch mechanism. It renders a default PageStandardComponent which can be extended with additional variants via PAGE_VARIANT_COMPONENTS_TOKEN. Projection is driven by TemplateRef slots declared on IPageOptions, plus a default <ng-content> fallback for the body.


Usage

<smart-page [options]="options">
  <p>12 people have access to this workspace.</p>
</smart-page>

Components

PageComponent (<smart-page>)

Main wrapper component. Always renders via NgComponentOutlet. Looks up the target variant component from a merged map of baseMap + the optional PAGE_VARIANT_COMPONENTS_TOKEN map, keyed by options.variant (default 'standard'). Falls back to PageStandardComponent when the variant is unknown. Captures <ng-content> into a TemplateRef and passes it to the target component as options.bodyTpl unless the caller provided an explicit bodyTpl.

PageStandardComponent (<smart-page-standard>)

Default concrete implementation and the value for baseMap['standard']. Renders a Tailwind-styled header with title, optional back button, optional search input, endButtons via <smart-button>, and a body section driven by options.bodyTpl.

PageBaseComponent (abstract)

Abstract base directive for extending custom page variants. Exposes options, cssClass, back(), isMobile, and a contentTpl view child for variants that need to re-project the main content.

API

Inputs

InputTypeDefaultDescription
optionsInputSignal<IPageOptions|null>-Page configuration (see IPageOptions)
classInputSignal<string>''External CSS classes (alias for cssClass)

SmartPageVariant

Variant identifier used to select a concrete page component from the merged variant map. The intersection with (string & {}) keeps 'standard' as a suggested literal while allowing arbitrary custom keys.

IPageOptions

FieldTypeDescription
titlestringRequired. Page title (translated).
hideHeaderbooleanHide the entire header block.
hideMenuButtonbooleanReserved for layouts with a menu button.
showBackButtonbooleanRender a back arrow that calls Location.back().
endButtonsArray<IIconButtonOptions>Action buttons rendered via <smart-button>.
search{ text: Signal<string>; set: ... }Inline search input.
variantSmartPageVariantSelects which variant component to render. Defaults to 'standard'.
bodyTplTemplateRef<unknown>Explicit body template. Falls back to projected <ng-content> when absent.
breadcrumbsTplTemplateRef<unknown>Breadcrumbs slot.
metaTplTemplateRef<unknown>Meta slot (status, timestamps, etc.).
avatarTplTemplateRef<unknown>Avatar slot.
bannerTplTemplateRef<unknown>Banner slot.
filtersTplTemplateRef<unknown>Filters slot.
logoTplTemplateRef<unknown>Logo slot.
statsTplTemplateRef<unknown>Stats slot.
subtitleTplTemplateRef<unknown>Subtitle slot.
navTplTemplateRef<unknown>Navigation slot.
sidebarTplTemplateRef<unknown>Sidebar slot.

IIconButtonOptions (relevant fields)

FieldTypeDescription
iconstringIcon name (tracked by the @for)
textstringButton label (translated)
numbernumberOptional badge counter
handler() => voidClick handler
disabled$Observable<boolean>Optional disabled stream

PAGE_VARIANT_COMPONENTS_TOKEN

InjectionToken of type Partial<Record<SmartPageVariant, Type<PageBaseComponent>>>. Provide a map to register additional variants. The map is merged on top of baseMap (which contains the default { standard: PageStandardComponent }), so you can both add new variants and override 'standard'.

Extending the base class

import { NgTemplateOutlet } from '@angular/common';
import {
  ChangeDetectionStrategy,
  Component,
  computed,
  input,
  TemplateRef,
  Type,
  viewChild,
  ViewEncapsulation,
} from '@angular/core';

import {
  IPageOptions,
  PAGE_VARIANT_COMPONENTS_TOKEN,
  PageBaseComponent,
  PageComponent,
  SmartPageVariant,
} from '@smartsoft001/angular';

/**
 * A custom page shell built on `PageBaseComponent`.
 *
 * The base contributes the `options` and `class` inputs, the `back()` helper
 * that delegates to `Location`, and the `appService` / `hardwareService`
 * injections; the implementation owns the chrome around the body.
 */
@Component({
  selector: 'docs-custom-page',
  template: `
    <div [class]="containerClasses()">
      @if (!options()?.hideHeader) {
        @if (options()?.breadcrumbsTpl) {
          <nav class="docs-page__breadcrumbs" aria-label="Breadcrumb">
            <ng-container [ngTemplateOutlet]="options()!.breadcrumbsTpl!" />
          </nav>
        }

        <header class="docs-page__header">
          @if (options()?.showBackButton) {
            <button
              type="button"
              class="docs-page__back"
              aria-label="Go back"
              (click)="back()"
            >
              &larr;
            </button>
          }
          <h1 class="docs-page__title">{{ options()?.title }}</h1>
        </header>
      }

      <!--
        Unlike the other wrappers, smart-page wraps its own ng-content in a
        TemplateRef and passes it down as options.bodyTpl, so projected content
        survives the NgComponentOutlet hop.
      -->
      <section class="docs-page__body">
        @if (options()?.bodyTpl; as bodyTpl) {
          <ng-container [ngTemplateOutlet]="bodyTpl" />
        }
      </section>
    </div>
  `,
  imports: [NgTemplateOutlet],
  encapsulation: ViewEncapsulation.None,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class CustomPageComponent extends PageBaseComponent {
  // NgComponentOutlet passes 'cssClass' by canonical name, not the 'class' alias.
  override cssClass = input<string>('');

  containerClasses = computed(() =>
    ['docs-page', this.isMobile ? 'docs-page--mobile' : '', this.cssClass()]
      .filter(Boolean)
      .join(' '),
  );
}

/**
 * The page wrapper resolves its shell from a map of variants rather than from
 * a single standard-component token, so the implementation is registered under
 * the variant name that `IPageOptions.variant` asks for. The map is merged
 * over the built-in one, so `'standard'` keeps working for everything else.
 */
const DOCS_PAGE_VARIANT_COMPONENTS: Partial<
  Record<SmartPageVariant, Type<PageBaseComponent>>
> = { docs: CustomPageComponent };

@Component({
  selector: 'docs-page-custom-example',
  changeDetection: ChangeDetectionStrategy.OnPush,
  imports: [PageComponent],
  providers: [
    {
      provide: PAGE_VARIANT_COMPONENTS_TOKEN,
      useValue: DOCS_PAGE_VARIANT_COMPONENTS,
    },
  ],
  template: `
    <smart-page [options]="options()">
      <p>Account settings and permissions</p>
    </smart-page>

    <ng-template #breadcrumbsTpl>
      <a href="#">Users</a>
      <span aria-hidden="true">/</span>
      <span>Alice Johnson</span>
    </ng-template>
  `,
})
export class PageCustomExampleComponent {
  breadcrumbsTpl = viewChild<TemplateRef<unknown>>('breadcrumbsTpl');

  options = computed<IPageOptions>(() => ({
    title: 'Alice Johnson',
    variant: 'docs',
    showBackButton: true,
    breadcrumbsTpl: this.breadcrumbsTpl(),
  }));
}

Preset

PagePresetComponent (<smart-page-preset>) is a styled 'preset' variant that extends PageStandardComponent. It renders a full application-shell layout instead of the plain standard header: a bordered <header>, a gray page body and a content card. Every utility is smart:-prefixed with explicit smart:dark:* twins.

Registration

The preset is dispatched by map key 'preset' — the built-in 'standard' variant is untouched. Register the ready-made map for PAGE_VARIANT_COMPONENTS_TOKEN:

Then set variant: 'preset' on IPageOptions:

Zones (all keyed by data-role)

data-roleSourceNotes
pageouter wrapperbg-gray-50 dark:bg-gray-900; picks up cssClass.
bannerbannerTplFull-width strip above the header.
headerheader shellSkipped entirely when hideHeader is true.
breadcrumbsbreadcrumbsTplRow at the top of the header container.
titletitle (h1) + subtitleTplTitle row also holds back button, avatar/logo, actions.
actionssearch + endButtonsRight side of the title row; hidden when both are absent.
metametaTpl + statsTplRow under the title; hidden when both are absent.
filtersfiltersTplBar under the header.
sidebarsidebarTplRendered as <aside> in a flex row (lg: and up).
bodybodyTpl / <ng-content>Rendered inside a bordered content card.

The back button (button[data-role="back"]) appears only when showBackButton is true and calls the inherited back(). A hamburger button[data-role="menu-button"] renders unless hideMenuButton is true.

cssClass

Registered through the token, the wrapper forwards inputs canonically via NgComponentOutlet ({ options, cssClass }), so the preset does override cssClass = input<string>('') (drops the class alias). External classes land on the data-role="page" wrapper.

Documented gaps

  • hideMenuButton only toggles a decorative hamburger button; there is no menu panel wired to it.
  • Unused slots (bannerTpl, breadcrumbsTpl, metaTpl, statsTpl, filtersTpl, sidebarTpl, avatarTpl, logoTpl, subtitleTpl) simply render nothing when omitted; navTpl is not consumed by this preset.

Source

The component lives in packages/shared/angular/src/lib/components/page and is documented for Claude Code by the angular-components-page skill.