85

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

Martin Prikryl
  • 188,800
  • 56
  • 490
  • 992
pedalpete
  • 21,076
  • 45
  • 128
  • 239
  • 4
    Or you could use grunt-html2js to convert your templates to js which AngularJs will then cache in its template cache. This is what the angular-ui guys do. – Beyers Jan 14 '14 at 00:10
  • Great idea Beyers, I didn't know about grunt-html2js. Will check it out. – pedalpete Jan 14 '14 at 09:53

6 Answers6

76

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;
    })()
);
dan richardson
  • 3,871
  • 4
  • 31
  • 38
Alon Gubkin
  • 56,458
  • 54
  • 195
  • 288
  • 2
    Oh, now that's just damn cool! Unfortunately, this would break with packing and minifying my js files, wouldn't it? Also, why is the 'currently executing script file always the last one'? I was under the impression all the script files are loaded into the global scope. – pedalpete Jan 14 '14 at 01:08
  • 1
    When the browser encounters a ` – Alon Gubkin Jan 14 '14 at 08:13
  • 1
    Also, this shouldn't break on minifying, but it should break on packing, so I've added a solution to that – Alon Gubkin Jan 14 '14 at 08:14
  • I think I see what you mean. You were assuming that I was loading everything in the page within a single script tag. That isn't the case , but I can work around that. Thanks for a great and creative answer! – pedalpete Jan 14 '14 at 09:52
  • I wasn't assuming that, it should work with multiple script tags. – Alon Gubkin Jan 14 '14 at 10:00
  • Oh, well, it's still a good answer, but it didn't work with multiple script tags. – pedalpete Jan 14 '14 at 10:26
  • The second option is working fine for me w/ multiple script tags. Although, at the moment the script tag w/ the directive is the last one. – JeffryHouser Mar 01 '14 at 19:30
  • This is a great answer and would have been what I was looking for if I was not using a requireJs style loader to load the script tags. In this situation I cannot guarantee that the last loaded script is the current one. This is a shame, I needed relative paths as I was spawning various clones of my repository to do large refactoring or testing. My hack solution has been to assign the active path to a property on the module, this is not ideal but better to change the value once rather than in each directive script. – Stephen Simpson Jun 27 '14 at 09:03
  • Same thing- actually jam these into a template cache... so very hard to make x-project components. – FlavorScape Jan 08 '15 at 18:13
  • 1
    Rather than replacing the name of the script, you could `path = currentScriptPath.split("/"); path.pop(); path.push("directive.html");` This doesn't have any dependencies on file names, and supports "directive.js", "/directive.js" and "path/to/directive.js". The code golf competition is to combine these into a single statement (using splice, et al). – Nathan MacInnes Feb 11 '15 at 11:27
  • Today I faced similar problem - I want to publish directive on Github, but I realised I can't force users to use path from my project, and at this moment it seems to work - thank you very much! – Radek Anuszewski May 30 '15 at 17:10
  • That's Such an out of the box solution – Daga Arihant Mar 26 '17 at 18:22
6

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.

Matt Way
  • 32,319
  • 10
  • 79
  • 85
  • I'm not sure why I only got notified of this today MWay, sorry for not responding. Are you suggesting that within the directive object, I have multiple templates? and then just point the `$compile` method to whichever template gets passed? I may have to use `$compile` anyway, so that may be a good suggestion. – pedalpete Jan 21 '14 at 23:12
4

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'
    }
});
Community
  • 1
  • 1
3

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.

Herb Stahl
  • 439
  • 1
  • 6
  • 23
2

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

dan richardson
  • 3,871
  • 4
  • 31
  • 38
  • This is going to be slow, hacky, and quite annoying to actually use, but +1 for the innovation! – Nathan MacInnes Feb 11 '15 at 11:08
  • Like I said, there isn't a way to actually do it so any method is going to have it's pros and cons. I haven't really found any performance issues as it's used only once per application load to find where the app lives. Also, it's actually quite easy to use, I've just not shown the full code that wraps this into an angularjs provider... might update my answer. – dan richardson Feb 11 '15 at 11:11
0

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.

Wtower
  • 18,848
  • 11
  • 103
  • 80