How to use a directive outside of a component - javascript

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)
}
}

Related

Failing to access child component using ViewChild in Angular

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',
...
})

How to add custom directive to Angular Material Dialog component

I've created a custom directive that adds an additional #Output() event to Ionic's ion-range element. It works well on normal pages, but when I try and use it within Angular Material's Dialog component, the custom event isn't firing for some reason. My custom directives are added to a Directives module, and I typically import this Directives module where I need to use it. Here's how my project is set up:
range-events.directive.ts
#Directive({
// tslint:disable-next-line:directive-selector
selector: 'ion-range'
})
export class RangeEventsDirective {
#Output() public ionStart: EventEmitter<RangeValue> = new EventEmitter();
public constructor(protected elemRef: ElementRef<IonRange>) {}
#HostListener('mousedown', ['$event'])
#HostListener('touchstart', ['$event'])
public onStart(ev: Event): void {
this.ionStart.emit(this.elemRef.nativeElement.value);
ev.preventDefault();
}
}
This directive is declared and exported here:
directives.module.ts
#NgModule({
declarations: [
...
RangeEventsDirective,
...
],
imports: [
CommonModule
],
exports: [
...
RangeEventsDirective,
...
]
})
export class DirectivesModule { }
I've defined a custom pop up component, that shows on hover, with an edit button. When this edit button is clicked, it creates my Dialog component.
Here's my popup component:
edit-kit-popup.component.ts
#Component({
selector: 'app-edit-kit-popup',
templateUrl: './edit-kit-popup.component.html',
styleUrls: ['./edit-kit-popup.component.scss'],
})
export class EditKitPopupComponent implements OnInit {
constructor(
private dialog: MatDialog,
) { }
ngOnInit() {}
edit() {
const modalRef = this.dialog.open(EditKitSectionModalComponent, {
width: '320px',
height: '476px',
position: {
top: '20px',
right: '20px'
},
data: {
...
}
});
}
}
As you can see I use a custom component to display the dialog, that is defined in: edit-kit-section-modal.component.ts.
This dialog has an ion-range element with the #Output() event, I've added in `edit-kit-section-modal.component.html:
<ion-range #sectionHeightRange (ionStart)="customRangeStart($event)"></ion-range>
Both of these components are defined and exported in the following modules file. I then import my DirectivesModule here so that I can use the directives in the components:
press-kit-components.module.ts
#NgModule({
imports: [
CommonModule,
FormsModule,
ReactiveFormsModule,
RouterModule,
MaterialModule,
PipesModule,
IonicModule,
...
DirectivesModule
],
declarations: [
...
EditKitItemPopupComponent,
EditKitSectionModalComponent
],
exports: [
...
EditKitItemPopupComponent,
EditKitSectionModalComponent,
]
})
export class PressKitComponentsModule { }
Since I am using a custom component, I make sure to add the EditKitSectionModalComponent as an entryComponent in my page where the popup is used.
Is the reason why it is not registering, because the Dialog isn't present on the page at page load? So when I trigger the Dialog, the directive isn't applied?
How should I be using custom directives with an Angular Material Dialog component?
Thanks!
Interestingly enough, when I changed my selector from:
#Directive({
// tslint:disable-next-line:directive-selector
selector: 'ion-range'
})
to:
#Directive({
// tslint:disable-next-line:directive-selector
selector: '[ion-range-events]'
})
and added the directive like this:
<ion-range ion-range-events #sectionHeightRange (ionStart)="customRangeStart($event)"></ion-range>
It worked...anyone know why I can't call by the HTML selector like I did in my original question? Because calling by selector ion-range worked on non-dialog components. Thanks!

angular2 outsource code in service

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!!

Angular 2+ Create ViewRef from Markup Injected into Dynamic Template

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

How to rendered dynamically created array component on template html in Angular2?

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

Categories