How to use a directive to pass default values in angular

Viewed 92

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>
2 Answers

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

For future reference I'm posting what I ultimately decided to do.

Chris Hamilton's answer got me very close to what I needed. However, in my example I needed to access to my Components so I could set their input properties. So instead of templateRef and viewContainer, I needed something more like ContentChildren. For some reason Angular (v12 for me) won't find the children within a structural directive. But if I changed it to an attribute directive it would work. Here is what I ended up with:

<ng-container [yesNoConfig]="{ labelKeyBase: 'MODULE.PAGE_X.' }">
  <div>
    <yes-no controlName="isAwesome"></yes-no>
  </div>
  <yes-no controlName="isClean"></yes-no>
</ng-container>

And...

@Directive({
  selector: '[yesNoConfig]',
})
export class YesNoConfigDirective implements AfterContentInit {
  private config: { [key: string]: string };

  @ContentChildren(YesNoComponent, { descendants: true }) children!: QueryList<YesNoComponent>;

  @Input() set yesNoConfig(config: { [key: string]: string }) {
    this.config = config;
  }

  ngAfterContentInit(): void {
    this.children.forEach((child: YesNoComponent) => {
      child.labelKeyBase = this.config?.labelKeyBase;
      // TODO: Replace with loop/match of config properties
    });
  }
}

While I would prefer a solution that is more flexible and less tightly bound, this gets the job done for what I need.

Related