Dynamic Property Binding for Angular Directive Inputs - javascript

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.

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

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

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

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

Add class to a div dynamically in angular js

I have a div like this
<div ng-model="star"></div>
I want to add a class 'active' to this div in angular js.
I tried like this in my angular js controller
$scope.star.addClass('active');
But,I am getting error. I am new to angular js. please help me...
Try this:
<div ng-class="{ active: star == 'yes', 'in-active': star == 'no' }" ng-bind="star"></div>
You can have one or more classes assigned based on the expression (true or false).
If a class name has a hyphen, enclose in single quote.
This is a better approach and preferred than changing in controller.
Also, because this is a div, you have to do ng-bind, not ng-model.
ng-model works on fields like input.
UPDATE:
Since you insist on changing the class in code, here it is:
$("div[ng-bind='star']").addClass('active');
If you want to access change class dynamically then you can put watch on your star variable in your controller like below :
$.watch('star',function(newVal, oldVal){
// put your code to here based on new value and old value
// you can add class to your div like :
// angular.element("div[ng-bind='star']").addClass('active');
});
Using a variable to control your class
<div ng-class="customClass"></div>
in controller
$scope.customClass = 'active custom-class1';
so, you can use if-else to change class name
if (something) {
$scope.customClass = 'active custom-class1';
} else {
$scope.customClass = 'deactive custom-class2';
}

Categories