Alejandro Serrano
Alejandro Serrano

Reputation: 87

How to pass variables from ng-template declared in parent component to a child component/directive?

So I want to know if there is a way to pass an ng-template and generate all it's content to include variables used in interpolation?

Also I'm still new to angular so besides removing the html element do I need to worry about removing anything else?

At the end of this there will be a link to a stackblitz.com repo which will have all the code shown below.

the following is my src/app/app.component.html code implementing my directive:

<hello name="{{ name }}"></hello>
<p>
  Start editing to see some magic happen :)
</p>
<!-- popup/popup.directive.ts contains the code i used in button tag -->
<button PopupDir="" body="this is a hardcoded message that is passed to popup box"> simple 
</button>

<ng-template #Complicated="">
  <div style="background-color: red;">
    a little more complicated but simple and still doable
  </div>
</ng-template>
<button PopupDir="" [body]="Complicated">
  complicated
</button>

<ng-template #EvenMoreComplicated="">
  <!-- name and data isn't being passed i need help here--> 
  <div style="background-color: green; min-height: 100px; min-width:100px;">
    {{name}} {{data}}
  </div>
</ng-template>
<button PopupDir="" [body]="EvenMoreComplicated">
  more complicated
</button>

the following is my src/app/popup/popup.directive.ts

import { Directive, Input, TemplateRef, ViewContainerRef, HostListener } from '@angular/core'

@Directive({
  selector: 'PopupDir, [PopupDir]'
})
export class Popup {
  @Input() body: string | TemplateRef<any>;
  viewContainer: ViewContainerRef;
  popupElement: HTMLElement;

  //i dont know if i need this
  constructor (viewContainer: ViewContainerRef) {
    this.viewContainer = viewContainer;
  }

  //adds onlick rule to parent tag
  @HostListener('click')
  onclick () {
    this.openPopup();
  }

  openPopup() {
    //Pcreate pupup html programatically
    this.popupElement = this.createPopup();

    //insert it in the dom
    const lastChild = document.body.lastElementChild;
    lastChild.insertAdjacentElement('afterend', this.popupElement);
  }

  createPopup(): HTMLElement {
    const popup = document.createElement('div');
    popup.classList.add('popupbox');

    //if you click anywhere on popup it will close/remove itself
    popup.addEventListener('click', (e: Event) => this.removePopup());
    //if statement to determine what type of "body" it is
    if (typeof this.body === 'string')
    {
      popup.innerText = this.body;
    } else if (typeof this.body === 'object')
    {
      const appendElementToPopup = (element: any) => popup.appendChild(element);
      //this is where i get stuck on how to include the context and then display the context/data that is passed by interpolation in ng-template
      this.body.createEmbeddedView(this.viewContainer._view.context).rootNodes.forEach(appendElementToPopup);
    }
    return popup;
  }

  removePopup() {
    this.popupElement.remove();
  }
}

this is the link to the repo displaying my problem: https://stackblitz.com/edit/popupproblem

Upvotes: 4

Views: 4390

Answers (1)

yurzui
yurzui

Reputation: 214037

First let's think how we're passing context to embedded view. You wrote:

this.body.createEmbeddedView(this.viewContainer._view.context)
                              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Your Popup component is hosted in AppComponent view so this.viewContainer._view.context will be AppComponent instance. But what I want you to tell:

1) Embedded view has already access to scope of the template where ng-template is defined.

2) If we pass context then it should be used only through template reference variables.

this.body.createEmbeddedView(this.viewContainer._view.context)
             ||
             \/

this.body.createEmbeddedView({
  name = 'Angular';
  data = 'this should be passed too'
})
             ||
             \/

<ng-template #EvenMoreComplicated let-name="name" let-data="data">
    {{name}} {{data}}

So in this case you do not need to pass context because it is already there.

this.body.createEmbeddedView({})
             ||
             \/
<ng-template #EvenMoreComplicated>
        {{name}} {{data}}

Why UI is not updating?

Angular change detection mechanism relies on tree of views.

         AppComponent_View
         /                \
ChildComponent_View    EmbeddedView
        |
 SubChildComponent_View

We see that there are two kind of views: component view and embedded view. TemplateRef(ng-template) represents embedded view.

When Angular wants to update UI it simply goes through that view two check bindings.

Now let's remind how we can create embedded view through low level API:

  • TemplateRef.createEmbeddedView

  • ViewContainerRef.createEmbeddedView

The main difference between them is that the former simply creates EmbeddedView while the latter creates EmbeddedView and also adds it to Angular change detection tree. This way embedded view becames part of change detection tree and we can see updated bindings.

It's time to see your code:

this.body.createEmbeddedView(this.viewContainer._view.context).rootNodes.forEach(appendElementToPopup);

It should be clear that you're using the first approach. That means you have to take care of the change detection yourself: either call viewRef.detectChanges() manually or attach to tree.

Simple solution could be:

const view = this.body.createEmbeddedView({});
view.detectChanges();
view.rootNodes.forEach(appendElementToPopup);

Stackblitz Example

But it will detect changes only once. We could call detectChanges method on each Popup.ngDoCheck() hook but there is an easier way that is used by Angular itself.

const view = this.viewContainer.createEmbeddedView(this.body);
view.rootNodes.forEach(appendElementToPopup);

We used the second approach of creating embedded view so that template will be automatically checked by Angular itself.

I'm still new to angular so besides removing the html element do I need to worry about removing anything else?

I think we should also destroy embedded view when closing popup.

removePopup() {
  this.viewContainer.clear();
  ...
}

Final Stackblitz Example

Upvotes: 6

Related Questions