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=...

Coder1
fonte
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.

Anders Ekdahl
fonte
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.

Vigrond
fonte
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.

windmaomao
fonte
ó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
12

Para quem precisa de mudança de path () sem recarregar controladores - Aqui está o plugin: https://github.com/anglibs/angular-location-update

Uso:

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

Com base em https://stackoverflow.com/a/24102139/1751321

PS Esta solução https://stackoverflow.com/a/24102139/1751321 contém um bug após o caminho (, falso) chamado - ele interromperá a navegação do navegador para trás / para frente até o caminho (, verdadeiro) chamado

Daniel Garmoshka
fonte
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

Lukus
fonte
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.

parlamento
fonte
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.

CGodo
fonte
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);
Dinesh Vaitage
fonte
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);
            };
        });
    }]);
Oded Niv
fonte
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.

Vivek
fonte
0

Desde a versão 1.2, você pode usar $ location.replace () :

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