Search code examples
angulartypescriptdirectiveangular2-directives

Angular 4 directive is displaying undefined for @input values


I'm new with angular directive so I had created a directive as follows:

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

@Directive({
  selector: "[bonusCard]"
})
export class BonusCard {
  @Input() bonus: string;
  @Input() amount: string;
  @Input() isSeleted: string;
  constructor(private el: ElementRef){
    el.nativeElement.innerHTML = this.getTemplate()
  }

  getTemplate(){
    return ''+
      '<div>'+
        '<div class="bonus">'+this.bonus+'</div>'+
        '<div class="amount">'+this.amount+'</div>'+
        '<div class="radio '+(this.isSeleted?'is_seleted':'')+'"></div>'+
      '</div>' +
      '';
  }
}

I'm using this directive in template as follows:

  <div class="column col-3" *ngFor="let b of bonusJson;let i = index" bonusCard [bonus]="b.bonus" [amount]="b.amount" [isSeleted]="i==activeBonus"></div>

where variable are defined as below:

bonusJson = [{
    bonus:1500,
    amount:2000
  },{
    bonus:1000,
    amount:1000
  },{
    bonus:500,
    amount:500
  },{
    bonus:0,
    amount:100
  }];
  activeBonus = 0;

But view is not rendering correctly it is showing undefind in view as below.

$enter image description here


Solution

  • TL;DR

    Don't use the value of @Input() properties inside the constructor. Use ngOninit() instead.


    Long answer

    You are executing your code in the constructor, but the constructor is executed before that the Inputs are bound.

    When you bootstrap your application, Angular will create a tree of components using the new keywoard for each of them and their constructor is thus executed. But in this phase change detection cycle isn't active yet, the tree hasn't still been attached to DOM and most of application data hasn't still been fetched and parsed.

    The constructor isn't the ideal place to put complex initialization logic, as you can read in this guide, written by a developer of the Angular team. All what you will need to do in the constructor is just basic initialization and dependency injection result.

    Things change in the ngOnInit() hook method. When Angular calls this method, instead, it has already attached each component to the DOM, injected all required dependencies and processed input bindings. You can safely use your Input() property with no fear.


    Specific answer

    Coming back to your question, you call getTemplate() it looks for this.bonus, this.amount and this.isSelected but the view of the parent component isn't still parsed and the input proprierites aren't still bound, so you receive undefined instead.

    To avoid this problem, move your code from the constructor to the ngOnInit hook.

    export class BonusCard implements OnInit {
      @Input() bonus: string;
      @Input() amount: string;
      @Input() isSeleted: string;
    
      constructor(private el: ElementRef) {}
    
      ngOnInit() {
        this.el.nativeElement.innerHTML = this.getTemplate()
      }
    

    Three final notes

    I want to provide three additional advises to improve your code:

    • Use a DomSanitizer to bind HTML inside a component, you can read more here.
    • Use a template string, you can use it substituting your quotes with backticks ( ` ), in this way you can write your template on several lines
    • you can use === instead of ==, if you are sure you are comparing two variables of the same type, like in this case.

    So:

    [isSeleted]="i===activeBonus"
    

    and

    return `<div>
              <div class="bonus"> ${this.bonus} </div>
              <div class="amount"> ${this.amount} </div>
              <div class="radio ${this.isSeleted?'is_seleted':''}"></div>
            </div>`;