Angular directive templateUrl relative to .js file

JavascriptAngularjsPath

Javascript Problem Overview


I'm building an angular directive which will be used in a few different locations. I can't always guarantee the file structure of the app the directive is used in, but I can force the user to put the directive.js and directive.html (not the real file names) in the same folder.

When the page evaluates the directive.js, it considers the templateUrl to be relative to itself. Is it possible to set the templateUrl to be relative to the directive.js file?

Or is it recommended to just include the template in the directive itself.

I'm thinking I may want to load different templates based on different circumstances, so would prefer to be able to use a relative path rather than updating the directive.js

Javascript Solutions


Solution 1 - Javascript

The currently executing script file will always be the last one in the scripts array, so you can easily find its path:

// directive.js

var scripts = document.getElementsByTagName("script")
var currentScriptPath = scripts[scripts.length-1].src;

angular.module('app', [])
    .directive('test', function () {
        return {
            templateUrl: currentScriptPath.replace('directive.js', 'directive.html')
        };
    });

If you're not sure what is the script name (for example if you're packing multiple scripts into one), use this:

return {
    templateUrl: currentScriptPath.substring(0, currentScriptPath.lastIndexOf('/') + 1) 
        + 'directive.html'
};

Note: In cases where a closure is used, your code should be outside to ensure that the currentScript is evaluated at the correct time, such as:

// directive.js
    
(function(currentScriptPath){
    angular.module('app', [])
        .directive('test', function () {
            return {
                templateUrl: currentScriptPath.replace('directive.js', 'directive.html')
        };
    });
})(
    (function () {
        var scripts = document.getElementsByTagName("script");
        var currentScriptPath = scripts[scripts.length - 1].src;
        return currentScriptPath;
    })()
);

Solution 2 - Javascript

As you said you wanted to provide different templates at different times to the directives, why not allow the template itself to be passed to the directive as an attribute?

<div my-directive my-template="template"></div>

Then use something like $compile(template)(scope) inside the directive.

Solution 3 - Javascript

In addition to the answer from Alon Gubkin I'd suggest to define a constant using an Immediately-Invoked Function Expression to store the path of the script and inject it into the directive:

angular.module('app', [])

.constant('SCRIPT_URL', (function () {
	var scripts = document.getElementsByTagName("script");
	var scriptPath = scripts[scripts.length - 1].src;
	return scriptPath.substring(0, scriptPath.lastIndexOf('/') + 1)
})())

.directive('test', function(SCRIPT_URL) {
	return {
		restrict :    'A',
		templateUrl : SCRIPT_URL + 'directive.html'
	}
});

Solution 4 - Javascript

This code is in a file called routes.js

The following did not work for me:

var scripts = document.getElementsByTagName("script")
var currentScriptPath = scripts[scripts.length-1].src;
var baseUrl = currentScriptPath.substring(0, currentScriptPath.lastIndexOf('/') + 1);

the following did:

var bu2 = document.querySelector("script[src$='routes.js']");
currentScriptPath = bu2.src;
baseUrl = currentScriptPath.substring(0, currentScriptPath.lastIndexOf('/') + 1);

My test is based on the following blog about using require to lazy load angular: http://ify.io/lazy-loading-in-angularjs/

require.js begets a requireConfig bootstrap

requireConfig begets an angular app.js

angular app.js begets my routes.js

I had the same code being served up by a revel web framework and asp.net mvc. In revel document.getElementsByTagName("script") produced a path to my require bootstrap js file and NOT my routes.js. in ASP.NET MVC it produced a path to Visual Studio's injected Browser Link script element that is put there during debugging sessions.

this is my working routes.js code:

define([], function()
{
    var scripts = document.getElementsByTagName("script");
    var currentScriptPath = scripts[scripts.length-1].src;
    console.log("currentScriptPath:"+currentScriptPath);
    var baseUrl = currentScriptPath.substring(0, currentScriptPath.lastIndexOf('/') + 1);
    console.log("baseUrl:"+baseUrl);
    var bu2 = document.querySelector("script[src$='routes.js']");
    currentScriptPath = bu2.src;
    console.log("bu2:"+bu2);
    console.log("src:"+bu2.src);
    baseUrl = currentScriptPath.substring(0, currentScriptPath.lastIndexOf('/') + 1);
    console.log("baseUrl:"+baseUrl);
    return {
        defaultRoutePath: '/',
            routes: {
            '/': {
                templateUrl: baseUrl + 'views/home.html',
                dependencies: [
                    'controllers/HomeViewController',
                    'directives/app-style'
                ]
            },
            '/about/:person': {
                templateUrl: baseUrl + 'views/about.html',
                dependencies: [
                    'controllers/AboutViewController',
                    'directives/app-color'
                ]
            },
            '/contact': {
                templateUrl: baseUrl + 'views/contact.html',
                dependencies: [
                    'controllers/ContactViewController',
                    'directives/app-color',
                    'directives/app-style'
                ]
            }
        }
    };
});

This is my console output when running from Revel.

currentScriptPath:http://localhost:9000/public/ngApps/1/requireBootstrap.js routes.js:8
baseUrl:http://localhost:9000/public/ngApps/1/ routes.js:10
bu2:[object HTMLScriptElement] routes.js:13
src:http://localhost:9000/public/ngApps/1/routes.js routes.js:14
baseUrl:http://localhost:9000/public/ngApps/1/ 

Another nice thing I have done is to take advantage of the require config and put some custom configurations in it. i.e. add

customConfig: { baseRouteUrl: '/AngularLazyBaseLine/Home/Content' } 

you can then get it by using the following code from inside of routes.js

var requireConfig = requirejs.s.contexts._.config;
console.log('requireConfig.customConfig.baseRouteUrl:' + requireConfig.customConfig.baseRouteUrl); 

sometimes you need to define a baseurl upfront, sometimes you need to dynamically generate it. Your choice for your situation.

Solution 5 - Javascript

Some might suggest it slightly "hacky", but I think until there is only 1 way to do it, anything is going to be hacky.
I've had a lot of luck with also doing this:

angular.module('ui.bootstrap', [])
  .provider('$appUrl', function(){
    this.route = function(url){
       var stack = new Error('dummy').stack.match(new RegExp(/(http(s)*\:\/\/)[^\:]+/igm));
       var app_path = stack[1];
       app_path = app_path.slice(0, app_path.lastIndexOf('App/') + 'App/'.length);
         return app_path + url;
    }
    this.$get = function(){
        return this.route;
    } 
  });

Then when using the code in an application after including the module in the app.
In an app config function:

.config(['$routeProvider', '$appUrlProvider', function ($routeProvider, $appUrlProvider) {

    $routeProvider
        .when('/path:folder_path*', {
            controller: 'BrowseFolderCntrl',
            templateUrl: $appUrlProvider.route('views/browse-folder.html')
        });
}]);

And in an app controller (if required):

var MyCntrl = function ($scope, $appUrl) {
    $scope.templateUrl = $appUrl('views/my-angular-view.html');
};

It creats a new javascript error and pulls out the stack trace. It then parses out all urls (excluding the calling line/char number).
You can then just pull out the first in the array which will be the current file where the code is running.

This is also helpful if you want to centralise the code and then pull out the second ([1]) in the array, to get the calling file location

Solution 6 - Javascript

As several users have pointed out, relevant paths are not helpful when building the static files, and I would highly recommend doing so.

There is a nifty feature in Angular called $templateCache, which more or less caches template files, and next time that angular requires one, instead of making an actual request it provides the cached version. This is a typical way to use it:

module = angular.module('myModule');
module.run(['$templateCache', function($templateCache) {
$templateCache.put('as/specified/in/templateurl/file.html',
    '<div>blabla</div>');
}]);
})();

So in this way you both tackle the problem of relative urls and you gain in performance.

Of course we love the idea of having separate template html files (in contrast to react), so the above by its own is no good. Here comes the build system, which can read all template html files and construct a js such as the above.

There are several html2js modules for grunt, gulp, webpack, and this is the main idea behind them. I personally use gulp a lot, so I particularly fancy gulp-ng-html2js because it does exactly this very easily.

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
QuestionpedalpeteView Question on Stackoverflow
Solution 1 - JavascriptAlon GubkinView Answer on Stackoverflow
Solution 2 - JavascriptMatt WayView Answer on Stackoverflow
Solution 3 - JavascriptMichael GrathView Answer on Stackoverflow
Solution 4 - JavascriptHerb StahlView Answer on Stackoverflow
Solution 5 - Javascriptdan richardsonView Answer on Stackoverflow
Solution 6 - JavascriptWtowerView Answer on Stackoverflow