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:
The other wraps every table in a component that gives it a colored border:
| Syntax | Description |
|---|---|
| Header | Title |
| Paragraph | Text |
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 , then yours.
A processor is an object:
| Field | Description |
|---|---|
component | The component that replaces the element. |
selector | A CSS selector for the elements to replace. |
extractOptions | Reads the component's inputs from the element, and optionally the content to project into it. |
nodeToReplace | Optional. 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:
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:
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 . In the providers of a page, they work on that page only:
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:
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:
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 :
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 {}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
WarningProcessors registered in a page replace those registered in the application configuration, for that page. Angular doesn't merge
multiproviders of a route with those of the application, so repeat the application processors in the page if you need both.
NoteYour 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
a list without the default processor for it.provideMainPageProcessor
Next: