DEV Community

Cover image for Stop Passing HTML Strings! The Senior Guide to Content Projection in Angular
Habibur Rahman
Habibur Rahman

Posted on

Stop Passing HTML Strings! The Senior Guide to Content Projection in Angular

You've successfully split your architecture into Smart and Dumb components. You're building a reusable CardComponent for your design system.

But then, the feature requests start rolling in.

  • "Can we add a button to the header?"
  • "Can we make the title bold?"
  • "Can we sometimes have an image in the footer?"

The Junior Solution:
Add an @Input() for every new request.
@Input() showHeaderButton: boolean
@Input() isTitleBold: boolean
@Input() footerImage: string

Result? A component with 30 inputs that is a nightmare to maintain. You end up with a template full of *ngIf="showButton" and confusing logic.

The Senior Solution:
Content Projection.

Instead of telling the component what data to render, you pass it the actual HTML to render. This is an implementation of the Inversion of Control principle. You give control of the content back to the parent.

Level 1: Multi-Slot Projection (The "Select" Attribute)

You can define specific zones for content using the select attribute. This works just like standard CSS selectors.

The Card Component:

@Component({
  selector: 'app-card',
  template: `
    <div class="card">
      <div class="card-header">
        <ng-content select="[card-header]"></ng-content>
      </div>

      <div class="card-body">
        <ng-content></ng-content>
      </div>

      <div class="card-footer">
         <ng-content select=".footer-action"></ng-content>
      </div>
    </div>
  `
})
export class CardComponent {}
Enter fullscreen mode Exit fullscreen mode

Usage:

<app-card>
  <h2 card-header>User Profile</h2>

  <p>This user has been active since 2018.</p>

  <button class="footer-action">Edit User</button>
</app-card>
Enter fullscreen mode Exit fullscreen mode

Level 2: The Styling "Gotcha"

This is where 90% of developers get stuck.

You project a button into your card. You want the card to style that button. You write CSS in card.component.css:

button { background: red; }
Enter fullscreen mode Exit fullscreen mode

It doesn't work. Why?

Because of Angular's View Encapsulation. The button technically belongs to the Parent component, not the Card component. The Card treats it as "alien content."

The Fix:
You have two options.

  1. The :host ::ng-deep Hack (Use carefully):

    :host ::ng-deep button { background: red; }
    

    This forces the style down. It's deprecated but still widely used.

  2. The Proper Way (Global Classes):

    Don't try to style the button inside the card. Instead, have the Card provide structural layout (padding, margins), and use a global button class (e.g., .btn-primary) on the projected element itself.

Level 3: Interacting with Content (@ContentChild)

Sometimes the Card needs to know what was projected. For example, "If a footer was projected, add a bottom border."

You can access the content programmatically!

@Component({...})
export class CardComponent implements AfterContentInit {
  // Look for the projected element
  @ContentChild('footer') footerContent: ElementRef;

  hasFooter = false;

  ngAfterContentInit() {
    // Check if it exists
    this.hasFooter = !!this.footerContent;
  }
}
Enter fullscreen mode Exit fullscreen mode

Summary

Stop micromanaging your components with configuration inputs. Open up slots.

  • If you want to change the text? Project it.
  • If you want to bold the title? Project an <b> tag.
  • If you want a button? Project a <button>.

Let the component handle the Container, and let the consumer handle the Content.

Top comments (0)