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

sabrymuhamad

sabrymuhamad

Posted on July 12, 2024

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.

💖 💪 🙅 🚩
sabrymuhamad
sabrymuhamad

Posted on July 12, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related