Apply a directive conditionally
Asked Answered
I

17

167

I am using Material 2 to add md-raised-button. I want to apply this directive only if certain condition becomes true.

For example:

<button md-raised-button="true"></button>

Another example: I created a basic dynamic reactive form in plunker. I am using formArrayName directive of reactive form for array of controls. I want to apply formArrayName directive only if specific condition becomes true, otherwise don't add formArrayName directive.

Here is a plunker link.

Illegitimate answered 16/6, 2017 at 19:58 Comment(2)
Yes md-raised-button it is attribute directive (material.angular.io/components/component/button)Illegitimate
Applying conditions on which directives are used would likely render AoT useless as you wouldn't be able to compile the templates unless the app was running.Skantze
C
81

I don't know if you can apply directives based on a condition, but a workaround would be having 2 buttons and display them based on a condition.

<button *ngIf="!condition"></button>
<button *ngIf="condition" md-raised-button></button> 

Edit: maybe this will be helpful.

Cloudlet answered 19/6, 2017 at 13:18 Comment(6)
Thank you for your response. But I already have used this technique in the plunker example which I added in question detail. This is good option but not good idea if code is complex. Because because this technique ruin the concept of reusing. You can see my example in plunker and notice how my code is duplicated because of this approach. That is why I don't want to use this technique. I want some better solution so I can generate dynamic elements with.Illegitimate
I see. You could wrap the code that is duplicated in component.Cloudlet
It is good idea but unfortunately it does not work in the case of reactive forms. With reactive forms, another issue appears. If I put input into separate component, then angular would require me to add [formGroup]="form" within that child component as well. But if I do so then my array binding would not work.Illegitimate
That's a terribly solution since it's duplicates code. Imagine it's not just button but a div with a lot of html code inside it.Eventide
It is not necessary to include formGroup to a child component that wraps input as you can simply pass your formControl as an @Input() control: FormControl inside the wrapper component and apply it to the native input via [formControl]="control"Postman
Unfortunately, angular has not exposed any elegant way, so we left with this way only.Slavic
L
108

If you just need to add an attribute in order to trigger CSS rules, you can use the below method: (this does not dynamically create/destroy a directive)

<button [attr.md-raised-button]="condition ? '' : null"></button>

Applied the same to your plunker: fork

Update:

How condition ? '' : null works as the value:

When its the empty string ('') it becomes attr.md-raised-button="", when its null the attribute will not exist.

Update: plunker update: fork (version issues fixed, please note the question was originally based on angular 4)

Ligurian answered 24/10, 2017 at 14:0 Comment(16)
@Luke Yes, if you mean by "(now)" as: since 2017-01-06 which was 5 months before the question was asked. see angular changelog 6th itemLigurian
@Ligurian angular 5.2.1, doesn't work. And I don't event understand why it should look like "condition ? '' : null" where both results are basically false. What should be instead of '' ? 'true' doesn't work also.Biotic
@ArseniiFomin You should ask a new question for that. In regards with how it works; I will edit my answer too explain.Ligurian
ah im trying it on an ng-container, makes sense it wouldnt work in this scenario. i wonder if there's a solution for this case, will post if soErysipeloid
so for anyone who doesnt want to create a html element in order for their directive to work and is using ng-container, i just went with passing in an [enabled]="booleanVar"Erysipeloid
Not working for me in Angular 6. The plunker sample doesn't get past the loading screen. Based on my reading, this approach may work for HTML attributes, but will not work for Angular directives. The original question was about Angular Directives from the ng material library.Reg
seems doesn't work on ng7, and unable to open the plunker, can you please create a stackblitz for itTap
@RezaRahmati It is still working. See here as requested.Ligurian
@Ligurian Thanks, btw question is about angular directives not simple attributes. please see this stackblitz.com/edit/angular-conditional-directive-2004Tap
@RezaRahmati Yes you'r right. I've added an update so the plunker fork work again. (adds the attribute but the directive is never executed) Have a look here it might solve your problemLigurian
@RezaRahmati I can verify this solution works on "simple attributes". Maybe this isn't what you were referring to, but for example, on input's multiple attribute: <input type="file" accept="type/jpg" [attr.multiple]="condition ? true : null"/>. Hopefully this is helpful to someone coming afterwards.Mccallion
@Mccallion thanks for comment, it works on Html attributes but not on angular directives (as attribute)Tap
@RezaRahmati Ahh, gotcha, just misunderstood your comment. Thanks for the clarification.Mccallion
Custom directive won't work for this approch. I have custome directive 'appOnlynumber' ary = [{numeric: true},{numeric:false}] <div *ngFor="let a of ary"> <input appOnlynumber="(a.numeric) ? '': null" /></div> In this scenario custom directive won't workManiac
In my case, this almost works, but my directive was called appTooltipDirective and this method put the attribute apptooltipdirective (no casing) on my element so it didn't work.Catsup
I had to use undefined instead of null.Lifeanddeath
C
81

I don't know if you can apply directives based on a condition, but a workaround would be having 2 buttons and display them based on a condition.

<button *ngIf="!condition"></button>
<button *ngIf="condition" md-raised-button></button> 

Edit: maybe this will be helpful.

Cloudlet answered 19/6, 2017 at 13:18 Comment(6)
Thank you for your response. But I already have used this technique in the plunker example which I added in question detail. This is good option but not good idea if code is complex. Because because this technique ruin the concept of reusing. You can see my example in plunker and notice how my code is duplicated because of this approach. That is why I don't want to use this technique. I want some better solution so I can generate dynamic elements with.Illegitimate
I see. You could wrap the code that is duplicated in component.Cloudlet
It is good idea but unfortunately it does not work in the case of reactive forms. With reactive forms, another issue appears. If I put input into separate component, then angular would require me to add [formGroup]="form" within that child component as well. But if I do so then my array binding would not work.Illegitimate
That's a terribly solution since it's duplicates code. Imagine it's not just button but a div with a lot of html code inside it.Eventide
It is not necessary to include formGroup to a child component that wraps input as you can simply pass your formControl as an @Input() control: FormControl inside the wrapper component and apply it to the native input via [formControl]="control"Postman
Unfortunately, angular has not exposed any elegant way, so we left with this way only.Slavic
K
44

As already noted this does not appear to be possible. One thing that can be used to at least prevent some duplication is ng-template. This allows you to extract the content of the element affected by the ngIf branching.

If you for example want to create a hierarchical menu component using Angular Material:

<!-- Button contents -->
<ng-template #contentTemplate>
    <mat-icon *ngIf="item.icon != null">{{ item.icon }}</mat-icon>
    {{ item.label }}
</ng-template>

<!-- Leaf button -->
<button *ngIf="item.children == null" mat-menu-item
    (click)="executeCommand()"
    [disabled]="enabled == false">
    <ng-container *ngTemplateOutlet="contentTemplate"></ng-container>
</button>
<!-- Node button -->
<ng-container *ngIf="item.children != null">
    <button mat-menu-item
        [matMenuTriggerFor]="subMenu">
        <ng-container *ngTemplateOutlet="contentTemplate"></ng-container>
    </button>

    <mat-menu #subMenu="matMenu">
        <menu-item *ngFor="let child of item.children" [item]="child"></menu-item>
    </mat-menu>
</ng-container>

Here the conditionally applied directive is matMenuTriggerFor, which should only be applied to menu items with children. The contents of the button are inserted in both places via ngTemplateOutlet.

Kinard answered 9/12, 2017 at 4:38 Comment(6)
This is the only answer which let's a developer to use conditional directives as well as code re-usability.Stung
This solution is really elegant, thank you!Rafferty
The nested ng-container confuses me (under Node button), only one is necessary(read sufficient) for this solution right?Coeducation
@Coeducation One is for the if statement to check for children, the other is for rendering the content template. The key is the template rendering, the rest is for example purposes. (The outer ng-container could also be something else, like a <div>, depending on what document structure is desired.)Kinard
This is, however, not an option if your parent container depends on ContentChildren. For example, the mat-table component inner elements cannot be put into a template outlet because the required header, row and footer definitions cannot be found. Unfortunately, this is where I am stuck at the moment. and have to duplicate some HTML.Daphie
This solution really helps us to prevent duplication in content of button, but still doesn't help with duplication of button's own attributes: leaf and node can have very different set of attributes, you'll have to duplicate all the attributesPuke
S
31

This may come late, but it is a viable and elegant method for applying a directive conditionally.

In the directive class create the input variable:

@Input('myDirective') options: any;

When applying the directive, set the apply property of the input variable:

<div [myDirective] = {apply: someCondition}></div>

In the method of the directive check for the variable this.options.apply and apply the directive logic based on the condition:

ngAfterViewInit(): void {
    if (!this.options.apply) {
        return;
    }

    // directive logic
}
Sochi answered 29/1, 2018 at 8:38 Comment(4)
This doesn't work for me, as the directive still exists in the component, it just performs no logic. I would like to have the directive's existence apply conditionally, specifically because there's css that checks for this directive.Beckibeckie
You have a different issue, than the one listed here (Apply the directive conditionally). Post your issue and people will help you. As a first idea, you could put the directive on a div and put an ng-container with an *ngIf around it to apply your condition. ngIf removes the div node from the DOM, so it might work. I am just guessing, you need to post your problem with code samples/description for ppl to help you.Sochi
This is not a good solution. Directive still get's initialized.Corned
This solutions is only viable if you work with a custom Directive. If you use a Directive from a library where you can't access logic (or should not touch it), you can't implement this.Horseleech
X
12

As others have also stated, directives can't be dynamically applied.

However, if you just want to toggle md-button's style from flat to raised, then this

<button md-button [class.mat-raised-button]="isRaised">Toggle Raised Button</button>

would do the trick. Plunker

Xever answered 20/6, 2017 at 4:53 Comment(1)
note that ripple will not work with only mat-raised-button classTamartamara
G
6

Currently, there is NO way to conditionally apply a directive to a component.This is not supported.The components which you have created can be added or removed conditionally.

There is already an issue created for the same with angular2, so it should be the case with angular4 aswell.

Alternatively you can go for the option with ng-if

<button ngIf="!condition"></button>
<button ngIf="condition" md-raised-button></button> 
Garibay answered 19/6, 2017 at 17:29 Comment(1)
already covered in LLL's answerCelebrate
S
6

Maybe it will help someone.

In the example below I have the my-button.component.html and I want to apply the *appHasPermission directive to the <button> only if the role attribute is set.

<ng-container *ngIf="role; else buttonNoRole" >
  <ng-container *appHasPermission="role">
    <!-- button with *appHasPermission -->
    <ng-template *ngTemplateOutlet="buttonNoRole;"></ng-template>
  </ng-container>
</ng-container>

<ng-template #buttonNoRole>
  <!-- button without *appHasPermission -->
  <button
    mat-raised-button type="button"
    [color]="color"
    [disabled]="disabled"
    [(appClickProgress)]="onClick"
    [key]="progressKey">
    <mat-icon *ngIf="icon">{{ icon }}</mat-icon> {{ label }}
  </button>
</ng-template>

That way you don't duplicate the <button> code.

Screenplay answered 27/2, 2020 at 13:26 Comment(2)
You'll still have code for two buttons in which case you might as well do *ngIf="condition" on it with double codeCoeducation
Seems wrong, doesn't look like template is actually need with what you wrote. If template is there it should utilized at least twice in this casePredilection
C
3

This could be a solution too:

[md-raised-button]="condition ? 'true' : ''"


It's working for angular 4, ionic 3 like this:

[color]="condition ? 'primary' : ''" where condition is a function that decides if this is an active page or not. The whole code look like this:

<button *ngFor="let page of ..." [color]="isActivePage(page) ? 'primary' : ''">{{ page.title }}</button>

Curr answered 11/9, 2017 at 10:35 Comment(1)
this works for kendotreeview, don't know why everyone is saying conditional directives are not working, [kendoTreeViewCheckable]="true ? true : false"Marileemarilin
M
1

Passing null to the directive removes it!

<button md-raised-button="condition ? true : null"></button>
Melitamelitopol answered 18/7, 2020 at 17:36 Comment(1)
this only works for conditionally removing attributes of the html tag, not for angular directivesImpetigo
L
1

I am working with Angular Material, adding an element on *ngIf didn't work properly for me (the element would disappear inside many newly generated material HTML tags lol).

I don't know if it's a good practice, but I used OnChanges and I had a sort of conditional directive - and it worked! :)

So this is how I solved it:

import { Directive, Renderer2, ElementRef, Input, OnChanges, SimpleChanges, AfterViewInit } from '@angular/core';

@Directive({
  selector: '[appDirtyInputIndicator]'
})
export class DirtyInputIndicatorDirective implements OnChanges, AfterViewInit {

  @Input('appDirtyInputIndicator') dirtyInputIndicator: boolean;
  span = this.renderer.createElement('span');

  constructor(private renderer: Renderer2, private el: ElementRef) {}

  ngOnChanges(changes: SimpleChanges): void {
    if (changes.dirtyInputIndicator && this.dirtyInputIndicator) {
      this.renderer.appendChild(this.el.nativeElement, this.span);
    } else {
      this.renderer.removeChild(this.el.nativeElement, this.span);
    }
  }

  ngAfterViewInit() {
    this.renderer.addClass(this.span, 'dirty_input_badge');
  }
}
Lawful answered 20/10, 2020 at 10:49 Comment(0)
C
0

I couldn't find a nice existing solution, so i built my own directive which does this.

import { Directive, ElementRef, Input } from '@angular/core';

@Directive({
  selector: '[dynamic-attr]'
})
export class DynamicAttrDirective {
  @Input('dynamic-attr') attr: string;
  private _el: ElementRef;

  constructor(el: ElementRef) {
    this._el = el;
  }

  ngOnInit() {
    if (this.attr === '') return null;
    const node = document.createAttribute(this.attr);
    this._el.nativeElement.setAttributeNode(node);
  }
}

Then your html:

<div dynamic-attr="{{hasMargin ? 'margin-left' : ''}}"></div>

Cryptocrystalline answered 19/2, 2018 at 14:10 Comment(2)
You can use just a @HostBinding('attr.dynamic-attr') @Input('dynamic-attr') attr: string; instead of ngOnInit + ElementRef.Collings
This is not an answer to the question which was how to add a directive dynamically, not an attribute.Afreet
E
0

I encountered the same issue, was able to do it this way:

component.html

<tr *ngFor="let Qus of finalModel | paginate:{itemsPerPage: 10, currentPage:p}">
<td appHighlightUsa  [isUsa]="Qus.country=='United States' ? true :false">{{Qus.id | flags}}</td>
<td appHighlightUsa  [isUsa]="Qus.country=='United States' ? true :false">{{Qus.accociateId}}</td>
<td appHighlightUsa  [isUsa]="Qus.country=='United States' ? true :false" >{{Qus.country}}</td>
<td appHighlightUsa  [isUsa]="Qus.country=='United States' ? true :false" >{{Qus.country}}</td>
</tr>
Electrolysis answered 8/12, 2022 at 12:14 Comment(0)
E
0

I've found a workaround for conditionally applying not only one, but multiple directives on an element. You set whatever conditions you desire in the component, then use them like below in the template:

  <div>
    <input type="text"> <- This is the targeted element.
    <div *ngIf="someCondition"
         someDirective>
    </div>
    <div *ngIf="anotherCondition"
         anotherDirective>
    </div>
  </div>

And inside the directives, you use elementRef to select the parent node, which is the wrapping div, then select the first child, which will always be the element you want to apply the directive on.

@Directive({
  selector: '[someDirective]'
})
export class SomeDirective implements OnInit {
  constructor(private elementRef: ElementRef) {}

  ngOnInit(): void {
    const directiveElement = this.elementRef.nativeElement.parentNode.children[0];
  }
}
Elate answered 31/7, 2023 at 9:17 Comment(0)
C
-1

I got another idea about what you could do.

You could store the html you want replaced in a variable as a string and then add / remove the directive from it as you wish, using the bypassSecurityTrustHtml method of the DomSanitizer.

I doesn't result in a clean solution but at least you don't need to repeat the code.

Cloudlet answered 20/6, 2017 at 17:6 Comment(0)
A
-2

yes it is possible.

html page with appActiveAhover directive :)

  <li routerLinkActive="active" #link1="routerLinkActive">
        <a [appActiveAhover]='link1.isActive?false:true' routerLink="administration" [ngStyle]="{'background':link1.isActive?domaindata.get_color3():none}">
          <i class="fa fa-users fa-lg" aria-hidden="true"></i> Administration</a>
      </li>
      <li  routerLinkActive="active" #link2="routerLinkActive">
        <a [appActiveAhover]='link2.isActive?false:true' routerLink="verkaufsburo" [ngStyle]="{'background':link2.isActive?domaindata.get_color3():none,'color':link2.isActive?color2:none}">
          <i class="fa fa-truck fa-lg" aria-hidden="true"></i> Verkaufsbüro</a>
      </li>
      <li  routerLinkActive="active" #link3="routerLinkActive">
        <a [appActiveAhover]='link3.isActive?false:true' routerLink="preisrechner" [ngStyle]="{'background':link3.isActive?domaindata.get_color3():none}">
          <i class="fa fa-calculator fa-lg" aria-hidden="true" *ngIf="routerLinkActive"></i> Preisrechner</a>
      </li>

directive

@Directive({
  selector: '[appActiveAhover]'
})
export class ActiveAhoverDirective implements OnInit {
  @Input() appActiveAhover:boolean;
  constructor(public el: ElementRef, public renderer: Renderer, public domaindata: DomainnameDataService) {
}

  ngOnInit() {
  }

  @HostListener('mouseover') onMouseOver() {
    if(this.appActiveAhover){
      this.renderer.setElementStyle(this.el.nativeElement, 'color', this.domaindata.domaindata.color2);
    }
  }

  @HostListener('mouseout') onMouseOut() {
    if(this.appActiveAhover){
      this.renderer.setElementStyle(this.el.nativeElement, 'color', 'white');
    }
  }

}
Alfy answered 22/11, 2017 at 9:20 Comment(1)
As a side note, the Renderer is deprecated, instead use Renderer2 : alligator.io/angular/using-renderer2Overdue
P
-3

As at 18th Jan 2019, This is how I added a directive conditionally in Angular 5 and above. I needed to change the color of the <app-nav> component based on darkMode. If the page was in dark mode or not.

This worked for me:

<app-nav [color]="darkMode ? 'orange':'green'"></app-nav>

I hope this helps someone.

EDIT

This changes the value of an attribute (color) based on a condition. It just happens that the color is defined using a directive. So anyone reading this please do not get confused, this is not applying a directive conditionally (ie. which means adding or removing a directive to the dom based on a condition)

Palingenesis answered 18/1, 2019 at 15:3 Comment(1)
You apply the directive in both cases just with different options (i.e., orange or green). The question asks for ignoring the directive at all based on a condition.Passional
M
-4

Use NgClass

[ngClass]="{ 'mat-raised-button': trueCondition }"

example of true condition:

this.element === 'Today'

or a boolean function

getTruth()

full example:

  <button [ngClass]="{ 'mat-raised-button': trueCondition }">TEXT</button>

If you want a default class:

  <button [ngClass]="{ 'mat-raised-button': trueCondition, 'default-class': !trueCondition }">TEXT</button>
Maybellmaybelle answered 16/6, 2017 at 20:4 Comment(5)
This is not what I asked. You are showing example for class. I am talking about attribute-directive.Illegitimate
@Illegitimate Unfortunately there's no other way to do it. (You can't set md-raised-button attribute as false)Pollack
@Illegitimate There is no way to apply directives conditionally. I've attempted to give you what you were looking for (end result) with a different approach (the "means").Maybellmaybelle
When you offer a creative alternative, it usually helps to put in a line of comments first to describe where you are going with your answer.Jacobite
@Jacobite thank you for your kind words. I was writing without direction, and that was a mistake. In the future, I hope to edit this post, as it could help even one person.Maybellmaybelle

© 2022 - 2024 — McMap. All rights reserved.