Access Parent Component from Angular Directive

October 13th 2017 Angular Ionic 2

Angular directives are a great way to extend behavior of Angular components and HTML elements. As long as you only need a reference to the host HTML element to implement your functionality, Angular dependency injection will easily inject it as an ElementRef, along with a Renderer to modify it:

import { Slides } from 'ionic-angular';
import { Directive, ElementRef, Renderer } from '@angular/core';

@Directive({
  selector: '[my-slides]'
})
export class MySlidesDirective {
  constructor(private element: ElementRef, private renderer: Renderer) { }
}

If you also require a reference to the host component, it gets a little more complicated.

It might seem a bit unusual to require host component access from the directive and that's why I originally intended to create a custom component not a directive to wrap my Slides component customizations for simpler reuse:

<my-slides>
  <ion-slide>1</ion-slide>
  <ion-slide>2</ion-slide>
  <ion-slide>3</ion-slide>
  <ion-slide>4</ion-slide>
  <ion-slide>5</ion-slide>
</my-slides>

The component template would place the Slide components into the wrapped Slides component:

<ion-slides>
  <ng-content></ng-content>
</ion-slides>

This would make it easy to access the nested Slides component from my custom component:

import { Slides } from 'ionic-angular';
import { Component, ViewChild, OnInit } from '@angular/core';

@Component({
  selector: 'my-slides',
  templateUrl: 'my-slides.html'
})
export class MySlidesCoponent implements OnInit {

  @ViewChild(Slides) slides: Slides;

  ngOnInit(): void {
    this.slides.ionSlideProgress.subscribe(progress => this.onProgress(progress));
  }
}

Against my expectations, this didn't work. It failed with a rather cryptic error message:

Error: Uncaught (in promise): Error: No provider for Slides!

Thanks to an existing GitHub issue I quickly realized that the custom component approach isn't going to work and that I'll need to create a directive instead.

Of course, this means that the Slides component will need to be accessible from within the directive. The obvious approach would be to pass it into the directive as an input:

import { Slides } from 'ionic-angular';
import { Directive, Input } from '@angular/core';

@Directive({
  selector: '[my-slides]'
})
export class MySlidesDirective {
  @Input() slides: Slides
}

For this to work, the Slides component would need to have a variable assigned, so that it could be used as the input value:

<ion-slides #slides my-slides [slides]="slides">
  <ion-slide>1</ion-slide>
  <ion-slide>2</ion-slide>
  <ion-slide>3</ion-slide>
  <ion-slide>4</ion-slide>
  <ion-slide>5</ion-slide>
</ion-slides>

This seemed awkward and error prone. I continued my search and finally stumbled across a more elegant solution in an Angular GitHub issue. Using the Host and Self decorators, the Slides component reference can be provided by dependency injection:

import { Slides } from 'ionic-angular';
import { Directive, OnInit, Host, Self } from '@angular/core';

@Directive({
  selector: '[home-slides]'
})
export class HomeSlidesDirective implements OnInit {

  constructor(@Host() @Self() private slidesComponent: Slides) { }

  ngOnInit(): void {
    this.slides.ionSlideProgress.subscribe(progress => this.onProgress(progress));
  }
}

With this change no additional binding is required in HTML for each use of the directive:

<ion-slides my-slides>
  <ion-slide>1</ion-slide>
  <ion-slide>2</ion-slide>
  <ion-slide>3</ion-slide>
  <ion-slide>4</ion-slide>
  <ion-slide>5</ion-slide>
</ion-slides>

This syntax is almost as simple as with the custom component. I could work with that.

Copyright
Creative Commons License