5

I'd like to create a structural directive which behaves as follows:

<p *myDirective="condition">This is some text</p>
  • If condition is false then the <p> tag is not rendered at all.
  • If condition is true then the <p> tag is rendered with an extra class attribute.

So, either there's nothing rendered, or:

<p class="my-added-class">This is some text</p>

In other words, it's a bit like *ngIf, but with additional behaviour.

I can find examples of how to do the include/exclude behaviour (in fact there is such an example in the Angular docs). I can also find examples of how to add a class to an element using the Renderer2 API.

However, I don't see how I can combine these techniques, because the first method manipulates the viewContainer to create an embedded view, whereas the second method uses the renderer to manipulate an element.

Is there a way to do this? Can I somehow create the embedded view and then manipulate the elements it creates? Or can I manipulate the template to change how the view is rendered?

[NOTE: @HostBinding does not work with structural directives, so that's not an option]

Gary McGill
  • 26,400
  • 25
  • 118
  • 202
  • 1
    Why don't you want to just use `*ngIf` along with `ngClass` and do the needful based on a condition? Something like `

    This is some text

    `
    – SiddAjmera Nov 15 '18 at 18:14
  • When you create the embedded view, you can grab the rendered elements with the rootNodes property: `const viewRef = this.viewContainer.createEmbeddedView(this.templateRef); viewRef.rootNodes[0].classList.add('my-added-class');` Demo: https://stackblitz.com/edit/angular-1mdlht – Alex K Nov 15 '18 at 18:15
  • @SiddAjmera: Errrm… because I want to wrap those two things together in a directive? So that every time I use it, I just need to type a single directive, and it does everything for me. I mean, I *could* just type everything out, but… I *could* just use HTML :-) – Gary McGill Nov 15 '18 at 22:15
  • @AlexK: that looks perfect - thanks! – Gary McGill Nov 15 '18 at 22:19

2 Answers2

3

I'd think of adding class on the DOM when it satisfied the expression passed to it (inside setter). You can grab the ElementRef dependency inside directive and append a class to it which its truthy.

@Input() set myDirective(condition: boolean) {
  if (condition) {
    this.viewContainer.createEmbeddedView(this.templateRef);
    this.elementRef.nativeElement.nextElementSibling.classList.add('my-added-class'); // renderer API can be used here
    // as Alex and Yurzui suggested
    // const view = this.viewContainer.createEmbeddedView(this.templateRef); 
    // view.rootNodes[0].classList.add('some-class')
  } else if (condition) {
    this.viewContainer.clear();
  }
}
Pankaj Parkar
  • 134,766
  • 23
  • 234
  • 299
  • Tried that. But it gives an error saying `ERROR TypeError: Cannot read property 'add' of undefined` – SiddAjmera Nov 15 '18 at 18:22
  • Don't forget that for structural directive elementRef refers to the comment node – yurzui Nov 15 '18 at 18:23
  • Another way is `const view = this.viewContainer.createEmbeddedView(this.templateRef); view.rootNodes[0].classList.add('some-class')` but we should know exactly that template contains element on top level – yurzui Nov 15 '18 at 18:25
3

An other way

just to play around :)

Using Renderer2 is universal safe

import {
  Directive,
  Renderer2,
  TemplateRef,
  ViewContainerRef,
  ElementRef,
  Input, OnInit } from '@angular/core';

@Directive({
  selector: '[appMy]'
})
export class MyDirective implements OnInit{
  constructor(
  private templateRef: TemplateRef<any>,
  private viewContainer: ViewContainerRef,
  private renderer: Renderer2) { }
  @Input() set appMy(condition: boolean) {
   if (condition) {
     this.viewContainer.createEmbeddedView(this.templateRef);
    } else  {
     this.viewContainer.clear();
   }
  }
  ngOnInit() {
    const elementRef = this.viewContainer.get(0).rootNodes[0] as ElementRef;
    this.renderer.addClass(elementRef, 'myclass');
  }
}

Following the @Pankaj way but with renderer

@Input() set appMy(condition: boolean) {
   if (condition) {
     const view = this.viewContainer.createEmbeddedView(this.templateRef);
     this.renderer.addClass(view.rootNodes[0], 'myclass');
   } else  {
     this.viewContainer.clear();
   }
  }
Whisher
  • 31,320
  • 32
  • 120
  • 201
  • 1
    Thanks. I went with your second suggestion, because it puts everything in one place, and because it doesn't involve injecting the ElementRef etc. One thing to note is that it works for `

    text

    ` but **not** for `text` because in the latter case there's no element to add a class to. (I can live with that).
    – Gary McGill Nov 16 '18 at 09:16