Search code examples
angularangular-directive2-way-object-databinding

How to 2-way bind custom directive in ANGULAR 8?


SUMMARY

  • Given below are code snippets of a directive called appSearchbox and its usage in HTML.
  • The input is 2-way binded using ruleSearchText.
  • The requirement for this directive is to dynamically add a 'X' mark to its end when there is a text, and when clicked it should clear the text in the input box.

QUESTION / PROBLEM

  1. How to clear the ruleSearchText variable value from the directive, so that the change gets triggered back again in ngOnChanges
  2. The function clearInputField clears the value, but it doesnt trigger ngOnchanges and also any other events that listens for the value change in the underlying component.

CODE EXAMPLE

<input
  #search
  name="search"
  type="text"
  class="form-control underlined"
  placeholder="Search Rule Name"
  [(ngModel)]="ruleSearchText"
  [appSearchbox]="ruleSearchText"
/>
import {
  Directive,
  ElementRef,
  Input,
  OnChanges,
  OnDestroy,
  Renderer2
} from '@angular/core';

/**
 * Directive to add CANCEL button functionality for input boxes.
 * Clicking cancel button will clear input box value.
 *
 * @export
 * @class SearchboxDirective
 * @implements {OnChanges}
 * @implements {OnDestroy}
 */
@Directive({
  selector: '[appSearchbox]',
})
export class SearchboxDirective implements OnChanges, OnDestroy {
  /**
   * Use inputs variable that holds value to bind with attribute.
   *
   * @example <input name="search" type="text" [(ngModel)]="searchTextVar" [appSearchbox]="searchTextVar"/>
   *
   * @memberof SearchboxDirective
   */
  @Input() public appSearchbox;

  public cancelContainer;
  public cancelStyle;
  public unlistener;

  /**
   * Creates an instance of SearchboxDirective.
   * @constructor
   * @param {ElementRef} el
   * @param {Renderer2} renderer
   * @memberof SearchboxDirective
   */
  public constructor(private el: ElementRef, private renderer: Renderer2) {
    this.cancelStyle = {
      top: '50%',
      right: '0.5rem',
      transform: 'translateY(-50%)',
      cursor: 'pointer',
    };
  }

  /**
   * Triggers when to ADD/REMOVE cancel icon to inputbox.
   *
   * @param {*} changes
   * @memberof SearchboxDirective
   */
  public ngOnChanges(changes) {
    if (
      changes.appSearchbox.currentValue === '' ||
      changes.appSearchbox.currentValue === undefined
    ) {
      this.toggleCancelIcon(false);
    } else if (changes.appSearchbox.currentValue !== '') {
      this.toggleCancelIcon(true);
    }
  }

  /**
   * Removes/cleans CLICK event listener of cancel icon.
   *
   * @memberof SearchboxDirective
   */
  public ngOnDestroy() {
    if (this.unlistener) {
      this.unlistener();
    }
  }

  /**
   * Show / Hide cancel icon.
   *
   * @param {true|false} state
   * @memberof SearchboxDirective
   */
  private toggleCancelIcon(state) {
    if (state === true) {
      const cancelBtn = this.renderer
        .parentNode(this.el.nativeElement)
        .querySelector('#inputCancelBtn');
      if (!cancelBtn) {
        this.cancelStyle['height'] = `${this.el.nativeElement.clientHeight}px`;
        this.renderer.setStyle(
          this.el.nativeElement,
          'padding-right',
          '2.5rem'
        );
        this.cancelContainer = this.renderer.createElement('div');
        this.renderer.setAttribute(
          this.cancelContainer,
          'id',
          'inputCancelBtn'
        );
        this.renderer.setAttribute(
          this.cancelContainer,
          'class',
          'd-inline-block position-absolute'
        );
        this.unlistener = this.renderer.listen(
          this.cancelContainer,
          'click',
          () => {
            this.cleatInputField();
          }
        );
        this.setRendererStyles(this.cancelContainer, this.cancelStyle);
        const cancelButton = this.renderer.createElement('span');
        this.renderer.setAttribute(
          cancelButton,
          'class',
          'icon icon-close-thin'
        );
        this.renderer.appendChild(this.cancelContainer, cancelButton);
        this.renderer.appendChild(
          this.renderer.parentNode(this.el.nativeElement),
          this.cancelContainer
        );
      }
    } else if (state === false) {
      const cancelBtn = this.renderer
        .parentNode(this.el.nativeElement)
        .querySelector('#inputCancelBtn');
      if (cancelBtn) {
        this.renderer.removeChild(
          this.renderer.parentNode(this.el.nativeElement),
          cancelBtn
        );
        this.renderer.setStyle(
          this.el.nativeElement,
          'padding-right',
          'init'
        );
      }
    }
  }

  /**
   * Loop through styles object and set given elements styles using Renderer2 setStyle method.
   *
   * @param {*} element
   * @param {Object} styles
   * @memberof SearchboxDirective
   */
  private setRendererStyles(element, styles) {
    for (const [key, value] of Object.entries(styles)) {
      this.renderer.setStyle(element, key, value);
    }
  }

  private clearInputField() {
    this.el.nativeElement.value = '';
    this.toggleCancelIcon(false);
  }
}


EXAMPLE

Input box with clear functionality


Solution

  • You can use output in the directive to pass the 'X' event form directive to the component like this:

    <input
      #search
      name="search"
      type="text"
      class="form-control underlined"
      placeholder="Search Rule Name"
      [(ngModel)]="ruleSearchText"
      [appSearchbox]="ruleSearchText"
      (clearSearch)="clearText($event)"
    />
    

    In the directive(appSearchbox) add this method on clearing text and the output decorator:

    @Output() clearSearch = new EventEmitter();
    ..
    ..
    ..
    
    clear() {
       this.clearSearch.emit(true);
    }
    
    

    In the parent component add this:

    clearText(status) {
       this.ruleSearchText = status ? '' : this.ruleSearchText;
    }