Você pode alterar um caminho sem recarregar o controlador no AngularJS?


191

Já foi perguntado antes e, pelas respostas, não parece bom. Gostaria de perguntar com este código de exemplo em consideração ...

Meu aplicativo carrega o item atual no serviço que o fornece. Existem vários controladores que manipulam os dados do item sem que o item seja recarregado.

Meus controladores recarregarão o item se ainda não estiver definido, caso contrário, ele usará o item atualmente carregado do serviço entre controladores.

Problema : eu gostaria de usar caminhos diferentes para cada controlador sem recarregar o Item.html.

1) Isso é possível?

2) Se isso não for possível, existe uma abordagem melhor para ter um caminho por controlador versus o que eu vim aqui?

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

Resolução.

Status : o uso da consulta de pesquisa, conforme recomendado na resposta aceita, permitiu fornecer URLs diferentes sem recarregar o 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');
      }
    });
  }
});

O conteúdo das minhas parciais é envolvido com ng-controller=...


acharam esta resposta: stackoverflow.com/a/18551525/632088 - ele usa reloadOnSearch: false, mas também verifica se há atualizações para o URL na barra de pesquisa (por exemplo, se o usuário clica o botão Voltar e Alterações de URL)
— Robert King

Respostas:


115

Se você não tem que usar URLs como #/item/{{item.id}}/fooe #/item/{{item.id}}/bar, mas #/item/{{item.id}}/?fooe #/item/{{item.id}}/?barem vez disso, você pode definir o seu trajecto para /item/{{item.id}}/ter reloadOnSearchpronto para false( https://docs.angularjs.org/api/ngRoute/provider/$routeProvider ). Isso informa ao AngularJS para não recarregar a exibição se a parte de pesquisa do URL mudar.


Obrigado por isso. Estou tentando descobrir onde eu mudaria o controlador.
— Coder1

Entendi. Adicionado um parâmetro do controlador ao meu array de visualizações e adicionado ng-controller="view.controller"à ng-includediretiva.
— Coder1

Você criaria uma observação em $ location.search () no itemCtrlInit e, dependendo do parâmetro de pesquisa, atualize $ scope.view.template. E então esse modelo pode ser envolvido com algo parecido <div ng-controller="itemFooCtrl">... your template content...</div>. Edição: É bom ver que você resolveu, seria ótimo se você atualizasse sua pergunta com a forma como resolveu, talvez com um jsFiddle.
— precisa

Vou atualizá-lo quando estiver 100% lá. Estou tendo algumas peculiaridades com escopo nos controladores filhos e ng-click. Se eu carregar diretamente do foo, o ng-click é executado dentro do escopo do foo. Clico na barra e os cliques em ng nesse modelo são executados no escopo pai.
— Coder1

1
Deve-se notar que você não precisa reformatar seus URLs. Pode ter sido o caso quando a resposta foi postada, mas a documentação ( docs.angularjs.org/api/ngRoute.$routeProvider ) agora diz: [reloadOnSearch = true] - {boolean =} - recarrega a rota quando apenas $ location. search () ou $ location.hash () muda.
— Rhys van der Waerden

93

Se você precisar alterar o caminho, adicione-o após o arquivo .config no arquivo do aplicativo. Então você pode fazer $location.path('/sampleurl', false);para impedir o recarregamento

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

O crédito vai para https://www.consolelog.io/angularjs-change-path-without-reloading para obter a solução mais elegante que encontrei.


6
Ótima solução. Existe alguma maneira de obter o botão Voltar do navegador para "honrar" isso?
— Mark B

6
Esteja ciente de que esta solução mantém a rota antiga e, se você solicitar $ route.current, obterá o caminho anterior, não o atual. Caso contrário, é um ótimo pequeno truque.
— jornare 8/08/14

4
Só queria dizer que a solução original foi postada de forma aguda por "EvanWinstanley" aqui: github.com/angular/angular.angular/jsues/1699#issuecomment-34841248
— chipit24

2
Estou usando essa solução há algum tempo e notei que, em algumas mudanças de caminho, recarregar os registros como falsos, mesmo quando um valor verdadeiro é passado. Alguém mais teve algum problema como esse?
— Will Hitchcock

2
Eu tive que adicionar $timeout(un, 200);antes da declaração de retorno, pois às vezes $locationChangeSuccessnão disparava depois apply(e a rota não foi alterada quando realmente necessário). Minha solução apura as coisas após invocação de caminho (..., false)
— snowindy

17

por que não colocar o controlador ng um nível acima,

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

E não defina o controlador na rota,

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

funciona para mim.


ótima dica, funciona perfeitamente para o meu cenário também! Muito obrigado! :)
— daveoncode 17/03/2015

4
Este é um grande "Os russos usaram lápis" resposta, funciona para mim :)
— inolasco

1
Eu falei cedo demais. Esta solução alternativa tem o problema de que, se você precisa para valores de carga de $ routeParams no controlador, eles não irá carregar, padrão para {}
— inolasco

@inolasco: funciona se você ler seu $ routeParams no manipulador $ routeChangeSuccess. Na maioria das vezes, provavelmente é isso que você deseja. a leitura apenas no controlador forneceria apenas os valores para o URL que foi carregado originalmente.
— plong0

@ plong0 Bom ponto, deve funcionar. No entanto, no meu caso, eu só precisava obter os valores de URL quando o controlador carregava no carregamento da página, para inicializar determinados valores. Acabei usando a solução proposta pela vigrond usando $ locationChangeSuccess, mas a sua também é outra opção válida.
— Inolaco 20/05


10

Embora esta postagem seja antiga e tenha uma resposta aceita, o uso de reloadOnSeach = false não resolve o problema para aqueles que precisam mudar o caminho real e não apenas os parâmetros. Aqui está uma solução simples a considerar:

Use ng-include em vez de ng-view e atribua seu controlador no modelo.

<!-- 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
})

A desvantagem é que você não pode usar o atributo resolve, mas isso é muito fácil de contornar. Além disso, você deve gerenciar o estado do controlador, como a lógica baseada em $ routeParams, à medida que a rota muda dentro do controlador à medida que a URL correspondente é alterada.

Aqui está um exemplo: http://plnkr.co/edit/WtAOm59CFcjafMmxBVOP?p=preview


1
Não tenho certeza de que o botão voltar se comportaria corretamente em plnkr ... Como eu mencionei, você precisa gerenciar algumas coisas por conta própria à medida que a rota muda .. não é a solução mais digna de código, mas funciona
— Lukus

2

Eu uso essa solução

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

Essa abordagem preserva a funcionalidade do botão Voltar e você sempre tem os routeParams atuais no modelo, ao contrário de outras abordagens que já vi.


1

As respostas acima, incluindo a do GitHub, tiveram alguns problemas para o meu cenário e também a alteração do botão voltar ou da URL direta do navegador estava recarregando o controlador, o que eu não gostei. Finalmente fui com a seguinte abordagem:

1. Defina uma propriedade nas definições de rota, chamada 'noReload', para aquelas rotas nas quais você não deseja que o controlador recarregue com a mudança de rota.

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

2. Na função de execução do seu módulo, coloque a lógica que verifica essas rotas. Isso impedirá a recarga apenas se noReloadhouver truee o controlador de rota anterior for o mesmo.

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, no controlador, ouça o evento $ routeUpdate para poder fazer o que for necessário quando os parâmetros de rota forem alterados.

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

    //(...)
}

Lembre-se de que, com essa abordagem, você não altera as coisas no controlador e atualiza o caminho de acordo. É o contrário. Você primeiro altera o caminho e, em seguida, ouve o evento $ routeUpdate para alterar as coisas no controlador quando os parâmetros de rota são alterados.

Isso mantém as coisas simples e consistentes, pois você pode usar a mesma lógica quando simplesmente muda de caminho (mas sem solicitações caras de $ http, se quiser) e quando recarrega completamente o navegador.


1

Existe uma maneira simples de mudar o caminho sem recarregar

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

Use este código para alterar o URL, a página não será redirecionada

O segundo parâmetro "false" é muito importante.

$location.path('/edit_draft_inbox/'+id, false);

isso não está correto para AngularJS docs.angularjs.org/api/ng/service/$location
— steampowered

0

Aqui está a minha solução mais completa que resolve algumas coisas que o @Vigrond e o @rahilwazir perderam:

  • Quando os parâmetros de pesquisa eram alterados, impedia a transmissão de um $routeUpdate .
  • Quando a rota é deixada inalterada, $locationChangeSuccessnunca é acionada, o que causa a próxima atualização de rota seja impedida.
  • Se no mesmo ciclo de resumo houvesse outra solicitação de atualização, desta vez desejando recarregar, o manipulador de eventos cancelaria essa 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);
            };
        });
    }]);

erro de digitação pathno final deve ser param, também este não funciona com hashes, ea url na minha barra de endereços não atualiza
— deltree

Fixo. Hmm estranho que funciona para mim, embora em URLs HTML5. Pode ainda ajudar alguém eu acho ...
— Oded Niv

0

Adicione a seguinte tag de cabeça interna

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

Isso impedirá a recarga.


Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.