I'm having the following directive that adds dynamic component to ng-container
#Directive({
selector: '[appAddingDirective]'
})
export class AddingDirective {
constructor(protected vc: ViewContainerRef) { }
public addComponent(factory: ComponentFactory<any>, inputs: any): void {
this.vc.clear();
const ref: ComponentRef<any> = this.vc.createComponent(factory);
Object.assign(ref.instance, inputs); // can't find more elegant way to assign inputs((
ref.instance.ngOnInit(); // IMPORTANT: if I remove this call ngOnInit will not be called
}
}
The directive is used in an obvious way.
#Component({
selector: 'app-wrapper',
template: `<ng-container appAddingDirective></ng-container>`
})
export class WrapperComponent implements AfterViewInit{
#ViewChild(DynamicItemDirective)
private dynamicItem: DynamicItemDirective;
constructor() { }
ngAfterViewInit(): void {
// hope it doesn't matter how we get componentFactory
this.dynamicItem.addComponent(componentFactory, {a: '123'});
}
}
Finally in a component that is loaded dynamically I have
#Component({
selector: 'app-dynamic',
template: '<p>Dynamic load works {{ a }}</p>'
})
export class DynamicComponent implements OnInit {
#Input() a: string;
constructor() {}
ngOnInit(): void {
console.log(this.a);
debugger;
}
}
Here are my questions.
If I remove ref.instance.ngOnInit() call in AddingDirective, I do not get in ngOnInit of DynamicComponent (debugger and console.log do not fire up). Do component lifecycle hooks work in a component that is created and attached dynamically? What is the best way to make these hooks work?
I don't see rendered string Dynamic load works 123 still if I remove {{ a }} in template (template: '<p>Dynamic load works</p>'), Dynamic load works is rendered as it should. What is the reason and how can I fix that?
Is there a better way to assing inputs than doing Object.assign(ref.instance, inputs) as above?
PS. I'm using Angular 11
Related
I have components called app.component which is the main component in the angular project.
Navigation to customer component is done by routing.
And
Folder structer
src\app
- app.component.html
- app.component.ts
and
src\app\components\customer
- customer.component.html
- customer.component.ts
In my app.component.html
<div class="top-container" #topContainerRef>
<router-outlet></router-outlet>
</div>
In my customer.component.ts
I want to get reference of the top most container div which is contained in app.components
I want to replace
document.getElementsByClassName('top-container')[0].scrollTop = some values
with something similar to
#ViewChild('topContainerRef', { read: ElementRef, static: false }) topContainerRef: ElementRef;
this.topContainerRef.nativeElement.scrollTop= "some value" //here the topContainerRef is undefined
Is there any way i can use elementRef instead of classname or Id's.
You cannot use ViewChild for the #topContainerRef to get a reference of this element, because it is not rendered by your CustomerComponent.
You either need to get the reference of this element inside the app component itself and find a way to pass it to all the other children that might need it (not recommended).
Or you can just build a service and use that to "request" the scrollTop change by whichever component has access to this element (in your case the app component).
I would do it something like this:
export class AppContainerService {
private scrollTopSource = new ReplaySubject<number>(1);
public scrollTop$ = this.scrollTopSource.asObservable();
public updateScrollTop(value: number) {
this.scrollTopSource.next(value);
}
}
Inside your CustomerComponent:
public class CustomerComponent implements OnInit {
// ...
constructor(private containerService: AppContainerService) {
}
ngOnInit() {
this.containerService.updateScrollTop(/* whatever value you need */);
}
// ...
}
And finally, the AppComponent that will react to the scrollTop changes:
export class AppComponent implements AfterViewInit {
#ViewChild('topContainerRef', { read: ElementRef, static: false }) topContainerRef: ElementRef;
private subscriptions = new Subscription();
constructor(private containerService: AppContainerService) {
}
ngAfterViewInit() {
this.subscriptions.add(this.containerService.scrollTop$.subscribe((value: number) => {
this.topContainerRef.nativeElement.scrollTop = value;
}));
}
ngOnDestroy() {
this.subscriptions.unsubscribe();
}
}
Don't forget about unsubscribing inside ngOnDestroy. This is important so that you don't have memory leaks
My starting point is some kind of HTML snippet, which is loaded at runtime from my backend, with placeholder tags:
<table>
<tbody>
<tr>
<td>
<span>Col1</span>
</td>
<td>
<span class="placeholderClass" data-placeholderdata="xy"></span>
</td>
</tr>
</tbody>
</table>
I want to display the HTML snippet but replace all <span class="placeholderClass"/> elements with my own angular component.
So far I read about the Dynamic component loader from angular, but as far as I understood this only allows my to load an dynamic component on a fixed position, and not a fixed component on a dynamic position.
Also I tried following post:
Angular 2 Dynamically insert a component into a specific DOM node without using ViewContainerRef
Section Component is my main component where I store the HTML and Value Component the component I want to replace.
Section Component:
HTML
<div [innerHTML]=sectionSnippet></div>
TS
#Component({
selector: 'section-view',
templateUrl: './section.component.html',
styleUrls: ['./section.component.sass'],
encapsulation: ViewEncapsulation.ShadowDom
})
export class SectionComponent implements OnInit {
#Input() sectionSnippetInput = '';
constructor(private sanitizer: DomSanitizer,
private resolver: ComponentFactoryResolver,
private injector: Injector,
private app: ApplicationRef) {
}
ngOnInit(): void {
this.replaceTags();
}
private replaceTags() {
let temp:HTMLDivElement = document.createElement('div');
temp.innerHTML = this.sectionSnippetInput;
let placeholders: HTMLCollectionOf<Element> = temp.getElementsByClassName('placeholderClass')
for (let i = 0; i < placeholders.length; i++) {
//dynamically create angular comp
let factory = this.resolver.resolveComponentFactory(ValueComponent);
//create instance with the value placeholder as parent
const ref = factory.create(this.injector, [], placeholders[i]);
//manually init comp
ref.instance.initElem = placeholders[i];
ref.instance.initComp();
//trigger re-rendering
ref.changeDetectorRef.detectChanges();
//attach newly created component to our angular context
this.app.attachView(ref.hostView);
}
this.sectionSnippetInput = temp.innerHTML;
}
get sectionSnippet() {
return this.sanitizer.bypassSecurityTrustHtml(this.sectionSnippetInput);
}
}
Value Component:
HTML
<span (click)="testCallback()">{{getFormatNumber()}}</span>
TS
#Component({
selector: 'value',
templateUrl: './value.component.html',
styleUrls: ['./value.component.sass']
})
export class ValueComponent implements AfterViewInit {
public param1:string|null = '';
public initElem: Element | null | undefined;
public testCallback(){
alert("Test")
}
ngAfterViewInit(): void{
this.initComp();
}
public initComp() {
// #ts-ignore
this.param1 = this.initElem.getAttribute("data-placeholderdata");
}
getFormatNumber(){
//some formatting code
return this.param1;
}
}
Which seems to work, at least for the initial rendering, but angular bindings like (click) does not work. My guess is that the newly created component is not correctly bound to the angular context, therefore it does not register the click events.
(Using angular V13)
As I am new to angular, can someone give me advice how to proceed. Is using the ComponentFactory a good idea of should I try something different?
I have a child TestComponent component as follows:
import { Component, OnInit, Input } from '#angular/core';
import { ApiService } from '../../../api.service';
#Component({
selector: 'app-test',
templateUrl: './test.component.html'
})
export class TestComponent implements OnInit {
constructor(private apiService: ApiService) { }
testDisplayMessage = 'No data to show';
ngOnInit() {
}
getMessage(param: string) {
this.callingTest = true;
this.apiService.getTest( param ).subscribe( data => {
this.setTestDisplayMessage( data );
this.callingTest = false;
}, err => {
console.log( JSON.stringify( err ) );
this.setTestDisplayMessage( 'Failed to get data' );
this.callingTest = false;
} );
}
setTestDisplayMessage( message: string ) {
this.testDisplayMessage = message;
}
}
contents of test.component.html
<p style="padding: 10px;">{{ testDisplayMessage }}</p>
Use in parent componet :
Trigger JS Code in parent component on button click,
import { TestComponent } from './test/test.component';
....
.....
#Component({
providers: [ TestComponent ],
templateUrl: 'parent.component.html'
})
export class ParentComponent implements OnInit {
...
constructor(private testComponent: TestComponent) { }
...
// Button on parent template triggers this method
getMessage() {
this.testComponent.getMessage('Hello');
}
...
}
Html tag added in parent component,
<app-test></app-test>
When I debugged above code trigger point, call to setTestDisplayMessage() happens the field testDisplayMessage in TestComponent gets changed but UI shows the old message 'No data to show', why is the message on change does not reflect on UI template? Or this is not the way it is supposed to get used? Shall I use #Input
Update:
Based on the pointers given in the following answers as well as comment sections, I changed my component as #ViewChild so in above parent component instead of passing the child component as an argument to constructor I declared it as child component using #ViewChild, so code changes as follows,
Earlier wrong code
constructor(private testComponent: TestComponent) { }
Solution
#ViewChild(TestComponent)
testComponent: TestComponent;
I found this article useful.
Use #ViewChild()
In html file:
<app-test #childComp></app-test>
In parent component.ts file
import { Component, OnInit, ViewChild } from '#angular/core';
....
.....
#Component( {
templateUrl: 'parent.component.html'
} )
export class ParentComponent implements OnInit {
#viewChild('childComp') childComp: any;
constructor() { }
...
// Button on parent template triggers this method
getMessage() {
this.childComp.getMessage('Hello');
}
...
}
Update:
Based on the pointers given in the following answers as well as comment sections, I changed my component as #ViewChild so in above parent component instead of passing the child component as an argument to constructor I declared it as child component using #ViewChild, so code changes as follows,
Earlier wrong code
constructor(private testComponent: TestComponent) { }
Solution
#ViewChild(TestComponent)
testComponent: TestComponent;
I found this article useful.
definitely use #Input() but on set method
#Input()
set someProperty(value) {
// do some code
}
now every time you pass new value here, code will run
basically, your approach is wrong, please use Input() or Services to share data between components.
however, if you want to make ur code work, the below may work
import change detector
constructor(private cdRef: ChangeDetectorRef) {
}
note: import reference ->
import { ChangeDetectorRef } from '#angular/core';
execute detect change after the value is updated
setTestDisplayMessage( message: string ) {
this.testDisplayMessage = message;
this.cdRef.detectChanges();
}
I hope this helps
In Angular 4 to dynamically create a component you can use ngComponentOutlet directive: https://angular.io/docs/ts/latest/api/common/index/NgComponentOutlet-directive.html
something like this:
Dynamic component
#Component({
selector: 'dynamic-component',
template: `
Dynamic component
`
})
export class DynamicComponent {
#Input() info: any;
}
App
#Component({
selector: 'my-app',
template: `
App<br>
<ng-container *ngComponentOutlet="component"></ng-container>
`
})
export class AppComponent {
this.component=DynamicComponent;
}
How do I pass #Input() info: any; information in this template <ng-container *ngComponentOutlet="component"></ng-container> ?
Such a feature was discussed in the pull request for ngComponentOutlet but was dropped for now.
Even the componentRef shown currently in https://angular.io/docs/ts/latest/api/common/index/NgComponentOutlet-directive.html is not public and therefore not available https://github.com/angular/angular/blob/3ef73c2b1945340ca6bd21f1790260c88698ae26/modules/%40angular/common/src/directives/ng_component_outlet.ts#L78
I'd suggest you create your own directive derived from https://github.com/angular/angular/blob/3ef73c2b1945340ca6bd21f1790260c88698ae26/modules/%40angular/common/src/directives/ng_component_outlet.ts#L72
and assign values to inputs like shown in Angular 2 dynamic tabs with user-click chosen components
this.compRef.instance.someProperty = 'someValue';
With the help of the post of #Günter Zöchbauer I solved a similar problem this way - I hope you can adapt it somehow.
First I defined some interfaces:
// all dynamically loaded components should implement this guy
export interface IDynamicComponent { Context: object; }
// data from parent to dynLoadedComponent
export interface IDynamicComponentData {
component: any;
context?: object;
caller?: any;
}
then I implemented them inside of the dynamically loaded component
dynamicLoadedComponentA.ts
// ...
export class DynamicLoadedComponentA implements IDynamicComponent {
// ...
// data from parent
public Context: object;
// ...
After that I built a new component which is responsible for the magic. Important here is that I had to register all dyn. loaded components as entryComponents.
dynamic.component.ts
#Component({
selector: 'ngc-dynamic-component',
template: ´<ng-template #dynamicContainer></ng-template>´,
entryComponents: [ DynamicLoadedComponentA ]
})
export class DynamicComponent implements OnInit, OnDestroy, OnChanges {
#ViewChild('dynamicContainer', { read: ViewContainerRef }) public dynamicContainer: ViewContainerRef;
#Input() public componentData: IDynamicComponentData;
private componentRef: ComponentRef<any>;
private componentInstance: IDynamicComponent;
constructor(private resolver: ComponentFactoryResolver) { }
public ngOnInit() {
this.createComponent();
}
public ngOnChanges(changes: SimpleChanges) {
if (changes['componentData']) {
this.createComponent();
}
}
public ngOnDestroy() {
if (this.componentInstance) {
this.componentInstance = null;
}
if (this.componentRef) {
this.componentRef.destroy();
}
}
private createComponent() {
this.dynamicContainer.clear();
if (this.componentData && this.componentData.component) {
const factory: ComponentFactory<any> = this.resolver.resolveComponentFactory(this.componentData.component);
this.componentRef = this.dynamicContainer.createComponent(factory);
this.componentInstance = this.componentRef.instance as IDynamicComponent;
// fill context data
Object.assign(this.componentInstance.Context, this.componentData.context || {});
// register output events
// this.componentRef.instance.outputTrigger.subscribe(event => console.log(event));
}
}
}
here the usage of this shiny new stuff:
app.html
<!-- [...] -->
<div>
<ngc-dynamic-component [componentData]="_settingsData"></ngc-dynamic-component>
</div>
<!-- [...] -->
app.ts
// ...
private _settingsData: IDynamicComponent = {
component: DynamicLoadedComponentA,
context: { SomeValue: 42 },
caller: this
};
// ...
I think for now you can use
https://www.npmjs.com/package/ng-dynamic-component
It is made specifically for this issue
I'm encountering a problem where if I dynamically load a component, none of the bindings in the template are working for me. As well as this the ngOnInit method is never triggered.
loadView() {
this._dcl.loadAsRoot(Injected, null, this._injector).then(component => {
console.info('Component loaded');
})
}
Dynamically loaded component
import {Component, ElementRef, OnInit} from 'angular2/core'
declare var $:any
#Component({
selector: 'tester',
template: `
<h1>Dynamically loaded component</h1>
<span>{{title}}</span>
`
})
export class Injected implements OnInit {
public title:string = "Some text"
constructor(){}
ngOnInit() {
console.info('Injected onInit');
}
}
This is my first time using dynamically loaded components so I think may be attempting to implement it incorrectly.
Here's a plunkr demonstrating the issue. Any help would be appreciated.
As Eric Martinez pointed out this is a known bug related to the use of loadAsRoot. The suggested workaround is to use loadNextToLocation or loadIntoLocation.
For me this was problematic as the component I was trying to dynamically load was a modal dialog from inside a component with fixed css positioning. I also wanted the ability to load the modal from any component and have it injected into the same position in the DOM regardless of what component it was dynamically loaded from.
My solution was to use forwardRef to inject my root AppComponent into the component which wants to dynamically load my modal.
constructor (
.........
.........
private _dcl: DynamicComponentLoader,
private _injector: Injector,
#Inject(forwardRef(() => AppComponent)) appComponent) {
this.appComponent = appComponent;
}
In my AppComponent I have a method which returns the app's ElementRef
#Component({
selector: 'app',
template: `
<router-outlet></router-outlet>
<div #modalContainer></div>
`,
directives: [RouterOutlet]
})
export class AppComponent {
constructor(public el:ElementRef) {}
getElementRef():ElementRef {
return this.el;
}
}
Back in my other component (the one that I want to dynamically load the modal from) I can now call:
this._dcl.loadIntoLocation(ModalComponent, this.appComponent.getElementRef(), 'modalContainer').then(component => {
console.log('Component loaded')
})
Perhaps this will help others with similar problems.
No need to clean component instance from DOM.
use 'componentRef' from angular2/core package to create and dispose component instance.
use show() to load the modal component at desired location and hide() to dispose the component instance before calling loadIntoLocation secondtime.
for eg:
#Component({
selector: 'app',
template: `
<router-outlet></router-outlet>
<div #modalContainer></div>
`,
directives: [RouterOutlet]
})
export class AppComponent {
private component:Promise<ComponentRef>;
constructor(public el:ElementRef) {}
getElementRef():ElementRef {
return this.el;
}
show(){
this.component = this._dcl.loadIntoLocation(ModalComponent,this.appComponent.getElementRef(), 'modalContainer').then(component => {console.log('Component loaded')})
}
hide(){
this.component.then((componentRef:ComponentRef) => {
componentRef.dispose();
return componentRef;
});
}
}