2

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.

Community
  • 1
  • 1
Billy Cottrell
  • 443
  • 5
  • 20
  • 1
    isn't it more logical just to add class for div and add styles using scss/css files? At the current moment template is unreadable. Also it may solve issue. – Andris Feb 24 '20 at 11:01
  • You are already styling a div using style attribute. Again for the same you have used ngStyle. May be because of this its not getting loaded. – Lakshmi Feb 24 '20 at 12:08
  • @Andris the point is to make a Model that has the template and CSS property so we can store multiple templates. Later on, we can retrieve a template and display it with the correct styles. I could possible use files and load those in, but I was hoping for an easier way to store, retrieve and inject the CSS. – Billy Cottrell Feb 24 '20 at 13:07
  • @LakshmiS I removed all style elements and only use the ngStyle property instead but this did not solve the issue. It just doesn't show up when inspecting the element, almost like it doesn't recognize it. – Billy Cottrell Feb 24 '20 at 13:20
  • 1
    @Billy Cottrell yes. I understand that. But you can change just class name and store all styling code in seperate css/scss file. It makes code cleaner. And there is no problem to have multiple templates. In project, on which one i am wotrking now there is now 4 templates, but i can add new ones very easy. So i suggest to use NgClass on element into which you are putting in sanitised InnerHtml. – Andris Feb 24 '20 at 13:28
  • @Andris okay but would that still work when using CSS variables within the CSS to change logo, colour or font based on user settings? Will this setup be efficient or will this slow down when having hundreds of templates in the database? – Billy Cottrell Feb 24 '20 at 13:40
  • 1
    @Billy Cottrell if only based on user settings you change only class, and everything else is stored in scss / css, app should run fast. In this case you would have hundreds of css specific classes. I will write an answer how i would do this. – Andris Feb 24 '20 at 13:53
  • @Andris Also if this would work how do I load a css file that's was received from the requested template? Thank you for your help and advice! – Billy Cottrell Feb 24 '20 at 13:56
  • Depends how big are differences between templates. If small (just some color changes), than same scss file for all templates is enough. – Andris Feb 24 '20 at 14:02
  • 1
    Either way now you are only using TrustHtml. But knowing that your template also contains Styling, you need to also use TrustStyles. Don't know if both can be used on same html. But i think not. So things should be done differently. – Andris Feb 24 '20 at 14:04
  • @Billy Cottrell This is open issue starting from 2017: https://github.com/angular/angular/issues/19645 – Andris Feb 24 '20 at 14:13
  • @Andris the differences between templates are quite big since the structure is completely different. So seperate styling is the best option. – Billy Cottrell Feb 24 '20 at 14:15
  • Removing tag `vmware-clarity` as the question does not relate to it. – Wand Maker Feb 24 '20 at 17:33
  • @WandMaker the reason I added in the tag is because I am working with clarity and clarity tags might be used within the template at some point. – Billy Cottrell Feb 25 '20 at 08:06
  • @BillyCottrell This question does not require attention of `vmware-clarity` team who responds to questions with that tag. If the issue is being seen with clarity components, only then, it may be good idea to add that tag. – Wand Maker Feb 25 '20 at 15:00

3 Answers3

1

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

Andris
  • 3,895
  • 2
  • 24
  • 27
  • The portal setup looks very interesting and might be the way to go, I will definitely look more into this matter. Could you just update the stackblitz link cause it doesn't seem to work I always get a `Cannot GET /api/angular/v1` error. Thanks for all the help! – Billy Cottrell Feb 25 '20 at 08:03
  • If I would use portal I would need to make a component for every template I would like to use. So if I would have about 500 templates I would need to make 500 components? That's just crazy. I was only hoping to save templates like regular data and get a certain template through a request to the server. I guess Angular does not support template loading from server request. – Billy Cottrell Feb 26 '20 at 15:18
0

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;
}
Andris
  • 3,895
  • 2
  • 24
  • 27
  • Okay I think I understand what you are saying, but will the ngClass work within the templateHtml variable, I just tried to use it there but didn't seem to work? So in the .ts file like this: `this.templateHTML = "
    ";`
    – Billy Cottrell Feb 24 '20 at 14:24
  • Well that's the problem I need to apply styles inside of templateHtml and if the stylesheet gets downloaded from the api how am I supposed to apply it then? – Billy Cottrell Feb 24 '20 at 14:30
  • 1
    @Billy Cottrell try to look on this question answers: https://stackoverflow.com/questions/38888008/how-can-i-use-create-dynamic-template-to-compile-dynamic-component-with-angular – Andris Feb 24 '20 at 14:48
0

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: Angular JIT compilation failed: '@angular/compiler' not loaded This is because the angular compiler is not included in the production environment, even when you try to add it manually.

Billy Cottrell
  • 443
  • 5
  • 20