Multi-Column Layout

The <smart-multi-column-layout> component provides a three-column application shell — a left navigation <aside>, a main content region, and a right secondary <aside>, with an optional top header. It follows the Base + Standard + Wrapper pattern with an InjectionToken-based extension mechanism. The abstract MultiColumnLayoutBaseComponent defines the shared API — optional IMultiColumnLayoutOptions and cssClass (alias class). MultiColumnLayoutStandardComponent is a barebones placeholder concrete implementation. MultiColumnLayoutComponent is the public wrapper that renders MultiColumnLayoutStandardComponent by default and accepts a custom replacement via MULTI_COLUMN_LAYOUT_STANDARD_COMPONENT_TOKEN.


Usage

<smart-multi-column-layout [options]="options()">
  <h2>Quarterly report is ready</h2>
  <p>The Q3 numbers are in. Open the attachment for the full breakdown.</p>
</smart-multi-column-layout>

<ng-template #headerTpl>
  <strong>Inbox</strong>
</ng-template>

<ng-template #navTpl>
  <a href="/inbox">Inbox</a>
  <a href="/drafts">Drafts</a>
  <a href="/sent">Sent</a>
</ng-template>

<ng-template #secondaryTpl>
  <span>Storage: 4.2 GB of 15 GB used</span>
</ng-template>

Components

MultiColumnLayoutComponent (<smart-multi-column-layout>)

Main wrapper component. Renders MultiColumnLayoutStandardComponent by default. When MULTI_COLUMN_LAYOUT_STANDARD_COMPONENT_TOKEN is provided, renders the injected component via NgComponentOutlet. Accepts main content via projected <ng-content>.

MultiColumnLayoutStandardComponent (<smart-multi-column-layout-standard>)

Barebones placeholder concrete implementation. Renders a wrapper <div> containing an optional <header> (when options.headerTpl is provided), an <aside class="nav">, a <main> element that projects <ng-content>, and an <aside class="secondary">. The external cssClass is applied to the wrapper. It does not include Tailwind UI styling — it exists solely as the default structural placeholder until a custom implementation is registered through the token.

MultiColumnLayoutBaseComponent (abstract)

Abstract base directive for extending custom multi-column-layout implementations. Exposes options as an InputSignal<IMultiColumnLayoutOptions | undefined> and cssClass as an InputSignal<string> (with alias class).

API

Inputs

InputTypeDefaultDescription
optionsInputSignal<IMultiColumnLayoutOptions | undefined>-Optional configuration (nav, secondary, header, width)
classInputSignal<string>''External CSS classes (alias for cssClass)

IMultiColumnLayoutOptions

The default MultiColumnLayoutStandardComponent consumes navTpl, secondaryTpl, and headerTpl. title, width, and secondaryWidth are reserved for custom implementations registered via MULTI_COLUMN_LAYOUT_STANDARD_COMPONENT_TOKEN.

MULTI_COLUMN_LAYOUT_STANDARD_COMPONENT_TOKEN

InjectionToken that allows replacing the default MultiColumnLayoutStandardComponent with a custom implementation. Provide a Type<MultiColumnLayoutBaseComponent> to override.

Extending the base class

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

import {
  IMultiColumnLayoutOptions,
  MULTI_COLUMN_LAYOUT_STANDARD_COMPONENT_TOKEN,
  MultiColumnLayoutBaseComponent,
  MultiColumnLayoutComponent,
} from '@smartsoft001/angular';

/**
 * A custom multi column layout built on `MultiColumnLayoutBaseComponent`.
 *
 * The base contributes the `options` and `class` inputs; the implementation
 * decides the column order and where each `TemplateRef` slot of
 * `IMultiColumnLayoutOptions` is projected.
 */
@Component({
  selector: 'docs-custom-multi-column-layout',
  template: `
    <div [class]="containerClasses()">
      @if (options()?.title) {
        <h1 class="docs-multi-column-layout__title">{{ options()!.title }}</h1>
      }

      @if (options()?.headerTpl) {
        <header class="docs-multi-column-layout__header">
          <ng-container [ngTemplateOutlet]="options()!.headerTpl!" />
        </header>
      }

      @if (options()?.navTpl) {
        <aside class="docs-multi-column-layout__nav">
          <ng-container [ngTemplateOutlet]="options()!.navTpl!" />
        </aside>
      }

      <!--
        smart-multi-column-layout renders a custom implementation through
        NgComponentOutlet, which does not forward projected content. Only
        options and cssClass arrive here, so the main column is owned by the
        implementation instead of relying on ng-content.
      -->
      <main class="docs-multi-column-layout__main">
        <p>Three unread conversations, oldest from Tuesday.</p>
      </main>

      @if (options()?.secondaryTpl) {
        <aside class="docs-multi-column-layout__secondary">
          <ng-container [ngTemplateOutlet]="options()!.secondaryTpl!" />
        </aside>
      }
    </div>
  `,
  imports: [NgTemplateOutlet],
  encapsulation: ViewEncapsulation.None,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class CustomMultiColumnLayoutComponent extends MultiColumnLayoutBaseComponent {
  // NgComponentOutlet passes 'cssClass' by canonical name, not the 'class' alias.
  override cssClass = input<string>('');

  containerClasses = computed(() => {
    const classes = ['docs-multi-column-layout'];
    const width = this.options()?.width;
    if (width) classes.push(`docs-multi-column-layout--${width}`);
    const secondaryWidth = this.options()?.secondaryWidth;
    if (secondaryWidth) {
      classes.push(`docs-multi-column-layout--secondary-${secondaryWidth}`);
    }
    const extra = this.cssClass();
    if (extra) classes.push(extra);
    return classes.join(' ');
  });
}

/**
 * Registering the implementation against
 * `MULTI_COLUMN_LAYOUT_STANDARD_COMPONENT_TOKEN` makes every
 * `<smart-multi-column-layout>` in this injector render it instead of the
 * standard variation. The slots are `<ng-template>` references owned by this
 * host, because `IMultiColumnLayoutOptions` takes `TemplateRef`s.
 */
@Component({
  selector: 'docs-multi-column-layout-custom-example',
  changeDetection: ChangeDetectionStrategy.OnPush,
  imports: [MultiColumnLayoutComponent],
  providers: [
    {
      provide: MULTI_COLUMN_LAYOUT_STANDARD_COMPONENT_TOKEN,
      useValue: CustomMultiColumnLayoutComponent,
    },
  ],
  template: `
    <smart-multi-column-layout [options]="options()" />

    <ng-template #headerTpl>
      <span>Unread first</span>
    </ng-template>

    <ng-template #navTpl>
      <a href="#">Inbox</a>
      <a href="#">Drafts</a>
      <a href="#">Sent</a>
    </ng-template>

    <ng-template #secondaryTpl>
      <span>Storage: 4.2 GB of 15 GB used</span>
    </ng-template>
  `,
})
export class MultiColumnLayoutCustomExampleComponent {
  headerTpl = viewChild<TemplateRef<unknown>>('headerTpl');
  navTpl = viewChild<TemplateRef<unknown>>('navTpl');
  secondaryTpl = viewChild<TemplateRef<unknown>>('secondaryTpl');

  options = computed<IMultiColumnLayoutOptions>(() => ({
    title: 'Inbox',
    width: 'full',
    secondaryWidth: 'sm',
    headerTpl: this.headerTpl(),
    navTpl: this.navTpl(),
    secondaryTpl: this.secondaryTpl(),
  }));
}

Preset

MultiColumnLayoutPresetComponent (<smart-multi-column-layout-preset>) is a fully styled drop-in replacement for the barebones standard component. It extends MultiColumnLayoutStandardComponent and renders a Tailwind app-shell scaffold with explicit light/dark classes (every utility is smart:-prefixed).

Structure

  • root (data-role="root") — full-height gray page (smart:min-h-full smart:bg-gray-50 smart:dark:bg-gray-900); merges the external cssClass.
  • header (data-role="header") — rendered only when options.headerTpl or options.title is set. White surface with a bottom border, matching the stacked-layout/sidebar-layout preset headers. Renders headerTpl, falling back to a title <h1> (data-role="title").
  • flex row containing:
    • nav (data-role="nav", <aside>) — rendered only when options.navTpl is set. White surface, smart:w-64 smart:shrink-0 smart:border-e, mirroring the sidebar-layout preset nav.
    • content (data-role="content", <main>) — gray page surface with smart:py-8; projects <ng-content> inside an inner container whose horizontal rhythm follows options.width.
    • secondary (data-role="secondary", <aside>) — rendered only when options.secondaryTpl is set. White surface on the trailing edge (smart:border-s); width follows options.secondaryWidth.

Options consumed

The preset consumes the full IMultiColumnLayoutOptions surface (the standard component only reads the templates):

  • width — 'constrained' centers the main content container at max-w-7xl; 'full' (default) uses full width. Both keep px-4 sm:px-6 lg:px-8.
  • secondaryWidth — 'sm' → w-64 (default), 'md' → w-80, 'lg' → w-96.
  • headerTpl / title, navTpl, secondaryTpl — as above.

Class recipes live in preset/preset-classes.util.ts (getMultiColumnLayout…Classes, including the width/secondaryWidth maps).

Token registration

When registered through the token, the wrapper passes inputs by canonical name via NgComponentOutlet, so the preset overrides cssClass as input<string>('') (dropping the class alias).

Gaps

  • The nav and secondary asides render on the flex row at all viewport sizes — there is no built-in mobile collapse/drawer behavior.
  • Files: preset/preset.component.ts, preset/preset.component.html, preset/preset-classes.util.ts; story Preset in multi-column-layout.component.stories.ts.

Source

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