Como incluir estilo específico de vista / parcial no AngularJS

132

Qual é a maneira correta / aceita de usar folhas de estilo separadas para as várias visualizações que meu aplicativo usa?

Atualmente, estou colocando um elemento de link no html da view / parcial na parte superior, mas me disseram que isso é uma prática ruim, embora todos os navegadores modernos o suportem, mas posso ver por que ele é mal visto.

A outra possibilidade é colocar as folhas de estilo separadas em meus index.html, headmas eu gostaria que ela carregasse a folha de estilo apenas se sua visualização estivesse sendo carregada em nome do desempenho.

Essa é uma prática ruim, pois o estilo não terá efeito até que o css seja carregado no servidor, levando a um flash rápido de conteúdo não formatado em um navegador lento? Ainda tenho que testemunhar isso, embora esteja testando localmente.

Existe uma maneira de carregar o CSS através do objeto passado para o Angular $routeProvider.when?

Desde já, obrigado!

Brandon
fonte
Eu validei sua afirmação "flash rápido de conteúdo não formatado". Usei <link>tags css nesse formato , com o Chrome mais recente, o servidor na minha máquina local (e "Desativar cache" ativado para simular as condições de "primeiro carregamento"). Eu imagino que pré-inserir uma <style>tag no html parcial no servidor evitaria esse problema.
elegante

Respostas:

150

Sei que essa pergunta é antiga agora, mas depois de fazer uma tonelada de pesquisas sobre várias soluções para esse problema, acho que posso ter encontrado uma solução melhor.

ATUALIZAÇÃO 1: Desde a publicação desta resposta, adicionei todo esse código a um serviço simples que publiquei no GitHub. O repo está localizado aqui . Sinta-se à vontade para conferir mais informações.

ATUALIZAÇÃO 2: Essa resposta é ótima se tudo o que você precisa é de uma solução leve para obter folhas de estilo para suas rotas. Se você deseja uma solução mais completa para gerenciar folhas de estilo sob demanda em todo o aplicativo, confira o projeto AngularCSS do Door3 . Ele fornece muito mais funcionalidade refinada.

Caso alguém no futuro esteja interessado, aqui está o que eu criei:

1. Crie uma diretiva personalizada para o <head>elemento:

app.directive('head', ['$rootScope','$compile',
    function($rootScope, $compile){
        return {
            restrict: 'E',
            link: function(scope, elem){
                var html = '<link rel="stylesheet" ng-repeat="(routeCtrl, cssUrl) in routeStyles" ng-href="{{cssUrl}}" />';
                elem.append($compile(html)(scope));
                scope.routeStyles = {};
                $rootScope.$on('$routeChangeStart', function (e, next, current) {
                    if(current && current.$$route && current.$$route.css){
                        if(!angular.isArray(current.$$route.css)){
                            current.$$route.css = [current.$$route.css];
                        }
                        angular.forEach(current.$$route.css, function(sheet){
                            delete scope.routeStyles[sheet];
                        });
                    }
                    if(next && next.$$route && next.$$route.css){
                        if(!angular.isArray(next.$$route.css)){
                            next.$$route.css = [next.$$route.css];
                        }
                        angular.forEach(next.$$route.css, function(sheet){
                            scope.routeStyles[sheet] = sheet;
                        });
                    }
                });
            }
        };
    }
]);

Esta diretiva faz o seguinte:

  1. Ele compila (usando $compile) uma string html que cria um conjunto de <link />tags para cada item no scope.routeStylesobjeto usando ng-repeate ng-href.
  2. Anexa esse conjunto compilado de <link />elementos à <head>tag.
  3. Em seguida, ele usa o $rootScopepara ouvir '$routeChangeStart'eventos. Para cada '$routeChangeStart'evento, ele pega o $$routeobjeto "atual" (a rota que o usuário está prestes a sair) e remove seus arquivos css parciais específicos da <head>tag. Ele também pega o "próximo" $$routeobjeto (a rota para a qual o usuário está prestes a ir) e adiciona qualquer um dos seus arquivos css parciais específicos à <head>tag.
  4. E a ng-repeatparte da <link />tag compilada lida com a adição e remoção de folhas de estilo específicas da página, com base no que é adicionado ou removido do scope.routeStylesobjeto.

Nota: isso requer que seu ng-appatributo esteja no <html>elemento, não em <body>ou qualquer coisa dentro dele <html>.

2. Especifique quais folhas de estilo pertencem a quais rotas usando o $routeProvider:

app.config(['$routeProvider', function($routeProvider){
    $routeProvider
        .when('/some/route/1', {
            templateUrl: 'partials/partial1.html', 
            controller: 'Partial1Ctrl',
            css: 'css/partial1.css'
        })
        .when('/some/route/2', {
            templateUrl: 'partials/partial2.html',
            controller: 'Partial2Ctrl'
        })
        .when('/some/route/3', {
            templateUrl: 'partials/partial3.html',
            controller: 'Partial3Ctrl',
            css: ['css/partial3_1.css','css/partial3_2.css']
        })
}]);

Essa configuração adiciona uma csspropriedade personalizada ao objeto usado para configurar a rota de cada página. Esse objeto é passado para cada '$routeChangeStart'evento como .$$route. Portanto, ao ouvir o '$routeChangeStart'evento, podemos pegar a csspropriedade que especificamos e anexar / remover essas <link />tags, conforme necessário. Observe que especificar uma csspropriedade na rota é completamente opcional, pois foi omitida no '/some/route/2'exemplo. Se a rota não tiver uma csspropriedade, a <head>diretiva simplesmente não fará nada para essa rota. Observe também que você pode até ter várias folhas de estilo específicas da página por rota, como no '/some/route/3'exemplo acima, em que a csspropriedade é uma matriz de caminhos relativos às folhas de estilo necessárias para essa rota.

3. Você está pronto Essas duas coisas configuram tudo o que é necessário e, na minha opinião, o código mais limpo possível.

Espero que ajude alguém que possa estar lutando com esse problema tanto quanto eu.

tenente
fonte
2
Santo Moly, obrigado por isso! Exatamente o que eu estava procurando :). Apenas testei agora e funciona perfeitamente (além de fácil de implementar). Talvez você deva criar uma solicitação pull e inseri-la no núcleo. Eu sei que os caras do AngularJS estavam olhando no CSS com escopo, isso poderia ser um passo na direção certa?
smets.kevin
Esses caras são muito mais espertos do que eu. Tenho certeza de que eles teriam pensado nessa solução (ou similar) antes e optaram por não implementá-la no núcleo por qualquer motivo.
tennisgent
Qual é o local correto para o arquivo css? Css: 'css / parcial1.css' implica a pasta css na raiz da pasta angular do aplicativo?
Cordle
É relativo ao seu index.htmlarquivo. Assim, no exemplo acima, index.htmlestaria na raiz e a csspasta na raiz, contendo todos os arquivos css. mas você pode estruturar seu aplicativo da maneira que desejar, desde que use os caminhos relativos corretos.
tennisgent
1
@Kappys, o script remove o estilo da visualização anterior quando você passa para uma nova visualização. Se você não quer que isso aconteça, basta remover o seguinte código a partir da directiva: angular.forEach(current.$$route.css, function(sheet){ delete scope.routeStyles[sheet]; });.
tennisgent 26/02
34

A solução do @ tennisgent é ótima. No entanto, acho que é um pouco limitado.

A modularidade e o encapsulamento no Angular vão além das rotas. Com base na maneira como a web está caminhando para o desenvolvimento baseado em componentes, é importante aplicar isso também nas diretivas.

Como você já sabe, no Angular podemos incluir modelos (estrutura) e controladores (comportamento) em páginas e componentes. O AngularCSS permite a última peça que falta: anexar folhas de estilo (apresentação).

Para uma solução completa, sugiro usar o AngularCSS.

  1. Suporta ngRoute, UI Router, diretivas, controladores e serviços da Angular.
  2. Não é necessário ter ng-appna <html>tag. Isso é importante quando você tem vários aplicativos em execução na mesma página
  3. Você pode personalizar onde as folhas de estilo são injetadas: cabeça, corpo, seletor personalizado, etc.
  4. Suporta pré-carregamento, persistência e bloqueio de cache
  5. Oferece suporte a consultas de mídia e otimiza o carregamento da página por meio da API matchMedia

https://github.com/door3/angular-css

aqui estão alguns exemplos:

Rotas

  $routeProvider
    .when('/page1', {
      templateUrl: 'page1/page1.html',
      controller: 'page1Ctrl',
      /* Now you can bind css to routes */
      css: 'page1/page1.css'
    })
    .when('/page2', {
      templateUrl: 'page2/page2.html',
      controller: 'page2Ctrl',
      /* You can also enable features like bust cache, persist and preload */
      css: {
        href: 'page2/page2.css',
        bustCache: true
      }
    })
    .when('/page3', {
      templateUrl: 'page3/page3.html',
      controller: 'page3Ctrl',
      /* This is how you can include multiple stylesheets */
      css: ['page3/page3.css','page3/page3-2.css']
    })
    .when('/page4', {
      templateUrl: 'page4/page4.html',
      controller: 'page4Ctrl',
      css: [
        {
          href: 'page4/page4.css',
          persist: true
        }, {
          href: 'page4/page4.mobile.css',
          /* Media Query support via window.matchMedia API
           * This will only add the stylesheet if the breakpoint matches */
          media: 'screen and (max-width : 768px)'
        }, {
          href: 'page4/page4.print.css',
          media: 'print'
        }
      ]
    });

Diretivas

myApp.directive('myDirective', function () {
  return {
    restrict: 'E',
    templateUrl: 'my-directive/my-directive.html',
    css: 'my-directive/my-directive.css'
  }
});

Além disso, você pode usar o $cssserviço para casos extremos:

myApp.controller('pageCtrl', function ($scope, $css) {

  // Binds stylesheet(s) to scope create/destroy events (recommended over add/remove)
  $css.bind({ 
    href: 'my-page/my-page.css'
  }, $scope);

  // Simply add stylesheet(s)
  $css.add('my-page/my-page.css');

  // Simply remove stylesheet(s)
  $css.remove(['my-page/my-page.css','my-page/my-page2.css']);

  // Remove all stylesheets
  $css.removeAll();

});

Você pode ler mais sobre o AngularCSS aqui:

http://door3.com/insights/introducing-angularcss-css-demand-angularjs

castillo.io
fonte
1
Eu realmente gosto da sua abordagem aqui, mas queria saber como ela poderia ser usada em um aplicativo de produção em que todos os estilos de css precisem ser concatenados juntos? Para modelos html, uso $ templateCache.put () para o código de produção e seria bom fazer algo semelhante para css.
Tom Makin
Se você precisar obter CSS concatenado do servidor, sempre poderá fazer algo como /getCss?files=file1(.css),file2,file3 e o servidor responderão com todos os 3 arquivos em uma determinada ordem e concatenados.
Petr Urban
13

Pode acrescentar uma nova folha de estilo à cabeça $routeProvider. Para simplificar, estou usando uma string, mas também poderia criar um novo elemento de link ou criar um serviço para folhas de estilo

/* check if already exists first - note ID used on link element*/
/* could also track within scope object*/
if( !angular.element('link#myViewName').length){
    angular.element('head').append('<link id="myViewName" href="myViewName.css" rel="stylesheet">');
}

O maior benefício da pré-codificação na página é que já existem imagens de plano de fundo e menor probabilidade de FOUC

charlietfl
fonte
Não seria este realizar a mesma coisa como apenas incluindo o <link>no <head>do index.html estaticamente, embora?
Brandon
não se o whenfor da rota não tiver sido chamado. Pode colocar esse código em controllerretorno de chamada de whendentro do routeProvider, ou talvez dentro de resolvecallback que gatilhos prováveis mais cedo
charlietfl
Oh ok, meu mal, isso clica em não. Parece bastante sólido, exceto que você poderia explicar como é o pré-carregamento se eu estiver injetando quando de qualquer maneira?
Brandon
1
não é pré-carregamento se você anexá-lo em routeprovider... que o comentário era sobre incluí-lo na cabeça da página quando a página é servido
charlietfl
Desculpe, estou com falta de sono se você não sabe. Enfim, é assim que estou agora. Tentar descobrir se a sobrecarga de carregar todas as minhas folhas de estilo de uma só vez é melhor do que ter alguma FOUC quando o usuário alterna as visualizações. Eu acho que essa não é realmente uma pergunta relacionada ao Angular, mas sobre o aplicativo da Web UX. Obrigado, no entanto, provavelmente seguirei sua sugestão se decidir não fazer o pré-carregamento.
Brandon
5

@ sz3, engraçado o suficiente hoje, eu tive que fazer exatamente o que você estava tentando alcançar: ' carregar um arquivo CSS específico somente quando um usuário acessar ' uma página específica. Então, eu usei a solução acima.

Mas estou aqui para responder à sua última pergunta: ' onde exatamente devo colocar o código. Alguma idéia ?

Você estava certo ao incluir o código na resolução , mas precisa alterar um pouco o formato.

Dê uma olhada no código abaixo:

.when('/home', {
  title:'Home - ' + siteName,
  bodyClass: 'home',
  templateUrl: function(params) {
    return 'views/home.html';
  },
  controler: 'homeCtrl',
  resolve: {
    style : function(){
      /* check if already exists first - note ID used on link element*/
      /* could also track within scope object*/
      if( !angular.element('link#mobile').length){
        angular.element('head').append('<link id="home" href="home.css" rel="stylesheet">');
      }
    }
  }
})

Acabei de testar e está funcionando bem , ele injeta o html e carrega meu 'home.css' somente quando eu bati na rota '/ home'.

Uma explicação completa pode ser encontrada aqui , mas basicamente resolva: deve obter um objeto no formato

{
  'key' : string or function()
} 

Você pode nomear a ' chave ' como quiser - no meu caso, chamei de ' estilo '.

Então, para o valor, você tem duas opções:

  • Se for uma sequência , é um alias para um serviço.

  • Se for função , será injetado e o valor de retorno será tratado como dependência.

O ponto principal aqui é que o código dentro da função será executado antes que o controlador seja instanciado e o evento $ routeChangeSuccess seja acionado.

Espero que ajude.

Denison Luz
fonte
2

Perfeito, obrigado!! Só tive que fazer alguns ajustes para fazê-lo funcionar com o ui-router:

    var app = app || angular.module('app', []);

    app.directive('head', ['$rootScope', '$compile', '$state', function ($rootScope, $compile, $state) {

    return {
        restrict: 'E',
        link: function ($scope, elem, attrs, ctrls) {

            var html = '<link rel="stylesheet" ng-repeat="(routeCtrl, cssUrl) in routeStyles" ng-href="{{cssUrl}}" />';
            var el = $compile(html)($scope)
            elem.append(el);
            $scope.routeStyles = {};

            function applyStyles(state, action) {
                var sheets = state ? state.css : null;
                if (state.parent) {
                    var parentState = $state.get(state.parent)
                    applyStyles(parentState, action);
                }
                if (sheets) {
                    if (!Array.isArray(sheets)) {
                        sheets = [sheets];
                    }
                    angular.forEach(sheets, function (sheet) {
                        action(sheet);
                    });
                }
            }

            $rootScope.$on('$stateChangeStart', function (event, toState, toParams, fromState, fromParams) {

                applyStyles(fromState, function(sheet) {
                    delete $scope.routeStyles[sheet];
                    console.log('>> remove >> ', sheet);
                });

                applyStyles(toState, function(sheet) {
                    $scope.routeStyles[sheet] = sheet;
                    console.log('>> add >> ', sheet);
                });
            });
        }
    }
}]);
CraigM
fonte
Eu não precisava exatamente remover e adicionar em qualquer lugar, pois meu css estava bagunçado, mas isso foi de grande ajuda com o ui-router! Obrigado :)
imsheth 14/03
1

Se você só precisa que seu CSS seja aplicado a uma visão específica, estou usando este trecho útil dentro do meu controlador:

$("body").addClass("mystate");

$scope.$on("$destroy", function() {
  $("body").removeClass("mystate"); 
});

Isso adicionará uma classe à minha bodytag quando o estado for carregado e a removerá quando o estado for destruído (ou seja, alguém muda de página). Isso resolve meu problema relacionado de precisar apenas que o CSS seja aplicado a um estado no meu aplicativo.

Matt
fonte
0

'use strict'; angular.module ('app') .run (['$ rootScope', '$ state', '$ stateParams', função ($ rootScope, $ state, $ stateParams) {$ rootScope. $ state = $ state; $ rootScope . $ stateParams = $ stateParams;}]) .config (['$ stateProvider', '$ urlRouterProvider', função ($ stateProvider, $ urlRouterProvider) {

            $urlRouterProvider
                .otherwise('/app/dashboard');
            $stateProvider
                .state('app', {
                    abstract: true,
                    url: '/app',
                    templateUrl: 'views/layout.html'
                })
                .state('app.dashboard', {
                    url: '/dashboard',
                    templateUrl: 'views/dashboard.html',
                    ncyBreadcrumb: {
                        label: 'Dashboard',
                        description: ''
                    },
                    resolve: {
                        deps: [
                            '$ocLazyLoad',
                            function($ocLazyLoad) {
                                return $ocLazyLoad.load({
                                    serie: true,
                                    files: [
                                        'lib/jquery/charts/sparkline/jquery.sparkline.js',
                                        'lib/jquery/charts/easypiechart/jquery.easypiechart.js',
                                        'lib/jquery/charts/flot/jquery.flot.js',
                                        'lib/jquery/charts/flot/jquery.flot.resize.js',
                                        'lib/jquery/charts/flot/jquery.flot.pie.js',
                                        'lib/jquery/charts/flot/jquery.flot.tooltip.js',
                                        'lib/jquery/charts/flot/jquery.flot.orderBars.js',
                                        'app/controllers/dashboard.js',
                                        'app/directives/realtimechart.js'
                                    ]
                                });
                            }
                        ]
                    }
                })
                .state('ram', {
                    abstract: true,
                    url: '/ram',
                    templateUrl: 'views/layout-ram.html'
                })
                .state('ram.dashboard', {
                    url: '/dashboard',
                    templateUrl: 'views/dashboard-ram.html',
                    ncyBreadcrumb: {
                        label: 'test'
                    },
                    resolve: {
                        deps: [
                            '$ocLazyLoad',
                            function($ocLazyLoad) {
                                return $ocLazyLoad.load({
                                    serie: true,
                                    files: [
                                        'lib/jquery/charts/sparkline/jquery.sparkline.js',
                                        'lib/jquery/charts/easypiechart/jquery.easypiechart.js',
                                        'lib/jquery/charts/flot/jquery.flot.js',
                                        'lib/jquery/charts/flot/jquery.flot.resize.js',
                                        'lib/jquery/charts/flot/jquery.flot.pie.js',
                                        'lib/jquery/charts/flot/jquery.flot.tooltip.js',
                                        'lib/jquery/charts/flot/jquery.flot.orderBars.js',
                                        'app/controllers/dashboard.js',
                                        'app/directives/realtimechart.js'
                                    ]
                                });
                            }
                        ]
                    }
                })
                 );
rambaburoja
fonte
Um exemplo simples de código sem contexto raramente é uma resposta suficiente para uma pergunta. Além disso, esta pergunta já tem uma resposta altamente aceita.
AJ X.