I have an angular2 app that displays chart data from a rest api.
I created the drawing code in my bulletchart.component file.
Now I want to outsource the code into a service.
But it seems like there is only one active instance of the data service.
This is my page content where I want to load the charts.
<div class="col">
<app-bulletchart [ID]="1" [apiAddress]="'http://url1'"></app-bulletchart>
</div>
<div class="col">
<app-bulletchart [ID]="2" [apiAddress]="'http://url2'"></app-bulletchart>
</div>
The template for the app.bulletchart is this:
<div class="panel test{{ID}}">
</div>
In my bulletchart.service file I change the DOM of the app-bulletchart with some methods like this:
initSvg() {
const identifier = '.test' + this.ID;
console.log("ident " + identifier);
this.svg = d3.select(identifier).append('svg')
.attr('class', 'bullet')
There are also methods that update the chart
drawRange() {
console.log("range " + this.ID);
// Update the range rects.
const range = this.g.selectAll('rect.range')
.data(this.ranges);
range.enter().append('rect')
I set the ID property of the bulletchart.service in the ngOnInit in bulletchart.component
But when I now try to use this.bulletchart.drawRange(); this method is only called for ID 1 and not for ID 2.
I don't understand why, because I thought it would do something like this:
create App-Bulletchart (ID=1) -> create instance of bulletchart.service (with ID = 1)
create App-Bulletchart (ID=2) -> create instance of bulletchart.service (with ID = 2)
Edit:
I added providers: [BulletchartService] to my bulletchart.component file and removed it from the app.module and now it works.
But why?!
you may include service provider in the component to ensure service is created for each instance of component
#Component({
...
providers:[BulletchartService]
...
})
Example
#Injectable()
export class AppService{
Id: string;
someMethod(){
console.log(this.Id);
}
}
#Component({
selector: 'my-child',
template: `<h1>Child ID {{Id}}</h1>
<button (click)="invokeService()" >Invoke service</button>
`,
providers:[AppService]
})
export class ChildComponent {
#Input() Id: string;
constructor(private svc: AppService){}
ngOnInit(){
this.svc.Id = this.Id;
}
invokeService(){
this.svc.someMethod();
}
}
#Component({
selector: 'my-app',
template: `<h1>Hello {{name}}</h1>
<my-child [Id]="1" ></my-child>
<my-child [Id]="2" ></my-child>
`
})
export class AppComponent {
name = 'Angular';
}
#NgModule({
imports: [ BrowserModule ],
declarations: [ AppComponent, ChildComponent ],
bootstrap: [ AppComponent ]
})
export class AppModule { }
Check this Plunker.
Hope this Helps!!
Related
I have created a popover module, which exports two components and three directives. I am not able to use the directives outside of the main component. If I do I get an editor error of:
No provider for MatPopoverComponent
and a browser error of:
ERROR Error: NodeInjector: NOT_FOUND [MatPopoverComponent]
Is there any way for me to use the directives outside of the component?
This current usage works:
<mat-popover>
<mat-popover-content>Some Content</mat-popover-content>
<button toggle-popover>Click Me</button>
</mat-popover>
However, I would like to use it like this:
<mat-popover>
<mat-popover-content>Some Content</mat-popover-content>
</mat-popover>
<button toggle-popover>Click Me</button>
This is one of the directives (All three are the same except for the body of the click event).
#Directive({
selector: '[toggle-popover]'
})
export class TogglePopoverDirective {
public constructor(private el: ElementRef<HTMLElement>, #Host() private popover: MatPopoverComponent) { }
#HostListener('click')
public onClick() {
this.popover.toggle(this.el)
}
}
This is the main component using a directive outside of this gives me the errors above.
#Component({
selector: 'mat-popover',
templateUrl: './popover.component.html',
styleUrls: ['./popover.component.scss']
})
export class MatPopoverComponent {
#ContentChild(MatPopoverContentComponent)
public content: MatPopoverContentComponent
public toggle(el: ElementRef<HTMLElement>) {
this.content.elementRef = el
this.content.togglePopover()
}
public open(el: ElementRef<HTMLElement>) {
this.content.elementRef = el
this.content.openPopover()
}
public close() {
this.content.closePopover()
}
}
This is the popover module:
#NgModule({
imports: [
CommonModule,
BrowserModule,
MatDialogModule
],
bootstrap: [MatPopoverComponent],
declarations: [
MatPopoverComponent,
MatPopoverContentComponent,
OpenPopoverDirective,
ClosePopoverDirective,
TogglePopoverDirective
],
exports: [
MatPopoverComponent,
MatPopoverContentComponent,
OpenPopoverDirective,
ClosePopoverDirective,
TogglePopoverDirective
]
})
export class MatPopoverModule { }
I was able to create what I was looking for by removing the #Host() decorator, and provide a template reference to the directive. It now looks like this:
<mat-popover #myPopover>
<mat-popover-content>Some Content</mat-popover-content>
</mat-popover>
<button [toggle-popover]="myPopover">Click Me</button>
#Directive({
selector: '[toggle-popover]'
})
export class TogglePopoverDirective {
#Input('toggle-popover')
public popover: MatPopoverComponent
public constructor(private el: ElementRef<HTMLElement>) { }
#HostListener('click')
public onClick() {
this.popover.toggle(this.el)
}
}
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 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
I have the following component below:
#Component({
selector: 'myselector',
providers: [ ],
directives: [ ChildComponent],
pipes: [ ],
template: '<myselector>This is {{testEmitter}}</myselector>'
})
export class ParentComponent{
#Input() testEmitter;
constructor(){
}
}
//My Child class goes as such:
#Component({
selector: 'childselector',
templateUrl: '<childselector><input type="text" (focus)="beginTest()"/></childselector>',
pipes: [],
directives: []
})
export class ChildComponent{
#Output() testEmitter: EventEmitter = new EventEmitter();
startTest: boolean = false;
constructor() {
}
beginTest(){
this.startTest = !this.startTest;
this.testEmitter.emit(this.startTest);
}
}
I am just trying to figure out how to display the value of the this.startTest variable from the ChildComponent to the ParentComponent. Right now, the {{testEmitter}} doesn't show anything in my ParentComponent html. I feel like I'm close. Your help is appreciated!
This code doesn't seem to make much sense.
#Component({
selector: 'myselector',
providers: [ ],
directives: [ ChildComponent],
pipes: [ ],
template: '<myselector>This is {{testEmitter}}</myselector>'
})
The template uses the selector <myselector> of the component it is the template of. While there are scenarios where this make sense (recursive structures like trees) this doesn't seem to be the intention here.
Also directives and pipes on #Component() are gone since a while and should be added to #NgModule() instead.
Obviously you are using some beta or RC version instead of final of Angular2 where this is still supported. I'd suggest to update to the newest Angular2 version.
This might be what you want instead:
#Component({
selector: 'parentselector',
directives: [ChildComponent],
template: '<childselector (testEmitter)="testEmitter=$event">This is {{testEmitter}}</childselector>'
})
export class ParentComponent{
#Input() testEmitter;
}
#Component({
selector: 'childselector',
templateUrl: '<input type="text" (focus)="beginTest()"/>',
})
export class ChildComponent{
#Output() testEmitter: EventEmitter = new EventEmitter();
startTest: boolean = false;
beginTest(){
this.startTest = !this.startTest;
this.testEmitter.emit(this.startTest);
}
}