NgUpgrade: Unable to use templateUrl when upgrading Angular1 components

4.5k views Asked by At

I want to upgrade a ng1 component to be used inside an ng2 component.

If I use just a template string the ng1 component, to be upgraded, it works. However, if I switch to using a templateUrl instead, the app crashes and give me this error:

angular.js:13920 Error: loading directive templates asynchronously is not supported
at RemoteUrlComponent.UpgradeComponent.compileTemplate (upgrade-static.umd.js:720)
at RemoteUrlComponent.UpgradeComponent (upgrade-static.umd.js:521)
at new RemoteUrlComponent (remote-url.component.ts:11)
at new Wrapper_RemoteUrlComponent (wrapper.ngfactory.js:7)
at View_AppComponent1.createInternal (component.ngfactory.js:73)
at View_AppComponent1.AppView.create (core.umd.js:12262)
at TemplateRef_.createEmbeddedView (core.umd.js:9320)
at ViewContainerRef_.createEmbeddedView (core.umd.js:9552)
at eval (common.umd.js:1670)
at DefaultIterableDiffer.forEachOperation (core.umd.js:4653)

Here is a plunk demonstrating my issue:

https://plnkr.co/edit/2fXvfc?p=info

I've followed the Angular 1 -> 2 upgrade guide and it seems that this code should work. I'm not quite sure why its not working.

9

There are 9 answers

3
Florian On BEST ANSWER

I found a quite cheap solution for the issue.

Just use template: require('./remote-url.component.html') instead of templateUrl: './remote-url.component.html' and it should work just fine!

0
Bogdanel On

As a workaround I used $templateCache and $templateRequest to put templates in $templateCache for Angular needed templates, on AngularJS run as follows:

app.run(['$templateCache', '$templateRequest', function($templateCache, $templateRequest) {
        var templateUrlList = [
            'app/modules/common/header.html',
            ...
        ];
        templateUrlList.forEach(function (templateUrl) {
            if ($templateCache.get(templateUrl) === undefined) {
                $templateRequest(templateUrl)
                    .then(function (templateContent) {
                        $templateCache.put(templateUrl, templateContent);
                    });
            }
        });
    }]);
0
Fabienne P On

After trying require with requireJS and the text plugin which did not work for me, I managed to make it work using 'ng-include' as follow:

angular.module('appName').component('nameComponent', {
template: `<ng-include src="'path_to_file/file-name.html'"></ng-include>`,

I hope this helps!

0
nikolasleblanc On

A pretty low-tech solution to this issue is to load your templates up in your index.html, and assign them IDs that match the templateUrls the directives are looking for, ie:

<script type="text/ng-template" id="some/file/path.html">
  <div>
    <p>Here's my template!</p>
  </div>
</script>

Angular then automatically puts the template into the $templateCache, which is where UpgradeComponent's compileTemplate is looking for the template to begin with, so without changing the templateUrl in your directive, things will work because the id matches the templateUrl.

If you check the source code of UpgradeComponent (see below), you can see commented out code that deals with fetching the url, so it must be in the works, but for the time being this could be a viable solution and even a scriptable one.

private compileTemplate(directive: angular.IDirective): angular.ILinkFn {
    if (this.directive.template !== undefined) {
      return this.compileHtml(getOrCall(this.directive.template));
    } else if (this.directive.templateUrl) {
      const url = getOrCall(this.directive.templateUrl);
      const html = this.$templateCache.get(url) as string;
      if (html !== undefined) {
        return this.compileHtml(html);
      } else {
        throw new Error('loading directive templates asynchronously is not supported');
        // return new Promise((resolve, reject) => {
        //   this.$httpBackend('GET', url, null, (status: number, response: string) => {
        //     if (status == 200) {
        //       resolve(this.compileHtml(this.$templateCache.put(url, response)));
        //     } else {
        //       reject(`GET component template from '${url}' returned '${status}: ${response}'`);
        //     }
        //   });
        // });
      }
    } else {
      throw new Error(`Directive '${this.name}' is not a component, it is missing template.`);
    }
  }
0
ViES On

I use webpack's require.context for this:

templates-factory.js

import {resolve} from 'path';

/**
 * Wrap given context in AngularJS $templateCache
 * @param ctx - A context module
 * @param dir - module directory
 * @returns {function(...*): void} - AngularJS Run function
 */
export const templatesFactory = (ctx, dir, filename) => {
    return $templateCache => ctx.keys().forEach(key => {

        const templateId = (() => {
            switch (typeof filename) {
                case 'function':
                    return resolve(dir, filename(key));
                case 'string':
                    return resolve(dir, filename);
                default:
                    return resolve(dir, key);
            }
        })();

        $templateCache.put(templateId, ctx(key));
    });
};

app.html-bundle.js

   import {templatesFactory} from './templates-factory';

    const ctx = require.context('./', true, /\.html$/);

    export const AppHtmlBundle = angular.module('AppHtmlBundle', [])
        .run(templatesFactory(ctx, __dirname))
        .name;

Don't forget to add html-loader to your webpack.config.js:

 [{
    test: /\.html$/,
    use: {
        loader: 'html-loader',
        options: {
            minimize: false,
            root: path.resolve(__dirname, './src')
        }
    }
}]

Also you may need to convert relative paths to absolute one. I use my self-written babel plugin ng-template-url-absolutify for this purpose:

[{
    test: /\.(es6|js)$/,
    include: [path.resolve(__dirname, 'src')],
    exclude: /node_modules/,
    loader: 'babel-loader',
    options: {
        plugins: [
            '@babel/plugin-syntax-dynamic-import',
            ['ng-template-url-absolutify', {baseDir: path.resolve(__dirname, 'src'), baseUrl: ''}]
        ],

        presets: [['@babel/preset-env', {'modules': false}]]
    }
},
0
theandrewlane On

If you don't want to modify your Webpack configuration, the quick/dirty solution is to use the raw-loader import syntax:

template: require('!raw-loader!./your-template.html')

0
Kampana On

I've created a method utility to solve this issue. Basically it adds the template url content to angular's templateCache, using requireJS and "text.js":

   initTemplateUrls(templateUrlList) {
    app.run(function ($templateCache) {
      templateUrlList.forEach(templateUrl => {
        if ($templateCache.get(templateUrl) === undefined) {
          $templateCache.put(templateUrl, 'temporaryValue');
          require(['text!' + templateUrl],
            function (templateContent) {
              $templateCache.put(templateUrl, templateContent);
            }
          );
        }
      });
    });

What you should do is put this method utility in appmodule.ts for example, and then create a list of templateUrls that you are about to upgrade from your angular directive, for example:

const templateUrlList = [
      '/app/@fingerprint@/common/directives/grid/pGrid.html',
    ];
0
Dan King On

Most of the answers given here involve pre-loading the template in some way so as to make it available synchronously to the directive.

If you want to avoid doing this - e.g. if you have a large AngularJS application that contains many templates, and you don't want to download them all up front - you can simply wrap your directive in a synchronously loaded version instead.

E.g., if you have a directive called myDirective, which has an asynchronously loaded templateUrl which you don't want to download up front, you can do this instead:

angular
  .module('my-module')
  .directive('myDirectiveWrapper', function() {
    return {
      restrict: 'E',
      template: "<my-directive></my-directive>",
    }
  });

Then your Upgraded Angular directive just needs to supply 'myDirectiveWrapper' instead of 'myDirective' in it's super() call to the extended UpgradeComponent.

0
Rick Hopkins On

This is really frustating because the Angular upgrade documentation specifically says it's ok to use templateUrl. Never mentions this async issue. I've found a way around it by using the $templateCache. I didn't want to change my angular 1 directive because it is used my angular 1 apps and will also be used by angular 4 apps. So I had to find a way to modify it on the fly. I used $delegate, $provider, and $templateCache. My code is below. I also use this to remove the replace attribute since it is deprecated.

function upgradeDirective(moduleName, invokedName) {
    /** get the invoked directive */
    angular.module(moduleName).config(config);

    config.$inject = ['$provide'];
    decorator.$inject = ['$delegate', '$templateCache'];

    function config($provide) {
        $provide.decorator(invokedName + 'Directive', decorator);
    }

    function decorator($delegate, $templateCache) {
        /** get the directive reference */
        var directive = $delegate[0];

        /** remove deprecated attributes */
        if (directive.hasOwnProperty('replace')){
            delete directive.replace;
        }

        /** check for templateUrl and get template from cache */
        if (directive.hasOwnProperty('templateUrl')){
            /** get the template key */
            var key = directive.templateUrl.substring(directive.templateUrl.indexOf('app/'));

            /** remove templateUrl */
            delete directive.templateUrl;

            /** add template and get from cache */
            directive.template = $templateCache.get(key);
        }

        /** return the delegate */
        return $delegate;
    }
}

upgradeDirective('moduleName', 'moduleDirectiveName');