I have created dynamic component instances by selecting pre-existing components. For example,
#Component({
selector: 'dynamic-component',
template: `<div #container><ng-content></ng-content></div>`
})
export class DynamicComponent {
#ViewChild('container', {read: ViewContainerRef}) container: ViewContainerRef;
public addComponent(ngItem: Type<WidgetComponent>,selectedPlugin:Plugin): WidgetComponent {
let factory = this.compFactoryResolver.resolveComponentFactory(ngItem);
const ref = this.container.createComponent(factory);
const newItem: WidgetComponent = ref.instance;
newItem.pluginId = Math.random() + '';
newItem.plugin = selectedPlugin;
this._elements.push(newItem);
return newItem;
}
}
My pre-existed components are ChartWidget and PatientWidget which extended the class WidgetComponent that I wanted to add in the container. For example,
#Component({
selector: 'chart-widget',
templateUrl: 'chart-widget.component.html',
providers: [{provide: WidgetComponent, useExisting: forwardRef(() => ChartWidget) }]
})
export class ChartWidget extends WidgetComponent implements OnInit {
constructor(ngEl: ElementRef, renderer: Renderer) {
super(ngEl, renderer);
}
ngOnInit() {}
close(){
console.log('close');
}
refresh(){
console.log('refresh');
}
...
}
chart-widget.compoment.html (using primeng Panel)
<p-panel [style]="{'margin-bottom':'20px'}">
<p-header>
<div class="ui-helper-clearfix">
<span class="ui-panel-title" style="font-size:14px;display:inline-block;margin-top:2px">Chart Widget</span>
<div class="ui-toolbar-group-right">
<button pButton type="button" icon="fa-window-minimize" (click)="minimize()"</button>
<button pButton type="button" icon="fa-refresh" (click)="refresh()"></button>
<button pButton type="button" icon="fa-expand" (click)="expand()" ></button>
<button pButton type="button" (click)="close()" icon="fa-window-close"></button>
</div>
</div>
</p-header>
some data
</p-panel>
data-widget.compoment.html (same as chart-widget using primeng Panel)
#Component({
selector: 'data-widget',
templateUrl: 'data-widget.component.html',
providers: [{provide: WidgetComponent, useExisting: forwardRef(() =>DataWidget) }]
})
export class DataWidget extends WidgetComponent implements OnInit {
constructor(ngEl: ElementRef, renderer: Renderer) {
super(ngEl, renderer);
}
ngOnInit() {}
close(){
console.log('close');
}
refresh(){
console.log('refresh');
}
...
}
WidgetComponent.ts
#Component({
selector: 'widget',
template: '<ng-content></ng-content>'
})
export class WidgetComponent{
}
Now I added the components by selecting a component from the existed components (e.g. chart-widget and data-widget) in the following way and stored the instances into an array.
#Component({
templateUrl: 'main.component.html',
entryComponents: [ChartWidget, DataWidget],
})
export class MainComponent implements OnInit {
private elements: Array<WidgetComponent>=[];
private WidgetClasses = {
'ChartWidget': ChartWidget,
'DataWidget': DataWidget
}
#ViewChild(DynamicComponent) dynamicComponent: DynamicComponent;
addComponent(): void{
let ref= this.dynamicComponent.addComponent(this.WidgetClasses[this.selectedComponent], this.selectedComponent);
this.elements.push(ref);
this.dynamicComponent.resetContainer();
}
}
Now, I am facing problem to render the components using innerHtml in main.component.html. It render the html but I am not able to use button click event or other event on it. I have also tried to render chart using primeng but its also not working.
main.component.html
<dynamic-component [hidden]="true" ></dynamic-component>
<widget *ngFor="let item of elements">
<div [innerHTML]="item._ngEl.nativeElement.innerHTML | sanitizeHtml">
</div>
</widget>
I have also implemented a sanitizeHtml Pipe but its giving still same result. So, as I understand innerHTML is only showing the html data but I can't use any button event as well as the js chart. I have also tried to show the items like this {{item}} under tag. But it display like a text [object object]. So, could anyone give a solution for it? How can I render the components allowing the button events and js chart? Thanks.
EDIT: See my Plunker here https://plnkr.co/edit/lugU2pPsSBd3XhPHiUP1?p=preview
You can see here, it is possible to add chart or data widget dynamically and I am showing it using innerHTML. So, the button events are not working here. If I coding like {{item}} then it shows [object object] text. You can also see in console the component array data. The main Question is, How can I active the button events on it (e.g. if i click close or refresh button then it will call the related functions)?
I would create structural directive like:
view.directive.ts
import { ViewRef, Directive, Input, ViewContainerRef } from '#angular/core';
#Directive({
selector: '[view]'
})
export class ViewDirective {
constructor(private vcRef: ViewContainerRef) {}
#Input()
set view(view: ViewRef) {
this.vcRef.clear();
this.vcRef.insert(view);
}
ngOnDestroy() {
this.vcRef.clear()
}
}
then
app.component.ts
private elements: Array<{ view: ViewRef, component: WidgetComponent}> = [];
...
addComponent(widget: string ): void{
let component = this.dynamicComponent.addComponent(this.WidgetClasses[widget]);
let view: ViewRef = this.dynamicComponent.container.detach(0);
this.elements.push({view,component});
this.dynamicComponent.resetContainer();
}
and
app.component.html
<widget *ngFor="let item of elements">
<ng-container *view="item.view"></ng-container>
</widget>
So i have just moved view from dynamic component container to desired place.
Plunker Example
Related
I want to add navigation path to all my buttons in the left menu (which is not the main menu).
I am getting the menu items name as #Input. I have created a dictionary for all the items name and their navigation path.
Here is the HTML:
<div class="row-styles" id="elements" *ngFor="let item of elements">
<button *ngIf="(item.action !== NO_ACCESS )" class="inner-children" routerLinkActive="active" id="inner-children"
[routerLink]="">
<span>{{item.resource}}</span>
</button>
</div>
Here is the TS file
import { Component, Input, OnInit } from '#angular/core';
#Component({
selector: 'apm-menu-resource',
templateUrl: './menu-resource.component.html',
styleUrls: ['./menu-resource.component.less']
})
export class MenuResourceComponent implements OnInit {
#Input() public elements = [];
constructor() {
const menupath = new Map<string, string>();
menupath.set('General', '/Adigem/config/general');
menupath.set('Messaging', '/Adigem/config/messaging');
menupath.set('Server', '/Adigem/config/email/server');
menupath.set('Alerting', '/Adigem/config/email/alert');
menupath.set('Network', '/Adigem/config/network');
menupath.set('Inventory', '/Adigem/config/inventory');
menupath.set('External port', '/Adigem/config/snmp/external-port');
menupath.set('Cloud Data', '/Adigem/config/clouddata');
menupath.set('Performance', '/Adigem/config/Performance');
menupath.set('CFG', '/Adigem/config/cfg');
menupath.set('System', '/Adigem/config/system');
console.log(menupath);
}
ngOnInit() {
}
}
I want to know what to add in the router link in the HTML so that it navigates to the proper menu item.
If you have access to the elements array, that's being passed to the component, you could simplify things a lot - you just add the target path to each of the items and your MenuResourceComponent won't have to deal with any path-related logic.
From your snippets I infer that there is a resource property, which is the element's title. If so, the elements array can be modified like this:
elements = [
{resource:'General', path: '/Adigem/config/general'},
{resource:'Messaging', path: '/Adigem/config/messaging'},
...
]
and then in the template:
<div class="row-styles" id="elements" *ngFor="let item of elements">
<button *ngIf="(item.action !== NO_ACCESS )" class="inner-children"
routerLinkActive="active" id="inner-children"
[routerLink]="item.path">
<span>{{item.resource}}</span>
</button>
</div>
However, if you have no other options and need to menupath map, then you can make it a class field:
import { Component, Input } from '#angular/core';
#Component({
selector: 'apm-menu-resource',
templateUrl: './menu-resource.component.html',
styleUrls: ['./menu-resource.component.less']
})
export class MenuResourceComponent{
#Input() public elements = [];
menupath = new Map<string, string>();
constructor() {
this.menupath.set('General', '/Adigem/config/general');
this.menupath.set('Messaging', '/Adigem/config/messaging');
this.menupath.set('Server', '/Adigem/config/email/server');
this.menupath.set('Alerting', '/Adigem/config/email/alert');
this.menupath.set('Network', '/Adigem/config/network');
this.menupath.set('Inventory', '/Adigem/config/inventory');
this.menupath.set('External port', '/Adigem/config/snmp/external-port');
this.menupath.set('Cloud Data', '/Adigem/config/clouddata');
this.menupath.set('Performance', '/Adigem/config/Performance');
this.menupath.set('CFG', '/Adigem/config/cfg');
this.menupath.set('System', '/Adigem/config/system');
console.log(this.menupath);
}
}
and the route binding looks like:
[routerLink]="menupath.get(item.resource)"
I wouldn't encourage the second solution, because you will have to handle the potential case where you receive an item, which is unknown for your menupath map.
Also I have a concern with the NO_ACCESS constant that you use in your template. There is no such property of the component, so this probably breaks the compilation.
I have a dialog box that is displaying a separate child component. The child component is below:
#Component({
selector: 'userEdit',
templateUrl: './edituser.component.html',
styleUrls: ['./edituser.component.css']
})
export class EditUserComponent implements OnInit {
public theName: string;
public theAddress: string;
constructor() {
this.theName = '';
this.theAddress = '';
}
ngOnInit() {
}
}
The dialog box code and template are below:
#Component({
selector: 'app-userdialog',
templateUrl: './userdialog.component.html',
styleUrls: ['./userdialog.component.css']
})
export class UserDialogComponent implements OnInit {
#ViewChild('userEdit', {static: false})
userEdit: EditUserComponent;
constructor(
public dlgRef: MatDialogRef<UserDialogComponent>,
#Inject(MAT_DIALOG_DATA) public theData: UsrStuff) { }
ngOnInit() {
}
ngAfterViewInit() {
console.log('Name: ' + this.userEdit.theName);
}
addUser() {
// TODO: implement adding a user
}
closeBox() {
this.dlgRef.close();
}
}
and
<div id="attribdlg">
<h3>Add New User</h3>
<userEdit theName="" theAddress=""></userEdit>
<mat-dialog-actions>
<button mat-raised-button color="primary" (click)="addUser()">Add User</button>
<button mat-raised-button color="primary" (click)="closeBox()">Done</button>
</mat-dialog-actions>
</div>
Based on the documentation and examples Ihave seen, this setup should enable me to print to the console the value pf userEdit's theName property in the ngAfterViewInit() function.
Unfortunately, this does not appear to be working.When the console log is called, I get the following failure message:
null: TypeError: Cannot read property 'theName' of undefined
Obviously, there is some kind of initialization of the child component that is supposed to happen, but I do not see this being done anywhere in the documentation! I am missing something.
How can I get this child component and its properties available to my dialog?
Two options:
Set an id to the component you wish to have with ViewChild():
TypeScript:
#ViewChild('userEdit', {static: false})
HTML:
<userEdit #userEdit theName="" theAddress=""></userEdit>
Select by directive or component:
TypeScript:
#import { EditUserComponent } from '...';
#ViewChild(EditUserComponent, {static: false})
HTML:
<userEdit theName="" theAddress=""></userEdit>
I highly recommend you to use app perfix for the component's selector!!!
#Component({
...
selector: 'app-user-edit',
...
})
I would like to set the body of <ng-content> while instantiating a component dynamically using ComponentFactoryResolver.
I see that I can get access to input & output using ComponentRef, but not a way to set <ng-content>.
Please note <ng-content> I'm planning on setting can contain simple text/can span dynamically created components
#Component({
selector: 'app-component-to-project',
template: `<ng-content></ng-content>`
})
export class ComponentToProject implements AfterContentInit {
ngAfterContentInit() {
// We will do something important with content here
}
}
#Directive({
selector: 'appProjectionMarker'
})
export class ProjectionMarkerDirective implements OnInit {
constructor(private viewContainerRef: ViewContainerRef, private componentFactoryResolver: ComponentFactoryResolver) {
}
ngOnInit() {
const componentFactory: ComponentFactory<ComponentToProject> = this.componentFactoryResolver.resolveComponentFactory(ComponentToProject);
const componentRef: ComponentRef<ComponentToProject> = this.viewContainerRef.createComponent(componentFactory);
// Question: How to set content before the child's afterContentInit is invoked
}
}
#Component({
selector: 'appTestComponent',
template: `<div appProjectionMarker></div>`
})
export class TestComponent {}
There is the projectableNodes parameter for the vcRef.createComponent method
createComponent<C>(componentFactory: ComponentFactory<C>, index?: number, injector?: Injector, projectableNodes?: any[][], ngModule?: NgModuleRef<any>): ComponentRef<C>;
You can use it to dynamically inject one component into another.
Let's say we have the following component
#Component({
selector: 'card',
template: `
<div class="card__top">
<h2>Creating a angular2 component with ng-content dynamically</h2>
</div>
<div class="card__body">
<ng-content></ng-content>
</div>
<div class="card__bottom">
<ng-content></ng-content>
</div>
`
})
export class CardComponent {}
We want to create it dynamically and insert some controls to its ng-content locations. It could be done like follows:
const bodyFactory = this.cfr.resolveComponentFactory(CardBodyComponent);
const footerFactory = this.cfr.resolveComponentFactory(CardFooterComponent);
let bodyRef = this.vcRef.createComponent(bodyFactory);
let footerRef = this.vcRef.createComponent(footerFactory);
const cardFactory = this.cfr.resolveComponentFactory(CardComponent);
const cardRef = this.vcRef.createComponent(
cardFactory,
0,
undefined,
[
[bodyRef.location.nativeElement],
[footerRef.location.nativeElement]
]
);
Plunker Example
See also
Why is projectableNodes an any[][]?
Pawel Kozlowski - Reactive parenting with Angular 2 - NG-BE 2016
I would like to create a ViewRef from markup that is dynamically inserted into a template. Is this possible based on the following code sample?
template.html:
<ng-container *ngTemplateOutlet="dynamic; context: cntx"></ng-container>
<ng-template #dynamic>
<div [innerHTML]="markup"></div>
</ng-template>
Injected markup from API call to bind to div's innerHTML attribute:
<div>
<div id="forViewRef"></div>
</div>
component.ts:
#ContentChild('#forViewRef', { read: ViewContainerRef }): someHndl;
private _nativeElem: any;
constructor(
private sanitizer: DomSanitizer,
private _vcRef: ViewContainerRef,
private _resolver: ComponentFactoryResolver) {
// to ensure template has been created, #dynamic
this._nativeElem = this._vcRef.element.nativeElement;
}
// listen to lifecycle hook
ngAfterContentChecked() {
if (this._nativeElem !== undefined)
// childContent ref is undefined
console.log(this.someHndl);
// markup is in the DOM
console.log(this._nativeElem.querySelectorAll('#forViewRef'));
}
To create component dynamically inside <div id="forViewRef"></div> you can do the following:
Let's say we need to load the following component
#Component({
selector: 'dynamic-comp',
template: `
<h2>Dynamic component</h2>
<button (click)="counter = counter + 1">+</button> {{ counter }}
`
})
export class DynamicComponent {
counter = 1;
}
so first add it to declarations and entryComponents array of your #NgModule
...
declarations: [ ..., DynamicComponent ],
entryComponents: [ DynamicComponent ],
bootstrap: [ AppComponent ]
})
export class AppModule { }
after that create
template.html
<button (click)="createComponent()">Create component</button>
<div id="forViewRef"></div>
and finally write
component.ts
export class AppComponent {
compRef: ComponentRef<DynamicComponent>;
constructor(private injector: Injector,
private resolver: ComponentFactoryResolver,
private appRef: ApplicationRef) {}
createComponent() {
const compFactory = this.resolver.resolveComponentFactory(DynamicComponent);
this.compRef = compFactory.create(this.injector, null, '#forViewRef');
this.appRef.attachView(this.compRef.hostView);
}
ngOnDestroy() {
if(this.compRef) {
this.compRef.destroy();
}
}
}
I use appRef.attachView in order to include dynamic component to change detection cycle
Plunker Example
See also
Display custom tag in google maps infowindow angular2
Angular2 - Component into dynamicaly created element
Add a component dynamically to a child element using a directive
I want to show a popover as the user clicks on the input field which works fine but I want the data-content attribute of that popover be coming from the template of a child component. Here is an example:
parent.ts
import {Component,AfterViewInit} from 'angular2/core';
import {bootstrap} from 'angular2/platform/browser';
import {ChildComponent} from './child_test.ts';
#Component({
selector: 'app',
template: `<input type='text' data-toggle="popover" data-trigger="focus" data-placement="bottom" [attr.data-content]="getPopoverContent()" />`,
providers: [ChildComponent]
})
class AppComponent implements AfterViewInit{
constructor(private _child: ChildComponent) {}
getPopoverContent(){
return this._child; //returning empty object instead of child template
}
ngAfterViewInit(){
$("input").popover();
}
}
bootstrap(AppComponent);
child.ts
import {Component} from 'angular2/core';
#Component({
selector: "child-component",
template: "<div>Popover content from child.</div>"
})
export class ChildComponent{};
Should I use DynamicComponentLoader instead of dependency injection? if so then how can I achieve this?
Here's a workaround:
Assign the a temporary variable to the component you want to display
<transaction-filter #popoverComponent></transaction-filter>
Important: The component above must expose an ElementRef in its definition
constructor(public el: ElementRef) {}
Create the element that will show the popover
<button class="btn-sm btn-link text-muted"
data-animation="true"
data-placement="bottom"
title="Create Rule"
[popover]="popoverComponent">
Create Rule...
</button>
Now the popover directive itself:
/// <reference path="../../typings/tsd.d.ts"/>
import 'bootstrap'
import { Directive, ElementRef, Input} from 'angular2/core'
declare var $: JQueryStatic;
#Directive({
selector: '[popover]',
})
export class PopoverDirective {
#Input('popover') _component: any
_popover: JQuery
constructor(private _el: ElementRef) { }
ngOnInit() {
// Hide the component
this._component.el.nativeElement.style.display = "none"
// Attach it to the content option
this._popover = $(this._el.nativeElement)
.popover({
html: true,
content: this._component.el.nativeElement
})
// When the below event fires, the component will be made visible and will remain
this._popover.on('shown.bs.popover', () => {
this._component.el.nativeElement.style.display = "block"
})
}
}
One problem is that binding to an attribute stringifies the value
[attr.data-content]
therefore this approach won't work.
It seems the Bootstrap popover expects a string, therefore this would be fine but stringifying an Angular component won't you give it's HTML.