2

I'm working with a dynamic content, and I am using ngTemplateOutlet with custom directive to create it, but now I need to access the created content elements. I wasn't able to find any relatable answers regarding my problem in a wast list of similar questions.

My current setup (I will omit unnecessary parts to make the example as clear as possible)

The content host component:

(grid.host.component.ts)
@ContentChildren(VvsGridInputCellDirective, { descendants: false }) public inputCols: QueryList<VvsGridInputCellDirective<T>>;

public getTemplateElement(): HTMLElement {
     // checks for undefined and null
     return this.inputCols.first().templateRef.elementRef;
}

the significant part of the host component template:

(host.template.html)
<td *ngFor="let col of visibleColumns" [class.resized-col]="col?.width > 0">
    <div class="cell-height-wrapper" [style.width]="col?.width > 0 ? col.width+'mm':'auto'">
        <ng-template [ngTemplateOutlet]="col.templateRef" [ngTemplateOutletContext]="{$implicit: data}" ></ng-template>
    </div>
</td>

this.getTemplateElement() returns the elementRef of ng-template itself (which is just some comments), well because the ng-template does not exist in DOM. I need to get the contents of that ng-template directive, but I don't know how;

This is the directive that passes the template for the ngTemplateOutlet:

(host.cell.directive.ts)
@Directive({
    selector: "ng-template[appVvsGridInputCell]",
    exportAs: "appVvsGridInputCell"
})
export class VvsGridInputCellDirective<T> {

public definition: ColumnDefinition<T>;
public model: T;

constructor(public templateRef: TemplateRef<any>) {
    this.definition = new ColumnDefinition();
}

And finally the usage of the directive. This is a content I want to access:

(uses.all.above.component.template.html)
<ng-template appVvsGridInputCell #cell="appVvsGridInputCell" let-data header="COMMENT">
    <div class="no-padding">
        <input class="form-control" type="text" [(ngModel)]="data.comment">
    </div>
</ng-template>

So that is all there is to it (besides a lot of clutter). Please let me know how can I access the element that was defined in the ng-template appVvsGridInputCell, because I want to control it within the host.component. Also leave a note if you need any clarification.

I looked through a lot of questions, but those are ones that i didn't close:

Stackblitz

I created a proof of concept stackblitz project that replicates my problem: https://stackblitz.com/edit/angular-nx8gyx

Ernis
  • 645
  • 11
  • 22

1 Answers1

4

You cannot get the template content elements from the TemplateRef. The template reference is just a reference to the template not to the rendered template. It is represented as a comment node in the HTML DOM as you found and has no connection to the content.

I think for your case the solution is to define another directive that is applied to the input elements inside the templates. This way you can query easily also all the input elements using another @ContentChildren query.

For example such a directive could look like this:

@Directive({
    selector: '[appVvsTemplateInput]'
})
export class VvsTemplateInputDirective {
  @Input('appVvsTemplateInput') inputCell: VvsGridInputCellDirective;

  constructor(public inputElementRef: ElementRef) {
  }
}

I added the inputCell property so you can then later match the input to the correct cell. Here is a sample column template using also this directive:

<ng-template appVvsGridInputCell #cell3="appVvsGridInputCell" let-data header="COMMENT">
  <div class="regular-padding">
    <input [appVvsTemplateInput]="cell3" type="text" [(ngModel)]="data.comment">
  </div>
</ng-template>

Now in the grid component you add the @ContentChildren query for this directive and you will get all the inputs with the directive. From these you can find the one that corresponds to the column you look for.

Here is the relevant code from the component:

@ContentChildren(VvsTemplateInputDirective) private columnInputs: QueryList<VvsTemplateInputDirective>;

public getTemplateInputElement(): HTMLElement {
  const foundColumn = this.columnInputs.toArray().find((templateInputDirective) => templateInputDirective.inputCell === this._visibleInputCols[1]);
  return foundColumn.inputElementRef.nativeElement;
}

There is another solution which is a more hacky solution in my view. You can use a @ViewChildren query to get all the elements that are the template outlets. These are again comment elements. From this comment elements you can get the main template content element by getting the nextSibling of the nativeElement. Angular attaches dynamic content as the sibling of the container element. I think is a bit dangerous to rely on this. Also you will need to do more direct DOM traversal to find the input element.

To make the querying more easy it is helpful to add a name to the element with the ngTemplateOutlet directive.

<ng-container #templateOutlet
    [ngTemplateOutlet]="col.templateRef" 
    [ngTemplateOutletContext]="{$implicit: inputRow}" >
</ng-container>

Then you can query by the name in the component and get the content element as the nextSibling.

@ViewChildren('templateOutlet') private templateOutlets: QueryList<ElementRef>;

public getTemplateContentElement(): HTMLElement {
  return this.templateOutlets.toArray()[1].nativeElement.nextSibling;
}

Here is also a link to a fork of your StackBlitz where I have added both of this approaches.

Aleš Doganoc
  • 11,568
  • 24
  • 40
  • The first solution is what I was looking for. It haven't occurred to me that I can assign the `cell` itself as an input for identification and later query the directives for those elements – Ernis Apr 17 '20 at 05:07