5

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: Template example

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!

Billy Cottrell
  • 443
  • 5
  • 20

5 Answers5

1

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

Andrei
  • 10,117
  • 13
  • 21
  • I understand what you are trying to say but this solution is only possible when I would manually code in the templates into components, but users that login can also provide their own custom templates which is not supported with this method. – Billy Cottrell Sep 22 '20 at 20:42
  • @Andrei is it possible to assemble two components in one? Something that you can use as component template inheritance (template of parent component + template of child component) ? – savke Feb 16 '23 at 08:43
  • currenty it is not possible with the inheritance in any of the modern frameworks as far as I am concerned. It could be done via composition. for example pass a component inside of another component via content – Andrei Feb 16 '23 at 08:47
  • @Andrei I have tried something like that with composition, but I run in problems with overriding methods and with duplicated constructor call (https://stackoverflow.com/questions/75461080/is-there-better-way-to-do-angular-component-inheritance-with-inheriting-template) – savke Feb 16 '23 at 08:51
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

Owen Kelvin
  • 14,054
  • 10
  • 41
  • 74
  • So this is really interesting but about the styling would that mean that I need to use slot everywhere, what about table styling? I don't really think that slots will provide a good solution when users create there templates. About the sanitization this system is meant to be used only be the company itself through 1 account. So basically the 3rd bullet point is how this system works. Because templates are manged on pcs but displayed on tv screens and kiosks that are activated with a code so we can see them in the dashboard with there respective ip address and license. – Billy Cottrell Oct 15 '20 at 07:35
  • I have been thinking about this and I am wondering how do you make a template available for the individual user who posted the template? – Billy Cottrell Oct 19 '20 at 10:07
  • I believe you can associate a template id with a particular user id using relations table. Say a table name `template_user` with fields `template_id` and `user_id`. With that you will hence return to a user only templates associated with them – Owen Kelvin Oct 19 '20 at 10:20
  • Yes but how can you make it so that user made styles can be applied to this user template using innerHtml without having to use slot tags in the html? – Billy Cottrell Oct 19 '20 at 12:23
  • So I will accept this answer and apply the bounty to this. Before I will accept the answer is it possible to edit the post to add in a solution that shows how to add external styles without having to use slots? – Billy Cottrell Oct 20 '20 at 07:03
  • I have added a way you can allow users to use inline styling instead – Owen Kelvin Oct 20 '20 at 09:36
  • Oh cool so I checked if it was possible to add in a style tag and still use classes aswell. Seems like this works just perfect so now I can gather the templates from the server aswell as the styles and append the styles in the template by using the style tag. This seems like a perfect solution! Thank you very much for your time and effort this is awesome!! – Billy Cottrell Oct 20 '20 at 10:08
  • Great, thanks too, I have learned a lot from trying to solve this challenge – Owen Kelvin Oct 20 '20 at 10:13
  • Same here, since the solution we had for the time being was way to complicated for what we wanted and we couldn't even launch it in production. This method looks way cleaner, professional and probably even more efficient than what we have now. – Billy Cottrell Oct 20 '20 at 10:21
0

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.

cpc
  • 608
  • 6
  • 22
  • 1
    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. – Billy Cottrell Sep 25 '20 at 08:42
  • I have added in a few example of external HTML and CSS together with a possible result of one of the templates. – Billy Cottrell Sep 25 '20 at 09:01
  • Ok I understand why you bundle the compiler now. So the users can essentially "code" their own UI. I cant think of any solution doing this securely though. Maybe you should consider different approaches. – cpc Sep 25 '20 at 10:36
  • that is why I am asking this question to see if this is possible to use in Angular and if not, if there is a library instead that may solve this then I would like to look into it. That's also why I am asking if those options don't provide a solution what language or environment could be used to achieve this. Like for example all requirements can be achieved when using ASP.NET and C# with a short explanation or example. – Billy Cottrell Sep 25 '20 at 11:20
0

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

Owen Kelvin
  • 14,054
  • 10
  • 41
  • 74
  • hmmm this approach seems interesting, but can this be used when users can create there own templates? Since yhere should be the possibility that users can upload or enter there own templates. Or even use the standard templates and edit those as they please. – Billy Cottrell Sep 26 '20 at 09:05
  • In that case you can have a template suggestion section, Users can make suggestions of template and the development team can decide on templates worth adding. You can even consider awards for users who make suggestions e.g they earn a badge and if there template is approved and added, they earn another badge, something to encourage them – Owen Kelvin Sep 26 '20 at 14:18
  • Since I had suggested using lazy loading, you may just have a list of templates. Once a template is added, the list is provided from the backend. Therefore when a new template is added, users can be notified. – Owen Kelvin Sep 26 '20 at 14:21
  • So I think I kinda understand what you are trying to say and this might be the way to go, but what do you mean with lazy loading a list of templates provided from the backend? Could you give me a small example on how this could be achieved? – Billy Cottrell Sep 26 '20 at 14:37
  • I mean you can save the list of availabe templates on a centraol server say` ['alpha', 'beta', ...]` from the an api rather than hard coding them on the frontend. this way the list a user uses will be up to date, rather than having the user update the app before the changes can reflect in their app – Owen Kelvin Sep 26 '20 at 14:46
  • Hmm okay so I have been checking with the customer whether this is something they want or not but apparently they rather have an editor where they create their templates directly so there is no need for a developer to add it in the application. – Billy Cottrell Sep 29 '20 at 06:53
0

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

Gourav Garg
  • 2,856
  • 11
  • 24
  • I can't really say that it is a website builder platform its really only a html page to display bookings of Office365, but the user should be able to upload or create their own custom templates. So I mean it is one way of doing it, but it will limit the layout possibilities which can't be sacrificed so I'd rather have templates server side being loaded in then having to hard code them without options of custom layouts (except for styling). This product is meant to be used so they don't need to ask to add layouts to the system which is like Owen Kelvin mentioned. – Billy Cottrell Oct 02 '20 at 07:11
  • Again it's not a bad idea, but just not practical when having 100 different layouts (with that I mean different HTML structures) which have to be hard coded. Also this application will have loads of integrations with other measurement tools so being able to minimize space is the most crucial part here. – Billy Cottrell Oct 02 '20 at 07:14
  • Have you tried formly.dev, but this one is for forms. I am not sure this will help you – Gourav Garg Oct 03 '20 at 14:29
  • I checked formly.dev and understand how it works but I don't think that this could work for this project. In the case of forms perhaps yes since you have a limited set of fields (types) and you can easily use it in different structures. In the case of this project their isn't a limited a set of structures, instead if you define a table or list of cards as a structure each can be restructured internally aswell which gives you infinite options to edit. So I don't think their is an ability to create such a general component to be used differently in each template. – Billy Cottrell Oct 04 '20 at 13:07
  • The only idea I had was too look into Angular Universal to see if it was possible to do some server side rendering for the templates, but I have no clue how it works nor if it can be used for this. – Billy Cottrell Oct 04 '20 at 13:13