Angular multiple templates in one component based on id (with template store)

戏子无情 提交于 2020-12-29 13:14:59

问题


I have a special project and I haven't been able to find any information on how I can achieve this.

So on this website companies can register and login. When a company is logged in they have an overview of devices and groups where devices can be divided in different groups for easy recognition. Now the hard part of the website is template management. Each device will display a template which could be a general specified template, a template that was assigned to a specific group or to an individual device. The templates that are chosen are either standard provided templates, custom templates made by the company or custom templates tailored by me. (The last 2 options are only visible for the company itself)

The main reason for this is to display different templates with that I mean structural differences like a table, cards and even custom structures.

So at the moment I am able to display templates based on the id of the company. These templates are integrated within the angular app. So now it kinda looks like this (its just a small example):

    this.companyName = this.route.snapshot.params['company'];
    if(this.companyName == "Google"){
        this.template = `<div [ngStyle]="{'border-left':(tr.state=='busy')?'10px solid #D4061C':'10px solid #2CC52E'}">{{data}}</div>`;
        this.styles = "div{color: red}";
    }

What happens afterwards is the creation of a component on the fly by keeping the compiler in the build. So this means that this project cannot be build in production mode as the compiler is required. Which means that deploying the project is awful because the code is visible in the browser and the size is much larger so it takes too much time to load everything in. I kinda want to step away from this method and use something else which is easier to use

So what I want to know is:

  • is it possible to load in html from either data in the database or from HTML files.
  • is this possible by using any other library with Angular.
  • is there a way to create an overview of templates that I offer to companies that displays a preview of that template as well?
  • Is there a way to retrieve the ip and mac address of the device that is displaying the template.

If it isn't possible to use Angular for this what environment like language, frameworks, etc. do you advise to use instead?

If more information is required don't hesitate to ask away!

Thanks in Advance!

Edit 1:

I have tried to use [innerHTML] to load in the template but this doesn't work properly with data binding or data interpolation strings.

I'll give you an example of HTML I would like to load in:

    <div class='exellys' style='width: 1080px ;height: 1920px;background-color: #212121;'>
        <div class='clr-row' style='padding:45px 0px 10px 25px; position: relative; width: inherit; height: 115px;'>
            <div class='clr-col-5' style='float: left;'>
                <div style='width: 230px; height: 60px; background: url(/assets/exellys/exellys.png); background: url(https://www.exellys.com/App_SkinMaster/images/sprite-new.svg), linear-gradient(transparent, transparent); background-repeat: no-repeat; float: left;'></div>
            </div>
            <div class='clr-col-7' style='padding-top: 10px; float: right;'>
                <div class='exellys medium' style='text-align: right;'>{{date | date: 'EEEE d MMMM y'}}</div>
            </div>
        </div>
        <div class='clr-row' style='position: relative; width: inherit;'>
            <div class='exellys medium' style='width: inherit;padding-right:10px; text-align: right;'>{{date | date: 'HH:mm'}}</div>
        </div>
        <div class='clr-row' style='position: relative; width: inherit;'>
            <div class='exellys large' style='padding-top: 150px; width: inherit; text-align: center; font-weight: bold;'>WELCOME TO EXELLYS</div>
        </div>
        <div class='clr-row' style='position: relative; width: inherit;'>
            <div class='exellys medium-large' style='padding-top: 75px; width: inherit; text-align: center;'>Training Schedule</div>
        </div>
        <div class='clr-row' style='position: relative; width: inherit;'>
            <table class='table table-noborder exellys' style='table-layout: fixed; padding: 100px 20px 0px 35px;'>
                <thead>
                    <tr>
                        <th class='left exellys hcell' style='font-weight: bold; font-size: 37px; width: 15%; padding-left: 0px;'>HOUR</th>
                        <th class='left exellys hcell' style='font-weight: bold; font-size: 37px; width: 40%;'>ROOM</th>
                        <th class='left exellys hcell' style='font-weight: bold; font-size: 37px;'>SUBJECT</th>
                    </tr>
                </thead>
            </table>
            <table class='table table-noborder exellys' style='table-layout: fixed; border-collapse: separate; border-spacing: 0 5px; padding: 0px 20px 0px 35px; margin-top:0px;'>
                <tbody style='padding-left: 0px;'>
                    <tr *ngFor='let tr of bookings'>
                        <td class='left exellys dcell' style='font-size: 37px; padding-left: 10px; width: 15%;' [ngStyle]="{'border-left': (tr.state=='busy')? '10px solid #D4061C' : '10px solid #2CC52E'}">{{tr.localeStart | date: 'HH:mm'}}</td>
                        <td class='left exellys dcell' style='font-size: 37px; width: 40%;' [innerHTML]="tr.scheduleLocation"></td>
                        <td class='left exellys dcell' style='font-size: 37px;'>{{tr.subject}}</td>
                    </tr>
                </tbody>
            </table>
        </div>
    </div>

Next to this HTML I am also loading following styles:

    .main {
        color: #b0943c;
        font-family: 'Omnes-Medium', Helvetica, sans-serif;
        width: 1080px;
        height: 1920px;
        background-color: #212121;
    }
    
    .exellys {
        color: #b0943c;
    }
    
    .exellys.medium {
        font-size: 43px;
        font-family: 'Omnes-Regular', Helvetica, sans-serif;
    }
    
    .exellys.medium-large {
        font-size: 55px;
    }
    
    .exellys.large {
        font-family: 'Refrigerator-Deluxe-Regular', Helvetica, sans-serif;
        font-size: 75px;
    }
    
    .exellys.dcell {
        line-height: 45px;
        overflow: hidden;
        text-overflow: ellipsis;
        white-space: nowrap;
        padding-left: 0px;
    }
    
    .exellys.hcell {
        padding: 0px 0px 20px 0px;
    }
    
    table.table.table-noborder th {
        border-bottom: 5px solid #996633;
    }
    
    table td {
        border-top: 2px dashed #996633;
    }

Entering this kind of template can easily generate issues especially in innerHTML because of XSS protection. So I would like to know whether there is a different solution to this since there might be hundreds of customers with hundreds of different templates.

An example how a template could look like:

EDIT 2:

What I mean with:

is this possible by using any other library with Angular.

is if it is not possible to achieve this using standard methods are there any libraries to could enable me to achieve this anyways.

EDIT 3:

So the idea of a template suggestion system is really nice, but the customer wants to create it and add it directly without other customers to see this.

This way I need to be able to save HTML files in the backend (whether it are templates or full pages doesn't matter) and load it inside of the angular application.

For as far as I am understanding all the answers below this will not be possible in Angular.

My question now is in which environment or language can I achieve this template mechanism? Or is there still a unknown method that is safe to use for production in Angular?

Thanks in advance!

Update 15/12/2020:

After implementing Owen Kelvins idea, I have found a few issues with this. Using ngFor loops to loop over data doesn't work. Also adding pipelines inside of the interpolation strings do not work.

To solve the pipeline issue you can solve this by making changes to the prev.toString() line:

    templateString$ = combineLatest([this.templatingInfo$, this.template$]).pipe(
        map(([info, template]) =>
            Object.entries(info).reduce((prev, next) => {
                var value = next[1].toString();
                var pipe = "";
                var pipevalue = "";
                var match = prev.toString().match(new RegExp(`{{\\s${next[0]}\\s(\\|\\s\\w*\\:\\s\\'\.*\\'\\s)+}}`));
                if (match != null) {
                    pipe = match[1].substring(2);
                    if (pipe.split(":")[0] == "date") {
                        pipevalue = pipe.substr(5).replace(/['"]/g, "");
                        value = formatDate(value, pipevalue, this.locale);
                        return prev.toString().replace(new RegExp(`{{\\s${next[0]}\\s(\\|\\s\\w*\\:\\s\\'\.*\\'\\s)+}}`), formatDate(next[1].toString(), pipe.substring(5).replace(/['"]+/g, ""), this.locale));
                    }
                }
                return prev.toString().replace(new RegExp(`{{\\s${next[0]}\\s}}`), next[1].toString());
            }, template)
        ),
        map(template => this._sanitizer.bypassSecurityTrustHtml(template))
    );

Ofcourse this method doesn't work completely as in some cases it still doesn't display it correctly. Like when you have: <div>{{date | date: 'EEEE d MMMM y' }} - {{date | date: 'HH:mm' }}</div>, as in this case only the first one would be correct.

I would like to know how I could fix both the ngFor loop as the pipeline issue.

Thanks in Advance!


回答1:


I believe the easiest solution will be to bind to [innerHTML] as earlier mentioned by @capc0

You raised below concern

Hi @capc0 your answer is completely correct. But, yes there is a but! I am using interpolation strings inside my html, innerHTML works fine but that is with static HTML. I am talking about HTML that has data interpolation strings which doesn't work properly with innerHTML

Consider below approach to deal with this problem

Lets say we are to interpolate title and cost from the below object

  templatingInfo$ = of({
  title: 'Template Title',
    cost: 200
  });

I will also assume that the templates are received in the form of an Observable

  templates$ = of([
    { 
      id: 1,
      name: 'Alpha',
      value: `
        <div class='red'> 
          <h1>{{ title }}</h1>
          <p> This is my lovely Template! Purchase it at \${{ cost }} </p>
        </div>
      `
    },
    { 
      id: 2,
      name: 'Beta',
      value: `
        <div class='blue'> 
          <h1>{{ title }}</h1>
          <p> This is my lovely Template! Purchase it at \${{ cost }} </p>
        </div>
      `
    },
   ...

Now the only challenge is to replace the interpolated section with the correct info

I will solve this with the below approach

Define variables to track the selected template

  selected = 1;
  selectedTemplateSubject$ = new BehaviorSubject(this.selected);
  selectedTemplate$ = this.selectedTemplateSubject$.asObservable();

use combineLatest to combine the variables with template

  template$ = combineLatest([this.templates$, this.selectedTemplate$]).pipe(
    map(([templates, selected]) => templates.find(({id}) => id == Number(selected)).value),
    )
  templateString$ = combineLatest([this.templatingInfo$, this.template$ ]).pipe(
    map(([info, template]) => 
      Object.entries(info).reduce((prev, next) => 
        prev.toString().replace(new RegExp(`{{\\s${next[0]}\\s}}`), next[1].toString())
            , template)
        ),
    )

The above works unfortunately styles will not be applied.

Option 1 With that we can use encapsulation: ViewEncapsulation.None, in our @Component({}) object see Angular 2 - innerHTML styling

NB: WE ARE LITERALLY DEACTIVATING ANGULAR PROTECTION AGAINST XSS ATTACK

With the above said, you now have a few options

  • Sanitize the template string before saving it to the database
  • Manually sanitize the template string before rendering it
  • Only make the template available for the individual users who posted it. This way they probably will only attack themselves :)

See this Sample

Option 2 The other option is to use DomSanitizer as explainer in This Post

Lets assume users have included inline styles like below

  templates$ = of([
    {
      id: 1,
      name: "Alpha",
      value: `
        <div> 
          <h1 style='color: red'>{{ title }}</h1>
          <p style='color: blue'> This is Alpha! Purchase it at \${{ cost }} </p>
        </div>
      `
    },
    {
      id: 2,
      name: "Beta",
      value: `
        <div> 
          <h1 style='color: brown'>{{ title }}</h1>
          <p style='color: green'> This is Alpha! Purchase it at \${{ cost }} </p>
        </div>
      `
    },
    ...

We can add the line map(template => this._sanitizer.bypassSecurityTrustHtml(template)) to map the resultant string to a trusted string. The code will look like

import { Component } from "@angular/core";
import { of, BehaviorSubject, combineLatest } from "rxjs";
import { map } from "rxjs/operators";

import { DomSanitizer } from "@angular/platform-browser";

@Component({
  selector: "my-app",
  templateUrl: "./app.component.html",
  styleUrls: ["./app.component.css"]
})
export class AppComponent {
  constructor(private _sanitizer: DomSanitizer) {}
  templatingInfo$ = of({
    title: "Template Title",
    cost: 200
  });
  selected = 1;

  selectedTemplateSubject$ = new BehaviorSubject(this.selected);
  selectedTemplate$ = this.selectedTemplateSubject$.asObservable();
  templates$ = of([
    {
      id: 1,
      name: "Alpha",
      value: `
        <div> 
          <h1 style='color: red'>{{ title }}</h1>
          <p style='color: blue'> This is Alpha! Purchase it at \${{ cost }} </p>
        </div>
      `
    },
    {
      id: 2,
      name: "Beta",
      value: `
        <div> 
          <h1 style='color: brown'>{{ title }}</h1>
          <p style='color: green'> This is Alpha! Purchase it at \${{ cost }} </p>
        </div>
      `
    },
    {
      id: 3,
      name: "Gamma",
      value: `
        <div> 
          <h1 style='color: darkred'>{{ title }}</h1>
          <p style='color: green'> This is Alpha! Purchase it at \${{ cost }} </p>
        </div>
      `
    }
  ]);

  template$ = combineLatest([this.templates$, this.selectedTemplate$]).pipe(
    map(
      ([templates, selected]) =>
        templates.find(({ id }) => id == Number(selected)).value
    )
  );
  templateString$ = combineLatest([this.templatingInfo$, this.template$]).pipe(
    map(([info, template]) =>
      Object.entries(info).reduce(
        (prev, next) =>
          prev
            .toString()
            .replace(new RegExp(`{{\\s${next[0]}\\s}}`), next[1].toString()),
        template
      )
    ),
    map(template => this._sanitizer.bypassSecurityTrustHtml(template))
  );
}

See Below Demo on Stackblitz




回答2:


You should load different components, rather than different templates. (it is still possible to apply different template for one component, but it is hard to do as well as it makes performance of your application worse, and also it harder to maintain. look for dynamic compilation if you still want this option)

you can register a set of components for example as some token and then show them

{
 provide: COMPONENTS_OF_CHOICE,
 multi: true,
 useValue: OneOfMyComponents
}

or

{
 provide: COMPONENTS_OF_CHOICE,
 useValue: [OneOfMyComponents, SecondOfMyComponents]
}

it is impossible to retrieve the ip and mac address of the device. it would be not secure and browser does not expose that data




回答3:


is it possible to load in html from either data in the database or from HTML files.

Yes. You can for example create a "template editor" where the customer can build a template and you store that view in the database. It is not very simple but possible. You can extract the HTML from the database and display it e.g. via <div [innerHTML]="data"></div>. You have to sanitze user input etc. though, because of injection security risks (xss). It might be better If you define a set of "building blocks" where the companies can combine multiple of those blocks into a template and you construct that UI dynamically (and do not store any inline HTML in the database).

is this possible by using any other library with Angular.

what kind of library, can you specify? Generally I dont see a problem why not.

is there a way to create an overview of templates that I offer to companies that displays a preview of that template as well?

Yes. As mentioned above, if you store all templates in a database table e.g. templates you can query all templates (maybe with a key on the companyId) and show them with dummy data.

Is there a way to retrieve the ip and mac address of the device that is displaying the template.

I dont know, but as @Andrei mentioned I suppose it is not possible.




回答4:


If I am getting you right, you want to create something like website builder platform for end users so that they can add their design.

If yes, I will say add some designs (by several component for a specific part) and give them choice to add that specific design which is already in your application.

This way you do not need to use innerHTML and will use angular security too.

By the way I dont think this question is related to angular. It should be part of your design




回答5:


From what I understand from the problem, You need customized templates for different companies but you are faced by risk of XSS attacks if you bind your templates to innerHTML and also large bundles that may lead to slow page loads.

This is How I would go about the problem

  • Define a Mixin that would hold general information about each company, for example if each template has groups and devices, we may have a mixin that looks like below
export type Constructor<T = {}> = new (...args: any[]) => T;
export const templateMixin = <T extends Constructor>(BaseClass: T = class { } as T) =>
  class extends BaseClass {
    devises$ = Observable<any[]>;
    groups$ = Observable<any[]>;
    data: any = { };
    // All other variables and functions that may be common in template file
  };
  • Create the templates as angular components... I know this sounds weird because of bundle size but we will solve this problem next
@Component({
  selector: 'app-alpha-template',
  template: `
    `<div 
       [ngStyle]="{'border-left':(tr.state=='busy')?'10px solid #D4061C':'10px solid #2CC52E'}">
     {{data}}
    </div>`
  `,
  styleUrls: ['./e-learning-edit-course.component.css']
})
export class AlphaTemplate extends templateMixin { };

The above is just an example, you may need a better naming style if you have more templates than the greek letters Now we have solved the problem of XSS attacks. The next problem is bundle size

  • We note that different groups of individuals will load different templates, so the best approach would be to use lazy loading

You may define a route and set the children routes as the lazy loaded templateComponents



来源:https://stackoverflow.com/questions/63998467/angular-multiple-templates-in-one-component-based-on-id-with-template-store

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!