10

I have been trying to find a solution for this everywhere.

I have a project with different 'skins', which are basically different sets of templates/Css.

I am trying to have my components use the skin based on a variable THEME_DIR.

Unfortunately, I cannot find how to make that happens. I looked into the Dynamic Component Loader on angular.io without success.

I also looked at a few answers here without success either.

Does anyone have an idea?

This is what I tried so far:

import { ComponentFactoryResolver, ViewContainerRef } from '@angular/core';

// @Component({
//     templateUrl: '../../assets/theme/'+THEME_DIR+'/login.template.html',
// })

export class LoginComponent implements, AfterViewInit {


    private log = Log.create('LoginPage');

    constructor(private mzksLsRequestService: MzkLsRequestService,
                private componentFactoryResolver: ComponentFactoryResolver,
                public viewContainerRef: ViewContainerRef) {
    }



    ngAfterViewInit() {
        let componentFactory = this.componentFactoryResolver.resolveComponentFactory(new Component({
            templateUrl: '../../assets/theme/default/login.template.html',
        }));
        let viewContainerRef = this.viewContainerRef;
        viewContainerRef.clear();
        let componentRef = viewContainerRef.createComponent(componentFactory);

    }

}
millerf
  • 686
  • 1
  • 8
  • 16

3 Answers3

15

You can do it like this:

import {
  Compiler, Component, Injector, VERSION, ViewChild, NgModule, NgModuleRef,
  ViewContainerRef
} from '@angular/core';


@Component({
  selector: 'my-app',
  template: `
      <h1>Hello {{name}}</h1>
      <ng-container #vc></ng-container>
  `
})
export class AppComponent {
  @ViewChild('vc', {read: ViewContainerRef}) vc;
  name = `Angular! v${VERSION.full}`;

  constructor(private _compiler: Compiler,
              private _injector: Injector,
              private _m: NgModuleRef<any>) {
  }

  ngAfterViewInit() {
    const tmpCmp = Component({
        moduleId: module.id, templateUrl: './e.component.html'})(class {
    });
    const tmpModule = NgModule({declarations: [tmpCmp]})(class {
    });

    this._compiler.compileModuleAndAllComponentsAsync(tmpModule)
      .then((factories) => {
        const f = factories.componentFactories[0];
        const cmpRef = f.create(this._injector, [], null, this._m);
        cmpRef.instance.name = 'dynamic';
        this.vc.insert(cmpRef.hostView);
      })
  }
}

Just make sure that the URL is correct and the template is loaded into the client.

Read Here is what you need to know about dynamic components in Angular for more details.

Max Koretskyi
  • 101,079
  • 60
  • 333
  • 488
  • Thanks a lot for that. I think now my problem is webpack complaining when compiling the templates... I need to find a way around that... – millerf Jul 28 '17 at 16:05
  • 1
    Just tried on the fresh new Angular cli project, it works fine with webpack. What build process do you use? – Max Koretskyi Jul 28 '17 at 16:23
  • 1
    I use an ionic boilerplate. It complains when I put a variable in the template path ( see THEME_DIR in my code) as it seems it doesn't find the right path – millerf Jul 28 '17 at 16:28
  • 2
    I see, unfortunately I'm not familiar with that. I think you should create another question as it's not really related to the one I answered – Max Koretskyi Jul 28 '17 at 16:30
  • you can also accept or upvote this answer if it helped, thanks – Max Koretskyi Jul 28 '17 at 16:43
  • I did upvoted it :) I actually fixed it. Somehow the regexp of webpack transpiler would'nt get the concatenation of variable. I used a function and it works perfectly! – millerf Jul 28 '17 at 17:42
  • So your code works for very simple template but I cannot seem to inject the rest of the services I uses... – millerf Jul 28 '17 at 18:05
  • @millerf, what do you mean `inject the rest of the services`? what services? – Max Koretskyi Jul 28 '17 at 18:09
  • I'll detail it tomorrow. Thanks for your help anyway! – millerf Jul 28 '17 at 18:17
  • Let's say I want to use services in the newly created components, or even some directives in the template. Somehow I cannot get them working. I think it is because the newly created Module doesn't have the right imports, right? – millerf Jul 29 '17 at 10:35
  • yes, if you want to use components or directives from another module, just specify them in the imports. if you want to use services, add them to providers. In this line `f.create(this._injector,` you pass the injector from the parent module, so you should be able to access providers from the parent module – Max Koretskyi Jul 29 '17 at 12:07
  • Can I just somwhow retrieve all imports from the parent module and pass them in? Also, in the class declaration, if I import a NavController for example: ` constructor(private navCtrl: NavController) { this.log.info('test'); } ` I get this: ` ERROR Error: Uncaught (in promise): Error: Can't resolve all parameters for class_1: (?). ` – millerf Jul 29 '17 at 12:26
  • _Also, in the class declaration_ - what class declaration? – Max Koretskyi Jul 29 '17 at 12:30
  • Or can we just use the parent module directly? I am using a websocket connexion in one of the service and I cannot have it reinitiated. I would need to use the same service instance. – millerf Jul 29 '17 at 12:31
  • by "Class declaration" I mean this line: ` const tmpCmp = Component({ moduleId: module.id, templateUrl: './e.component.html'})(class { }); ` – millerf Jul 29 '17 at 12:32
  • so what are you trying to inject there? Where is `NavController` defined? Try using `Inject` like this `constructor(Inject(NavController) navCtrl)` and import `Inject` from the `@angular/core` – Max Koretskyi Jul 29 '17 at 15:07
  • _Can I just somwhow retrieve all imports from the parent module and pass them in?_ - no, you should specify them in the `imports` manually – Max Koretskyi Jul 29 '17 at 15:08
  • can you add the code you're using to the question details? – Max Koretskyi Jul 30 '17 at 12:57
  • You should have all needed here. https://wetransfer.com/downloads/761ce65f25e7bb39c256bb16652a2edb20170731100648/0f96e9 All the code we are interested in is in /src/app/pages/login.compinent.ts There is 2 questions: - first is I couldn't inject the navController in the contructor (line 35), I had to do it in a 'hacky' way - in app.module.ts, I created a 'component' object I tried to re-use line 46 of login.component.ts, in order not to have to re-create the whole list of dependencies... Thanks a ot for the time oyu spent on that btw... – millerf Jul 31 '17 at 10:10
  • @angularindepth-com I managed to get it working, but I still have trouble with AoT. It has been put on the backburner for now, anyway... Do you encounter the same problem? – millerf Oct 27 '17 at 07:23
  • @millerf, no, I answered your question :). You didn't accept it so I was wondering if it solved your problem or not – Max Koretskyi Oct 27 '17 at 07:29
  • @angularindepth-com my bad... I actually didn't know you could 'accept' an answer... – millerf Oct 27 '17 at 07:30
  • @millerf, thanks for accepting my answer. Yeah, it's a way of telling the author that his solution solved the problem. Upvote is more of a way to say that the solution is good. Good luck! – Max Koretskyi Oct 27 '17 at 07:33
  • I was able to get this partially working on Angular 7 - thanks. However, I don't seem to be able to get any sort of directives working or property bindings in the new template. I get the error: "ERROR Error: Uncaught (in promise): Error: Template parse errors: Can't bind to 'ngForOf' since it isn't a known property of 'p'." Is it possible to get these working? Also seems to strip out any – azsl1326 Dec 23 '18 at 05:50
  • **** UPDATE: Importing BrowserModule seems to fix the structural directive and property bindings issue – azsl1326 Dec 23 '18 at 06:13
  • Was a solution ever shown on how to properly Inject services in the constructor? Following the suggestion above doesn't work and the link to download the sample code is expired. For the time being, I just did the following: "this.cmpRef.instance.parentComponent = this" which allows me to access all services Injected into the 'parentComponent'. I was wondering if there was a cleaner way? – azsl1326 Dec 23 '18 at 06:59
  • @azsl1326 can you ask a separate question and post a link here? I'll take a look – Max Koretskyi Dec 26 '18 at 16:48
  • @millerf I am also getting same error regarding the path. It is not able to recognise the path since it is defined in variable. Can you please tell how you fixed this problem? – Amit Nair Feb 05 '19 at 07:18
  • I am getting error for property moduleId: module.id, ERROR in src/app/app.component.ts(18,17): error TS2580: Cannot find name 'module' @MaxKoretskyiakaWizard can you please help me? I am using Angular 7.2 – Shivaji More Jun 12 '19 at 10:35
  • @MaxKoretskyi I've tried your solution but it doesn't works with angular 9 version, it says JIT compilation failed..do you have any idea ? – Nimesh khatri Jul 10 '20 at 10:05
  • @MaxKoretskyi I'm trying change url with same component file instead of app.component... – Nimesh khatri Jul 10 '20 at 10:06
  • First I got `Error: Angular JIT compilation failed: '@angular/compiler' not loaded!`. I fixed it by `import "@angular/compiler";`. Then I got `Error: Component '' is not resolved: templateUrl: ./e.component.html. Did you run and wait for resolveComponentResources()'?`. I can't find how to import / use `resolveComponentResources`. Could you please advise? – Halfist Feb 10 '21 at 10:15
  • @Halfist Have you found a way how to deal with this `resolveComponentResources` issue? – Frimlik May 25 '23 at 09:25
  • @Frimlik, as far as I remember, this only works with `"aot": false` in `angular.json`. – Halfist May 25 '23 at 09:44
2

I had the problem when trying to load dynamicaly templates from the server (i wanted to make security check, translation on server side before serving html.

I've solved it after changing webpack config. In fact, after doing ng eject, it created a webpack.config.js which contains a .ts loader @ngtools/webpack and :

new AotPlugin({
  "mainPath": "main.ts",
  "replaceExport": false,
  "hostReplacementPaths": {
    "environments\\environment.ts": "environments\\environment.ts"
  },
  "exclude": [],
  "tsConfigPath": "src/main/front/tsconfig.app.json",
  "skipCodeGeneration": true
})

This last one, is the origin of the problem. It concerns the AOT (Ahead Of Time). According to the documentation : ngtools on the options section, it's mentionned :

skipCodeGeneration. Optional, defaults to false. Disable code generation and do not refactor the code to bootstrap. This replaces templateUrl: "string" with template: require("string")

If you dont want your templateUrl to be compiled AOT, i recommand you to remove the AotPlugin, and to use of the ts-loader instead of @ngtools/webpack see :

ts-loader

The rule for ts will look like this :

{
    test: /\.tsx?$/,
    loader: 'ts-loader'
}

Now you can load fresh templates from a relative URL on demand. Example :

@Component({
    selector : "custom-component",
    templateUrl : "/my_custom_url_on_server"
})
export class CustomComponent {
}

See Issue

Yacine MEDDAH
  • 1,211
  • 13
  • 17
1

As of Ivy (I think) we can use static variables (for example enviroment) in templateUrl. for example:

    import { environment } from 'src/environments/environment';
    @Component({
        selector: 'home',
        templateUrl: `./skins/${environment.skin}/home.page.html`,
        styleUrls: ['./skins/${environment.skin}/home.page.scss']
    })
hex
  • 713
  • 1
  • 11
  • 17