DEV Community

sabrymuhamad
sabrymuhamad

Posted on • Edited on

Customizable and re-usable embedded views using NgTemplateOutlet in Angular (Declarative way)

There are different ways to customize the view of angular component, one of the most famous ways is using content projection "ng-content"
but actually I find "ng-content" won't be very flexible for the upcoming example and I really doubt if it would work at all.

Before we jump into a real life example I want to emphasis that we could use the "imperative" way to solve this example as well but I'm afraid that it would be more complicated
than using NgTemplateOutlet directive "declarative"...

and now to our first example:

  • Using Angular material stepper I need to make a customizable stepper header in some cases so I want to conditionally show and hide the default header and this an easy part using some CSS so I created a class called hide-stepper-header
        .hide-stepper-header {
            .mat-horizontal-stepper-header-container {
                display: none;
            }
        }
Enter fullscreen mode Exit fullscreen mode
<div class="tw-mt-10 tw-w-1/2 tw-mx-auto" [ngClass]="{'hide-stepper-header': stepperHeader()}">
            <div class="tw-flex tw-mb-4" *ngIf="stepperHeader()">
                <ng-container [ngTemplateOutlet]="stepperHeader()!"></ng-container>
            </div>

            <mat-stepper #stepper [(selectedIndex)]="selectedIndex">
                <mat-step>
                    <ng-template matStepLabel>Fill out your name</ng-template>
                    <p>Step 1 body</p>
                    <div>
                        <button mat-button matStepperNext>Next</button>
                    </div>
                </mat-step>

                <mat-step>
                    <ng-template matStepLabel>Done</ng-template>
                    <p>You are now done.</p>
                    <div>
                        <button mat-button matStepperPrevious>Back</button>
                        <button mat-button (click)="stepper.reset()">Reset</button>
                    </div>
                </mat-step>
            </mat-stepper>
        </div>
Enter fullscreen mode Exit fullscreen mode
        stepperHeader = input<TemplateRef<any>>();
        selectedIndex = model(0);
Enter fullscreen mode Exit fullscreen mode

this class would be triggered dynamically depends on if there is a custom header provided to the stepper or not.

  • in the above code snippet I created a stepper component which takes a model signal that works like an Input and Output to listen to the active step changes.

  • and this is the parent component re-using the stepper component in customizable view

    export class EmbeddedViewsComponent {
        steps = [
            {stepTitle:'Step title 1', isActive:false},
            {stepTitle:'Step title 2', isActive:true}
        ];
        activeIndex = 0;
    }   
Enter fullscreen mode Exit fullscreen mode
<app-stepper [stepperHeader]="stepperHeaderRef" [(selectedIndex)]="activeIndex"></app-stepper>

    <ng-template #stepperHeaderRef>
        <ng-container *ngFor="let step of steps;let i = index;let last = last">
            <div class="step"
                [ngClass]="{'active':activeIndex === i, 'completed':activeIndex > i, 'pending':activeIndex < i}">
                <div class="bullet">
                    <mat-icon *ngIf="activeIndex > i">check</mat-icon>
                    <div [ngClass]="{'active':activeIndex === i}"></div>
                </div>
                <div class="number"><span class="tw-uppercase">{{step.stepTitle}}</span></div>
                <div class="tw-w-max" [ngClass]="{'active':activeIndex === i}">
                    {{activeIndex === i ? 'In Progress' :
                    activeIndex < i ? 'Pending' : 'Completed' }} </div>
                </div>
                <div *ngIf="!last" class="step-link"></div>
        </ng-container>
    </ng-template>
Enter fullscreen mode Exit fullscreen mode
.step-link {
        @apply tw-h-[1px] tw-bg-neutral-300 tw-w-full tw-mt-4;
    }

    .step {
        .bullet {
            @apply tw-mb-4 tw-flex tw-items-center tw-justify-center tw-bg-green-100 tw-w-[32px] tw-h-[32px] tw-rounded-full;

            mat-icon {
                @apply tw-text-white;
            }

            .active {
                @apply tw-bg-green-400 tw-w-[12px] tw-h-[12px] tw-rounded-full;
            }
        }

        .number {
            @apply tw-text-neutral-600 tw-tracking-widest tw-w-32;
        }

        &>.active {
            @apply tw-text-green-500;
        }

        &.pending {
            @apply tw-opacity-60;
        }

        &.completed {
            .bullet {
                @apply tw-bg-green-600;
            }
        }
    }
Enter fullscreen mode Exit fullscreen mode
  • here in the parent component I provide number of steps for the header to show and actually I could also provide the steps content to show in our re-usable component

You could also check the src code in my github repo.

Billboard image

The Next Generation Developer Platform

Coherence is the first Platform-as-a-Service you can control. Unlike "black-box" platforms that are opinionated about the infra you can deploy, Coherence is powered by CNC, the open-source IaC framework, which offers limitless customization.

Learn more

Top comments (0)

Heroku

This site is built on Heroku

Join the ranks of developers at Salesforce, Airbase, DEV, and more who deploy their mission critical applications on Heroku. Sign up today and launch your first app!

Get Started

👋 Kindness is contagious

Discover a treasure trove of wisdom within this insightful piece, highly respected in the nurturing DEV Community enviroment. Developers, whether novice or expert, are encouraged to participate and add to our shared knowledge basin.

A simple "thank you" can illuminate someone's day. Express your appreciation in the comments section!

On DEV, sharing ideas smoothens our journey and strengthens our community ties. Learn something useful? Offering a quick thanks to the author is deeply appreciated.

Okay