Czy możesz zmienić ścieżkę bez ponownego ładowania kontrolera w AngularJS?

191

Pytanie zostało zadane wcześniej, a na podstawie odpowiedzi nie wygląda dobrze. Chciałbym zapytać o ten przykładowy kod ...

Moja aplikacja ładuje bieżący element do usługi, która go udostępnia. Istnieje kilka kontrolerów, które manipulują danymi elementu bez ponownego ładowania elementu.

Moi kontrolerzy ponownie załadują element, jeśli nie został jeszcze ustawiony, w przeciwnym razie użyją aktualnie załadowanego elementu z usługi między kontrolerami.

Problem : Chciałbym użyć różnych ścieżek dla każdego kontrolera bez ponownego ładowania pliku Item.html.

1) Czy to możliwe?

2) Jeśli nie jest to możliwe, czy istnieje lepsze podejście do posiadania ścieżki na kontroler niż to, co tutaj wymyśliłem?

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

Rozkład.

Status : użycie zapytania wyszukiwania zgodnie z zaleceniami w zaakceptowanej odpowiedzi pozwoliło mi podać różne adresy URL bez ponownego ładowania głównego kontrolera.

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

Treść w moich stronicach jest zapakowana ng-controller=...

Koder 1
źródło
znalazł tę odpowiedź: stackoverflow.com/a/18551525/632088 - używa reloadOnSearch: false, ale sprawdza również aktualizacje adresu URL na pasku wyszukiwania (np. jeśli użytkownik kliknie przycisk Wstecz i zmiany adresu URL)
Robert King

Odpowiedzi:

115

Jeśli nie trzeba używać adresów jak #/item/{{item.id}}/fooi #/item/{{item.id}}/barale #/item/{{item.id}}/?fooi #/item/{{item.id}}/?barzamiast tego można skonfigurować trasy dla /item/{{item.id}}/mieć reloadOnSearchzestaw do false( https://docs.angularjs.org/api/ngRoute/provider/$routeProvider ). To mówi AngularJS, aby nie przeładowywał widoku, jeśli zmieni się część wyszukiwania adresu URL.

Anders Ekdahl
źródło
Dziękuję za to. Próbuję dowiedzieć się, gdzie chciałbym zmienić kontroler.
Koder 1
Rozumiem. Dodano parametr kontrolera do mojej tablicy widoków, a następnie dodano ng-controller="view.controller"do ng-includedyrektywy.
Koder1,
Stwórz zegarek na $ location.search () w itemCtrlInit iw zależności od parametru wyszukiwania zaktualizuj $ scope.view.template. A potem te szablony mogą być owinięte czymś takim <div ng-controller="itemFooCtrl">... your template content...</div>. EDYCJA: Miło cię widzieć z rozwiązaniem, byłoby wspaniale, gdybyś zaktualizował swoje pytanie o to, jak rozwiązałeś, być może za pomocą jsFiddle.
Anders Ekdahl,
Zaktualizuję go, gdy będę tam w 100%. Mam pewne dziwactwa z zasięgiem w kontrolerach potomnych i kliknięciem ng. Jeśli ładuję bezpośrednio z foo, kliknięcie ng jest wykonywane w zakresie foo. Następnie klikam pasek, a kliknięcia ng w tym szablonie są wykonywane w zakresie nadrzędnym.
Koder1,
1
Należy zauważyć, że tak naprawdę nie trzeba formatować adresów URL. Może tak być w przypadku opublikowania odpowiedzi, ale dokumentacja ( docs.quarejs.org/api/ngRoute.$routeProvider ) mówi teraz: [reloadOnSearch = true] - {boolean =} - przeładuj trasę, gdy tylko $ location. zmiany search () lub $ location.hash ().
Rhys van der Waerden
93

Jeśli chcesz zmienić ścieżkę, dodaj ją po pliku .config w pliku aplikacji. Następnie możesz zrobić, $location.path('/sampleurl', false);aby zapobiec przeładowaniu

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

Kredyt trafia do https://www.consolelog.io/angularjs-change-path-without-reloading za najbardziej eleganckie rozwiązanie, jakie znalazłem.

Vigrond
źródło
6
Świetne rozwiązanie. Czy jest jakiś sposób, aby przycisk Wstecz przeglądarki „honorował” to?
Mark B
6
Pamiętaj, że to rozwiązanie zachowuje starą trasę, a jeśli poprosisz o $ route.current, otrzymasz poprzednią ścieżkę, a nie bieżącą. W przeciwnym razie jest to świetny mały hack.
jornare
4
Chciałem tylko powiedzieć, że oryginalne rozwiązanie zostało ostro opublikowane przez „EvanWinstanley” tutaj: github.com/angular/angular.js/issues/1699#issuecomment-34841248
chipit24
2
Korzystam z tego rozwiązania od jakiegoś czasu i zauważyłem, że przy niektórych zmianach ścieżki ponownie ładuję rejestry jako fałszywe, nawet gdy przekazywana jest prawdziwa wartość. Czy ktoś jeszcze miał takie problemy?
Will Hitchcock,
2
Musiałem dodać $timeout(un, 200);instrukcję return przed zwrotem, ponieważ czasem $locationChangeSuccesswcale się nie uruchamiał apply(a trasa nie została zmieniona, gdy była naprawdę potrzebna). Moje rozwiązanie rozwiązuje problem po wywołaniu ścieżki (..., false)
snowindy,
17

dlaczego po prostu nie ustawić kontrolera ng o jeden poziom wyżej,

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

I nie ustawiaj kontrolera na trasie,

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

mi to pasuje.

windmaomao
źródło
świetna wskazówka, działa również idealnie w moim scenariuszu! Dziękuję Ci bardzo! :)
daveoncode
4
To świetna odpowiedź „Rosjanie używali ołówków”, działa dla mnie :)
inolasco
1
Mówiłem za wcześnie. W tym obejściu występuje problem polegający na tym, że jeśli chcesz załadować wartości z $ routeParams do kontrolera, nie zostaną one załadowane, domyślnie {}
inolasco
@inolasco: działa, jeśli czytasz swój $ routeParams w module obsługi $ routeChangeSuccess. Najczęściej jest to prawdopodobnie to, czego chcesz. odczyt tylko w kontrolerze dostarczy ci tylko wartości adresu URL, który został pierwotnie załadowany.
plong0
@ plong0 Dobry punkt, powinien działać. W moim przypadku jednak potrzebowałem tylko wartości adresu URL, gdy kontroler ładuje się po załadowaniu strony, aby zainicjować pewne wartości. Skończyło się na użyciu rozwiązania zaproponowanego przez vigrond przy użyciu $ locationChangeSuccess, ale twoje jest również inną prawidłową opcją.
inolasco
12

Dla tych, którzy potrzebują zmiany path () bez ponownego ładowania sterowników - oto wtyczka: https://github.com/anglibs/angular-location-update

Stosowanie:

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

Na podstawie https://stackoverflow.com/a/24102139/1751321

PS To rozwiązanie https://stackoverflow.com/a/24102139/1751321 zawiera błąd po wywołaniu ścieżki (, false) - spowoduje przerwanie nawigacji przeglądarki do tyłu / do przodu, dopóki ścieżka ((prawda) nie zadzwoni

Daniel Garmoshka
źródło
10

Chociaż ten post jest stary i ma zaakceptowaną odpowiedź, użycie reloadOnSeach = false nie rozwiązuje problemu dla tych z nas, którzy muszą zmienić rzeczywistą ścieżkę, a nie tylko parametry. Oto proste rozwiązanie do rozważenia:

Użyj ng-include zamiast ng-view i przypisz kontroler do szablonu.

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

Jedynym minusem jest to, że nie można użyć atrybutu resolver, ale dość łatwo można go obejść. Musisz także zarządzać stanem kontrolera, na przykład logiką opartą na $ routeParams, gdy trasa zmienia się w kontrolerze, gdy zmienia się odpowiedni adres URL.

Oto przykład: http://plnkr.co/edit/WtAOm59CFcjafMmxBVOP?p=preview

Lukus
źródło
1
Nie jestem pewien, czy przycisk Wstecz będzie działał poprawnie w plnkr ... Jak już wspomniałem, musisz samodzielnie zarządzać niektórymi rzeczami, gdy zmienia się trasa. Nie jest to najbardziej godne kodu rozwiązanie, ale działa
Lukus
2

Korzystam z tego rozwiązania

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

Takie podejście zachowuje funkcję przycisku Wstecz i zawsze masz bieżące routeParams w szablonie, w przeciwieństwie do niektórych innych podejść, które widziałem.

parlament
źródło
1

Odpowiedzi powyżej, w tym GitHub, miały pewne problemy z moim scenariuszem, a także przycisk Wstecz lub bezpośrednia zmiana adresu URL z przeglądarki ładowała kontroler, co mi się nie podobało. W końcu zastosowałem następujące podejście:

1. Zdefiniuj właściwość w definicjach tras, zwaną „noReload” dla tych tras, w których nie chcesz, aby kontroler ładował się ponownie po zmianie trasy.

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

2. W funkcji uruchamiania modułu umieść logikę, która sprawdza te trasy. Zapobiegnie to przeładowaniu tylko wtedy, gdy noReloadjest, truea poprzedni kontroler trasy jest taki sam.

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. Na koniec, w kontrolerze, nasłuchuj zdarzenia $ routeUpdate, abyś mógł zrobić wszystko, co musisz zrobić, gdy zmienią się parametry trasy.

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

    //(...)
}

Pamiętaj, że przy takim podejściu nie zmieniasz rzeczy w kontrolerze, a następnie odpowiednio aktualizujesz ścieżkę. Jest na odwrót. Najpierw zmieniasz ścieżkę, a następnie nasłuchujesz zdarzenia $ routeUpdate, aby zmienić rzeczy w kontrolerze po zmianie parametrów trasy.

Dzięki temu wszystko jest proste i spójne, ponieważ możesz używać tej samej logiki zarówno po prostu zmieniając ścieżkę (ale bez drogich żądań $ http, jeśli chcesz), jak i po całkowitym przeładowaniu przeglądarki.

CGodo
źródło
1

Istnieje prosty sposób zmiany ścieżki bez ponownego ładowania

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

Użyj tego kodu, aby zmienić adres URL, strona nie zostanie przekierowana

Drugi parametr „false” jest bardzo ważny.

$location.path('/edit_draft_inbox/'+id, false);
Dinesh Vaitage
źródło
nie jest to poprawne w przypadku AngularJS docs.quarejs.org/api/ng/service/$location
steampowered
0

Oto moje pełniejsze rozwiązanie, które rozwiązuje kilka rzeczy, których @Vigrond i @rahilwazir przegapili:

  • Gdy parametry wyszukiwania zostały zmienione, uniemożliwiłoby to nadawanie $routeUpdate .
  • Gdy trasa pozostanie niezmieniona, $locationChangeSuccessnigdy się nie uruchamia, co powoduje następne uniemożliwia aktualizację trasy.
  • Jeśli w tym samym cyklu podsumowania pojawiło się kolejne żądanie aktualizacji, tym razem chcąc przeładować, program obsługi zdarzeń anuluje to przeładowanie.

    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
źródło
literówka pathna końcu powinna być param, również to nie działa z hashami, a adres URL w moim pasku adresu nie aktualizuje się
deltree 18.10.16
Naprawiony. Hmm, to dziwne, ale działa na mnie w przypadku adresów URL HTML5. Chyba nadal może pomóc komuś ...
Oded Niv
0

Dodaj następujący znacznik głowy

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

Zapobiegnie to przeładowaniu.

Vivek
źródło