Search code examples
angularangular-materialdirectiveangular-ng-if

Angular directive that sets ngIf from within the directive


I have a custom directive that dynamically sets things on the inputs such as the placeholder, required, hints, and tooltips. I would like to also conditionally set it with an *ngIf, but I am not sure how to do that dynamically. We are using flex and have a responsive form so if we do other tricks to hide the field, then as long as it does not affect the layout, I'm cool. so basically if we set visibility to false, the component will not show.

Here is a link to the stackblitz - https://stackblitz.com/edit/angular-uffqz6?file=src/app/config-from.directive.ts

I know there is already an angularjs question for this - Angular directive that sets ngIf

The idea here is that someone is maintaining Label, required, visibility, help, and tooltips from another source. This is used in a product that has multiple subscribers and some of them do not use the application the same way, hence it is nice to make it dynamic. I hate the "Show Field Descriptions" functionality, but it was pushed - I know it's weird. I've tried to strip it down to get to the meat of the example... Help is much appreciated and maybe just maybe there is some stuff in here to help someone else out in creating a house of cards..

import { AfterViewInit, Directive, ElementRef, Input, OnDestroy, OnInit, Renderer2, Self } from '@angular/core';
import { NgModel } from '@angular/forms';
import { MatInput } from '@angular/material/input';
import { MatTooltip } from '@angular/material/tooltip';
import { Observable, of, Subscription } from 'rxjs';
import { FieldConfigService } from './config.service';

@Directive({
  selector: 'input[configFrom]'
})
export class ConfigFromDirective implements OnInit, AfterViewInit, OnDestroy {
  protected _visibleHelp = false;
  protected helpMessage: string;
  protected hint: any;
  protected inputElement: any;
  protected subs: Subscription[] = [];
  protected name: string;

  @Input() configFrom = '';

  constructor(
    protected model: NgModel,
    // @Optional() @Self() public ngControl: NgControl,
    private configService: FieldConfigService,
    protected el: ElementRef,
    protected renderer: Renderer2,
    @Self() protected input: MatInput,
    protected matTooltip: MatTooltip
  ) {
    if (el.nativeElement) {
      this.inputElement = el.nativeElement;
    }
  }

  ngOnInit() {
    this.name = this.model.name;

    console.log('Looking at', this.name);

    this.subs.push(this.configService.findByObject(this.configFrom).subscribe(configuration => {
        let config = configuration[this.configFrom][this.name];
        this.input.placeholder = config.label;
        this.input.required = config.required;
        this.matTooltip.message = config.helpMessage;
        this.helpMessage = config.helpMessage;
      })
    );

    this.subs.push(this.configService.showHelp$.subscribe(showHelp => {
        this._visibleHelp = showHelp;
        this.checkShowMessage();
      })
    );
  }

  ngAfterViewInit(): void {
    this.checkShowMessage();
  }

  ngOnDestroy(): void {
    this.subs.forEach(sub => {
      if (sub && !sub.closed) {
        sub.unsubscribe();
      }
    });
  }

  protected checkShowMessage() {
    let input = this.getMatFormFieldParent();

    if (input) {
      if (!this.hint) {
        this.hint = this.renderer.createElement('mat-hint');
        this.renderer.appendChild(input, this.hint);
        this.renderer.addClass(this.hint, 'help-hint');
      }
      if (this._visibleHelp) {
        this.renderer.addClass(this.hint, 'help-hint');
        this.renderer.removeClass(this.hint, 'hidden');
      } else {
        this.renderer.addClass(this.hint, 'hidden');
        this.renderer.removeClass(this.hint, 'help-hint');
      }

      this.hint.innerHTML = this.helpMessage;
    }
  }

  protected getMatFormFieldParent(): any {
    let input = this.inputElement;

    let depth = 0;
    while (depth < 5 && input) {
      if (
        input.parentElement &&
        input.parentElement.tagName == 'MAT-FORM-FIELD'
      ) {
        return input.parentElement;
      }
      input = input.parentElement;
      depth++;
    }

    return null;
  }
}

Solution

  • One thing that I have working now is to just set display to none. I am not sure of all the ramifications of this, but it does not show the element and flex takes care of shifting things around. The component is still there in the DOM unlike an ngIf. Any feedback or better solutions welcome!

    //existing code
    this.matTooltip.message = config.helpMessage;
    this.helpMessage = config.helpMessage;
    
    //additional change for visibility.
    if (!config.visible) {
      this.getMatFormFieldParent().style.display = 'none';
    }