Diretiva angular templateUrl relativa ao arquivo .js

85

Estou construindo uma diretiva angular que será usada em alguns locais diferentes. Nem sempre posso garantir a estrutura de arquivos do aplicativo em que a diretiva é usada, mas posso forçar o usuário a colocar directive.jse directive.html(não os nomes reais dos arquivos) na mesma pasta.

Quando a página avalia o directive.js, ele considera o templateUrlcomo sendo relativo a si mesmo. É possível definir o templateUrlpara ser relativo ao directive.jsarquivo?

Ou é recomendável incluir apenas o modelo na própria diretiva.

Estou pensando que posso querer carregar diferentes modelos com base em diferentes circunstâncias, então preferiria poder usar um caminho relativo em vez de atualizar o directive.js

pedalpete
fonte
4
Ou você pode usar grunt-html2js para converter seus modelos em js que o AngularJs irá então armazenar em cache em seu cache de modelos. Isso é o que os caras do ui angular fazem.
Beyers
Ótima ideia Beyers, eu não sabia sobre grunt-html2js. Vou dar uma olhada.
pedalpete

Respostas:

76

O arquivo de script em execução no momento será sempre o último na matriz de scripts, portanto, você pode encontrar facilmente seu caminho:

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

Se você não tiver certeza de qual é o nome do script (por exemplo, se estiver compactando vários scripts em um), use este:

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

Observação : nos casos em que um encerramento é usado, seu código deve estar externo para garantir que o currentScript seja avaliado no momento correto, como:

// 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;
    })()
);
Alon Gubkin
fonte
2
Oh, isso é muito legal! Infelizmente, isso iria quebrar com a compactação e minimização de meus arquivos js, não é? Além disso, por que o 'arquivo de script em execução no momento é sempre o último'? Tive a impressão de que todos os arquivos de script são carregados no escopo global.
pedalpete
1
Quando o navegador encontra uma <script>tag, ele a executa imediatamente antes de continuar a renderizar a página, portanto, em um escopo de script global, a última <script>tag é sempre a última.
Alon Gubkin
1
Além disso, isso não deve quebrar na redução, mas deve quebrar na embalagem, então adicionei uma solução para isso
Alon Gubkin
Acho que entendi o que você quis dizer. Você estava presumindo que eu estava carregando tudo na página em uma única tag de script. Não é o caso, mas posso contornar isso. Obrigado por uma resposta ótima e criativa!
pedalpete
1
Em vez de substituir o nome do script, você poderia path = currentScriptPath.split("/"); path.pop(); path.push("directive.html");Isso não tem nenhuma dependência de nomes de arquivo e oferece suporte a "diretiva.js", "/diretiva.js" e "caminho / para / diretiva.js". O código da competição de golfe é combiná-los em uma única instrução (usando splice, et al).
Nathan MacInnes
6

Como você disse que queria fornecer modelos diferentes em momentos diferentes para as diretivas, por que não permitir que o próprio modelo seja passado para a diretiva como um atributo?

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

Em seguida, use algo como $compile(template)(scope)dentro da diretiva.

Matt Way
fonte
Não tenho certeza porque só fui notificado sobre isso hoje MWay, desculpe por não responder. Você está sugerindo que dentro do objeto de diretiva, eu tenho vários modelos? e apenas apontar o $compilemétodo para qualquer modelo que seja passado? Posso ter que usar de $compilequalquer maneira, então essa pode ser uma boa sugestão.
pedalpete
4

Além da resposta de Alon Gubkin , sugiro definir uma constante usando uma expressão de função invocada imediatamente para armazenar o caminho do script e injetá-lo na diretiva:

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'
    }
});
Michael Grath
fonte
3

Este código está em um arquivo chamado routes.js

O seguinte não funcionou para mim:

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

o seguinte fez:

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

Meu teste é baseado no seguinte blog sobre o uso de require para lazy load angular: http://ify.io/lazy-loading-in-angularjs/

require.js gera um bootstrap requireConfig

requireConfig gera um app.js angular

app.js angular gera meu routes.js

Eu tinha o mesmo código sendo servido por um framework web revel e asp.net mvc. Em revel, document.getElementsByTagName ("script") produziu um caminho para o meu arquivo js de bootstrap necessário e NÃO para o meu routes.js. na ASP.NET MVC, ele produziu um caminho para o elemento de script do link do navegador injetado no Visual Studio que é colocado lá durante as sessões de depuração.

este é meu código de trabalho routes.js:

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

Esta é a saída do meu console ao executar o 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/ 

Outra coisa legal que fiz foi tirar proveito da configuração necessária e colocar algumas configurações personalizadas nela. ie adicionar

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

você pode obtê-lo usando o seguinte código de dentro de routes.js

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

às vezes você precisa definir uma baseurl antecipadamente, às vezes você precisa gerá-la dinamicamente. Sua escolha para sua situação.

Herb Stahl
fonte
2

Alguns podem sugerir um pouco "hacky", mas acho que até que haja apenas uma maneira de fazer isso, qualquer coisa vai ser hacky.
Tive muita sorte também fazendo isso:

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

Então, ao usar o código em um aplicativo após incluir o módulo no aplicativo.
Em uma função de configuração de aplicativo:

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

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

E em um controlador de aplicativo (se necessário):

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

Ele cria um novo erro de javascript e extrai o rastreamento da pilha. Em seguida, analisa todos os urls (excluindo a linha de chamada / número de caractere).
Você pode então simplesmente retirar o primeiro do array, que será o arquivo atual onde o código está sendo executado.

Isso também é útil se você deseja centralizar o código e, em seguida, extrair o segundo ( [1]) na matriz, para obter a localização do arquivo de chamada

dan richardson
fonte
Isso vai ser lento, hacky e muito chato de usar, mas +1 para a inovação!
Nathan MacInnes
Como eu disse, não há uma maneira de realmente fazer isso, então qualquer método terá seus prós e contras. Eu realmente não encontrei nenhum problema de desempenho, pois ele é usado apenas uma vez por carregamento do aplicativo para descobrir onde o aplicativo está. Além disso, é muito fácil de usar. Só não mostrei o código completo que envolve isso em um provedor angularjs ... pode atualizar minha resposta.
dan richardson
0

Como vários usuários apontaram, os caminhos relevantes não são úteis ao construir os arquivos estáticos, e eu recomendo fortemente fazer isso.

Existe um recurso bacana no Angular chamado $ templateCache , que mais ou menos armazena em cache os arquivos de modelo, e da próxima vez que o angular requer um, em vez de fazer uma solicitação real, ele fornece a versão em cache. Esta é uma maneira típica de usá-lo:

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

Assim, você enfrenta o problema dos urls relativos e ganha em desempenho.

É claro que amamos a ideia de ter arquivos HTML de template separados (em contraste com o react), então o que foi dito acima não é bom. Aí vem o sistema de compilação, que pode ler todos os arquivos html de template e construir um js como o acima.

Existem vários módulos html2js para grunt, gulp, webpack, e esta é a ideia principal por trás deles. Eu pessoalmente uso muito o gulp, então gosto particularmente do gulp-ng-html2js porque ele faz exatamente isso com muita facilidade.

Wtower
fonte