Angular - Service injecting dynamic component? - javascript

I have a working code which injects any component via a service to the HTML:
ModalWindow.ts:
#Component({
selector: 'modal-window'
template: `
<div class="modal-dialog" role="document">
<div class="modal-content"><ng-content></ng-content></div>
</div>
`
})
export class ModalWindow {
}
Modalcontent.ts :
#Component({
selector: 'modal-content'
template: `
I'm beeing opened as modal!
`
})
export class ModalContent {
}
ModalService.ts :
/*1*/ #Injectable()
/*2*/ export class ModalService {
/*3*/
/*4*/ constructor(private _appRef: ApplicationRef, private _cfr: ComponentFactoryResolver, private _injector: Injector) {
/*5*/ }
/*6*/
/*7*/ open(content: any) {
/*8*/ const contentCmpFactory = this._cfr.resolveComponentFactory(content);
/*9*/ const windowCmpFactory = this._cfr.resolveComponentFactory(ModalWindow);
/*10*/
/*11*/ const contentCmpt = contentCmpFactory.create(this._injector);
/*12*/ const windowCmpt = windowCmpFactory.create(this._injector, [[contentCmpt.location.nativeElement]]);
/*13*/
/*14*/ document.querySelector('body').appendChild(windowCmpt.location.nativeElement);
/*15*/
/*16*/ this._appRef.attachView(contentCmpt.hostView);
/*17*/ this._appRef.attachView(windowCmpt.hostView);
/*18*/ }
/*19*/ }
App.ts:
#Component({
selector: 'my-app',
template: `
<button (click)="open()">Open modal window</button>
`,
})
Result (when click a button which calls this service method ) :
I already know what contentCmpFactory and windowCmpFactory are (lines #8,9)
But I don't udnerstnad what's going on later. Regarding lines #11,#12 - the docs says "creates a new component".
Questions :
1 - line #12 : What does [[contentCmpt.location.nativeElement]] do ? (the docs says its type is projectableNodes?: any[][] - What do they mean ??)
2 - line #14 : What does [[windowCmpt.location.nativeElement]] do ?
3 - line #16,#17 : what and why do I need them if I already did appendChild ? (docs says : Attaches a view so that it will be dirty checked. - so ?).
PLUNKER

Answers:
1) Angular takes ComponentFactory and create component instance with given element injector and with array of projectable nodes
windowCmpFactory.create(this._injector, [[contentCmpt.location.nativeElement]]);
1.1 Element Injector will be used when angular will resolve dependency
const value = startView.root.injector.get(depDef.token, NOT_FOUND_CHECK_ONLY_ELEMENT_INJECTOR);
Here is also simple illustration of dependency resolution algorithm for app without lazy loading. With lazy loading it will look a litte more complicated.
For more details see design doc element injector vs module injector
1.2 Projectable nodes are the node elements, which are "projected"(transcluded) in the ng-content that we have in the template of our component.
In order to project something our component template has to contain ng-content node.
#Component({
selector: 'modal-window',
template: `
<div class="modal-dialog">
<div class="modal-content">
<ng-content></ng-content> // <== place for projection
</div>
</div>
`
})
export class ModalWindow {
We can use component above in parent component template as follows:
<modal-window>
<modal-content></modal-content>
<div>Some other content</div>
</modal-window>
So our final result will look like:
<modal-window>
<div class="modal-dialog">
<div class="modal-content">
<modal-content></modal-content> // our projectable nodes
<div>Some other content</div> // replaced ng-content
</div>
</div>
</modal-window>
So when we're passing projectable nodes to create method
windowCmpFactory.create(this._injector, [[contentCmpt.location.nativeElement]]);
we do the same things as described above.
We'are getting reference (contentCmpt.location) to the host element of created early contentCmpt component. This is modal-content element. And then angular will do all magic to project it in ng-content place.
In example above i added one div
<modal-window>
<modal-content></modal-content>
<div>Some other content</div> <== here
</modal-window>
So the real code should looks like:
let div = document.createElement('div');
div.textContent = 'Some other content';
windowCmpFactory.create(this._injector, [[contentCmpt.location.nativeElement, div]]);
In conclusion Why is projectableNodes an any[][]?
2) During the next line
document.querySelector('body').appendChild(windowCmpt.location.nativeElement);
we're getting reference to created in memory modal-window element. ComponentRef allows us to do this because it stores reference to the host element in location getter
export abstract class ComponentRef<C> {
/**
* Location of the Host Element of this Component Instance.
*/
abstract get location(): ElementRef;
and then inseting it in document.body tag as last child. So we see it on the page.
3) Let's say our ModalContent has not just static content but will perform some operations for interaction.
#Component({
selector: 'modal-content',
template: `
I'm beeing opened as modal! {{ counter }}
<button (click)="counter = counter + 1">Increment</button>
`
})
export class ModalContent {
counter = 1;
}
If we remove
this._appRef.attachView(contentCmpt.hostView);
then our view will not being updated during change detection cycle because we created view via ComponentFactory.create and our view is not part of any item in change detection tree (unlike creation via ViewContainerRef.createComponent). Angular opened API for such purposes and we can easily add view to root views https://github.com/angular/angular/blob/master/packages/core/src/application_ref.ts#L428 and after that our component will be updated during Application.tick https://github.com/angular/angular/blob/master/packages/core/src/application_ref.ts#L558

Related

Is there an Angular way of doing: document.getElementById(id).style.display = "none";

I have a div with the id of 1. I'm trying to set the display to none dynamically. Is there an Angular way of doing this. Currently, I'm using vanilla javascript. I was asking about doing this dynamically because there will be over 60 divs that will be created from an array.
In my html
<div *ngFor="let item of items; i = index;">
<div id={{i}} (click)=hideDiv()></div>
</div>
In my method
hideDiv() {
return document.getElementById('1').style.display = "none";
}
That works but I'm looking for the Angular way of doing the above.
It was suggested that I use #ViewChild. Here's what I've changed. I can't use a Template Reference Variable as the html divs are created dynamically. Unless someone can let me know how to create the template variables dynamically. Although I don't think it's possible to create template variables with a loop.
#ViewChild('imgId', { static: true }) elementRef: ElementRef<HTMLDivElement>;
imgId: string;
Then in the method I have:
this.imgId = event.path[0].attributes[1].value;
this.elementRef.nativeElement.style.display = "none";
The event.path[0].attributes[1].value gets me the id of the image. The imgId shows when I console log it. It's still not changing the display on the div to none. Also I'm getting the error:
Cannot read properties of undefined (reading 'nativeElement')
Yes, you can use the ViewChild query in Angular to do this. In your component, define a query like this:
#ViewChild('#1') elementRef: ElementRef<HTMLDivElement>;
Implement the AfterViewInit interface in your component, and inside it, use this:
this.elementRef.nativeElement.style.display = "none";
You can simply use ngIf for this
Component
shouldDisplay: boolean = true;
hide(): void {
this.shouldDisplay = false;
}
show(): void {
this.shouldDisplay = true;
}
Html
<button (click)="hide()">Hide</button>
<button (click)="show()">Show</button>
<div *ngIf="shouldDisplay">this is the content</div>
Here is the working example
This is the Angular way:
template
<div *ngIf="showMe"></div>
or
<div [hidden]="!showMe"></div>
TypeScript:
showMe: boolean;
hideDiv() {
this.showMe = false;
}
For dynamic items where your don't know how many you will get the best approach would be to add a directive that would store and adjust that for you:
#Directive({ selector: '[hide-me]' })
export class HideDirective {
#Input() id!: string;
#HostBinding('style.display')
shouldShow: string = '';
}
then in your component just address them by ID:
#Component({
selector: 'my-app',
styleUrls: ['./app.component.css'],
template: `
<div *ngFor="let item of items; let index = index;">
<div hide-me id="{{index}}" (click)="hideDiv(index)">Some value</div>
</div>
`,
})
export class AppComponent {
#ViewChildren(HideDirective) hideDirectives!: QueryList<HideDirective>;
items = [null, null, null];
hideDiv(id: number) {
this.hideDirectives.find((p) => p.id === id.toString()).shouldShow = 'none';
}
}
Stackblitz: https://stackblitz.com/edit/angular-ivy-pnrdhv?file=src/app/app.component.ts
An angular official example: https://stackblitz.com/edit/angular-ivy-pnrdhv?file=src/app/app.component.ts
How about passing the div reference to the hideDiv method directly in the Dom using a template variable like this.
<div *ngFor="let item of items; i = index;">
<div #divElement (click)=hideDiv(divElement)></div>
And in your hide div method you will have access to the element directly
hideDiv(div) { div.style.display = "none";}
Here is a Stackblitz example
https://stackblitz.com/edit/angular-ivy-w1s3jl
There are many ways to do this, but in my opinion this is a simple solution the achieves your goal with less code.
PS:
It is always recommended to use the angular Renderer2 to manipulate Dom elements. This service has the method setStyle which you can use for your code.

Angular Updating Parent div Class when Click

I have the following code and i wish to update the parent class when click on the image. The image will call "SelectVariation" method when clicked. Is there any way to do this?
component.html :
<div class="clr-row">
<ng-container *ngFor="let variOption of product.variOptions">
<div class="card clickable clr-col-2 variationCard"
*ngFor="let variOptionTwo of variOption.variOptionTwos"> //Update this class
<div class="card-img" (click)="selectVariation(variOptionTwo.id, $event)">
<clr-tooltip>
<img src="{{variOptionTwo.url}}" class="variationImgScale" clrTooltipTrigger>
<clr-tooltip-content clrPosition="top-right" clrSize="m" *clrIfOpen>
<span>{{variOption.optName}} - {{variOptionTwo.optName}}</span>
</clr-tooltip-content>
</clr-tooltip>
</div>
</div>
component.ts :
selectVariation(id: number, event: any) {
//Update parent class
}
In the child component use
#Output() variableHere = new EventEmitter<>();
this.variableHere.emit(this.variableToSend);
Then in the parent associate the variable to a method in html child template definition:
<app-child (variableHere)="manageVariable($event)"></app-achild>
In the parent component define the method and do a variable equels the result of the method for example:
manageVariable(event) {
this.variableToUpdate = event;
}
If you have to check if the variable has changed his state call what you need to check in an ngDoCheck().
Take the advantage of the EventEmitter in angular with output
parent.component.html
<my-child-comp (onSelectVariation)="myVariation($event)" ></my-child-comp>
parent.component.ts
myVariation(myVars) {
console.log(myVars)
}
child.component.html
<button (click)="onSelectVariation.emit(myData)">trigger variation</button>
child.component.ts
#Output() onSelectVariation = new EventEmitter();
Name which you have defined in the output should be used as a event in it host element in parent

How to use ContentChild annotation in Angular2

I try to make my own component that needs to have reference to a child element define by user.
In my html template i have this :
<fn-photo-editor fxLayout="row" fxLayoutGap="20px" class="coloredContainerX box">
<div fxFlex.gt-sm="80" fxFlex="33" fxLayout="row" fxLayoutAlign="center center">
<canvas fnPhotoEditorCanvas height="1080px" width="1920px" fxFlex dropzone="copy"></canvas>
</div>
</fn-photo-editor>
In my Component.ts I have this :
#ContentChild(FnPhotoEditorCanvasDirective) canvas:HTMLCanvasElement;
and in my component template, just :
<ng-content></ng-content>
I use this directive on the canvas to get it's reference, since just using #ContentChild('canvas') seems to not work.
#Directive({
selector: '[fnPhotoEditorCanvas]',
})
export class FnPhotoEditorCanvasDirective { }
So its this code, i get a reference to a FnPhotoEditorCanvasDirective object but it does not contains or represent a canvas object.
What i'm missing?
#ContentChild('canvas') works only if the element has a template variable #canvas
<canvas #canvas fnPhotoEditorCanvas height="1080px" width="1920px" fxFlex dropzone="copy"></canvas>
This query
#ContentChild(FnPhotoEditorCanvasDirective) canvas:HTMLCanvasElement;
will return the instance of the FnPhotoEditorCanvasDirective, not a HTMLCanvasElement.
Perhaps you want
#ContentChild(FnPhotoEditorCanvasDirective, {read: ElementRef}) canvas:HTMLCanvasElement;
then you can access the HTMLCanvasElement like
ngAfterContentInit() {
this.canvas.nativeElement...
}
See also angular 2 / typescript : get hold of an element in the template

angular 2 bind to component selector

I have a component that I needs to be hidden when a property is true. Is there a way to solve this within the component itself.
Example:
#Component({
selector: 'prio-tab',
changeDetection: ChangeDetectionStrategy.OnPush,
template:
`
<div [hidden]="!active">
stuff
</div>
`
})
export class PrioTabComponent {
#Input() title;
active:boolean = false;
}
Here I would like to have the actual "prio-tab" element to depend on active-flag. Not just the content inside prio-tab.
Or is it maybe possible to use itself when declaring the prio-tab tag, like this:
<prio-tab [hidden]="this.active">
stuff
</prio-tab>
I guess a working solution would be to create a reference to the prio-tab component in its parent and then go through the parent. But how would I do if I have multiple prio-tab's ?
You can use #HostBinding()
export class PrioTabComponent {
#Input() title;
#HostBinding('hidden')
active:boolean = false;
}

Element class names with different values in Angular2

I'm trying to update multiple circle bars on a array of data coming from the database.
Here is my html:
<div class="row pie-charts">
<ba-card *ngFor="let goal of pGoal; let i = index" class = "pie-chart-item-container col-xlg-3 col-lg-3 col-md-6 col-sm-12 col-xs-12">
<div class="pie-chart-item">
<div class = "pie-chart-item">
<div class="circle-{{i}}"></div>
</div>
</div>
<div class="description">
<div>{{ goal.Name }}</div>
<div class="description-stats">{{ goal.GoalDescription }}</div>
</div>
<i class="chart-icon i-person"></i>
</ba-card>
</div>
Here is my angular code:
ngOnInit() {
this._personalgoalsService.getGoalSummary()
.subscribe( result => {
this.pGoal = result;
jQuery('.circle-0').circliful({
percent: 33
});
jQuery('.circle-1').circliful({
percent: 88
});
})
}
If I hard code the class name when I run circliful (or any other js chart) they all get updated with the same values. There must be easy way to create multiple objects of the same class names with different values. So I tried to use the ngFor and add the - index number. It seems when I create a class or id dynamically in html it doesn't get created correctly in the dom and then I can't find it in the js.
Thanks,
Edit:
Basically I was trying to update the same "element" with different values. I wasn't sure how to create multiple circle graphs of the same type with different values.
<div class="row pie-charts">
<circle *ngFor="let goal of goals | async" [goal]="goal"></circle>
</div>
So I created a component and passed in my goal object. But I still wasn't getting a unique element to create the circle. Actually my array of goals was 2, but I was getting 4 graphs drawn with the same default value.
Then I created a CircleDirective :
#Directive(
{
selector: '[myCircle]'
}
)
export class CircleDirective implements OnInit{
#Input() goal : iPersonalGoalModel;
private el: HTMLElement;
constructor(el: ElementRef) {
this.el = el.nativeElement;
}
ngOnInit() {
jQuery(this.el).circliful({
percent : this.goal.GoalCompletionPercentage
});
}
}
#Component({
selector: 'circle',
encapsulation: ViewEncapsulation.None,
directives: [CircleDirective],
providers: [],
styles: [require('./circle.css')],
template: require('./circle.html')
})
export class CircleComponent implements OnInit {
#Input() goal : iPersonalGoalModel;
ngOnInit() {
console.log(this.goal.GoalCompletionPercentage);
}
}
So now my component is using the circle directive. This directive is called in the circle.html.
<div myCircle
class="circle"
[goal]="goal"
data-dimension="250"
data-info="Sweet"
data-width="30"
data-fontsize="38"
data-fgcolor="#61a9dc"
data-bgcolor="#eee"
data-fill="#ddd"
data-total="100"
>
</div
Notice two things here. I'm using the attribute myCircle and I'm passing the goal through to the circle directive. I know there are some ways to clean this up, but this works. It only displays two circle graphs and with the two correct values.
The frustration was I knew what the problem was, but I ran around the solution. The problem right now there is multiple solutions for the same problem that are simplistic in nature. I hope this helps someone in the future.

Categories

Resources