Angular Elements: How to pass a template to the component?

Viewed 157

I built an Angular component that displays an array of markers on a map. When one of these markers gets clicked a popup will be displayed. The content of this popup can be defined by the user of my component.

I solved this by accepting an ng-template as @Input parameter.

<my-map
  [markers]="markers"
  [popupTemplate]="popupTemplate">
</my-map>

<ng-template #popupTemplate let-marker>
  <h1>{{marker.title}}</h1>
  <div>{{marker.subtitle}}</div>
</ng-template>

This works fine! But now I am publishing this component as a Web Component using Angular Elements. How do I solve the templating part for the Web Component?

2 Answers

I can not see the problem. well, generally you should define a "default template"

In you component, I suppose you has a *ngFor

<div *ngFor="let marker of markers">

    <ng-container *ngTemplateOutlet="template?template:defaultTemplate;
                        context:{$implicit:marker}" >
    </ng-container>
</div>

<ng-template #defaultTemplate let-marker>
  <h1>{{marker.title}}</h1>
  <div>{{marker.subtitle}}</div>
</ng-template>

A possible solution to this may be to accept a template string and the component would need to know how to deal with it.

A function for handling template strings:

export const template = (str, vars): string => new Function(`return \`${ str.replace(/\${/g, '${this.') }\`;`).call(vars);

Essentially builds a function on the fly to treat a given string as if it were a `template ${string}`

Usage:

let myTemplate = 'Just a ${field}';
let vars       = { field: 'test' };
let str        = template(myTemplate, vars); // "Just a test"

Option 1:

The Angular component could accept a template string and perform this template against it, then feed it to the DOM sanitizer for safe HTML to be used as an [innerHTML]

Annoying, but an option.

Option 2:

Could accept it as content

<my-map [markers]="markers">
  <template>
    <h1>${marker.title}</h1>
    <div>${marker.subtitle}</div>
  </template>
</my-map>

Angular Elements doesn't support ngContent or ngContentChildren (doesn't convert them to <slot> or anything either), so this option would require fancy footwork on behalf of the Angular Component.

The component's template needs to spit out the given content, something like

<ng-content></ng-content>

Somewhere in it's template so it'll render the given content.

Then in the component, grab the DOM contents to be templated. (probably after ngOnInit)

this.hackyTemplateHtml = this.elRef.nativeElement.querySelector('template').innerHtml;

If requiring multiple templates, could have the implementation send them with a unique attribute or class <template class='marker-template'>

For the template loop, would need to send each marker to be templated:

<div *ngFor="let marker of markers; trackBy: trackByTitle">
  <ng-container *ngTemplateOutlet="hackyTemplateHtml ? hackyTemplate : defaultTemplate;
                    context:{$implicit:marker}" >
  </ng-container>
</div>

<ng-template #defaultTemplate let-marker>
  <h1>{{marker.title}}</h1>
  <div>{{marker.subtitle}}</div>
</ng-template>

<ng-template #hackyTemplate let-marker>
  <div [outerHTML]="templateHacky(marker)"></div>
</ng-template>

The ngFor should probably implement a trackBy so it's not re-templating constantly. and the [outerHTML] is so it replaces the <div>, so it's not wrappered.

Back in the component:

trackByTitle(idx, marker) {
  return marker.title;
}

templateHacky(marker: Marker) {
  let templated = template(this.hackyTemplateHtml, { marker });
  return this.sanitizer.bypassSecurityTrustHtml(templated);
}

Also definitely hacky. Best I could think of as plain HTML/JS implementation. I'm working on a solution for react/vue to be able to send the equivalent of an Angular TemplateRef to Angular Elements to be treated as a normal template. Needs further thinking of how to do it frameworkless though.

Related