Cómo incluir vista / estilo específico parcial en AngularJS

132

¿Cuál es la forma correcta / aceptada de usar hojas de estilo separadas para las diversas vistas que usa mi aplicación?

Actualmente estoy colocando un elemento de enlace en el html de view / partial en la parte superior, pero me han dicho que es una mala práctica, aunque todos los navegadores modernos lo admiten, pero puedo ver por qué está mal visto.

La otra posibilidad es colocar las hojas de estilo separadas en mis index.html, headpero me gustaría que solo cargara la hoja de estilo si su vista se carga en nombre del rendimiento.

¿Es esta una mala práctica ya que el estilo no tendrá efecto hasta después de que se cargue el CSS desde el servidor, lo que provocará un destello rápido de contenido sin formato en un navegador lento? Todavía tengo que presenciar esto, aunque lo estoy probando localmente.

¿Hay alguna manera de cargar el CSS a través del objeto pasado a Angular's $routeProvider.when?

¡Gracias por adelantado!

Brandon
fuente
Validé su afirmación de "flash rápido de contenido sin formato". Utilicé <link>etiquetas css en este formato , con el último Chrome, el servidor en mi máquina local (y "Desactivar caché" para simular condiciones de "primera carga"). Me imagino que la inserción previa de una <style>etiqueta en el html parcial en el servidor evitaría este problema.
poshest

Respuestas:

150

Sé que esta pregunta es antigua ahora, pero después de investigar un montón sobre varias soluciones a este problema, creo que podría haber encontrado una mejor solución.

ACTUALIZACIÓN 1: desde que publiqué esta respuesta, he agregado todo este código a un servicio simple que he publicado en GitHub. El repositorio se encuentra aquí . No dude en consultarlo para obtener más información.

ACTUALIZACIÓN 2: esta respuesta es excelente si todo lo que necesita es una solución liviana para obtener hojas de estilo para sus rutas. Si desea una solución más completa para administrar las hojas de estilo a pedido en toda su aplicación, puede consultar el proyecto AngularCSS de Door3 . Proporciona una funcionalidad mucho más fina.

En caso de que alguien en el futuro esté interesado, esto es lo que se me ocurrió:

1. Cree una directiva personalizada para el <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 directiva hace lo siguiente:

  1. Compila (usando $compile) una cadena html que crea un conjunto de <link />etiquetas para cada elemento del scope.routeStylesobjeto usando ng-repeaty ng-href.
  2. Agrega ese conjunto compilado de <link />elementos a la <head>etiqueta.
  3. Luego usa el $rootScopepara escuchar '$routeChangeStart'eventos. Para cada '$routeChangeStart'evento, toma el $$routeobjeto "actual" (la ruta que el usuario está a punto de abandonar) y elimina sus archivos css específicos parciales de la <head>etiqueta. También toma el "siguiente" $$routeobjeto (la ruta a la que va a ir el usuario) y agrega cualquiera de sus archivos css específicos parciales a<head> etiqueta.
  4. Y la ng-repeatparte de la <link />etiqueta compilada maneja todas las adiciones y eliminaciones de las hojas de estilo específicas de la página en función de lo que se agrega o elimina del scope.routeStylesobjeto.

Nota: esto requiere que su ng-appatributo esté en el <html>elemento, no en el elemento <body>o dentro de él <html>.

2. Especifique qué hojas de estilo pertenecen a qué rutas utilizando $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']
        })
}]);

Esta configuración agrega una csspropiedad personalizada al objeto que se utiliza para configurar la ruta de cada página. Ese objeto se pasa a cada '$routeChangeStart'evento como .$$route. Entonces, al escuchar el '$routeChangeStart'evento, podemos tomar la csspropiedad que especificamos y agregar / eliminar esas <link />etiquetas según sea necesario. Tenga en cuenta que especificar una csspropiedad en la ruta es completamente opcional, ya que se omitió en el '/some/route/2'ejemplo. Si la ruta no tiene una csspropiedad, la <head>directiva simplemente no hará nada por esa ruta. Tenga en cuenta también que incluso puede tener varias hojas de estilo específicas de la página por ruta, como en el '/some/route/3'ejemplo anterior, donde la csspropiedad es una matriz de rutas relativas a las hojas de estilo necesarias para esa ruta.

3. Has terminado Esas dos cosas configuran todo lo que se necesitaba y lo hace, en mi opinión, con el código más limpio posible.

Espero que ayude a alguien más que pueda estar luchando con este problema tanto como yo.

tennisgent
fuente
2
Santo Moly, gracias por esto! Exactamente lo que estaba buscando :). Acabo de probarlo ahora y funciona perfectamente (además de fácil de implementar). Tal vez debería crear una solicitud de extracción para esto y llevarlo al núcleo. Sé que los chicos de AngularJS estaban estudiando css con alcance, ¿este podría ser un paso en la dirección correcta?
smets.kevin
Esos tipos son mucho más inteligentes que yo. Estoy seguro de que habrían pensado esta solución (o una similar) antes y habrían optado por no implementarla en el núcleo por cualquier razón.
tennisgent
¿Cuál es el lugar correcto para el archivo CSS? ¿Css: 'css / partial1.css' implica la carpeta css en la raíz de la carpeta angular de la aplicación?
Cordle
Es relativo a su index.htmlarchivo. Entonces, en el ejemplo anterior, index.htmlestaría en la raíz y la csscarpeta estaría en la raíz, que contiene todos los archivos css. pero puede estructurar su aplicación como desee, siempre que use las rutas relativas correctas.
tennisgent
1
@Kappys, el script elimina el estilo de la vista anterior cuando se mueve a una nueva vista. Si no desea que esto suceda, basta con quitar el siguiente código de la Directiva: angular.forEach(current.$$route.css, function(sheet){ delete scope.routeStyles[sheet]; });.
tennisgent
34

La solución de @ tennisgent es excelente. Sin embargo, creo que es un poco limitado.

La modularidad y la encapsulación en angular van más allá de las rutas. Según la forma en que la web avanza hacia el desarrollo basado en componentes, también es importante aplicar esto en las directivas.

Como ya sabe, en Angular podemos incluir plantillas (estructura) y controladores (comportamiento) en páginas y componentes. AngularCSS habilita la última pieza que falta: adjuntar hojas de estilo (presentación).

Para una solución completa, sugiero usar AngularCSS.

  1. Admite ngRoute, enrutador UI, directivas, controladores y servicios de Angular.
  2. No requiere tener ng-appen la <html>etiqueta. Esto es importante cuando tienes múltiples aplicaciones ejecutándose en la misma página
  3. Puede personalizar dónde se inyectan las hojas de estilo: cabeza, cuerpo, selector personalizado, etc.
  4. Admite precarga, persistencia y almacenamiento en caché
  5. Admite consultas de medios y optimiza la carga de la página a través de la API de matchMedia

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

Aquí hay unos ejemplos:

Rutas

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

Directivas

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

Además, puede usar el $cssservicio 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();

});

Puede leer más sobre AngularCSS aquí:

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

castillo.io
fuente
1
Realmente me gusta su enfoque aquí, pero me preguntaba cómo podría usarse en una aplicación de producción donde todos los estilos CSS deben concatenarse juntos. Para las plantillas html, uso $ templateCache.put () para el código de producción y sería bueno hacer algo similar para css.
Tom Makin
Si necesita obtener CSS concatenado del servidor, siempre puede hacer algo como /getCss?files=file1(.css),file2,file3 y el servidor respondería con los 3 archivos en el orden dado y concatenado.
Petr Urban
13

Podría agregar una nueva hoja de estilo para encabezar dentro $routeProvider. Para simplificar, estoy usando una cadena pero también podría crear un nuevo elemento de enlace, o crear un servicio para hojas 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">');
}

El mayor beneficio de prelaoding en la página es que las imágenes de fondo ya existirán, y menos posibilidades de FOUC

charlietfl
fuente
No sería esto lograr lo mismo que simplemente incluyendo la <link>de la <head>de la index.html estáticamente, sin embargo?
Brandon
no si whenno se ha llamado a la ruta. Puede poner este código en controllerdevolución de llamada de whendentro del routeProvider, o tal vez dentro de resolvedevolución de llamada que probablemente los factores desencadenantes más pronto
charlietfl
Oh está bien, mi mal, eso hace clic en no. Parece bastante sólido, excepto ¿podría explicar cómo se precarga si lo estoy inyectando cuando de todos modos?
Brandon
1
no se precarga si lo agrega routeprovider... ese comentario fue sobre incluirlo en la cabecera de la página principal cuando se
publica la
-_- lo siento, me falta dormir si no puedes decirlo. De todos modos, ahí es donde estoy ahora. Intentar averiguar si la sobrecarga de cargar todas mis hojas de estilo a la vez es mejor que tener algo de FOUC cuando el usuario cambia de vista. Supongo que esa no es una pregunta relacionada con Angular tanto como lo es sobre la aplicación web UX. Sin embargo, gracias, probablemente iré con su sugerencia si decido no hacer la precarga.
Brandon
5

@ sz3, lo suficientemente divertido hoy tuve que hacer exactamente lo que intentabas lograr: ' cargar un archivo CSS específico solo cuando un usuario accede ' a una página específica. Entonces utilicé la solución anterior.

Pero estoy aquí para responder a su última pregunta: '¿ dónde exactamente debo poner el código? Algunas ideas ?

Tenías razón al incluir el código en la resolución , pero debes cambiar un poco el formato.

Echa un vistazo al siguiente código:

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

Acabo de probar y funciona bien , inyecta el html y carga mi 'home.css' solo cuando llego a la ruta '/ home'.

La explicación completa se puede encontrar aquí , pero básicamente resuelva: debería obtener un objeto en el formato

{
  'key' : string or function()
} 

Puedes nombrar la ' clave ' como quieras, en mi caso llamé ' estilo '.

Entonces, para el valor, tiene dos opciones:

  • Si es una cadena , es un alias para un servicio.

  • Si es función , entonces se inyecta y el valor de retorno se trata como la dependencia.

El punto principal aquí es que el código dentro de la función se ejecutará antes de que se instancia el controlador y se active el evento $ routeChangeSuccess.

Espero que ayude.

Denison Luz
fuente
2

¡¡Increíble gracias!! Solo tuve que hacer algunos ajustes para que funcione con 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
fuente
No necesitaba exactamente eliminar y agregar en todas partes ya que mi CSS estaba en mal estado, ¡pero esto fue de gran ayuda con ui-router! Gracias :)
imsheth
1

Si solo necesita que su CSS se aplique a una vista específica, estoy usando este fragmento útil dentro de mi controlador:

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

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

Esto agregará una clase a mi bodyetiqueta cuando se carga el estado y la eliminará cuando se destruya el estado (es decir, alguien cambia de página). Esto resuelve mi problema relacionado de solo necesitar CSS para ser aplicado a un estado en mi aplicación.

Mate
fuente
0

'uso estricto'; 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
fuente
Un ejemplo de código simple sin contexto rara vez es una respuesta suficiente a una pregunta. Además, esta pregunta ya tiene una respuesta muy aceptada.
AJ X.