¿Se puede cambiar una ruta sin volver a cargar el controlador en AngularJS?

191

Ya se ha preguntado antes, y por las respuestas no se ve bien. Me gustaría preguntar con este código de muestra en consideración ...

Mi aplicación carga el elemento actual en el servicio que lo proporciona. Hay varios controladores que manipulan los datos del elemento sin que el elemento se vuelva a cargar.

Mis controladores volverán a cargar el elemento si aún no está configurado; de lo contrario, utilizará el elemento actualmente cargado del servicio, entre los controladores.

Problema : me gustaría usar diferentes rutas para cada controlador sin volver a cargar Item.html.

1) ¿Es eso posible?

2) Si eso no es posible, ¿hay un mejor enfoque para tener una ruta por controlador en comparación con lo que se me ocurrió aquí?

app.js

var app = angular.module('myModule', []).
  config(['$routeProvider', function($routeProvider) {
    $routeProvider.
      when('/items', {templateUrl: 'partials/items.html',   controller: ItemsCtrl}).
      when('/items/:itemId/foo', {templateUrl: 'partials/item.html', controller: ItemFooCtrl}).
      when('/items/:itemId/bar', {templateUrl: 'partials/item.html', controller: ItemBarCtrl}).
      otherwise({redirectTo: '/items'});
    }]);

Item.html

<!-- Menu -->
<a id="fooTab" my-active-directive="view.name" href="#/item/{{item.id}}/foo">Foo</a>
<a id="barTab" my-active-directive="view.name" href="#/item/{{item.id}}/bar">Bar</a>
<!-- Content -->
<div class="content" ng-include="" src="view.template"></div>

controller.js

// Helper function to load $scope.item if refresh or directly linked
function itemCtrlInit($scope, $routeParams, MyService) {
  $scope.item = MyService.currentItem;
  if (!$scope.item) {
    MyService.currentItem = MyService.get({itemId: $routeParams.itemId});
    $scope.item = MyService.currentItem;
  }
}
function itemFooCtrl($scope, $routeParams, MyService) {
  $scope.view = {name: 'foo', template: 'partials/itemFoo.html'};
  itemCtrlInit($scope, $routeParams, MyService);
}
function itemBarCtrl($scope, $routeParams, MyService) {
  $scope.view = {name: 'bar', template: 'partials/itemBar.html'};
  itemCtrlInit($scope, $routeParams, MyService);
}

Resolución.

Estado : el uso de la consulta de búsqueda como se recomienda en la respuesta aceptada me permitió proporcionar diferentes URL sin volver a cargar el controlador principal.

app.js

var app = angular.module('myModule', []).
  config(['$routeProvider', function($routeProvider) {
    $routeProvider.
      when('/items', {templateUrl: 'partials/items.html',   controller: ItemsCtrl}).
      when('/item/:itemId/', {templateUrl: 'partials/item.html', controller: ItemCtrl, reloadOnSearch: false}).
      otherwise({redirectTo: '/items'});
    }]);

Item.html

<!-- Menu -->
<dd id="fooTab" item-tab="view.name" ng-click="view = views.foo;"><a href="#/item/{{item.id}}/?view=foo">Foo</a></dd>
<dd id="barTab" item-tab="view.name" ng-click="view = views.bar;"><a href="#/item/{{item.id}}/?view=foo">Bar</a></dd>

<!-- Content -->
<div class="content" ng-include="" src="view.template"></div>

controller.js

function ItemCtrl($scope, $routeParams, Appts) {
  $scope.views = {
    foo: {name: 'foo', template: 'partials/itemFoo.html'},
    bar: {name: 'bar', template: 'partials/itemBar.html'},
  }
  $scope.view = $scope.views[$routeParams.view];
}

directives.js

app.directive('itemTab', function(){
  return function(scope, elem, attrs) {
    scope.$watch(attrs.itemTab, function(val) {
      if (val+'Tab' == attrs.id) {
        elem.addClass('active');
      } else {
        elem.removeClass('active');
      }
    });
  }
});

El contenido dentro de mis parciales está envuelto con ng-controller=...

Codificador1
fuente
encontró esta respuesta: stackoverflow.com/a/18551525/632088 : usa reloadOnSearch: false, pero también busca actualizaciones de la url en la barra de búsqueda (por ejemplo, si el usuario hace clic en el botón Atrás y la url cambia)
robert king

Respuestas:

115

Si no tiene que usar URL como #/item/{{item.id}}/fooy #/item/{{item.id}}/barpero, #/item/{{item.id}}/?fooy en su #/item/{{item.id}}/?barlugar, puede configurar su ruta para /item/{{item.id}}/que se reloadOnSearchestablezca en false( https://docs.angularjs.org/api/ngRoute/provider/$routeProvider ). Eso le dice a AngularJS que no vuelva a cargar la vista si cambia la parte de búsqueda de la URL.

Anders Ekdahl
fuente
Gracias por eso. Estoy tratando de averiguar dónde cambiaría el controlador.
Codificador1
Entendido. Agregué un parámetro de controlador a mi matriz de vista, luego agregué ng-controller="view.controller"a la ng-includedirectiva.
Codificador1
Crearía un reloj en $ location.search () en itemCtrlInit y, según el parámetro de búsqueda, actualice $ scope.view.template. Y luego esa plantilla podría envolverse con algo así <div ng-controller="itemFooCtrl">... your template content...</div>. EDITAR: Me alegra verte resuelto, sería genial si actualizaras tu pregunta con cómo lo resolviste, tal vez con un jsFiddle.
Anders Ekdahl
Lo actualizaré cuando esté 100% allí. Tengo algunas peculiaridades con alcance en los controladores secundarios y ng-click. Si cargo directamente desde foo, ng-click se ejecuta dentro del alcance de foo. Luego hago clic en la barra y los ng-clicks en esa plantilla se ejecutan en el ámbito primario.
Codificador1
1
Debe tenerse en cuenta que en realidad no necesita reformatear sus URLS. Este puede haber sido el caso cuando se publicó la respuesta, pero la documentación ( docs.angularjs.org/api/ngRoute.$routeProvider ) ahora dice: [reloadOnSearch = true] - {boolean =} - recarga la ruta cuando solo $ ubicación. search () o $ location.hash () cambios.
Rhys van der Waerden
93

Si necesita cambiar la ruta, agregue esto después de su .config en su archivo de aplicación. Entonces puedes hacer $location.path('/sampleurl', false);para evitar la recarga

app.run(['$route', '$rootScope', '$location', function ($route, $rootScope, $location) {
    var original = $location.path;
    $location.path = function (path, reload) {
        if (reload === false) {
            var lastRoute = $route.current;
            var un = $rootScope.$on('$locationChangeSuccess', function () {
                $route.current = lastRoute;
                un();
            });
        }
        return original.apply($location, [path]);
    };
}])

El crédito va a https://www.consolelog.io/angularjs-change-path-without-reloading para obtener la solución más elegante que he encontrado.

Vigrond
fuente
66
Gran solución ¿Hay alguna forma de obtener el botón de retroceso del navegador para "honrar" esto?
Mark B
66
Tenga en cuenta que esta solución mantiene la ruta anterior, y si solicita $ route.current, obtendrá la ruta anterior, no la actual. De lo contrario, es un gran truco.
jornare
44
Solo quería decir que la solución original fue publicada por "EvanWinstanley" aquí: github.com/angular/angular.js/issues/1699#issuecomment-34841248
chipit24
2
He estado usando esta solución durante un tiempo y me di cuenta de que en algunos cambios de ruta, volver a cargar los registros como falsos incluso cuando se pasa un valor verdadero. ¿Alguien más ha tenido algún problema como este?
Will Hitchcock el
2
Tuve que agregar $timeout(un, 200);antes de la declaración de devolución ya que a veces $locationChangeSuccessno se disparó en absoluto después apply(y la ruta no se cambió cuando realmente se necesitaba). Mi solución aclara las cosas después de invocar la ruta (..., falso)
snowindy
17

¿por qué no simplemente poner el controlador ng un nivel más alto,

<body ng-controller="ProjectController">
    <div ng-view><div>

Y no configure el controlador en la ruta,

.when('/', { templateUrl: "abc.html" })

esto funciona para mi.

windmaomao
fuente
gran consejo, ¡funciona perfectamente para mi escenario también! ¡muchas gracias! :)
daveoncode
44
Esta es una gran respuesta "los rusos usaron lápices", funciona para mí :)
inolasco
1
Hablé muy pronto Esta solución tiene el problema de que si necesita cargar valores de $ routeParams en el controlador, no se cargarán, por defecto a {}
inolasco
@inolasco: funciona si lee su $ routeParams en el controlador $ routeChangeSuccess. Más a menudo que no, esto es probablemente lo que quieres de todos modos. leer solo en el controlador solo le daría los valores para la URL que se cargó originalmente.
plong0
@ plong0 Buen punto, debería funcionar. Sin embargo, en mi caso, solo necesitaba obtener los valores de URL cuando el controlador se carga al cargar la página, para inicializar ciertos valores. Terminé usando la solución propuesta por vigrond usando $ locationChangeSuccess, pero la suya también es otra opción válida.
inolasco
12

Para aquellos que necesitan un cambio de ruta () sin recargar los controladores: aquí está el complemento: https://github.com/anglibs/angular-location-update

Uso:

$location.update_path('/notes/1');

Basado en https://stackoverflow.com/a/24102139/1751321

PD Esta solución https://stackoverflow.com/a/24102139/1751321 contiene un error después de la ruta (falsa) llamada: interrumpirá la navegación del navegador hacia atrás / adelante hasta que la ruta (verdadera) llame

Daniel Garmoshka
fuente
10

Aunque esta publicación es antigua y se ha aceptado una respuesta, el uso de reloadOnSeach = false no resuelve el problema para aquellos de nosotros que necesitamos cambiar la ruta real y no solo los parámetros. Aquí hay una solución simple a considerar:

Use ng-include en lugar de ng-view y asigne su controlador en la plantilla.

<!-- In your index.html - instead of using ng-view -->
<div ng-include="templateUrl"></div>

<!-- In your template specified by app.config -->
<div ng-controller="MyController">{{variableInMyController}}</div>

//in config
$routeProvider
  .when('/my/page/route/:id', { 
    templateUrl: 'myPage.html', 
  })

//in top level controller with $route injected
$scope.templateUrl = ''

$scope.$on('$routeChangeSuccess',function(){
  $scope.templateUrl = $route.current.templateUrl;
})

//in controller that doesn't reload
$scope.$on('$routeChangeSuccess',function(){
  //update your scope based on new $routeParams
})

El único inconveniente es que no puede usar el atributo resolver, pero eso es bastante fácil de manejar. También debe administrar el estado del controlador, como la lógica basada en $ routeParams a medida que la ruta cambia dentro del controlador a medida que cambia la url correspondiente.

Aquí hay un ejemplo: http://plnkr.co/edit/WtAOm59CFcjafMmxBVOP?p=preview

Lukus
fuente
1
No estoy seguro de que el botón Atrás se comporte correctamente en plnkr ... Como mencioné, debe administrar algunas cosas por su cuenta a medida que cambia la ruta ... No es la solución más digna de código, pero funciona
Lukus
2

Yo uso esta solucion

angular.module('reload-service.module', [])
.factory('reloadService', function($route,$timeout, $location) {
  return {
     preventReload: function($scope, navigateCallback) {
        var lastRoute = $route.current;

        $scope.$on('$locationChangeSuccess', function() {
           if (lastRoute.$$route.templateUrl === $route.current.$$route.templateUrl) {
              var routeParams = angular.copy($route.current.params);
              $route.current = lastRoute;
              navigateCallback(routeParams);
           }
        });
     }
  };
})

//usage
.controller('noReloadController', function($scope, $routeParams, reloadService) {
     $scope.routeParams = $routeParams;

     reloadService.preventReload($scope, function(newParams) {
        $scope.routeParams = newParams;
     });
});

Este enfoque conserva la funcionalidad del botón de retroceso, y siempre tiene los routeParams actuales en la plantilla, a diferencia de otros enfoques que he visto.

parlamento
fuente
1

Las respuestas anteriores, incluida la de GitHub, tuvieron algunos problemas para mi escenario y también el botón de retroceso o el cambio directo de URL desde el navegador estaba recargando el controlador, lo que no me gustó. Finalmente fui con el siguiente enfoque:

1. Defina una propiedad en las definiciones de ruta, llamada 'noReload' para aquellas rutas en las que no desea que el controlador se vuelva a cargar en el cambio de ruta.

.when('/:param1/:param2?/:param3?', {
    templateUrl: 'home.html',
    controller: 'HomeController',
    controllerAs: 'vm',
    noReload: true
})

2. En la función de ejecución de su módulo, coloque la lógica que verifica esas rutas. Evitará la recarga solo si noReloades así truey el controlador de ruta anterior es el mismo.

fooRun.$inject = ['$rootScope', '$route', '$routeParams'];

function fooRun($rootScope, $route, $routeParams) {
    $rootScope.$on('$routeChangeStart', function (event, nextRoute, lastRoute) {
        if (lastRoute && nextRoute.noReload 
         && lastRoute.controller === nextRoute.controller) {
            var un = $rootScope.$on('$locationChangeSuccess', function () {
                un();
                // Broadcast routeUpdate if params changed. Also update
                // $routeParams accordingly
                if (!angular.equals($route.current.params, lastRoute.params)) {
                    lastRoute.params = nextRoute.params;
                    angular.copy(lastRoute.params, $routeParams);
                    $rootScope.$broadcast('$routeUpdate', lastRoute);
                }
                // Prevent reload of controller by setting current
                // route to the previous one.
                $route.current = lastRoute;
            });
        }
    });
}

3. Finalmente, en el controlador, escuche el evento $ routeUpdate para que pueda hacer lo que sea necesario cuando cambien los parámetros de la ruta.

HomeController.$inject = ['$scope', '$routeParams'];

function HomeController($scope, $routeParams) {
    //(...)

    $scope.$on("$routeUpdate", function handler(route) {
        // Do whatever you need to do with new $routeParams
        // You can also access the route from the parameter passed
        // to the event
    });

    //(...)
}

Tenga en cuenta que con este enfoque, no cambia las cosas en el controlador y luego actualiza la ruta en consecuencia. Es al revés. Primero cambia la ruta, luego escucha el evento $ routeUpdate para cambiar las cosas en el controlador cuando cambian los parámetros de la ruta.

Esto mantiene las cosas simples y consistentes, ya que puede usar la misma lógica tanto cuando simplemente cambia la ruta (pero sin costosas solicitudes de $ http si lo desea) como cuando recarga completamente el navegador.

CGodo
fuente
1

Hay una manera simple de cambiar la ruta sin recargar

URL is - http://localhost:9000/#/edit_draft_inbox/1457

Use este código para cambiar la URL, la página no será redirigida

El segundo parámetro "falso" es muy importante.

$location.path('/edit_draft_inbox/'+id, false);
Dinesh Vaitage
fuente
esto no es correcto para AngularJS docs.angularjs.org/api/ng/service/$location
impulsado por vapor el
0

Aquí está mi solución más completa que resuelve algunas cosas que @Vigrond y @rahilwazir se perdieron:

  • Cuando se cambiaron los parámetros de búsqueda, evitaría transmitir a $routeUpdate.
  • Cuando la ruta no se modifica, $locationChangeSuccessnunca se activa, lo que impide la próxima actualización de la ruta.
  • Si en el mismo ciclo de resumen hubo otra solicitud de actualización, esta vez deseando volver a cargar, el controlador de eventos cancelará esa recarga.

    app.run(['$rootScope', '$route', '$location', '$timeout', function ($rootScope, $route, $location, $timeout) {
        ['url', 'path'].forEach(function (method) {
            var original = $location[method];
            var requestId = 0;
            $location[method] = function (param, reload) {
                // getter
                if (!param) return original.call($location);
    
                # only last call allowed to do things in one digest cycle
                var currentRequestId = ++requestId;
                if (reload === false) {
                    var lastRoute = $route.current;
                    // intercept ONLY the next $locateChangeSuccess
                    var un = $rootScope.$on('$locationChangeSuccess', function () {
                        un();
                        if (requestId !== currentRequestId) return;
    
                        if (!angular.equals($route.current.params, lastRoute.params)) {
                            // this should always be broadcast when params change
                            $rootScope.$broadcast('$routeUpdate');
                        }
                        var current = $route.current;
                        $route.current = lastRoute;
                        // make a route change to the previous route work
                        $timeout(function() {
                            if (requestId !== currentRequestId) return;
                            $route.current = current;
                        });
                    });
                    // if it didn't fire for some reason, don't intercept the next one
                    $timeout(un);
                }
                return original.call($location, param);
            };
        });
    }]);
Oded Niv
fuente
el error tipográfico pathal final debería ser param, también esto no funciona con hashes, y la url en mi barra de direcciones no se actualiza
deltree
Fijo. Hmm raro, funciona para mí aunque en URL html5. Todavía puedo ayudar a alguien, supongo ...
Oded Niv
0

Agregue la siguiente etiqueta dentro de la cabeza

  <script type="text/javascript">
    angular.element(document.getElementsByTagName('head')).append(angular.element('<base href="' + window.location.pathname + '" />'));
  </script>

Esto evitará la recarga.

Vivek
fuente
0

Desde aproximadamente la versión 1.2, puede usar $ location.replace () :

$location.path('/items');
$location.replace();
Brent Washburne
fuente