Dynamic template URLs in Angular 2
JavascriptAngularJavascript Problem Overview
I've been playing around with Angular 2 for the past few days and wondered if it was possible to provide a dynamic templateUrl
to the @View
decorator.
I have tried passing it a function and returning a string form it but the entire function just get's turned into a string.
I haven't really used Angular 1.x before either so I don't know if I'm just going about this in the wrong way, but is this possible, or is there a better way to create dynamic views?
For example I might want to display a form if the user is not logged in, but display a text message if they are logged in.
Something like this doesn't work:
@Component({
selector: 'my-component'
})
@View({
// This doesn't work
templateUrl: function() {
return this.isLoggedIn ? 'logged-in.html' : 'logged-out.html';
}
})
class MyComponent {
constructor() {
this.loggedIn = false;
}
}
Any help would be appreciated.
Javascript Solutions
Solution 1 - Javascript
Although maybe not the most elegant solution, I used the DynamicComponentLoader and ElementRef to dynamically assign template value to a component. In fact, I was looking for a solution where I can add multiple custom components into a placeholder.
I tried service injection in the function as outlined by shmck this doesn't work as the services are not available yet when the template function is called. Indeed, this
refers to the Window object.
Reference URLs for the solution I used are to be found on: https://stackoverflow.com/questions/32977271/create-dynamic-anchorname-for-dynamiccomponentloader-with-ng-for-in-angular2
I also refer this way to Plnkr1 and Plnkr2.
The site Dartdocs provides nice documentation on Angular 2 DynamicComponentLoader class, also applicable to TypeScript.
In short:
A simple Component as the to be used template
@Component({
selector: 'dt2-simple-block',
properties: ["idx"],
template: `<h1>Simple block for {{ idx }} </h1>`,
directives: []
})
class dt2SimpleBlock {
constructor() {
}
}
Constructor of the Component that holds all Components to be added (my app requires multiple childs to be included:
constructor(loader: DynamicComponentLoader, elementRef: ElementRef) {
//iterate
for (var i = 0; i < toSomething; i++) {
// build the template
var blockdirective = 'dt2-simple-block'
var template = '<' + blockdirective +
' idx="' + this.userBlocks.userHomePanelBlocks[i] +
'"></' + blockdirective + '>';
console.log(template); // debugging purpose
var directives = [dt2SimpleBlock];
loader.loadNextToLocation(toComponent(template, directives), elementRef);
}
And the helper function to be put somewhere as util
function toComponent(template, directives = []) {
@Component({ selector: 'fake-component' })
@View({ template, directives })
class FakeComponent { }
return FakeComponent;
}
Solution 2 - Javascript
My solution:
Angular 2.0 ViewResolver Class
class myViewResolver extends ViewResolver{
resolve(component: Type): ViewMetadata {
var view = super.resolve(component);
// TODO: Write logic here:-)
view.templateUrl = 'app/app.html';
return view;
}
}
bootstrap(App,[
provide(ViewResolver , {useClass:myViewResolver})
]);
Solution 3 - Javascript
Not quite what you asked for, but it's worth mentioning:
Another simple solution, which works for most use cases, is to put the logic in the template itself, like so:
@Component({
selector: 'my-component'
})
@View({
// Note1: Here, I use template instead of templateUrl.
// Note2: I use ES6 string interpolation + require() to embed/load the other templates, but you can do it however you like.
template: `
<div [ngSwitch]="loggedIn">
<template [ngSwitchCase]="true"> ${require('./logged-in.html')} </template>
<template ngSwitchDefault> ${require('./logged-out.html')} </template>
</div>`
})
class MyComponent {
constructor() {
this.loggedIn = false;
}
}
Downside for this solution is that your served js file ends up containing both templates, so this might be an issue for big templates (but only one template is actually rendered and the js size overhead is acceptable in many cases).
Solution 4 - Javascript
My solution:(The beauty about this is lazy loading for html and css files.)
This is home.componenet.ts
import { Component } from '@angular/core';
import { DynamicHTMLOutlet } from './../../directives/dynamic-html-outlet/dynamicHtmlOutlet.directive';
import { TranslateService, LangChangeEvent } from 'ng2-translate/ng2-translate';
@Component({
selector: 'lib-home',
templateUrl: './app/content/home/home.component.html',
directives: [DynamicHTMLOutlet]
})
export class HomeComponent {
html_template = `./app/content/home/home_`;
html: string;
css: string;
constructor(translate: TranslateService) {
this.html = this.html_template + translate.currentLang;
this.css = './app/content/home/home.component.css';
translate.onLangChange.subscribe((event: LangChangeEvent) => {
this.html = this.html_template + translate.currentLang;
this.css = './app/content/home/home.component.css';
});
}
}
The directive I used and made few changes: This is in home.componenet.html
<dynamic-html-outlet [htmlPath]="html" [cssPath]="css"></dynamic-html-outlet>
This is the directive for dynamic components:
import {
Component,
Directive,
ComponentFactory,
ComponentMetadata,
ComponentResolver,
Input,
ReflectiveInjector,
ViewContainerRef,
} from '@angular/core';
import { TranslatePipe } from 'ng2-translate/ng2-translate';
declare var $:any;
export function createComponentFactory(resolver: ComponentResolver, metadata: ComponentMetadata): Promise<ComponentFactory<any>> {
const cmpClass = class DynamicComponent {};
const decoratedCmp = Component(metadata)(cmpClass);
return resolver.resolveComponent(decoratedCmp);
}
@Directive({
selector: 'dynamic-html-outlet',
})
export class DynamicHTMLOutlet {
@Input() htmlPath: string;
@Input() cssPath: string;
constructor(private vcRef: ViewContainerRef, private resolver: ComponentResolver) {
}
ngOnChanges() {
if (!this.htmlPath) return;
$('dynamic-html') && $('dynamic-html').remove();
const metadata = new ComponentMetadata({
selector: 'dynamic-html',
templateUrl: this.htmlPath +'.html',
styleUrls: [this.cssPath],
pipes: [TranslatePipe]
});
createComponentFactory(this.resolver, metadata)
.then(factory => {
const injector = ReflectiveInjector.fromResolvedProviders([], this.vcRef.parentInjector);
this.vcRef.createComponent(factory, 0, injector, []);
});
}
}
Solution 5 - Javascript
Compile your application with aot "ng serve --aot".
export let DEFAULT_PREFIX :string= './app.component';
//or localStorage.getItem('theme')
export function getMySuperTemplate(template: string) {
return DEFAULT_PREFIX + template + '.html';
}
@Component({
selector: 'app-root',
templateUrl: getMySuperTemplate('2'),
styleUrls:['./app.component.css']
})
Solution 6 - Javascript
Update to @Eyal Vardi's answer (ViewResolver
is deprecated):
import { Directive, Type, Component } from '@angular/core';
import { DirectiveResolver } from '@angular/compiler';
class myViewUrlResolver extends DirectiveResolver {
resolve(type: Type<any>, throwIfNotFound?: boolean): Directive {
let view = <any>super.resolve(type, throwIfNotFound);
if (typeof view["templateUrl"] !== "undefined") {
console.log("Yay!");
let originalUrl = (<Component>view).templateUrl;
(<Component> view).templateUrl = environment.nativeScriptAppPrePathPrefix + originalUrl.replace(".html", ".tns.html");
}
if (typeof view["styleUrls"] !== "undefined") {
console.log("Yay2!");
let originalUrls = (<Component>view).styleUrls;
originalUrls.forEach((originalUrl, at) => (<Component>view).styleUrls[at] = environment.nativeScriptAppPrePathPrefix + originalUrl.replace(".html", ".tns.css"));
}
return view;
}
}
platformNativeScriptDynamic().bootstrapModule(AppModule,{
providers: [
{ provide: DirectiveResolver, useClass: myViewUrlResolver }
]
});
Solution 7 - Javascript
It appears this way of creating dynamic templates will not be available to Angular 2 due to security matters. Unfortunate, coming from Angular 1 my previous application was dynamically driven this way.
For Angular 2 - This could be a differeny way of doing the same (link example below). By updating the template html files to be components in the application, then injecting them into (the place where you were trying to create the templateUrl with a string etc) view component template parameter as elements (using DynamicComponentLoader).
https://angular.io/docs/js/latest/api/core/DynamicComponentLoader-class.html
Solution 8 - Javascript
Hope, github example for you will help you! There are example to compile dynamic html. So, you can load HTML by any of your Service and then compile it.
Solution 9 - Javascript
1- install this library
npm i -D html-loader
============================================================
2- In webpack.config use html-loader for html files
{ test: /\.html$/, loaders: ['html-loader'] }
============================================================
3- If you use ionic , you can copy webpack.config.js from the path "node_modules/@ionic/app-scripts/config/webpack.config.js" then add the html loader to it
=============================================================
4-If you use ionic In package.json add these lines
"config": {
"ionic_bundler": "webpack",
"ionic_webpack": "webpack.config.ionic.js"
},
=============================================================
5-Then you can use it as below
@Component({
selector: 'page-login',
// templateUrl:"./login.html"
template: function(){
if(globalVariables.test==2) {
return require("./login2.html")
}
else
{
return require("./login.html")
}
}(),
})
======================================
6-If there is unresolved error with require function you can put it in declarations.d.ts file as the below :
declare var require: any;
Solution 10 - Javascript
I know this doesn't technically answer the question asked, but, in many cases, you can achieve the desired effect by creating a new component that extends the intended component and uses a different templateUrl
. Then, use *ngIf
in the parent component to load the correct template.
Component using template 1:
@Component({
selector: 'template-one-component',
templateUrl: './template-one.html'
})
export class TemplateOneComponent {
title = 'This component uses one template';
}
Component using template 2:
@Component({
selector: 'template-two-component',
templateUrl: './template-two.html'
})
export class TemplateTwoComponent extends TemplateOneComponent {
}
Parent component:
@Component({
selector: 'parent-component',
template: `
<template-one-component *ngIf="useTemplateOne; else useTemplateTwo"></template-one-component>
<ng-template #useTemplateTwo>
<template-two-component></template-two-component>
<ng-template>
`
})
export class ParentComponent {
useTemplateOne: boolean;
}