Skip to content

Components

Angular components inside Markdown: registration, children, and how a tag is told apart from HTML.

A tag in your Markdown can become a real Angular component. This is the centre of Blasdoc, so it is worth being precise about how a tag is recognised, how it is resolved, and what happens when it is not.

Using one

<app-button variant="primary">Comprar</app-button>

Import it, then use it

A page is an ordinary Angular component, so the components its Markdown may use are the ones in its imports — exactly as they would be for an HTML template. Nothing is registered, and no name is written twice.

ts
@Component({
  selector: 'app-guide-page',
  templateMD,
  imports: [AppButton, AppCallout],
})
export class GuidePage {}
md
<app-button variant="primary">Comprar</app-button>

The tag comes from each component's own selector, so it cannot drift from the class. Directives and pipes may sit in imports too and are simply left out: Blasdoc resolves a Markdown tag against a plain element selector.

When to write components instead

imports covers the ordinary case. The explicit list is there for the two things a selector cannot express, and it wins over imports:

A lazy loader, which arrives only when a document uses it:

ts
components: [
  { selector: 'app-chart', load: () => import('./chart').then((m) => m.Chart) },
]

Or a tag that differs from the component's own selector:

ts
components: { 'app-cta': AppButton }

Application-wide, and overriding Blasdoc's own

provideBlasdoc() registers for every document at once. That is what it is for now: the components a whole site shares, and — more usefully — replacing the structural components Blasdoc itself renders, such as the code block and its tabs.

ts
provideBlasdoc({
  components: {
    'blasdoc-callout': MyCallout,
    'app-button': AppButton,
  },
});

A page's own imports win over anything registered this way, so one page can differ from the rest without touching the application.

Children

Children are projected through <ng-content />, and they are Markdown:

md
<app-callout type="warning">
  This is **Markdown** inside a component, with a [link](/docs).
</app-callout>

Nesting works to any depth, and a component may span several Markdown blocks.

Self-closing

md
<app-spacer />

HTML is not a component

<div> stays a div. The rule is the tag name: a name containing a dash is a component, everything else is HTML.

A plain div, rendered as HTML.
<div>A plain div, rendered as HTML.</div>

You can change the rule:

ts
provideBlasdoc({
  parseOptions: {
    isComponentName: (name) => name.startsWith('app-'),
  },
});

Unknown components

The parser does not know your registry — it records a component node either way. The runtime is what reports the problem:

text
BLASDOC_UNKNOWN_COMPONENT  Unknown component "app-missing".

Nothing is instantiated on a guess. See Diagnostics.

The Markdown boundary

CommonMark only recognises HTML-shaped tags, so <app-callout type="warning"> arrives as a raw HTML block while <app-button [disabled]="x"> arrives as plain text — [disabled] is not a valid HTML attribute name. Blasdoc re-reads both through its own tokenizer, so the two spellings produce the same IR.

Two consequences worth knowing:

A paragraph is transparent to tags: it is only materialised once no tag crossed its boundary, so a component can span several blocks, and a paragraph holding nothing but tags is dropped rather than wrapping your component in a stray <p>.