Reputation: 87
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
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}}
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);
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();
...
}
Upvotes: 6