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!
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)
}
}
So I have been looking around on how to load CSS and HTML from the server.
What I want to achieve is to request a certain template to be displayed which sends the HTML and CSS to the website and loads it in together with some user-defined styles like colour
So far I was able to inject HTML using:
<div [innerHTML]="template | sanitizeHtml"></div>
and
import { Pipe, PipeTransform, SecurityContext } from '#angular/core';
import { DomSanitizer } from '#angular/platform-browser';
#Pipe({
name: 'sanitizeHtml'
})
export class SanitizeHtmlPipe implements PipeTransform {
constructor(private sanitizer: DomSanitizer) { }
transform(value: any): any {
return this.sanitizer.bypassSecurityTrustHtml(value);
}
}
Which I have seen from different posts and blogs (thank you for that).
The HTML I have been building works like a charm:
this.template = "<div class='template' style='width: 1080px; height: 1920px; background-color: #212121;'><div class='clr-row' style='padding:45px 0px 10px 25px; position: relative; width: inherit;'><div class='clr-col-5'><div style='width: 230px; height: 60px; background-image: url(*LINK_TO_IMAGE*); background-repeat: no-repeat; float: left;'></div></div></div></div>"
This HTML is a part of the complete template.
So what I would like to do is to use styles on this by using variables.
So what I have tried is to make a style object:
public style: {};
public template: string;
ngOnInit(){
this.style = {
template: {
"color": "#D8B088",
}
}
this.template = "<div [ngStyle]='style.template' class='template' style='width: 1080px; height: 1920px; background-color: #212121;'><div class='clr-row' style='padding:45px 0px 10px 25px; position: relative; width: inherit;'><div class='clr-col-5'><div style='width: 230px; height: 60px; background-image: url(*LINK_TO_IMAGE*); background-repeat: no-repeat; float: left;'></div></div></div></div>"
}
I have added the style object to the template by using [ngStyle]='style.template', for some reason the style didn't get loaded, so I tried to use camelCasing instead but still no success.
So does someone know how to get the CSS to work in this case, and eventually use user-defined styles?
Thanks in advance.
Edit 1:
I have also included the Sanitize pipe in the app.module.ts:
#NgModule({
declarations: [
...,
SanitizeHtmlPipe
],
...
});
(for those who were wondering)
Edit 2:
So I have been working out what I kinda want to have with these templates:
A user can register multiple devices of where they want to display the bookings from office 365. A user can setup templates in 2 ways, but this does not matter. When a user wants to display the template for a certain device they go to /device/:deviceid/template/:templateid.
This way the component will load in the template of that device.
So first we load in the device settings which contains the user styles for the template. Afterwards, we load in the data from office365 that has to be displayed in the template and finally load in the template with the template styles.
So there will be 3 requests to the server.
DeviceSettings -- Data Office365 -- Template
So far I have been able to load in the data and place this in the template, but the template was available locally and not from the server.
The reason why I want to have the templates to be requested from the server is that there will be an admin portal where those templates will be made and managed.
These templates will have a name, the HTML and the CSS.
For big template differences you can use Angular CDK Portal: https://material.angular.io/cdk/portal/overview
Example here: https://stackblitz.com/angular/mkvvyvgqxox?file=src%2Fapp%2Fcdk-portal-overview-example.ts
Instead of using [ngStyle] in sanitized HTML, I would instead just change class for dom element, into which sanitized HTML is inserted:
<div [ngClass]="templateClass" [innerHTML]="templateHtml"></div>
In this way code is more readable and styling code is separated from HTML.
Css for templates would look like this:
.template-class-1 {
background-color: #f44336;
}
.template-class-2 {
background-color: #4caf50;
}
Update 14/10/2020:
The previous solution required the compiler to be included that way you couldn't build the project in production mode. Thanks to Owen Kelvins answer it is now possible to add dynamic html and css while still being to build to production since it doesn't require the compiler:
Angular multiple templates in one component based on id (with template store)
For adding custom CSS you can either use Owen Kelvins method or append the "" tag at the end of the html and add in your custom CSS together with the end tag.
Original Answer:
I have found the solution to this subject. Thanks to someone in the discord server "The Coding Den", he messaged me about this and give me a link to Dynamically load template for a component on Github. After scrolling through this long post I found the answer of Alarm9k. This is how I used it to create a component that could display different templates based on a given id through a server request, I have also added some comments to explain it.
import { Component, AfterViewInit, Compiler, NgModule, ViewChild, ViewContainerRef, OnInit } from '#angular/core';
import { CommonModule } from '#angular/common';
import { BookingService } from 'src/app/services/booking.service';
import { ApplicationModel } from 'src/app/models/application.model';
import { Booking } from 'src/app/models/vo/booking';
import { Subscription } from 'rxjs';
import { SplitStringPipe } from '../../utils/split-string.pipe';
import { HttpClientModule } from '#angular/common/http';
import { BrowserAnimationsModule } from '#angular/platform-browser/animations';
import { BrowserModule } from '#angular/platform-browser';
#Component({
selector: 'app-bookings-template',
templateUrl: './bookings-template.component.html',
styleUrls: ['./bookings-template.component.css']
})
export class BookingsTemplateComponent implements AfterViewInit {
public template: string;
public date: Date;
public locale: string;
public id: string;
#ViewChild('container', { read: ViewContainerRef, static: false }) container: ViewContainerRef;
constructor(private compiler: Compiler, private bs: BookingService, private apm: ApplicationModel) { }
ngAfterViewInit() {
// Must clear cache.
this.compiler.clearCache();
// fill in template from server request
this.template = "<div class="test">{{test}}</div>;
var styles = ".test{color:red}";
// Define the component using Component decorator.
const component = Component({
template: this.template + "<div>Hard Coded html for error checks and loading spinner</div>",
styles: [styles]
})(class implements OnInit {
//example properties
public date: Date;
public bookings: Array<Booking>;
public isLoading: boolean = true;
public hasError: boolean = false;
public errorMessage: string;
public errorMessageSub: Subscription;
public bs: BookingService;
public apm: ApplicationModel;
// Do not pass any parameters in the constructor or it will break!
// Instead pass it within the factory method down below as a property!
constructor() {
// refresh template every minute
setInterval(() => {
this.ngOnInit();
}, 60000);
// refresh date every second
setInterval(() => {
this.date = new Date();
}, 1000);
}
ngOnInit() {
// get data to fill in template
}
ngOnDestroy() {
//remove error subscription
this.errorMessageSub.unsubscribe();
}
});
// Define the module using NgModule decorator.
//Modules can be changed based on your needs
const module = NgModule({
imports: [
CommonModule,
BrowserAnimationsModule,
BrowserModule,
HttpClientModule],
declarations: [component, SplitStringPipe],
providers: [BookingService]
})(class { });
// Asynchronously (recommended) compile the module and the component.
this.compiler.compileModuleAndAllComponentsAsync(module)
.then(factories => {
// Get the component factory.
const componentFactory = factories.componentFactories[0];
// Create the component and add to the view.
const componentRef = this.container.createComponent(componentFactory);
// pass parameters that would go in the constructor as properties
// subscriptions should also work.
componentRef.instance.bs = this.bs;
componentRef.instance.apm = this.apm;
componentRef.instance.errorMessageSub = this.apm.getMessageError().subscribe(me => componentRef.instance.errorMessage = me);
});
}
}
The BookingsTemplateComponent acts as the parent of the anonymous component class which acts as the child. This way the child can be added to the parent thanks to #ViewChild where the container name is specified and matches with the parent html id:
<div #container></div> (in this case).
You will also need to add some things to the app module:
import { NgModule, CompilerFactory, Compiler, COMPILER_OPTIONS } from '#angular/core';
import { JitCompilerFactory } from '#angular/platform-browser-dynamic';
import { CommonModule } from '#angular/common';
export function createCompiler(compilerFactory: CompilerFactory) {
return compilerFactory.createCompiler();
}
#NgModule({
declarations: [
// components and pipes
...
],
imports: [
CommonModule, // required
... //other modules
],
providers: [
// different services
...,
// these are need to add the compiler manually to the project
{ provide: COMPILER_OPTIONS, useValue: {}, multi: true },
{ provide: CompilerFactory, useClass: JitCompilerFactory, deps: [COMPILER_OPTIONS] },
{ provide: Compiler, useFactory: createCompiler, deps: [CompilerFactory] }
],
bootstrap: [AppComponent]
})
export class AppModule { }
WARNING:
The most important factor of this is that you cannot build the project in production mode. The reason for this is because JIT compilation doesn't work and you will get the following error:
This is because the angular compiler is not included in the production environment, even when you try to add it manually.
I am trying to set up a fadeInOut animation on a component.
My app module imports BrowserAnimationsModule.
I created an animation and a trigger in a separate file:
import { animate, style, animation, trigger, useAnimation, transition } from '#angular/animations';
export const fadeIn = animation([style({ opacity: 0 }), animate('500ms', style({ opacity: 1 }))]);
export const fadeOut = animation(animate('500ms', style({ opacity: 0 })));
export const fadeInOut = trigger('fadeInOut', [
transition('void => *', useAnimation(fadeIn)),
transition('* => void', useAnimation(fadeOut))
]);
Then, I created a component and verified that the component itself works:
import { Component, OnInit } from '#angular/core';
import { Globals } from '#app/globals';
import { fadeInOut } from '#app/animations';
#Component({
selector: 'app-global-alert',
template: `
<div class="global-alert" *ngIf="globalAlert">
<div class="global-alert-message"><ng-content></ng-content></div>
<div class="close" (click)="closeGlobalAlert()"></div>
</div>
`,
styles: [],
animations: [fadeInOut]
})
export class GlobalAlertComponent implements OnInit {
private globalAlert: boolean;
constructor(private globals: Globals) {
this.globalAlert = globals.hasGlobalAlert;
}
ngOnInit() {}
closeGlobalAlert() {
this.globals.hasGlobalAlert = false;
this.globalAlert = false;
}
}
Note that I am storing the state of whether this alert should appear in a globals.ts file, although that's unrelated:
import { Injectable } from '#angular/core';
#Injectable()
export class Globals {
hasGlobalAlert = true;
}
So I use the component inside another component's html like so:
<div>
lots of html
</div>
<app-global-alert>Hello world</app-global-alert>
This works, the alert is dismissed when you click the close button, everything works as expected. However, when I try to add my trigger to it
<app-global-alert [#fadeInOut]>Hello world</app-global-alert>
I get a console error
Error: Found the synthetic property #fadeInOut. Please include either "BrowserAnimationsModule" or "NoopAnimationsModule" in your application.
I've Googled this, but AFAICT I've covered all the gotchas in most of the replies: I've included the animations declaration in the component, etc.
What did I miss?
As stated in official docs, animation should be added to component's metadata property. There's such property in GlobalAlertComponent here:
#Component({
animations: [fadeInOut],
...
})
export class GlobalAlertComponent implements OnInit {
...
This allows to use animation on any element inside of html part of this component. But #fadeInOut animation has been used in another component's html here:
<div>
lots of html
</div>
<app-global-alert [#fadeInOut]>Hello world</app-global-alert>
Make sure this component has import and animation property in its metadata.
I get a console error Error: Found the synthetic property #fadeInOut. Please include either "BrowserAnimationsModule" or "NoopAnimationsModule" in your application.
This error occurs when you have not imported "BrowserAnimationsModule" or "NoopAnimationsModule" module in your component containing Module. If your animation component is in App module then check if app.module is having the following in the #NgModule -
#NgModule({
imports: [
BrowserModule,
BrowserAnimationsModule //or NoopAnimationsModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
You need to add BrowserAnimationsModule
In app-module.ts file in import section.
import: [
BrowserAnimationsModule
]
Hope it will help.
Happy coding :)
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 handle a click event of x3dom's shape html element in my parent component. The onclick event is fired by x3dom. I can only delegate it with an ugly hack (see below).
Because shape is not a known Html tag by Angular 2 I have to define ashape component? or Not?
In Parent Component:
<shape (click)="doStuffInParent()" ></shape> <!-- click is not fired by x3dom -->
Shape component so far:
import {Component} from '#angular/core';
import { Input, Output, EventEmitter} from '#angular/core';
#Component({
selector: 'Shape',
providers: [],
directives: [],
pipes: []
})
export class Shape {
#Output() notify: EventEmitter<string> = new EventEmitter<string>(); // wrong!
constructor() {}
}
Edit: Maybe I don't need the component? The onclick event is fired by X3dom and not by me.
another solution for me would be if I can just call my component method from a regular onclick event what I asked here.
Update I hacked a solution what solved at least my problem:
call Angular 2 component method from html event
Working Demo : https://plnkr.co/edit/qQudgi9touIFOe52m4JY?p=preview
<Shape (myClick)="doStuffInParent($event)"></Shape>
in Component code,
doStuffInParent(value){
console.log(value); //Angular2
}
import {Component} from '#angular/core';
import { Input, Output, EventEmitter} from '#angular/core';
#Component({
selector: 'Shape',
providers: [],
directives: [],
pipes: [],
template:`<div (click)="click()">Shap Component</div>`
})
export class Shape {
#Output() myClick: EventEmitter<string> = new EventEmitter<string>();
click(){
this.myClick.next('Angular2');
}
}