exportAs in Angular: What It Does and When to Use It

May 22, 2025 | 12 Minute Read

Ever built a directive exposing public methods or signals… only to realize you can’t cleanly access them when using it in another component template? In this tutorial, we’ll fix that using exportAs, signals, and a modern declarative API, just like Angular Material sometimes does.

Let’s See What’s Broken

This is our app. It looks good at first glance, but the wizard is purely decorative:

The wizard is purely decorative, it doesn't function correctly at all

All three steps are visible: “Personal Info”, “Work Details”, and “Review & Submit”.

The navigation buttons don’t work.

It’s not actually doing anything at all.

Looking into the code, we can easily see why.

All we’ve got is a stepTracker directive:

<div stepTracker [stepLabels]="steps()" class="stepper">
    ...
</div>

And the static three steps inside of it:

<section>
  <h3>Personal Info</h3>
  <p>Fill out your name, email, and contact information.</p>
</section>
<section>
  <h3>Work Details</h3>
  <p>Enter your job title, company, and experience.</p>
</section>
<section>
  <h3>Review & Submit</h3>
  <p>Please review your information before submitting.</p>
</section>

The directive is set up to track state with a “current” signal and exposes some public methods, but there’s no way to call them from this template:

export class StepTrackerDirective {
  readonly stepLabels = input<string[]>();
  private readonly current = signal(0);

  next() {
    this.current.update(i => i + 1);
  }
  
  prev() {
    this.current.update(i => i - 1);
  }

  reset() {
    this.current.set(0);
  }
}

Trying the Obvious Fix (and Why It Fails)

We can try adding a template reference variable (#wizard) and calling wizard.next() from a button click:

<div stepTracker ... #wizard>
    ...
</div>
...
<button (click)="wizard.next()">Next</button>

But Angular throws an error, because (#wizard) is referencing the native DOM element, not our directive:

Screenshot of the error: Property 'next' does not exist on type 'HTMLDivElement'

The Imperative Approach: viewChild

To get around this, we can use the viewChild() function in the root component class:

import { ..., viewChild } from '@angular/core';

export class App {
    protected wizard = viewChild.required(StepTrackerDirective);
}

Then, in the template, we can bind to this “wizard” signal and call its methods:

<button (click)="wizard().next()">Next</button>

This works… but it’s imperative.

We’re not accessing the directive in code, we’re only trying to use its behavior in the template.

Feels a little clunky, right?

Fixing It the Angular Way: exportAs

Now, if this were a component, the first example with the template reference variable would have worked just fine.

This directive is a different story though.

Template reference variables default to the component instance, but not for directives.

That’s where exportAs comes in:

@Directive({
  selector: '[stepTracker]',
  exportAs: 'stepper'
})

This will create an alias that can then be accessed with a template reference variable.

In the root component template, we can add this reference:

<div stepTracker ... #wizard="stepper">
    ...
</div>

And then call the directive’s methods directly from the template, without using viewChild:

<button (click)="wizard.next()">Next</button>

This is clean, declarative, and works great with signals.

Only Show the Step That Matters

Next, we need make sure only the current step is shown.

We need to expose the current step index from the directive:

export class StepTrackerDirective {
  ...
  readonly stepIndex = this.current;
}

Then we can add logic to the template to conditionally show each step based on the “stepIndex” signal from the directive:

<section *ngIf="wizard.stepIndex() === 0">
  ...
</section>
<section *ngIf="wizard.stepIndex() === 1">
  ...
</section>
<section *ngIf="wizard.stepIndex() === 2">
  ...
</section>

Now, navigation works and the UI reflects the current state:

The wizard is now functional, with the correct step being shown

When we click “Next” and “Back”, we’re actually stepping through this now!

So, our wizard is stepping, but it’s still walking into walls 😂.

If I’m on the last step and I click “next” again, there’s no step for me to navigate to which results in an error:

Screenshot of the wizard with error when navigating past the last step

Let’s fix this.

Don’t Let It Break: Add Navigation Guards

In the directive, we need to add a few signals to detect when we’re on the first or last step:

export class StepTrackerDirective {
  ...
  readonly totalSteps = computed(() => this.stepLabels()?.length ?? 0);
  readonly isFirstStep = computed(() => this.stepIndex() === 0);
  readonly isLastStep = computed(() => this.stepIndex() === this.totalSteps() - 1);
}

Then we prevent navigation if we’re already at the boundary:

export class StepTrackerDirective {
  ...
  next() {
    if (!this.isLastStep()) {
      this.current.update(i => i + 1);
    }
  }
  
  prev() {
    if (!this.isFirstStep()) {
      this.current.update(i => i - 1);
    }
  }
}

In the template, we then want to disable the “Back” button when we’re on the first step:

<button 
  (click)="wizard.prev()" 
  [disabled]="wizard.isFirstStep()">
  Back
</button>

Then, we want to add logic to show the the “Next” button and hide the “Submit” button unless we’re on the last step:

@if (!wizard.isLastStep()) {
  <button (click)="wizard.next()">Next</button>
} @else {
  <button (click)="submit()">Submit</button>
}

Now the wizard can’t go out of bounds, and it’s much more polished:

Now the wizard can't go out of bounds, and it's much more polished

The “back” is properly disabled on the first step and the last step the “next” button is hidden and the “submit” button is shown.

Make the Wizard Feel Smarter

Now we want to enhance the stepper header to include “Step X of Y” and the current step’s label.

This is done with one more computed signal in the directive that returns the current step label based on the index:

export class StepTrackerDirective {
  ...
  readonly currentLabel = computed(() =>
    this.stepLabels()?.[this.stepIndex()] ?? ''
  );
}

Then, we can update the template to display the step count and current step label:

<p class="step-count">
  Step {{ wizard.stepIndex() + 1 }} of {{ wizard.totalSteps() }}
</p>
<h2 class="step-label">
  {{ wizard.currentLabel() }}
</h2>

Now, the UI is both reactive and user-friendly all made possible with exportAs and a template reference variable:

The wizard is now fully functional, with the correct step being shown and the step count and label being displayed

How Angular Material Does This

If you’ve ever used Angular Material, this concept might feel familiar.

They use exportAs in components like the Stepper, which lets you reference the stepper component directly in your templates exactly like we just did:

<mat-horizontal-stepper #wizard="matStepper">
  ...
</mat-horizontal-stepper>
<footer>
  <p>You're currently on step {{ wizard.selectedIndex + 1 }}</p>
  <button mat-button (click)="wizard.reset()">Reset</button>
</footer>

Same pattern, same benefits.

Final Thoughts + When You Don’t Need exportAs

So that’s exportAs, the little-known key to building clean, declarative APIs for your directives and components in Angular.

But, you don’t need it if:

  • You’re only accessing the directive/component from TypeScript
  • You’re not exposing behavior to the template
  • The directive/component is static or visual only

But if you’re building reusable, template-driven logic, it’s probably a must.

If you found this helpful, don’t forget to subscribe, check out my other Angular tutorials for more tips and tricks, and maybe buy some Angular swag from my shop!

Additional Resources

Want to See It in Action?

Check out the demo code showcasing these techniques in the StackBlitz project below. If you have any questions or thoughts, don’t hesitate to leave a comment.