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.
Related
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!
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 have tried too many tricky ways,
such as Renderer2 or ɵDomAdapter,
the script tag is integrated well in the html,
but when loading the url with the structured-data tool of google,
the ld+json script is not rendered !
Is there a way to make google render the page after loading the component ?
I used this variant in Angular 9 TypeScript
import { Component, OnInit } from '#angular/core';
import { DomSanitizer, SafeHtml } from '#angular/platform-browser';
#Component({
selector: 'app-schema-org',
template: '<div [innerHTML]="jsonLD"></div>',
})
export class SchemaOrgComponent implements OnInit {
jsonLD: SafeHtml;
constructor(private sanitizer: DomSanitizer) { }
ngOnInit() {
const json = {
'#context': 'http://schema.org',
'#type': 'Organization',
'url': 'https://google.com',
'name': 'Google',
'contactPoint': {
'#type': 'ContactPoint',
'telephone': '+1-000-000-0000',
'contactType': 'Customer service',
},
};
// Basically telling Angular this content is safe to directly inject into the dom with no sanitization
this.jsonLD = this.getSafeHTML(json);
}
getSafeHTML(value: {}) {
const json = JSON.stringify(value, null, 2);
const html = `<script type="application/ld+json">${json}</script>`;
// Inject to inner html without Angular stripping out content
return this.sanitizer.bypassSecurityTrustHtml(html);
}
}
And then called it <app-schema-org></app-schema-org>
For me the example above (https://stackoverflow.com/a/47299603/5155484) has no sense because it imports OnInit and implements OnChange and uses ngOnInit with a parameter for changes.
So here is my working fixed example.
There are a couple of ways to achieve this. The code below is the best solution I have come up with. This example will also work with Angular Universal.
import { Component, OnInit } from '#angular/core';
import { DomSanitizer, SafeHtml } from '#angular/platform-browser';
#Component({
selector: 'app-root',
template: '<div [innerHTML]="jsonLD"></div>'
})
export class JsonLdComponent implements OnChanges {
jsonLD: SafeHtml;
constructor(private sanitizer: DomSanitizer) { }
ngOnInit(changes: SimpleChanges) {
const json = {
"#context": "http://schema.org",
"#type": "Organization",
"url": "https://google.com",
"name": "Google",
"contactPoint": {
"#type": "ContactPoint",
"telephone": "+1-000-000-0000",
"contactType": "Customer service"
}
};
// Basically telling Angular this content is safe to directly inject into the dom with no sanitization
this.jsonLD = this.getSafeHTML(json);
}
getSafeHTML(value: {}) {
const json = JSON.stringify(value, null, 2);
const html = `${json}`;
// Inject to inner html without Angular stripping out content
return this.sanitizer.bypassSecurityTrustHtml(html);
}
}
I go into more detail in this blog post here https://coryrylan.com/blog/angular-seo-with-schema-and-json-ld
I also took this technique and wrapped it up into a npm package to make
it more reusable. https://github.com/coryrylan/ngx-json-ld
I would like to have custom errors in my Angular2 app. Thus I have extended ErrorHandler in my component:
import { Component, ErrorHandler, OnInit } from '#angular/core';
import { GenericError } from './generic-error.component';
#Component({
selector: 'custom-error-handler',
templateUrl: 'app/error-handler/custom-error-handler.component.html?' + +new Date()
})
export class CustomErrorHandler extends ErrorHandler {
errorText: string;
constructor() {
super(false);
}
ngOnInit() {
this.errorText = 'Initial text!';
}
public handleError(error: any): void {
if (error.originalError instanceof GenericError) {
console.info('This is printed to console!');
this.errorText = "I want it to print this in the template!";
}
else {
super.handleError(error);
}
}
}
My template simply contains:
<span style="color:red">{{errorText}}</span>
First I see "Initial text!" in the template as set in ngOnInit. That's as expected.
I can then throw a new exception like this from a different component:
throw new GenericError();
and it hits the code with handleError and prints to console but it doesn't update my template errorText with:
"I want it to print this in the template!"
It's like it ignores my template, when inside the handleError function.
What could be the problem here?
* ADDED MORE INFORMATION *
I thought I should add some more information. So here is the module I made for CustomErrorHandler (maybe the problem is with the providers?):
import { NgModule, ErrorHandler } from '#angular/core';
import { CommonModule } from '#angular/common';
import { CustomErrorHandler } from './custom-error-handler.component';
#NgModule({
declarations: [
CustomErrorHandler
],
imports: [
CommonModule
],
exports: [
CustomErrorHandler
],
providers: [
{ provide: ErrorHandler, useClass: CustomErrorHandler }
]
})
export class CustomErrorModule { }
There is indeed only one instance of the CustomErrorHandler (I checked with the Augury Chrome plugin).
For completeness, here is is the GenericError component:
export class GenericError {
toString() {
return "Here is a generic error message";
}
}
The solution was to add a service as suggested in the question's comment track. This way I can set the property in the component and eventually show it in the template.
I created the service, so that it has a function which takes one parameter. Injected the service, call the service's function from the handleError in the component function, and send the text I want in the template as the parameter. Then I use an observable, to get the text back to the component.
In the constructor of the component, I added this observer.
let whatever = this.cs.nameChange.subscribe((value) => {
setTimeout(() => this.errorText = value);
});
I needed to add the setTimeout, or else it would not update the template before the second time the observable was changed.
Phew! The Angular team should make this global exception handling easier in future releases.
I have a PermissionService, which provide user roles. At the server-side data will not be uploaded if the user is not corresponds on role. The back-end is written with asp.net web api, which will use attributes to secure data. On upload page will be static upload user roles, the idea is just to show or hide elements on page which depending from user role.
The PermissionsService check avaiable role in its array. There are methods like isSeller(), isManager(). And all what i want is to provide accessibility from each view. For now i have this implementation.
permission.service
import { Injectable } from "#angular/core";
export enum Roles {
Admin,
Manager,
Moderator,
Seller
}
interface IPermissionDictionary {
[key: string]: boolean;
}
#Injectable()
export class PermissionService {
private permissions: IPermissionDictionary = {};
public constructor() {
this.emitPermissions();
}
private emitPermissions(): void {
let selector = document.querySelectorAll("#roles > span");
let availableRoles = Array.from(selector).map(element => element.textContent);
for (let role in Roles) {
if (!/^\d+$/.test(role)) { // for strings types in Roles
this.permissions[role] = availableRoles.indexOf(role) > -1;
}
}
}
public isInRole(role: string): boolean {
return this.permissions[role];
}
public isAdmin() {
return this.isInRole(Roles[Roles.Admin]);
}
public isSeller() {
return this.isInRole(Roles[Roles.Seller]);
}
public isManager() {
return this.isInRole(Roles[Roles.Manager]);
}
public isModerator() {
return this.isInRole(Roles[Roles.Moderator]);
}
}
app.component
import { Component } from "#angular/core";
import { ROUTER_DIRECTIVES } from "#angular/router";
import { PermissionService } from "./share/permission.service";
import { HomeComponent } from "./home/home.component";
import { OrderComponent } from "./order/order.component";
#Component({
selector: "admin-panel",
templateUrl: "../app/app.template.html",
directives: [ROUTER_DIRECTIVES],
precompile: [HomeComponent, OrderComponent]
})
export class AppComponent {
constructor(private permissionService: PermissionService) {
}
}
main.ts
import { bootstrap } from "#angular/platform-browser-dynamic";
import { AppComponent } from "./app.component";
import { APP_ROUTES_PROVIDER } from "./app.routes";
import { HTTP_PROVIDERS } from '#angular/http';
import { PermissionService } from "./share/permission.service";
bootstrap(AppComponent, [APP_ROUTES_PROVIDER, HTTP_PROVIDERS, PermissionService]);
For now to access the method of PermissionService need to inject it in component constructor. And in template is is use like
<div *ngIf="permissionService.isAdmin()">will show if you are admin</div>
But every time to inject my service in each component where i want to use it seems for me strange. And i just want to get access it from every part of my app like:
<div *ngIf="isAdmin()">will show if you are admin</div>
I think the person who asked this question has another version of Angular2 (perhaps a pre-release?), but in the latest version if you need to export a service for all the app you do it in the following way.
First, in your main.ts you must have a class that you bootstrap, like this:
platformBrowserDynamic().bootstrapModule(AppModule);
In this class "AppModule" (or whatever it is in your case), you should be able to add a global service provider in this way:
...
import {GlobalService} from './global-service.service'
#NgModule({
...
providers: [MyGlobalService],
...
})
export class AppModule{ ...}
In this way MyGlobalService is available for all other components.
Hopefully this will be useful to someone :).
Some option could be to create top level super class with the permission methods and then just subclass in view .ts. Not sure if this suits you as you still need to import super class into your components and extend it. It can also violate the "is-a".