Comment inclure un style spécifique à la vue / partielle dans AngularJS

132

Quelle est la manière correcte / acceptée d'utiliser des feuilles de style distinctes pour les différentes vues utilisées par mon application?

Actuellement, je place un élément de lien dans le html de view / partial en haut, mais on m'a dit que c'est une mauvaise pratique même si tous les navigateurs modernes le supportent, mais je peux voir pourquoi c'est mal vu.

L'autre possibilité est de placer les feuilles de style séparées dans mon index.html headmais je voudrais qu'il ne charge la feuille de style que si sa vue est chargée au nom de la performance.

Est-ce une mauvaise pratique puisque le style ne prendra effet qu'après le chargement du CSS depuis le serveur, ce qui entraîne un flash rapide du contenu non formaté dans un navigateur lent? Je n'ai pas encore été témoin de cela bien que je le teste localement.

Existe-t-il un moyen de charger le CSS via l'objet passé à Angular $routeProvider.when?

Merci d'avance!

Brandon
la source
J'ai validé votre assertion "Flash rapide de contenu non formaté". J'ai utilisé des <link>balises css dans ce format , avec le dernier Chrome, le serveur sur ma machine locale (et "Désactiver le cache" pour simuler les conditions de "premier chargement"). J'imagine que pré-insérer une <style>balise dans le partiel html sur le serveur éviterait ce problème.
chic du

Réponses:

150

Je sais que cette question est ancienne maintenant, mais après avoir fait une tonne de recherches sur diverses solutions à ce problème, je pense que j'ai peut-être trouvé une meilleure solution.

MISE À JOUR 1: Depuis la publication de cette réponse, j'ai ajouté tout ce code à un service simple que j'ai publié sur GitHub. Le repo se trouve ici . N'hésitez pas à le consulter pour plus d'informations.

MISE À JOUR 2: Cette réponse est excellente si tout ce dont vous avez besoin est une solution légère pour extraire des feuilles de style pour vos itinéraires. Si vous souhaitez une solution plus complète pour gérer les feuilles de style à la demande dans toute votre application, vous pouvez consulter le projet AngularCSS de Door3 . Il fournit des fonctionnalités beaucoup plus fines.

Au cas où quelqu'un dans le futur serait intéressé, voici ce que j'ai proposé:

1. Créez une directive personnalisée pour l' <head>élément:

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

Cette directive fait les choses suivantes:

  1. Il compile (en utilisant $compile) une chaîne html qui crée un ensemble de <link />balises pour chaque élément de l' scope.routeStylesobjet en utilisant ng-repeatet ng-href.
  2. Il ajoute cet ensemble d' <link />éléments compilés à la <head>balise.
  3. Il utilise ensuite le $rootScopepour écouter les '$routeChangeStart'événements. Pour chaque '$routeChangeStart'événement, il saisit l' $$routeobjet "courant" (la route que l'utilisateur est sur le point de quitter) et supprime son ou ses fichiers css spécifiques partiels de la <head>balise. Il saisit également l' $$routeobjet "suivant" (la route vers laquelle l'utilisateur est sur le point de se rendre) et ajoute l'un de ses fichiers css partiellement spécifiques à la <head>balise.
  4. Et la ng-repeatpartie de la <link />balise compilée gère tous les ajouts et suppressions de feuilles de style spécifiques à la page en fonction de ce qui est ajouté ou supprimé de l' scope.routeStylesobjet.

Remarque: cela nécessite que votre ng-appattribut soit sur l' <html>élément, pas sur <body>ou quoi que ce soit à l'intérieur de <html>.

2. Spécifiez quelles feuilles de style appartiennent à quels itinéraires à l'aide de $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']
        })
}]);

Cette configuration ajoute une csspropriété personnalisée à l'objet utilisé pour configurer l'itinéraire de chaque page. Cet objet est transmis à chaque '$routeChangeStart'événement en tant que .$$route. Ainsi, lors de l'écoute de l' '$routeChangeStart'événement, nous pouvons récupérer la csspropriété que nous avons spécifiée et ajouter / supprimer ces <link />balises si nécessaire. Notez que la spécification d'une csspropriété sur l'itinéraire est complètement facultative, car elle a été omise de l' '/some/route/2'exemple. Si la route n'a pas de csspropriété, la <head>directive ne fera simplement rien pour cette route. Notez également que vous pouvez même avoir plusieurs feuilles de style spécifiques à une page par itinéraire, comme dans l' '/some/route/3'exemple ci-dessus, où la csspropriété est un tableau de chemins relatifs vers les feuilles de style nécessaires pour cet itinéraire.

3. Vous avez terminé. Ces deux choses configurent tout ce qui était nécessaire et il le fait, à mon avis, avec le code le plus propre possible.

J'espère que cela aidera quelqu'un d'autre qui pourrait être aux prises avec ce problème autant que moi.

tennisgent
la source
2
Holy moly, merci pour ça! Exactement ce que je cherchais :). Je viens de le tester maintenant et cela fonctionne parfaitement (plus facile à mettre en œuvre). Vous devriez peut-être créer une pull request pour cela et l'introduire dans le noyau. Je sais que les gars d'AngularJS cherchaient des css étendus, cela pourrait être un pas dans la bonne direction?
smets.kevin le
Ces types sont bien plus intelligents que moi. Je suis sûr qu'ils auraient déjà imaginé cette solution (ou une solution similaire) et choisi de ne pas l'implémenter dans le noyau pour une raison quelconque.
tennisgent
Quel est le bon emplacement pour le fichier css? Est-ce que css: 'css / partial1.css' implique le dossier css à la racine du dossier angular app?
Cordle
C'est relatif à votre index.htmlfichier. Donc, dans l'exemple ci-dessus, index.htmlserait à la racine et le cssdossier serait à la racine, contenant tous les fichiers css. mais vous pouvez structurer votre application comme vous le souhaitez, à condition d'utiliser les bons chemins relatifs.
tennisgent
1
@Kappys, le script supprime le style de la vue précédente lorsque vous passez à une nouvelle vue. Si vous ne voulez pas que cela se produise, il suffit de retirer le code suivant de la directive: angular.forEach(current.$$route.css, function(sheet){ delete scope.routeStyles[sheet]; });.
tennisgent
34

La solution de @ tennisgent est excellente. Cependant, je pense que c'est un peu limité.

La modularité et l'encapsulation dans Angular vont au-delà des routes. Compte tenu de la façon dont le Web évolue vers le développement basé sur les composants, il est important de l'appliquer également dans les directives.

Comme vous le savez déjà, dans Angular, nous pouvons inclure des modèles (structure) et des contrôleurs (comportement) dans les pages et les composants. AngularCSS active la dernière pièce manquante: attacher des feuilles de style (présentation).

Pour une solution complète, je suggère d'utiliser AngularCSS.

  1. Prend en charge le ngRoute d'Angular, le routeur d'interface utilisateur, les directives, les contrôleurs et les services.
  2. N'a pas besoin d'avoir ng-appdans la <html>balise. Ceci est important lorsque plusieurs applications s'exécutent sur la même page
  3. Vous pouvez personnaliser l'emplacement d'injection des feuilles de style: tête, corps, sélecteur personnalisé, etc ...
  4. Prend en charge le préchargement, la persistance et le contournement du cache
  5. Prend en charge les requêtes multimédias et optimise le chargement de la page via l'API matchMedia

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

Voici quelques exemples:

Itinéraires

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

Directives

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

De plus, vous pouvez utiliser le $cssservice pour les cas extrêmes:

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

});

Vous pouvez en savoir plus sur AngularCSS ici:

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

castillo.io
la source
1
J'aime vraiment votre approche ici, mais je me demandais comment elle pourrait être utilisée dans une application de production où tous les styles css doivent être concaténés ensemble? Pour les modèles html, j'utilise $ templateCache.put () pour le code de production et ce serait bien de faire quelque chose de similaire pour css.
Tom Makin
Si vous avez besoin d'obtenir du CSS concaténé à partir du serveur, vous pouvez toujours faire quelque chose comme /getCss?files=file1(.css),file2,file3 et le serveur répondrait avec les 3 fichiers dans l'ordre donné et concaténé.
Petr Urban
13

Pourrait ajouter une nouvelle feuille de style à l'intérieur $routeProvider. Pour plus de simplicité, j'utilise une chaîne mais je pourrais également créer un nouvel élément de lien ou créer un service pour les feuilles de style

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

Le plus grand avantage du pré-codage dans la page est que toutes les images d'arrière-plan existent déjà, et moins FOUC

charlietfl
la source
Cela n'accomplirait-il pas la même chose que d'inclure simplement le <link>dans <head>le fichier index.html de manière statique?
Brandon
pas si le whenpour l'itinéraire n'a pas été appelé. Peut mettre ce code dans le controllerrappel de l' whenintérieur du routeProviderou peut - être dans le resolverappel qui déclenche probablement plus tôt
charlietfl
Oh ok, mon mauvais, ça clique sur non. Ça a l'air assez solide, sauf que pouvez-vous expliquer comment son préchargement si je l'injecte quand de toute façon?
Brandon
1
ce n'est pas un préchargement si vous l'ajoutez dans routeprovider... ce commentaire
visait
-_- désolé, je manque de sommeil si vous ne pouvez pas le dire. Bref, c'est un peu là où j'en suis maintenant. Essayer de comprendre si la surcharge de chargement de toutes mes feuilles de style à la fois est meilleure que d'avoir un FOUC lorsque l'utilisateur change de vue. Je suppose que ce n'est vraiment pas une question liée à Angular autant que celle de l'UX des applications Web. Merci cependant, j'irai probablement avec votre suggestion si je décide de ne pas faire de préchargement.
Brandon
5

@ sz3, assez drôle aujourd'hui, je devais faire exactement ce que vous essayiez d'accomplir: " charger un fichier CSS spécifique uniquement lorsqu'un utilisateur accède " à une page spécifique. J'ai donc utilisé la solution ci-dessus.

Mais je suis ici pour répondre à votre dernière question: « où dois-je mettre exactement le code. Des idées ?

Vous aviez raison d'inclure le code dans la résolution , mais vous devez changer un peu le format.

Jetez un œil au code ci-dessous:

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

Je viens de tester et cela fonctionne bien , il injecte le html et il charge mon 'home.css' uniquement lorsque je frappe la route '/ home'.

Une explication complète peut être trouvée ici , mais fondamentalement résoudre: devrait obtenir un objet au format

{
  'key' : string or function()
} 

Vous pouvez nommer la « clé » comme vous le souhaitez - dans mon cas, j'ai appelé « style ».

Ensuite, pour la valeur, vous avez deux options:

  • S'il s'agit d'une chaîne , il s'agit d'un alias pour un service.

  • Si c'est une fonction , alors elle est injectée et la valeur de retour est traitée comme la dépendance.

Le point principal ici est que le code à l'intérieur de la fonction va être exécuté avant que le contrôleur ne soit instancié et que l'événement $ routeChangeSuccess soit déclenché.

J'espère que cela pourra aider.

Denison Luz
la source
2

Super merci!! Juste eu à faire quelques ajustements pour le faire fonctionner avec 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
la source
Je n'avais pas vraiment besoin de supprimer et d'ajouter partout car mon css était foiré, mais cela a été d'une grande aide avec ui-router! Merci :)
imsheth
1

Si vous avez seulement besoin que votre CSS soit appliqué à une vue spécifique, j'utilise cet extrait de code pratique dans mon contrôleur:

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

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

Cela ajoutera une classe à ma bodybalise lorsque l'état se charge, et la supprimera lorsque l'état est détruit (c'est-à-dire que quelqu'un change de page). Cela résout mon problème connexe consistant à ne nécessiter que l'application de CSS à un état de mon application.

Mat
la source
0

'use strict'; angular.module ('app') .run (['$ rootScope', '$ state', '$ stateParams', function ($ rootScope, $ state, $ stateParams) {$ rootScope. $ state = $ state; $ rootScope . $ stateParams = $ stateParams;}]) .config (['$ stateProvider', '$ urlRouterProvider', function ($ 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
la source
Un exemple de code simple sans contexte est rarement une réponse suffisante à une question. De plus, cette question a déjà une réponse très acceptée.
AJ X.