Files
ISA-Frontend/.claude/skills/angular-template/references/projection-patterns.md
Lorenz Hilpert 6f238816ef feat: add Angular template skill for modern template patterns
Add comprehensive skill for Angular 20+ template best practices covering:
- Modern control flow (@if, @for, @switch, @defer)
- Content projection (ng-content)
- Template references (ng-template, ng-container)
- Variable declarations (@let)
- Expression binding patterns
- Performance optimization strategies
- Migration guides from legacy syntax

Includes 4 reference files with detailed examples:
- control-flow-reference.md: Advanced @if/@for/@switch patterns
- defer-patterns.md: Lazy loading and Core Web Vitals optimization
- projection-patterns.md: ng-content advanced techniques
- template-reference.md: ng-template/ng-container usage

All files optimized for context efficiency (~65% reduction from initial draft)
while preserving all essential patterns, best practices, and examples.
2025-10-23 20:42:28 +02:00

6.3 KiB
Raw Blame History

Content Projection Patterns

Advanced ng-content, ng-template, and ng-container techniques.

Basic Projection

Single Slot

@Component({
  selector: 'ui-panel',
  template: `<div class="panel"><ng-content></ng-content></div>`
})
export class PanelComponent {}

Multi-Slot with Selectors

@Component({
  selector: 'ui-card',
  template: `
    <header><ng-content select="card-header"></ng-content></header>
    <main><ng-content select="card-body"></ng-content></main>
    <footer><ng-content></ng-content></footer> <!-- default -->
  `
})

Usage:

<ui-card>
  <card-header><h3>Title</h3></card-header>
  <card-body><p>Content</p></card-body>
  <button>Action</button> <!-- default slot -->
</ui-card>

Selectors: Element (card-title), class (.actions), attribute ([slot='footer']) Fallback: <ng-content select="title">Default</ng-content> Aliasing: <h3 ngProjectAs="card-header">Title</h3>

Conditional Projection

ng-content always instantiates (even if hidden). Use ng-template for truly conditional content:

@Component({
  selector: 'ui-expandable',
  imports: [NgTemplateOutlet],
  template: `
    <div (click)="toggle()">
      <ng-content select="header"></ng-content>
      <span>{{ isExpanded() ? '▼' : '▶' }}</span>
    </div>
    @if (isExpanded()) {
      <ng-container *ngTemplateOutlet="contentTemplate()"></ng-container>
    }
  `
})
export class ExpandableComponent {
  isExpanded = signal(false);
  contentTemplate = contentChild<TemplateRef<unknown>>('content');
  toggle() { this.isExpanded.update(v => !v); }
}

Usage:

<ui-expandable>
  <header><h3>Click to expand</h3></header>
  <ng-template #content>
    <app-heavy-component /> <!-- Only rendered when expanded -->
  </ng-template>
</ui-expandable>

Template-Based Projection

Accepting Template Fragments

@Component({
  selector: 'ui-list',
  imports: [NgTemplateOutlet],
  template: `
    <ul>
      @for (item of items(); track item.id) {
        <li>
          <ng-container
            *ngTemplateOutlet="itemTemplate(); context: { $implicit: item, index: $index }">
          </ng-container>
        </li>
      }
    </ul>
  `
})
export class ListComponent<T> {
  items = input.required<T[]>();
  itemTemplate = contentChild.required<TemplateRef<{ $implicit: T; index: number }>>('itemTemplate');
}

Usage:

<ui-list [items]="users()">
  <ng-template #itemTemplate let-user let-i="index">
    <div>{{i + 1}}. {{user.name}}</div>
  </ng-template>
</ui-list>

Multiple Template Slots

@Component({
  selector: 'ui-data-table',
  imports: [NgTemplateOutlet],
  template: `
    <table>
      <thead><ng-container *ngTemplateOutlet="headerTemplate()"></ng-container></thead>
      <tbody>
        @for (row of data(); track row.id) {
          <ng-container *ngTemplateOutlet="rowTemplate(); context: { $implicit: row }"></ng-container>
        }
      </tbody>
      <tfoot><ng-container *ngTemplateOutlet="footerTemplate()"></ng-container></tfoot>
    </table>
  `
})
export class DataTableComponent<T> {
  data = input.required<T[]>();
  headerTemplate = contentChild.required<TemplateRef<void>>('header');
  rowTemplate = contentChild.required<TemplateRef<{ $implicit: T }>>('row');
  footerTemplate = contentChild<TemplateRef<void>>('footer');
}

Querying Projected Content

Using ContentChildren

@Component({
  selector: 'ui-tabs',
  template: `
    <nav>
      @for (tab of tabs(); track tab.id) {
        <button [class.active]="tab === activeTab()" (click)="selectTab(tab)">
          {{tab.label()}}
        </button>
      }
    </nav>
    <ng-content></ng-content>
  `
})
export class TabsComponent {
  tabs = contentChildren(TabComponent);
  activeTab = signal<TabComponent | null>(null);

  ngAfterContentInit() {
    this.selectTab(this.tabs()[0]);
  }

  selectTab(tab: TabComponent) {
    this.tabs().forEach(t => t.isActive.set(false));
    tab.isActive.set(true);
    this.activeTab.set(tab);
  }
}

@Component({
  selector: 'ui-tab',
  template: `@if (isActive()) { <ng-content></ng-content> }`
})
export class TabComponent {
  label = input.required<string>();
  isActive = signal(false);
  id = Math.random().toString(36);
}

Real-World Examples

Modal/Dialog

@Component({
  selector: 'ui-modal',
  imports: [NgTemplateOutlet],
  template: `
    @if (isOpen()) {
      <div class="backdrop" (click)="close()">
        <div class="modal" (click)="$event.stopPropagation()">
          <header>
            <ng-content select="modal-title"></ng-content>
            <button (click)="close()">×</button>
          </header>
          <main><ng-content></ng-content></main>
          <footer>
            <ng-content select="modal-actions">
              <button (click)="close()">Close</button>
            </ng-content>
          </footer>
        </div>
      </div>
    }
  `
})
export class ModalComponent {
  isOpen = signal(false);
  open() { this.isOpen.set(true); }
  close() { this.isOpen.set(false); }
}

Form Field Wrapper

@Component({
  selector: 'ui-form-field',
  template: `
    <div class="form-field" [class.has-error]="error()">
      <label [for]="fieldId()">
        <ng-content select="field-label"></ng-content>
        @if (required()) { <span class="required">*</span> }
      </label>
      <div class="input-wrapper"><ng-content></ng-content></div>
      @if (error()) { <span class="error">{{error()}}</span> }
      @if (hint()) { <span class="hint">{{hint()}}</span> }
    </div>
  `
})
export class FormFieldComponent {
  fieldId = input.required<string>();
  required = input(false);
  error = input<string>();
  hint = input<string>();
}

Performance Notes

  • ng-content always instantiates projected content
  • For conditional projection, use ng-template + NgTemplateOutlet
  • Projected content evaluates in parent component context
  • Use computed() for expensive expressions in projected content

Common Pitfalls

  1. Using ng-content in structural directives: Won't work as expected. Use ng-template instead.
  2. Forgetting default slot: Unmatched content disappears without default <ng-content></ng-content>
  3. Template order matters: Content renders in template order, not usage order