Skip to content

Custom page components

A page processor replaces HTML elements of a page with an Angular component. NgDoc renders its own parts of a page this way: code blocks, callouts, demos, playgrounds, tabs and images all start as plain HTML and become components in the browser. Your own processors can add components to Markdown, or improve elements that Markdown already produces.

 See it

This page registers two processors. One replaces images with a component that zooms on hover and shows the image title in a tooltip:

NgDoc logo

The other wraps every table in a component that gives it a colored border:

SyntaxDescription
HeaderTitle
ParagraphText

How it works

The builder renders your Markdown to HTML. When a page is shown, NgDoc looks for the elements that each processor selects, reads inputs from them, and puts the processor's component in their place. The processors run in order: first the main processors, usually NG_DOC_DEFAULT_PAGE_PROCESSORS, then yours.

A processor is an NgDocPageProcessor object:

FieldDescription
componentThe component that replaces the element.
selectorA CSS selector for the elements to replace.
extractOptionsReads the component's inputs from the element, and optionally the content to project into it.
nodeToReplaceOptional. Returns another node to replace instead of the selected element, for example to keep the element.

Replace an element

First, the component. It is a standalone component with signal inputs:

image-viewer.component.tsTypeScript
import { ChangeDetectionStrategy, Component, input } from '@angular/core';
import { NgDocTooltipDirective } from '@ng-doc/ui-kit';

@Component({
  selector: 'image-viewer',
  imports: [NgDocTooltipDirective],
  template: `<img [src]="src()" [alt]="alt()" [ngDocTooltip]="title()" [delay]="0" />`,
  styles: `
    :host {
      display: flex;
      justify-content: center;
      padding: var(--ng-doc-base-gutter);
      border: 1px solid var(--ng-doc-border-color);
      border-radius: var(--ng-doc-radius-md);
      overflow: hidden;
    }

    img {
      width: 100%;
      max-height: 100px;
      transition: transform 0.2s ease-in-out;
    }

    img:hover {
      transform: scale(1.1);
    }
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ImageViewerComponent {
  readonly src = input('');
  readonly alt = input('');
  readonly title = input('');
}

Then the processor. It selects every img element and reads the inputs from its attributes:

image.processor.tsTypeScript
import { NgDocPageProcessor } from '@ng-doc/app';

import { ImageViewerComponent } from './image-viewer.component';

export const imageProcessor: NgDocPageProcessor<ImageViewerComponent> = {
  component: ImageViewerComponent,
  selector: 'img',
  extractOptions: (element: Element) => ({
    inputs: {
      src: element.getAttribute('src') ?? '',
      alt: element.getAttribute('alt') ?? '',
      title: element.getAttribute('title') ?? '',
    },
  }),
};

extractOptions is typed by the component: an input value of the wrong type fails to compile. For a signal input, pass the type that the input accepts.

Register a processor

Register processors with providePageProcessor. In the providers of a page, they work on that page only:

ng-doc.page.tsTypeScript
import { providePageProcessor } from '@ng-doc/app';
import { NgDocPage } from '@ng-doc/core';

import { imageProcessor } from './image.processor';

const MyPage: NgDocPage = {
  title: 'My page',
  mdFile: './index.md',
  providers: [providePageProcessor(imageProcessor)],
};

export default MyPage;

In the application configuration, they work on every page:

app.config.tsTypeScript
import { ApplicationConfig } from '@angular/core';
import { providePageProcessor } from '@ng-doc/app';

import { imageProcessor } from './image.processor';

export const appConfig: ApplicationConfig = {
  providers: [providePageProcessor(imageProcessor)],
};

Then write the element in Markdown or HTML as usual:

index.mdMarkdown
![NgDoc logo](assets/images/brand/logo.svg 'The NgDoc logo')

Wrap an element

To keep the element and put a component around it, pass the element as the component's content, and return a new anchor from nodeToReplace. The component projects the element with ng-content:

custom-table.component.tsTypeScript
import { ChangeDetectionStrategy, Component, ViewEncapsulation } from '@angular/core';

@Component({
  selector: 'custom-table',
  template: `<ng-content></ng-content>`,
  styles: `
    custom-table table {
      border: 1px solid var(--ng-doc-primary);
    }
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
  encapsulation: ViewEncapsulation.None,
})
export class CustomTableComponent {}
table.processor.tsTypeScript
import { Injector, Renderer2 } from '@angular/core';
import { NgDocPageProcessor } from '@ng-doc/app';

import { CustomTableComponent } from './custom-table.component';

export const tableProcessor: NgDocPageProcessor<CustomTableComponent> = {
  component: CustomTableComponent,
  selector: 'table',
  nodeToReplace: (element: Element, injector: Injector) => {
    // Get the renderer from the injector
    const renderer: Renderer2 = injector.get(Renderer2);
    // Create an anchor element to insert the `CustomTableComponent` in the correct place.
    const anchor: Element = renderer.createElement('div');

    // Insert the anchor before the table and return it
    return element.parentNode?.insertBefore(anchor, element) ?? element;
  },
  extractOptions: (element: Element) => ({
    // Provide the table element as the `ng-content` of the component.
    content: [[element]],
  }),
};

 Gotchas

Warning

Processors registered in a page replace those registered in the application configuration, for that page. Angular doesn't merge multi providers of a route with those of the application, so repeat the application processors in the page if you need both.

Note

Your processors run after the main ones, so they see the page after NgDoc has changed it. The default image processor wraps each image in NgDoc's image viewer, and the example processor above then replaces the image inside it. To replace NgDoc's own handling of an element instead, pass provideMainPageProcessor a list without the default processor for it.

Next: Dev server and builds

Edit this page