How do you create web components with customizable templates using Angular Elements? - javascript

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

Related

Angular: How to use a component dynamically in another component?

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

Dynamic Property Binding for Angular Directive Inputs

Hi I'm making a a drag directive but because of the way it works I can't get it to work on dynamic objects as it calls the id in the input.
#Input()
set Drag(options: any) {
this.stickerElement = document.getElementById(options.id);
}
Which works fine when the element isn't dynamic:
<div id="sticker" class="issues" [Drag]="{id: 'sticker'}">
but when it's set dynamically I can't figure out how to interpolate the ID dynamically.
<div [attr.id]="'Session' + Day.id" [Drag]="{id:'Session' + Day.id}"></div>
I've tried setting this.stickerElement with the #HostListener when you use it but that allows the directive to bubble and use child elements. I guess I can work around it but it doesn't feel right.
I feel like I'm missing some knowledge because no matter what I google nothing useful comes up about how to interpolate it correctly. Can you interpolate an attribute into a directive like this?
Thanks in Advance
B
I don't see any issue in the interpolation. However, document.getElementById(options.id) in Angular looks dirty. Instead you could use a template reference variable and directly send the HTMLElement.
Try the following
Template
<div appSticker #sticker [Drag]="{ref:sticker}"></div>
Directive
#Directive({ selector: "[appSticker]" })
export class StickerDirective {
stickerElement: HTMLElement;
#Input()
set Drag(options: any) {
this.stickerElement = options.ref;
}
constructor() {}
}
Also I don't see the directive binding in the <div> tag in your code.

How can I render template with context in an Angular component?

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/

Getting HTML attr. with ViewContainerRef?

I am adding a component to my template via *ngFor
<div class="filter-group" *ngFor="let type of entityTypes">
<filter-filter-group [type]="type" id="{{type.uri}}"></filter-filter-group>
</div>
And in my component I am getting my ViewChrildren like so
#ViewChildren(FilterGroupComponent, { read: ViewContainerRef }) filterGroup: QueryList<ViewContainerRef>;
this.filterGroup now contains my components which is exactly what I want as I then use the component Factory to dynamically insert components to each viewChild.
As you can see in the tmpl. each component has an id="{{type.uri}}"
Is there a way to get the value of id?
I have searched far and wide and so far am a little lost on how to achieve this. NativeElement could be an option, but I believe that is only available with ElementRef?
Possible Solution:
console.log(group['_data'].renderElement.id);
this gives me the id.... but i don't think this would be the way to do it? I mean it works, but feels wrong.
The ViewContainerRef has an elementRef as a child, which in turn has a nativeElement child (the HTML element per se). So you should be able to do group.elementRef.nativeElement.id to get the id
Note: Be aware that using console.log(element) that returns an HTML element will print the element as HTML, for these cases instead use console.dir
you can try like this
HTML
<div class="filter-group" *ngFor="let type of entityTypes">
<filter-filter-group [type]="type" #mycomp id="{{type.uri}}"></filter-filter-group>
</div>
TS
#ViewChildren('mycomp') mycompRef: any;
someEvent() {
console.log(this.mycompRef); // in this object you will get the id
}
#ViewChildren("vc", {read: ViewContainerRef}) containers: QueryList<ViewContainerRef>
ngAfterViewInit(): void {
this.containers.map(container => console.log(container["_lContainer"][0].id))
this.createLayout();
}
This worked for me in Angular 10

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

Categories