158

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.

Willi Mentzel
  • 27,862
  • 20
  • 113
  • 121
user2899728
  • 2,099
  • 4
  • 16
  • 28
  • Yes md-raised-button it is attribute directive (https://material.angular.io/components/component/button) – user2899728 Jun 17 '17 at 06:23
  • 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. – Reactgular Sep 11 '17 at 10:49

17 Answers17

107

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)

cambunctious
  • 8,391
  • 5
  • 34
  • 53
Aldracor
  • 2,133
  • 1
  • 19
  • 27
  • @Luke Yes, if you mean by **"(now)"** as: since 2017-01-06 which was 5 months before the question was asked. _see [angular changelog](https://github.com/angular/angular/blob/master/CHANGELOG.md#242-2017-01-06) 6th item_ – Aldracor Apr 16 '18 at 06:45
  • @Aldracor 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. – Arsenii Fomin May 09 '18 at 14:15
  • @ArseniiFomin You should ask a new question for that. In regards with how it works; I will edit my answer too explain. – Aldracor May 09 '18 at 14:41
  • 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 so – Ben Taliadoros Sep 06 '18 at 12:24
  • 1
    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" – Ben Taliadoros Sep 06 '18 at 12:40
  • 7
    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. – JeffryHouser Sep 28 '18 at 22:24
  • seems doesn't work on ng7, and unable to open the plunker, can you please create a stackblitz for it – Reza Nov 24 '18 at 03:55
  • @RezaRahmati It is still working. See [here](https://stackblitz.com/edit/angular-c3cjr1) as requested. – Aldracor Jan 02 '19 at 07:56
  • @Aldracor Thanks, btw question is about angular directives not simple attributes. please see this https://stackblitz.com/edit/angular-conditional-directive-2004 – Reza Jan 02 '19 at 17:11
  • @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](https://stackoverflow.com/questions/41298168/how-to-dynamically-add-a-directive) it might solve your problem – Aldracor Jan 03 '19 at 11:05
  • @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: ``. Hopefully this is helpful to someone coming afterwards. – kbpontius Mar 29 '19 at 20:49
  • 15
    @kbpontius thanks for comment, it works on Html attributes but not on angular directives (as attribute) – Reza Apr 01 '19 at 12:20
  • @RezaRahmati Ahh, gotcha, just misunderstood your comment. Thanks for the clarification. – kbpontius Apr 01 '19 at 18:52
  • Custom directive won't work for this approch. I have custome directive `'appOnlynumber'` `ary = [{numeric: true},{numeric:false}]
    ` In this scenario custom directive won't work
    – Rijo Oct 25 '19 at 00:45
  • 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. – Jesse Nov 20 '19 at 14:44
  • I had to use `undefined` instead of `null`. – electrocrat Sep 01 '20 at 01:57
79

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.

Ronin
  • 7,322
  • 6
  • 36
  • 54
LLL
  • 3,566
  • 2
  • 25
  • 44
  • 17
    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. – user2899728 Jun 19 '17 at 13:39
  • 1
    I see. You could wrap the code that is duplicated in component. – LLL Jun 19 '17 at 13:52
  • 3
    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. – user2899728 Jun 19 '17 at 14:10
  • 58
    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. – Shachar Har-Shuv Feb 11 '19 at 14:48
  • 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"` – Oleg K Sep 01 '20 at 14:38
  • 1
    Unfortunately, angular has not exposed any elegant way, so we left with this way only. – oomer Oct 29 '21 at 01:13
41

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.

H.B.
  • 166,899
  • 29
  • 327
  • 400
  • 13
    This is the only answer which let's a developer to use conditional directives as well as code re-usability. – Ravinder Payal Dec 28 '17 at 09:40
  • This solution is really elegant, thank you! – Timtim Jan 11 '22 at 20:17
  • The nested ng-container confuses me (under Node button), only one is necessary(read sufficient) for this solution right? – DFSFOT Feb 10 '22 at 13:07
  • 1
    @DFSFOT 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 `
    `, depending on what document structure is desired.)
    – H.B. Feb 11 '22 at 03:22
  • 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. – B. Feenstra Mar 01 '22 at 16:31
30

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
}
Tiha
  • 598
  • 6
  • 10
  • 7
    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. – Ran Lottem Nov 21 '18 at 13:48
  • 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. – Tiha Nov 22 '18 at 14:08
  • 1
    This is not a good solution. Directive still get's initialized. – Dino Oct 09 '19 at 08:56
  • 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. – Sixteen Dec 22 '21 at 10:04
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

Ankit Singh
  • 24,525
  • 11
  • 66
  • 89
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> 
Sajeetharan
  • 216,225
  • 63
  • 350
  • 396
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.

MhagnumDw
  • 893
  • 1
  • 10
  • 12
  • You'll still have code for two buttons in which case you might as well do `*ngIf="condition"` on it with double code – DFSFOT Feb 10 '22 at 12:50
  • 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 case – Adeel Shekhani Feb 12 '22 at 20:23
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>

zalog
  • 683
  • 9
  • 21
2

Passing null to the directive removes it!

<button md-raised-button="condition ? true : null"></button>
Yogesh Aggarwal
  • 1,071
  • 2
  • 12
  • 30
1

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>

Sreekumar P
  • 5,900
  • 11
  • 57
  • 82
Cappi10
  • 57
  • 3
  • You can use just a ```@HostBinding('attr.dynamic-attr') @Input('dynamic-attr') attr: string;``` instead of ngOnInit + ElementRef. – pinguinjkeke Dec 04 '18 at 12:44
  • 4
    This is not an answer to the question which was how to add a *directive* dynamically, not an attribute. – Chris Haines Jan 24 '19 at 12:38
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');
  }
}
Itay
  • 398
  • 2
  • 14
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>
Ali
  • 1,633
  • 7
  • 35
  • 58
Jaison
  • 1
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];
  }
}
-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.

LLL
  • 3,566
  • 2
  • 25
  • 44
-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');
    }
  }

}
  • 2
    As a side note, the Renderer is deprecated, instead use Renderer2 : https://alligator.io/angular/using-renderer2/ – tam.teixeira Nov 30 '17 at 00:13
-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)

rmcsharry
  • 5,363
  • 6
  • 65
  • 108
  • 3
    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. – Mojtaba Apr 05 '19 at 09:44
-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>
Moshe
  • 2,583
  • 4
  • 27
  • 70
  • 7
    This is not what I asked. You are showing example for class. I am talking about attribute-directive. – user2899728 Jun 17 '17 at 06:24
  • @user2899728 Unfortunately there's no other way to do it. (You can't set `md-raised-button` attribute as false) – Edric Jun 18 '17 at 03:33
  • @user2899728 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"). – Moshe Jun 18 '17 at 17:11
  • 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. – bvdb Aug 14 '19 at 06:58
  • 1
    @bvdb 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. – Moshe Aug 21 '19 at 03:00