Content projection

<ng-content> lets a component wrap content that its parent passes in. Cards, labels and every UI component are built this way. It works on Lynx, with four rules that come from Lynx's elements.

The Lynx rules

  1. Projected text must land inside a <text>. Lynx renders text only inside <text>. If your component takes text, put the <ng-content> inside a <text>. Text projected into a <view> doesn't appear.
  2. A nested <text> is inline. When the parent passes <text> elements into your <text>, they flow as styled runs, like <span>s.
  3. The host element is a real box. <app-card> becomes a plain, unstyled <view> in the layout, between the parent and your template. When the component must fill or stretch, put the classes on the host with host: { class: '…' }.
  4. Each projected element takes a native slot. Lynx has a fixed number of slots for native elements, the element pool. To show hundreds of items, use <list>, which only creates the rows on screen.

Project into a view

src/app/card.ts
import { Component } from '@angular/core';
import { LYNX_ELEMENTS } from '@blotch/angular-lynx';

@Component({
  selector: 'app-card',
  imports: [LYNX_ELEMENTS],
  template: `
    <view class="rounded-xl border border-border bg-card p-4">
      <ng-content />
    </view>
  `,
})
export class Card {}
<app-card>
  <text class="text-foreground">This text is projected into the card.</text>
</app-card>
  • Pass elements, not bare text, into a component that projects into a <view>. The parent wraps its words in <text>.

Project text

src/app/card-title.ts
import { Component } from '@angular/core';
import { LYNX_ELEMENTS } from '@blotch/angular-lynx';

@Component({
  selector: 'app-card-title',
  imports: [LYNX_ELEMENTS],
  template: `
    <text class="text-lg font-semibold text-foreground">
      <ng-content />
    </text>
  `,
})
export class CardTitle {}
<app-card-title>My card title</app-card-title>

<app-card-title>
  Order
  <text class="text-muted-foreground">#1042</text>
</app-card-title>
  • Bare text works here, because it lands inside the component's <text>.
  • Nested <text> styles part of the text. The second usage renders "Order #1042" on one line, with the number muted.

Fill the parent from the host

src/app/panel.ts
import { Component } from '@angular/core';
import { LYNX_ELEMENTS } from '@blotch/angular-lynx';

@Component({
  selector: 'app-panel',
  imports: [LYNX_ELEMENTS],
  // The host is the element the parent lays out, so it takes the flex-1.
  host: { class: 'flex flex-1 flex-col' },
  template: `
    <scroll-view scroll-orientation="vertical" class="h-full w-full">
      <ng-content />
    </scroll-view>
  `,
})
export class Panel {}
  • Put sizing on the host. A flex-1 on an inner view does nothing if the unstyled host around it sizes to its content.
  • A component with no content needs host sizing most. With nothing inside, its host can collapse to zero size, hiding everything in it.

Named slots

src/app/badge.ts
import { Component } from '@angular/core';
import { LYNX_ELEMENTS } from '@blotch/angular-lynx';

@Component({
  selector: 'app-badge',
  imports: [LYNX_ELEMENTS],
  template: `
    <text class="rounded-full bg-muted px-2 text-sm">
      <ng-content select="[icon]" />
      <ng-content />
    </text>
  `,
})
export class Badge {}
<app-badge>
  <text icon class="text-primary">★ </text>
  <text>Featured</text>
</app-badge>

select works as on the web, including inside a <text>.

Conditional projection

src/app/expandable-label.ts
import { Component, input } from '@angular/core';
import { LYNX_ELEMENTS } from '@blotch/angular-lynx';

@Component({
  selector: 'app-expandable-label',
  imports: [LYNX_ELEMENTS],
  template: `
    <text class="text-foreground">
      @if (expanded()) {
        <ng-content />
      } @else {
        <text>…</text>
      }
    </text>
  `,
})
export class ExpandableLabel {
  readonly expanded = input(true);
}

@if and @for work around projected content, inside a <view> or a <text>.

Content queries

src/app/item-list.ts
import { Component, ElementRef, contentChildren } from '@angular/core';
import { LYNX_ELEMENTS } from '@blotch/angular-lynx';

@Component({
  selector: 'app-item-list',
  imports: [LYNX_ELEMENTS],
  template: `
    <view>
      <text>Items: {{ items().length }}</text>
      <ng-content />
    </view>
  `,
})
export class ItemList {
  readonly items = contentChildren('item', { read: ElementRef });
}
<app-item-list>
  <text #item>Item A</text>
  <text #item>Item B</text>
</app-item-list>

contentChild() and contentChildren() find projected elements and components, as on the web. With read: ElementRef, you get the Lynx element, not a DOM node.