Search code examples
angularangular-directiveangular-components

How to use a directive to pass default values in angular


I have created a custom component that has many input properties. However, one thing I have noticed when using this component is that many of the input properties are the same within the parent component.

An example usage looks like this:

<yes-no
 controlName="isAwesome"
 labelKeyBase="MODULE.PAGE_X"
 validationKeyBase="VALIDATION.CUSTOM_X"
 someOtherProp="true"
></yes-no>

I have been using a library called transloco and noticed some functionality that would be very useful in this situation. Unfortunately, I have no idea how it would be implemented.

https://ngneat.github.io/transloco/docs/translation-in-the-template#utilizing-the-read-input

I'm wondering if there is a simplified example out there that demonstrates how to pass a value from a parent directive to a child component. Basically, my goal is to turn the above code into something like this:

<ng-container *yesNoConfig="labelKeyBase: 'MODULE.PAGE_X'; validationKeyBase: 'VALIDATION.CUSTOM_X'">
 <yes-no controlName="isAwesome"></yes-no>
 <yes-no controlName="isClean"></yes-no>
 <yes-no controlName="isShort"></yes-no>
</ng-container>

Solution

  • Here's a directive that applies arbitrary attributes to all immediate children of the container:

    @Directive({
      selector: '[setAttributes]',
    })
    export class SetAttributesDirective {
      @Input('setAttributes') atts: { [key: string]: string } = {};
    
      constructor(
        private templateRef: TemplateRef<any>,
        private viewContainer: ViewContainerRef
      ) {}
    
      ngOnInit() {
        const keys = Object.keys(this.atts);
        const viewRef = this.viewContainer.createEmbeddedView(this.templateRef);
        for (const node of viewRef.rootNodes) {
          if (typeof node.setAttribute !== 'function') continue;
          const el = node as HTMLElement;
          for (const key of keys) el.setAttribute(key, this.atts[key]);
        }
      }
    }
    

    Not very typesafe since viewRef.rootNodes is type any[] but what can you do... I put somewhat of a typeguard anyway. The first entry is actually the ng-container itself, and nested templates / ng-containers will also not have a setAttribute function, throwing an error without the typeguard. Could definitely be improved, but at least you get the gist.

    You'd use it like this:

    <ng-container *setAttributes="{ type: 'number', value: '5', min: '0', max: '10' }">
      <input />
      <input />
      <input />
      <input />
    </ng-container>
    

    Associated docs: https://angular.io/guide/structural-directives

    Stackblitz example: https://stackblitz.com/edit/angular-ivy-1eyjlo?file=src/app/app.component.html


    For your example you need to get the component instance from the DOM node. There is a function for that, although it's meant for debugging, not sure why it's not part of the regular API: https://angular.io/api/core/global/ngGetComponent

    You'll have to tell tsc that the function exists, then you can set your component's properties:

    declare global {
      interface Window {
        ng: { getComponent: (el: Element) => { [key: string]: any } | null };
      }
    }
    
    @Directive({
      selector: '[setInputs]',
    })
    export class SetInputsDirective {
      @Input('setInputs') atts: { [key: string]: any } = {};
    
      constructor(
        private templateRef: TemplateRef<any>,
        private viewContainer: ViewContainerRef
      ) {}
    
      ngOnInit() {
        const keys = Object.keys(this.atts);
        const viewRef = this.viewContainer.createEmbeddedView(this.templateRef);
        for (const node of viewRef.rootNodes) {
          if (!(node instanceof Element)) continue;
          const component = window.ng.getComponent(node);
          if (component === null) continue;
          for (const key of keys) component[key] = this.atts[key];
        }
      }
    }
    
    <ng-container *setInputs="{ labelKeyBase: 'MODULE.PAGE_X', validationKeyBase: 'VALIDATION.CUSTOM_X' }">
     <yes-no controlName="isAwesome"></yes-no>
     <yes-no controlName="isClean"></yes-no>
     <yes-no controlName="isShort"></yes-no>
    </ng-container>
    

    Stackblitz: https://stackblitz.com/edit/angular-ivy-mbcw9o?file=src/app/app.component.html