Dynamic template URLs in Angular 2

JavascriptAngular

Javascript 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;
}

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionLucasView Question on Stackoverflow
Solution 1 - JavascriptTomGView Answer on Stackoverflow
Solution 2 - JavascriptEyal VardiView Answer on Stackoverflow
Solution 3 - JavascriptYoav AharoniView Answer on Stackoverflow
Solution 4 - JavascriptDaniel abzakhView Answer on Stackoverflow
Solution 5 - JavascriptWeslley De SouzaView Answer on Stackoverflow
Solution 6 - JavascriptSebasView Answer on Stackoverflow
Solution 7 - JavascriptNick TarasView Answer on Stackoverflow
Solution 8 - JavascriptIvanView Answer on Stackoverflow
Solution 9 - JavascriptBahgat MashalyView Answer on Stackoverflow
Solution 10 - JavascriptnikojpapaView Answer on Stackoverflow