Jak uwzględnić widok / częściowy określony styl w AngularJS

131

Jaki jest prawidłowy / akceptowany sposób używania oddzielnych arkuszy stylów dla różnych widoków używanych w mojej aplikacji?

Obecnie umieszczam element link w kodzie HTML widoku / częściowej na górze, ale powiedziano mi, że jest to zła praktyka, mimo że wszystkie współczesne przeglądarki go obsługują, ale widzę, dlaczego jest niezadowolony.

Inną możliwością jest umieszczenie oddzielnych arkuszy stylów w pliku index.html head ale chciałbym, aby ładował arkusz stylów tylko wtedy, gdy jego widok jest ładowany w nazwie wydajności.

Czy jest to zła praktyka, ponieważ stylizacja nie zacznie obowiązywać, dopóki css nie zostanie załadowany z serwera, co prowadzi do szybkiego flashowania niesformatowanej zawartości w powolnej przeglądarce? Nie byłem jeszcze tego świadkiem, chociaż testuję to lokalnie.

Czy istnieje sposób na załadowanie CSS przez obiekt przekazany do Angulara? $routeProvider.when ?

Z góry dziękuję!

Brandon
źródło
Potwierdziłem twoją asercję dotyczącą „szybkiego flashowania niesformatowanej treści”. Użyłem <link>tagów css w tym formacie , z najnowszym Chrome, serwerem na moim komputerze lokalnym (i włączonym „Wyłącz pamięć podręczną”, aby zasymulować warunki „pierwszego ładowania”). Wyobrażam sobie, że wstępne wstawienie <style>tagu w części html na serwerze pozwoliłoby uniknąć tego problemu.
najpiękniejszy

Odpowiedzi:

150

Wiem, że to pytanie jest stare, ale po przeprowadzeniu wielu badań nad różnymi rozwiązaniami tego problemu, myślę, że mogłem znaleźć lepsze rozwiązanie.

AKTUALIZACJA 1: Od czasu opublikowania tej odpowiedzi dodałem cały ten kod do prostej usługi, którą opublikowałem na GitHub. Repozytorium znajduje się tutaj . Zapraszam do sprawdzenia, aby uzyskać więcej informacji.

AKTUALIZACJA 2: Ta odpowiedź jest świetna, jeśli potrzebujesz tylko lekkiego rozwiązania do pobierania arkuszy stylów dla twoich tras. Jeśli potrzebujesz bardziej kompletnego rozwiązania do zarządzania arkuszami stylów na żądanie w całej aplikacji, możesz sprawdzić projekt AngularCSS Door3 . Zapewnia znacznie bardziej szczegółową funkcjonalność.

Na wypadek, gdyby ktoś w przyszłości był zainteresowany, oto co wymyśliłem:

1. Utwórz niestandardową dyrektywę dla <head>elementu:

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

Ta dyrektywa ma następujące znaczenie:

  1. Kompiluje (używając $compile) ciąg html, który tworzy zestaw <link />tagów dla każdego elementu w scope.routeStylesobiekcie przy użyciu ng-repeati ng-href.
  2. Dołącza skompilowany zestaw <link />elementów do <head>znacznika.
  3. Następnie używa $rootScopedo nasłuchiwania '$routeChangeStart'zdarzeń. Przy każdym '$routeChangeStart'zdarzeniu pobiera „bieżący” $$routeobiekt (trasę, którą użytkownik ma zamiar opuścić) i usuwa ze <head>znacznika jego częściowo specyficzne pliki css . Przechwytuje również „następny” $$routeobiekt (trasę, do której ma się udać użytkownik) i dodaje do <head>znacznika dowolny ze swoich częściowo specyficznych plików css .
  4. A ng-repeatczęść zebranych <link />uchwyty znaczników wszystkich dodawanie i usuwanie arkuszy stylów strona specyficznych oparciu o to, co dostaje dodawane lub usuwane z scope.routeStylesobiektu.

Uwaga: wymaga to, aby twój ng-appatrybut znajdował się na <html>elemencie, a nie na <body>lub cokolwiek wewnątrz <html>.

2. Określ, które arkusze stylów należą do których tras, używając $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']
        })
}]);

Ta konfiguracja dodaje csswłaściwość niestandardową do obiektu, który jest używany do konfigurowania trasy każdej strony. Ten obiekt jest przekazywany do każdego '$routeChangeStart'zdarzenia jako .$$route. Tak więc podczas słuchania '$routeChangeStart'zdarzenia możemy pobrać określoną przez nas csswłaściwość i <link />w razie potrzeby dołączyć / usunąć te tagi. Zauważ, że określenie csswłaściwości na trasie jest całkowicie opcjonalne, ponieważ zostało pominięte w '/some/route/2'przykładzie. Jeśli trasa nie ma csswłaściwości, <head>dyrektywa po prostu nie zrobi nic dla tej trasy. Zauważ również, że możesz mieć nawet wiele arkuszy stylów specyficznych dla strony na trasę, jak w '/some/route/3'powyższym przykładzie, gdzie csswłaściwość jest tablicą względnych ścieżek do arkuszy stylów potrzebnych dla tej trasy.

3. Gotowe Te dwie rzeczy konfigurują wszystko, co było potrzebne, i moim zdaniem robi to z możliwie najczystszym kodem.

Mam nadzieję, że pomoże to komuś innemu, kto może borykać się z tym problemem tak samo jak ja.

tenisista
źródło
2
Holy moly, dzięki za to! Dokładnie to, czego szukałem :). Właśnie przetestowałem to teraz i działa doskonale (plus łatwe do wdrożenia). Może powinieneś utworzyć dla tego żądanie ściągnięcia i umieścić je w rdzeniu. Wiem, że faceci z AngularJS patrzyli na css z lunetą, to może być krok we właściwym kierunku?
smets.kevin
Ci goście są o wiele mądrzejsi niż ja. Jestem pewien, że wymyśliliby to (lub podobne) rozwiązanie wcześniej i zdecydowali się nie wdrażać go do rdzenia z jakiegokolwiek powodu.
tenisista
Jakie jest prawidłowe miejsce na plik css? Czy css: „css / parts1.css” implikuje folder css w katalogu głównym folderu aplikacji kątowej?
Cordle,
Jest względny w stosunku do twojego index.htmlpliku. Zatem w powyższym przykładzie index.htmlznajdowałby się w katalogu głównym, a cssfolder w katalogu głównym, zawierający wszystkie pliki css. ale możesz dowolnie kształtować strukturę aplikacji, o ile używasz prawidłowych ścieżek względnych.
tenisista
1
@Kappys, skrypt usuwa styl z poprzedniego widoku po przejściu do nowego widoku. Jeśli nie chcesz, aby tak się stało, po prostu usuń następujący kod z dyrektywą: angular.forEach(current.$$route.css, function(sheet){ delete scope.routeStyles[sheet]; });.
tenisista
34

Rozwiązanie @ tennisgent jest świetne. Myślę jednak, że jest trochę ograniczony.

Modułowość i enkapsulacja w Angular wykraczają poza trasy. Biorąc pod uwagę sposób, w jaki sieć zmierza w kierunku rozwoju opartego na komponentach, ważne jest, aby zastosować to również w dyrektywach.

Jak już wiesz, w Angular możemy dołączyć szablony (strukturę) i kontrolery (zachowanie) do stron i komponentów. AngularCSS umożliwia ostatni brakujący element: dołączanie arkuszy stylów (prezentacja).

Aby uzyskać pełne rozwiązanie, sugeruję użycie AngularCSS.

  1. Obsługuje ngRoute, UI Router, dyrektywy, kontrolery i usługi Angulara.
  2. Nie musi mieć ng-appw <html>tagu. Jest to ważne, gdy na tej samej stronie działa wiele aplikacji
  3. Możesz dostosować miejsce wstrzyknięcia arkuszy stylów: głowa, ciało, niestandardowy selektor itp.
  4. Obsługuje wstępne ładowanie, utrwalanie i pomijanie pamięci podręcznej
  5. Obsługuje zapytania o media i optymalizuje ładowanie strony przez matchMedia API

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

Oto kilka przykładów:

Trasy

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

Dyrektywy

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

Dodatkowo możesz skorzystać z $cssusługi dla skrajnych przypadków:

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

});

Możesz przeczytać więcej o AngularCSS tutaj:

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

castillo.io
źródło
1
Bardzo podoba mi się twoje podejście tutaj, ale zastanawiałem się, jak można je wykorzystać w aplikacji produkcyjnej, w której wszystkie style CSS muszą być połączone razem? W przypadku szablonów html używam $ templateCache.put () jako kodu produkcyjnego i fajnie byłoby zrobić coś podobnego dla css.
Tom Makin,
Jeśli chcesz pobrać połączone CSS z serwera, zawsze możesz zrobić coś takiego jak /getCss?files=file1(.css),file2,file3, a serwer odpowie wszystkimi trzema plikami w podanej kolejności i połączonymi.
Petr Urban
13

Można dołączyć nowy arkusz stylów, aby wejść do środka $routeProvider. Dla uproszczenia używam łańcucha, ale mógłbym również utworzyć nowy element łącza lub utworzyć usługę dla arkuszy stylów

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

Największą zaletą wstępnego kodowania na stronie jest to, że obrazy tła będą już istniały i mniej prawdopodobne FOUC

charlietfl
źródło
Nie byłoby to osiągnąć to samo, co właśnie w tym <link>w <head>od index.html statycznie, chociaż?
Brandon
nie, jeśli nie wywołano whentrasy. Można umieścić ten kod w controllerwywołaniu zwrotnym w whenramach routeProviderlub być może w resolvewywołaniu zwrotnym, które prawdopodobnie
uruchomi się
No dobra, moja wina, że ​​nie. Wygląda całkiem solidnie, ale czy mógłbyś wyjaśnić, jak to jest wstępne ładowanie, jeśli i tak go wstrzykuję?
Brandon
1
nie jest to wstępne ładowanie, jeśli routeproviderdodasz go do ... ten komentarz dotyczył umieszczania go w nagłówku strony głównej, gdy strona jest wyświetlana
charlietfl
-_- przepraszam, brakuje mi snu, jeśli nie możesz powiedzieć. W każdym razie, w tym właśnie teraz jestem. Próba ustalenia, czy obciążenie związane z ładowaniem wszystkich moich arkuszy stylów na raz jest lepsze niż posiadanie trochę FOUC, gdy użytkownik przełącza widoki. Wydaje mi się, że to naprawdę nie jest kwestia związana z Angularem, a raczej z UX aplikacji internetowej. Jednak dzięki, prawdopodobnie zgodzę się z twoją sugestią, jeśli nie zdecyduję się na wstępne ładowanie.
Brandon
5

@ sz3, dość zabawne dzisiaj musiałem zrobić dokładnie to, co próbujesz osiągnąć: „ załaduj określony plik CSS tylko wtedy, gdy użytkownik uzyska dostęp ” do określonej strony. Więc użyłem powyższego rozwiązania.

Ale jestem tutaj, aby odpowiedzieć na twoje ostatnie pytanie: „ gdzie dokładnie mam umieścić kod. Jakieś pomysły ?

Miałeś rację, włączając kod do rozwiązania , ale musisz nieco zmienić format.

Spójrz na poniższy kod:

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

Właśnie przetestowałem i działa dobrze , wstrzykuje html i ładuje mój „home.css” tylko wtedy, gdy trafiam na trasę „/ home”.

Pełne wyjaśnienie można znaleźć tutaj , ale w zasadzie rozwiązanie: należy pobrać obiekt w formacie

{
  'key' : string or function()
} 

Możesz nazwać „ klucz ” jak tylko chcesz - w moim przypadku nazwałem „ styl ”.

Następnie dla wartości masz dwie opcje:

  • Jeśli jest to ciąg znaków , jest to alias usługi.

  • Jeśli jest to funkcja , zostaje wstrzyknięta, a wartość zwracana jest traktowana jako zależność.

Głównym celem jest to, że kod wewnątrz funkcji zostanie wykonany przed utworzeniem instancji kontrolera i wywołaniem zdarzenia $ routeChangeSuccess.

Mam nadzieję, że to pomoże.

Denison Luz
źródło
2

Wspaniale! Dziękuję!! Musiałem tylko wprowadzić kilka poprawek, aby działał z routerem ui:

    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
źródło
Nie potrzebowałem wszędzie usuwania i dodawania, ponieważ mój css był pomieszany, ale było to bardzo pomocne w przypadku routera ui! Dzięki :)
imsheth
1

Jeśli potrzebujesz tylko swojego CSS do zastosowania w jednym określonym widoku, używam tego przydatnego fragmentu w moim kontrolerze:

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

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

Spowoduje to dodanie klasy do mojego bodytagu, gdy stan się załaduje, i usunie ją, gdy stan zostanie zniszczony (np. Ktoś zmieni strony). To rozwiązuje mój problem związany z potrzebą zastosowania CSS tylko do jednego stanu w mojej aplikacji.

Matt
źródło
0

„użyj ścisłego”; 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
źródło
Prosty przykład kodu bez kontekstu rzadko jest wystarczającą odpowiedzią na pytanie. Co więcej, to pytanie ma już bardzo akceptowaną odpowiedź.
AJ X.