I'm using ng-template in Angular8 to make a plural translation for displaying a notification to the user from the component but I cannot get full generated inner HTML of the template before attaching to DOM because context not bound yet.
How can I render a template with its context and get its inner HTML for this purpose?
I tried to use ViewContainerRef to render the template and attach it DOM and it works fine but I don't want to attach something to DOM and read it later.
Template:
<ng-template #activeDeactiveSuccessMessage let-card="card">
<span i18n="##card.notification">Notification</span>
<span i18n>{card.lockStatus, select,
LOCKED {Card number ending with {{card.number}} has been deactivated.}
UNLOCKED {Card number ending with {{card.number}} has been activated.}
other {Card number status changed to {{card.lockStatus}} }}</span>
</ng-template>
Component Code:
#ViewChild('activeDeactiveSuccessMessage', { static: false }) private activeDeactiveSuccessMessage: TemplateRef<any>;
Bellow is part of code to attach the rendered template to DOM and works fine:
let el = this._viewContainerRef.createEmbeddedView(this.activeDeactiveSuccessMessage, { card });
But I don't want to attach to DOM, want to get rendered template inside component before attaching.
used bellow code to get text but for second node which needed context, returns comment!:
let el = this.activeDeactiveSuccessMessage.createEmbeddedView({ card });
console.log(el.rootNodes[0].innerHTML); // --> Notification
console.log(el.rootNodes[1].innerHTML); // --> <!----><!----><!----><!---->
I expect the output of Card number ending with 6236 has been deactivated. for the second node.
Problem SOLVED!
The problem I encountered was because of my request to translate alert messages in the script at runtime, and I did not want to use ngx-translate.
Fortunately, in angular 9, with the help of #angular/localize, this problem has been solved and it is easy to translate texts into the script in this way:
#Component({
template: '{{ title }}'
})
export class HomeComponent {
title = $localize`You have 10 users`;
}
to read more visit https://blog.ninja-squad.com/2019/12/10/angular-localize/
Related
I have an DialogService for Angular Material:
constructor(private dialog: MatDialog){}
openDialog(dialogData){
const dialogRef = this.dialog.open(DialogComponent, {
data: dialogData
}
}
and a DialogComponent to open the dialog with:
let componentToRender
constructor(#Inject(MAT_DIALOG_DATA) public dialogData){
this.componentToRender = dialogdata.componentToRender
}
and this template for it:
<div class="dialog">
<ng-container></ng-container> // Here i want to dynamically render a given component
</div>
I want to give my dialogService with the dialogData a reference to an component that i want to be rendered inside my diaologComponent <ng-container>
The result should be, that i can call my service with a reference to a component to open a dialog container that renders this given component inside the component.html's ng-container. For example like so:
let dialogData = {}
dialogData.componentToRender = COMPONENT_TO_RENDER_INSIDE_OF_DIALOG
this.dialogService.openDialog(dialogData)
The idea is to make something like a dialog-container where the body can be any component i want to render inside of the dialog-container
I hope it is enough to write only the essential code, because I ask this question from another computer and could not copy paste the stuff I already have. thank you :)
For now I kind of solved this with ViewContainerRef.
I use the createComponent() method and give it the Component I want to render.
Then I insert the create ref inside my ng-template.
#ViewChild('container', {read: ViewContainerRef}) container!: ViewContainerRef
const componentRef = this.viewContainerRef.createComponent(MY_COMPONENT_DYNAMICALLY_GIVEN)
this.container.insert(componentRef.hostView)
This works but also renders my component selector tag around my content.
<my_inserted_component> <!-- I need to get rid of this :D -->
<!-- contents of my_inserted_component -->
</my_inserted_component>
That sadly results into Layouting problems. So now I need to find out how to change my CSS or (better) how to get rid of the outer tag with the component selector name.
EDIT: Also I should mention that I am on Angular 14
I would like to create a library of web components using Angular Elements that have default templates but allow developers to override the output.
For example, consider a search-results component. I might have a default template that looks like this:
<h1>Search results for: {{query}}</h1>
But a developer might want to change the output to this (as an arbitrary example -- it needs to be flexible):
<h1>You searched for <strong>{{query}}</strong></h1>
My first thought was to simply use ng-content like this:
<search-results>
<h1>You searched for <strong>{{query}}</strong></h1>
</search-results>
However, this does not work and will literally output {{query}}.
I then discovered that I could add a TemplateRef input parameter on my component and pass in an ng-template element which would parse expressions. This works well within an Angular application, but I'm not sure how to do this outside of an Angular context using the compiled web components that Angular Elements generates.
Can anyone explain how to do this?
I see two solutions.
Solution 1: HTML template
Define a HTML template and pass its id to the Angular component. There you clone that node (see example in link) and add it to the DOM.
Placeholders ({{query}}) do not work "out of the box" in that template. You could replace them manually or just update the template and watch for changes in the Angular component. (Mutation Observer)
I'm working on this idea right now... I'll post an update here once my code is on GitHub so you can have a look at it.
Solution 2: JS templates
Another idea is to work with JS templates. (EJS for example)
You define a template string that you pass to the Angular component. There you render it with the given data object.
You can create a function that parse the .....{{variable}}... to ...value...
replaceText(content: string) {
const match = content.match(/(\{\{\w+\}\})/g)
match?.forEach(x => {
const variable = x.slice(2).slice(0, -2) || "fool"
const value = (this as any)[variable] || ""
content = content.replace(x, value)
})
return content
}
Then, you store the "ng-content" innerHTML in ngAfterviewInit. When you need, you call to this function.
Imagine some like -see that the "ng-content" is under a div "inner" with display:none
#Component({
template: `
<div class="alert alert-{{ type }} alert-dismissible" *ngIf="show">
<div [innerHTML]="newContent"></div>
<button type="button" class="close">
<span (click)="show = false; closed.emit()">×</span>
</button>
</div>
<div #inner class="hidden">
<ng-content></ng-content>
</div>
`,
styles:[`
.hidden{
display:none
}
`]
})
In ngAfterVieInit
#ViewChild('inner', { static: false }) inner!: ElementRef;
content:any;
ngAfterViewInit(): void {
this.content = this.inner.nativeElement.innerHTML;
}
And when you need
this.newContent = this.satinizer.bypassSecurityTrustHtml(
this.replaceText(this.content || '')
);
See a simple stackblitz
Use bypassSecurityTrustHtml method of DomSanitizer, provided by an angular; and bind it with html <div [innerHtml]="getSearchText()"></div>.
public getSearchText() {
return this.domSanitizer.bypassSecurityTrustHtml(`You searched for <b>${this.searchText}</b>`);
}
For more visit the angular documentation https://angular.io/api/platform-browser/DomSanitizer
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
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
I am using the ComponentFactoryResolver to dynamically create an element to be inserted into my template, which uses ng-content for transclusion.
It all works wonderfully until I add a select attribute to my ng-content. Please see this plunker which demonstrates the problem. If I remove the content-top attribute from my HeaderComponent template on line 63 of app.ts, the template renders as expected.
I do however need to use select because there are two different template fragments to be injected so I can't simply remove it.
Any help appreciated.
Transclusion in angular will work only with direct children. One way to do it working might be using ngTemplateOutlet to lift up content from dynamic component:
<some-other-component>
<ng-template content-host></ng-template>
<ng-template top-content [ngTemplateOutlet]="topContent"></ng-template>
<ng-template bottom-content [ngTemplateOutlet]="bottomContent"></ng-template>
</some-other-component>
component.ts
topContent: TemplateRef<any>;
bottomContent: TemplateRef<any>
const componentRef = viewContainerRef.createComponent(componentFactory);
const instance = componentRef.instance;
componentRef.changeDetectorRef.detectChanges();
this.topContent = instance.templates.find(x => x.place === 'top-content').templateRef;
this.bottomContent = instance.templates.find(x => x.place === 'bottom-content').templateRef;
where templates property is declared on your dynamic component
#ViewChildren(TranscludeMeToDirective) templates: QueryList<TranscludeMeToDirective>;
Plunker Example
Meteor re renders a view when a document changes.
Template.story.data = function() {
var storyID = Session.get('storyID');
var story = Stories.findOne({
_id: storyID
})
if (!story)
return;
return story;
};
Here's a template helper for the story template, getting a story from the Stories document.
When fields like story.title changes I want the template to rerender. But when fields like story.viewingusers change I don't want to rerender the template. Anyway to make that happen?
The specific problem that triggered the question was solved by setting the publish function to not publish those fields. However that solution doesn't work for every usecase and a general solution is needed.
What you're looking for is the #constant template helper.
What i'd do in your case is wrap the popover markup in a constant block, then update the content in that block manually in the story.rendered function.
So something like this:
story.html:
<template name="story">
...
{{#constant}}
<!-- Popover markup here -->
{{/constant}}
...
</template>
client.js:
Template.story.rendered = function(){
//Update logic here
}