Angular directive templateUrl relative to .js file

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


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;
    })()
);

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.


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'
    }
});

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.