As I started my first open-source project called Ignite, I had many opportunities applying Angular concepts that I seldom use when doing my freelance jobs. When you have a deadline, you tend not to adhere very well to the paradigm that components should be reused as much as possible. As such, my components template span several hundred lines. My rule of thumb is that when a piece of functionality will not be reused anywhere do not bother to create a separate component for it. But when you are doing freelance and opensource project, you must always be open to the idea that everything should be reusable as much as possible.

Yesterday, I was able to add support for Admin LTE tabs (a bootstrap tabs) to the Ignite Frontend which is written in Angular. Bootstrap’s Tabs can be used in the following manner:

<ul class="nav nav-tabs" id="myTab" role="tablist">
  <li class="nav-item">
    <a class="nav-link active" id="home-tab" data-toggle="tab" href="#home" role="tab" aria-controls="home" aria-selected="true">Home</a>
  </li>
  <li class="nav-item">
    <a class="nav-link" id="profile-tab" data-toggle="tab" href="#profile" role="tab" aria-controls="profile" aria-selected="false">Profile</a>
  </li>
  <li class="nav-item">
    <a class="nav-link" id="contact-tab" data-toggle="tab" href="#contact" role="tab" aria-controls="contact" aria-selected="false">Contact</a>
  </li>
</ul>
<div class="tab-content" id="myTabContent">
  <div class="tab-pane fade show active" id="home" role="tabpanel" aria-labelledby="home-tab">...</div>
  <div class="tab-pane fade" id="profile" role="tabpanel" aria-labelledby="profile-tab">...</div>
  <div class="tab-pane fade" id="contact" role="tabpanel" aria-labelledby="contact-tab">...</div>
</div>

The question is how I can create a Tabs Component in such a way that it will be easily reused inside any Angular Project.

The answer:

Content Projection, Content Children and Query List.

It is very tempting to write a TabsComponent that will be used like this:

<app-tabs [items]="navItems"></app-tabs>

It seems easy, but this is not really the best way. Why? because you’ll end up creating a Frankenstein component that will not be easily customizable and reused. What do I mean?

Supposed you have three different tabs, and each tab has its own unique templates or designs? What you can do is to supply every property as @Input and you might even supply references to templates! That is a very bad idea, and will surely not scale.

So how we gonna solve it then?

The Plan…

So the plan is to use Content Projection like so:

 <app-tabs>
   <app-tab-link active="true">Activity</app-tab-link>
   <app-tab-link>General Info</app-tab-link>
   <app-tab-link>Security</app-tab-link>
   <app-tab-pane active="true">
      <h1>Activity</h1>
   </app-tab-pane>
   <app-tab-pane>
      <h1>General Info</h1>
   </app-tab-pane>
   <app-tab-pane>
      <h1>Security</h1>
   </app-tab-pane>
</app-tabs>

Isn’t it more syntactically beautiful?

Notice how you can apply different design / template for each <app-tab-pane>. Another benefit is that you do not need to have id, data-toggle and href attributes all over the place.

How does Content Projection Works?

If you are just starting with Angular you might want to read the article I wrote about it – here.

Creating The TabsComponent

Here’s how our TabsComponent would look like:

@Component({
  selector: 'app-tabs',
  template: `
    <div class="card">
      <div class="card-header p-2">
        <ul class="nav nav-pills">
          <ng-content select="app-tab-link"></ng-content>
        </ul>
      </div>
      <div class="card-body">
        <div class="tab-content">
          <ng-content select="app-tab-pane"></ng-content>
        </div>
      </div>
    </div>
  
  `,
})
export class TabsComponent {
  /...
}

Notice the two ng-content tags. What does it do is that it provides a slot into our component to allow us to “insert” a component that has a selector equals to the value of the select attribute.

<app-tabs>
  <app-tab-link></app-tab-link>
  <app-tab-pane></app-tab-pane>
</app-tabs>

<app-tab-link> and <app-tab-pane> will be inserted accordingly based on where their selectors are placed inside the TabsComponent’s <ng-content>.

We will apply the same concept to insert contents inside <app-tab-link> and <app-tab-pane> so we can provide unique templates / contents for each component.

Creating TabLinkComponent and TabPaneComponent

We would be implementing the same strategy as like how we create TabsComponent like so:

TabLinkComponent

@Component({
  selector: 'app-tab-link',
  template: `
    <li class="nav-item">
      <a href="" class="nav-link" [class.active]="active">
        <ng-content></ng-content>
      </a>
    </li>
  `,
  styleUrls: ['./tabs.component.css']
})
export class TabLinkComponent {
  @Input() active: boolean = false;
}

TabPaneComponent

@Component({
  selector: 'app-tab-pane',
  template: `
    <div class="tab-pane" [class.active]="active" [attr.hidden]="hidden">
      <ng-content></ng-content>
    </div>
  `,
  styleUrls: ['./tabs.component.css']
})
export class TabPaneComponent {
  @Input() active: boolean = false;

  get hidden(): boolean | null {
    return this.active ? null : this.active;
  }
}

Notice that on our TabLinkComponent we have an active property that will be toggled whenever the link is clicked. This is the same as the TabPaneComponent. The only difference is that we will apply a hidden attribute to it’s <div> element so that we have easy control over how the TabPaneComponents are being shown.

Querying Projected Contents and Controlling Their Visibility

The only problem left now is that how we will control the active state of TabLinkComponents and the visibility state of TabPaneComponent. There should be only 1 active TabLinkComponent and 1 visible TabPaneComponent at a time.

@ContentChildren(forwardRef(() => TabLinkComponent), {descendants: true}) links: QueryList<TabLinkComponent>;
@ContentChildren(forwardRef(() => TabPaneComponent), {descendants: true}) panes: QueryList<TabPaneComponent>;

Using the code above, we can now loop all over our Projected Contents from TabsComponent. Then we need to listen for events that the Projected Events might be needing. Like so:

  ngAfterViewInit(): void {
    this.links.forEach((link, index) => {
      link.clicked.pipe(
        takeUntil(this.destroy$)
      ).subscribe(() => {
        this.updateUi(index);
      });
    });
  }

Notice that we do this inside ngAfterViewInit lifecycle callback. The reason is that projected content might not be available inside ngOnInit. Notice also that each link has a clicked that we will be subscribing whenever the link emitted an event. Then whenever the clicked observable emits a value we update the UI like so:

  updateUi(selectedIndex): void {
    const callback = (element, index, selectedIndex) => {
      if (index === selectedIndex) {
        element.active = true;
      } else {
        element.active = false;
      }
    };

    this.links.forEach((link, index) => callback(link, index, selectedIndex));
    this.panes.forEach((pane, index) => callback(pane, index, selectedIndex));
  }

As you can see, we are assured that there will only be 1 active TabLinkComponent and 1 visible TabPaneComponent at a time.

This is how our components look after we have completed the implementation:

TabsComponent

@Component({
  selector: 'app-tabs',
  template: `
    <div class="card">
      <div class="card-header p-2">
        <ul class="nav nav-pills">
          <ng-content select="app-tab-link"></ng-content>
        </ul>
      </div>
      <div class="card-body">
        <div class="tab-content">
          <ng-content select="app-tab-pane"></ng-content>
        </div>
      </div>
    </div>
  
  `,
  styleUrls: ['./tabs.component.css']
})
export class TabsComponent implements OnInit, AfterViewInit, OnDestroy {
  @ContentChildren(forwardRef(() => TabLinkComponent), {descendants: true}) links: QueryList<TabLinkComponent>;
  @ContentChildren(forwardRef(() => TabPaneComponent), {descendants: true}) panes: QueryList<TabPaneComponent>;
  protected destroy$ = new Subject<void>();
  
  constructor() { }

  ngOnInit(): void {
    
  }

  ngAfterViewInit(): void {
    this.links.forEach((link, index) => {
      link.clicked.pipe(
        takeUntil(this.destroy$)
      ).subscribe(() => {
        this.updateUi(index);
      });
    });
  }

  ngOnDestroy(): void {
    this.destroy$.next();
    this.destroy$.complete();
  }

  updateUi(selectedIndex): void {
    const callback = (element, index, selectedIndex) => {
      if (index === selectedIndex) {
        element.active = true;
      } else {
        element.active = false;
      }
    };

    this.links.forEach((link, index) => callback(link, index, selectedIndex));
    this.panes.forEach((pane, index) => callback(pane, index, selectedIndex));
  }
}

TabLinkComponent

@Component({
  selector: 'app-tab-link',
  template: `
    <li class="nav-item">
      <a (click)="linkedClicked(); $event.preventDefault()" href="" class="nav-link" [class.active]="active">
        <ng-content></ng-content>
      </a>
    </li>
  `,
  styleUrls: ['./tabs.component.css']
})
export class TabLinkComponent {
  @Input() active: boolean = false;
  @Output() clicked: EventEmitter<any>  = new EventEmitter<any>();

  linkedClicked(): void {
    this.clicked.emit();
  }
}

TabPaneComponent

@Component({
  selector: 'app-tab-pane',
  template: `
    <div class="tab-pane" [class.active]="active" [attr.hidden]="hidden">
      <ng-content></ng-content>
    </div>
  `,
  styleUrls: ['./tabs.component.css']
})
export class TabPaneComponent {
  @Input() active: boolean = false;

  get hidden(): boolean | null {
    return this.active ? null : this.active;
  }
}

Conclusion

Hopefully, you learned something of value reading my article. Now, you can use Content Projection to facilitate code reuse inside your angular applications. But be wary not to abuse it and apply it whenever you want. You might not be needing it if you have a simple component where passing props would suffice.

Thanks! ☺️