Como lidar com links de hash âncora no AngularJS

318

Algum de vocês sabe como lidar bem com links de hash de âncora no AngularJS ?

Eu tenho a seguinte marcação para uma página de FAQ simples

<a href="#faq-1">Question 1</a>
<a href="#faq-2">Question 2</a>
<a href="#faq-3">Question 3</a>

<h3 id="faq-1">Question 1</h3>
<h3 id="faq-2">Question 2</h3>
<h3 id="fa1-3">Question 3</h3>

Ao clicar em qualquer um dos links acima, o AngularJS intercepta e direciona-me para uma página completamente diferente (no meu caso, uma página 404, pois não há rotas correspondentes aos links).

Meu primeiro pensamento foi criar uma rota correspondente a " / faq /: chapter " e, no controlador correspondente, verificar $routeParams.chapterapós um elemento correspondente e, em seguida, usar o jQuery para rolar para baixo.

Mas então o AngularJS caga em mim novamente e apenas rola para o topo da página de qualquer maneira.

Então, alguém aqui fez algo semelhante no passado e conhece uma boa solução para isso?

Edit: Mudar para html5Mode deve resolver meus problemas, mas meio que precisamos oferecer suporte ao IE8 + de qualquer maneira, por isso, temo que não seja uma solução aceita: /

Rasmus
fonte
Acho angular sugere usar em seu ng-href=""lugar.
sobreposto
10
Eu acho que ng-href só é aplicável se o URL contiver dados dinâmicos que precisam ser vinculados a um modelo ng. Eu meio que me pergunto se você atribuir um hashPrefix ao LocationProvider se ele irá ignorar o link é a de ID: docs.angularjs.org/guide/dev_guide.services.$location
Adam
Adam está correto no uso de ng-href.
Rasmus
Possível duplicata de links Anchor no Angularjs?
Michaël Benjamin Saerens
Este também é um problema para nova Angular: stackoverflow.com/questions/36101756/...
phil294

Respostas:

375

Você está procurando $anchorScroll().

Aqui está a documentação (de baixa qualidade).

E aqui está a fonte.

Basicamente, você apenas injeta e chama no seu controlador, e ele irá rolar para qualquer elemento com o ID encontrado em $location.hash()

app.controller('TestCtrl', function($scope, $location, $anchorScroll) {
   $scope.scrollTo = function(id) {
      $location.hash(id);
      $anchorScroll();
   }
});

<a ng-click="scrollTo('foo')">Foo</a>

<div id="foo">Here you are</div>

Aqui está um desentupidor para demonstrar

EDIT: para usar isso com roteamento

Configure seu roteamento angular como de costume e adicione o código a seguir.

app.run(function($rootScope, $location, $anchorScroll, $routeParams) {
  //when the route is changed scroll to the proper element.
  $rootScope.$on('$routeChangeSuccess', function(newRoute, oldRoute) {
    $location.hash($routeParams.scrollTo);
    $anchorScroll();  
  });
});

e seu link ficaria assim:

<a href="#/test?scrollTo=foo">Test/Foo</a>

Aqui está um Plunker demonstrando rolagem com roteamento e $ anchorScroll

E ainda mais simples:

app.run(function($rootScope, $location, $anchorScroll) {
  //when the route is changed scroll to the proper element.
  $rootScope.$on('$routeChangeSuccess', function(newRoute, oldRoute) {
    if($location.hash()) $anchorScroll();  
  });
});

e seu link ficaria assim:

<a href="#/test#foo">Test/Foo</a>
Ben Lesh
fonte
5
O problema pode surgir quando você adiciona o roteamento: se adicionar ngView, cada alteração no hash da url acionaria a recarga da rota ... No seu exemplo, não há roteamento e a URL não reflete o item atual ... Mas, obrigado por apontar para $ anchorScroll
Valentyn Shybanov
1
@blesh, chamar location.hash (X) altera a página, pois o roteamento controla as visualizações.
dsldsl
24
Essa solução faz com que meu aplicativo inteiro seja renderizado novamente.
2
@dsldsl && @OliverJosephAsh: $ location.hash () não recarregará a página. Se for, há algo mais acontecendo. Aqui está o mesmo problema com o tempo que a página está sendo carregada, você verá que ela não muda. Se você quiser rolar para uma marca de âncora na página atual sem recarregar a rota, basta fazer um link regular <a href="#foo">foo</a>. Meu exemplo de código foi mostrar a rolagem para um ID na mudança de rota.
Ben Lesh
4
@blesh: eu recomendo que você também remova o hash depois de rolar para a seção desejada, desta forma o URL não é poluído com coisas que realmente não deveriam estar lá. Use isto: $location.search('scrollTo', null)
kumarharsh
168

No meu caso, notei que a lógica de roteamento estava ativando se eu modificasse o $location.hash(). O seguinte truque funcionou ..

$scope.scrollTo = function(id) {
    var old = $location.hash();
    $location.hash(id);
    $anchorScroll();
    //reset to old to keep any additional routing logic from kicking in
    $location.hash(old);
};
slugslog
fonte
5
Agradecimentos brilhantes por isso, a lógica de roteamento absolutamente se recusou a se comportar, mesmo ao usar a solução .run (...) da @ blesh, e isso explica tudo.
LJS
8
Seu truque "salvar o antigo hash" tem sido um salva-vidas absoluto. Isso impede que a página seja recarregada enquanto mantém a rota limpa. Excelente ideia!
ThisLanham
2
Agradável. Mas, depois de implementar sua solução, o URL não está atualizando o valor do ID de destino.
Mohamed Hussain
1
Eu tive a mesma experiência que Mohamed ... De fato, parou a recarga, mas exibe a rota sem hash (e $ anchorScroll não teve efeito). 1.2.6 Hmmmm.
9608 Michael
3
Eu uso $location.hash(my_id); $anchorScroll; $location.hash(null). Isso impede a recarga e não preciso gerenciar a oldvariável.
MFB 25/06
53

Não há necessidade de alterar nenhum roteamento ou qualquer outra coisa, basta usar target="_self" ao criar os links

Exemplo:

<a href="#faq-1" target="_self">Question 1</a>
<a href="#faq-2" target="_self">Question 2</a>
<a href="#faq-3" target="_self">Question 3</a>

E use o idatributo em seus elementos html como este:

<h3 id="faq-1">Question 1</h3>
<h3 id="faq-2">Question 2</h3>
<h3 id="faq-3">Question 3</h3>

Não há necessidade de usar ## como apontado / mencionado nos comentários ;-)

Mauricio Gracia Gutierrez
fonte
1
Isso não funcionou para mim, mas a solução aqui funcionou: stackoverflow.com/a/19367249/633107
Splaktar
6
Obrigado por esta solução. Mas target = "_ self" é suficiente. Não é necessário dobrar # #
Christophe P
5
target = "_ self" é definitivamente a melhor resposta (não há necessidade de duplicar #, como apontado por Christophe P). Isso funciona, não importa se o Html5Mode está ativado ou desativado.
newman
3
Simples e completo. Trabalhou para mim sem precisar adicionar mais uma dependência angular.
adeady
4
Esta é a solução certa. Não é necessário envolver o serviço angular $ anchorScroll. Consulte a documentação para uma tag: https://developer.mozilla.org/en/docs/Web/HTML/Element/a
Yiling
41
<a href="##faq-1">Question 1</a>
<a href="##faq-2">Question 2</a>
<a href="##faq-3">Question 3</a>

<h3 id="faq-1">Question 1</h3>
<h3 id="faq-2">Question 2</h3>
<h3 id="faq-3">Question 3</h3>
Lincolnshire
fonte
Impressionante! De longe, a solução mais simples, mas alguma idéia de como vincular uma âncora em uma página separada? (ie / products # books)
8bithero
Eu acho que é o mesmo que a solução (/ produtos ## livros) em AngularJS
lincolnge
1
Da minha experiência, href = "##" funciona apenas quando $ anchorScroll é injetado.
Brian Park
9
este parece relativamente simples, mas não o seu trabalho :-(
brykneval
4
Adicionei target = "_ self" e funcionou como um encanto para todos os tipos de navegação na página (leia sliders, vá para seções diferentes e assim por diante). Obrigado por compartilhar esse grande e mais simples truque.
Kumar Nitesh
19

Se você sempre conhece a rota, pode simplesmente acrescentar a âncora assim:

href="#/route#anchorID

onde routeé a rota angular atual e anchorIDcorresponde a <a id="anchorID">algum lugar da página

cab1113
fonte
1
Isso aciona uma alteração normal da rota AngularJS e, portanto, é desencorajado. No meu caso, foi muito visual, uma vez que os vídeos do YouTube na página FAQ / Ajuda foram recarregados.
Robin Andersson
9
@ RobinWassén-Andersson, especificando reloadOnSearch: falsepara essa rota na configuração de suas rotas, angular não acionará uma alteração de rota e apenas rolará para o ID. Em combinação com a rota completa especificada na atag, eu diria que esta é a solução mais simples e direta.
Elise
Obrigado. Isso me ajudou. Como não uso nenhuma rota personalizada no meu aplicativo, fazer um href = "# / # anchor-name" funcionou muito bem!
Jordan
14

$anchorScroll funciona para isso, mas há uma maneira muito melhor de usá-lo nas versões mais recentes do Angular.

Agora, $anchorScrollaceita o hash como um argumento opcional, para que você não precise alterar $location.hashnada. ( documentação )

Esta é a melhor solução porque não afeta a rota. Não consegui que nenhuma das outras soluções funcionasse porque estou usando o ngRoute e a rota seria recarregada assim que eu definia $location.hash(id), antes que $anchorScrollpudesse fazer sua mágica.

Aqui está como usá-lo ... primeiro, na diretiva ou controlador:

$scope.scrollTo = function (id) {
  $anchorScroll(id);  
}

e depois na visualização:

<a href="" ng-click="scrollTo(id)">Text</a>

Além disso, se você precisar contabilizar uma barra de navegação fixa (ou outra interface do usuário), poderá definir o deslocamento para $ anchorScroll assim (na função de execução do módulo principal):

.run(function ($anchorScroll) {
   //this will make anchorScroll scroll to the div minus 50px
   $anchorScroll.yOffset = 50;
});
Rebecca
fonte
Obrigado. Como você implementaria sua estratégia para um link de hash combinado com uma alteração de rota? Exemplo: clique neste item de navegação, que abre uma visualização diferente e desce até um específico idnessa visualização.
Kyle Vassella
Sry to chatear ... Se você tiver uma chance, você pode dar uma olhada na minha pergunta sobre o Stack? Sinto que sua resposta aqui me aproximou tanto, mas não consigo implementá-la: stackoverflow.com/questions/41494330/… . Eu também estou usando ngRoutee a versão mais recente do Angular.
precisa saber é o seguinte
Sinto muito por não ter tentado esse caso específico ... mas você deu uma olhada em $ location.search () ou $ routeParams ? Talvez você possa usar um ou outro na inicialização do seu controlador - se o seu scrollTo search param estiver na URL, o controlador poderá usar $ anchorScroll como acima para rolar a página.
Rebecca
2
Ao passar o ID diretamente para $ anchorScroll, minhas rotas mudaram de algo como / contact # contact para just / contact. Esta deve ser a resposta aceita imho.
RandomUs1r
12

Esta foi a minha solução usando uma diretiva que parece mais angular porque estamos lidando com o DOM:

Plnkr aqui

github

CÓDIGO

angular.module('app', [])
.directive('scrollTo', function ($location, $anchorScroll) {
  return function(scope, element, attrs) {

    element.bind('click', function(event) {
        event.stopPropagation();
        var off = scope.$on('$locationChangeStart', function(ev) {
            off();
            ev.preventDefault();
        });
        var location = attrs.scrollTo;
        $location.hash(location);
        $anchorScroll();
    });

  };
});

HTML

<ul>
  <li><a href="" scroll-to="section1">Section 1</a></li>
  <li><a href="" scroll-to="section2">Section 2</a></li>
</ul>

<h1 id="section1">Hi, I'm section 1</h1>
<p>
Zombie ipsum reversus ab viral inferno, nam rick grimes malum cerebro. De carne lumbering animata corpora quaeritis. 
 Summus brains sit​​, morbo vel maleficia? De apocalypsi gorger omero undead survivor dictum mauris. 
Hi mindless mortuis soulless creaturas, imo evil stalking monstra adventus resi dentevil vultus comedat cerebella viventium. 
Nescio brains an Undead zombies. Sicut malus putrid voodoo horror. Nigh tofth eliv ingdead.
</p>

<h1 id="section2">I'm totally section 2</h1>
<p>
Zombie ipsum reversus ab viral inferno, nam rick grimes malum cerebro. De carne lumbering animata corpora quaeritis. 
 Summus brains sit​​, morbo vel maleficia? De apocalypsi gorger omero undead survivor dictum mauris. 
Hi mindless mortuis soulless creaturas, imo evil stalking monstra adventus resi dentevil vultus comedat cerebella viventium. 
Nescio brains an Undead zombies. Sicut malus putrid voodoo horror. Nigh tofth eliv ingdead.
</p>

Eu usei o serviço $ anchorScroll. Para neutralizar a atualização de página que acompanha a alteração de hash, fui adiante e cancelei o evento locationChangeStart. Isso funcionou para mim porque eu tinha uma página de ajuda conectada a um comutador de ng e as atualizações quebrariam essencialmente o aplicativo.

KhalilRavanna
fonte
Eu gosto da sua solução diretiva. No entanto, como você faria isso, queria carregar outra página e rolar para o local da âncora ao mesmo tempo. Sem angularjs, seria normalmente href = "location # hash". Mas sua diretiva impede que a página seja recarregada.
precisa saber é o seguinte
@CMCDragonkai com minha diretiva, não tenho certeza porque utilizo a chamada para $ anchorScroll, que parece apenas manipular a rolagem para um elemento atualmente na página. Você pode ter que mexer com $ location ou $ window para obter algo que envolva uma alteração de página.
precisa saber é o seguinte
2
Você precisa cancelar o registro do evento locationChangeStart: var off = scope. $ On ('$ locationChangeStart', function (ev) {off (); ev.preventDefault ();});
Boa captura @EugeneTskhovrebov, fui em frente e acrescentei isso à resposta em uma edição.
KhalilRavanna
5

Tente definir um prefixo de hash para rotas angulares $locationProvider.hashPrefix('!')

Exemplo completo:

angular.module('app', [])
  .config(['$routeProvider', '$locationProvider', 
    function($routeProvider, $locationProvider){
      $routeProvider.when( ... );
      $locationProvider.hashPrefix('!');
    }
  ])
Maxim Grach
fonte
Isso não afeta o resultado. Seria doce embora.
Rasmus
2
Você tem certeza. Por que isso não funciona? Se o prefixo de hash for!, O roteamento de hash deve ser #! Page. Portanto, o AngularJS deve detectar quando é apenas um #hash, deve ancorar a rolagem automaticamente e funcionar para os URLs do modo HTML5 e URLs do modo hash.
precisa saber é o seguinte
5

Eu contornei isso na lógica de rota do meu aplicativo.

function config($routeProvider) {
  $routeProvider
    .when('/', {
      templateUrl: '/partials/search.html',
      controller: 'ctrlMain'
    })
    .otherwise({
      // Angular interferes with anchor links, so this function preserves the
      // requested hash while still invoking the default route.
      redirectTo: function() {
        // Strips the leading '#/' from the current hash value.
        var hash = '#' + window.location.hash.replace(/^#\//g, '');
        window.location.hash = hash;
        return '/' + hash;
      }
    });
}
nicksanta
fonte
1
Isso não funciona: Erro: [$ injector: modulerr] Falha ao instanciar o ângulo do módulo devido a: Erro: 'manipulador' inválido em quando ()
geoidesic 4/15
5

Este é um post antigo, mas passei muito tempo pesquisando várias soluções, então queria compartilhar mais um. Apenas adicionando target="_self"ao<a> tag corrigiu isso para mim. O link funciona e me leva ao local apropriado na página.

No entanto, o Angular ainda injeta alguma estranheza com o # no URL, para que você possa ter problemas ao usar o botão Voltar para navegação e outros itens depois de usar esse método.

Benjamin
fonte
4

Esse pode ser um novo atributo para o ngView, mas eu consegui obter links de hash âncora para trabalhar angular-routeusando ongView autoscroll atributo e 'double-hashes'.

ngView (consulte rolagem automática)

(O código a seguir foi usado com fita angular)

<!-- use the autoscroll attribute to scroll to hash on $viewContentLoaded -->    
<div ng-view="" autoscroll></div>

<!-- A.href link for bs-scrollspy from angular-strap -->
<!-- A.ngHref for autoscroll on current route without a location change -->
<ul class="nav bs-sidenav">
  <li data-target="#main-html5"><a href="#main-html5" ng-href="##main-html5">HTML5</a></li>
  <li data-target="#main-angular"><a href="#main-angular" ng-href="##main-angular" >Angular</a></li>
  <li data-target="#main-karma"><a href="#main-karma" ng-href="##main-karma">Karma</a></li>
</ul>
Michael
fonte
3

Eu poderia fazer assim:

<li>
<a href="#/#about">About</a>
</li>
Niru
fonte
2

Aqui está uma solução alternativa suja, criando uma diretiva personalizada que rolará para o elemento especificado (com "faq" codificado)

app.directive('h3', function($routeParams) {
  return {
    restrict: 'E',
    link: function(scope, element, attrs){        
        if ('faq'+$routeParams.v == attrs.id) {
          setTimeout(function() {
             window.scrollTo(0, element[0].offsetTop);
          },1);        
        }
    }
  };
});

http://plnkr.co/edit/Po37JFeP5IsNoz5ZycFs?p=preview

Valentyn Shybanov
fonte
Isso realmente funciona, mas é, como você disse, sujo. Muito sujo. Vamos ver se alguém pode encontrar uma solução mais bonita, ou terei que concordar com isso.
Rasmus
O Angular já possui a funcionalidade scrollTo incorporada via $anchorScroll, veja minha resposta.
quer
O plunker mudou para ficar menos sujo: ele usa $ location.path () para que não haja "faq" codificado na fonte. E também tentou usar $ anchorScroll, mas parece devido a roteamento ele não funciona ...
Valentyn Shybanov
2
<a href="/#/#faq-1">Question 1</a>
<a href="/#/#faq-2">Question 2</a>
<a href="/#/#faq-3">Question 3</a>
felipe_dmz
fonte
2

Se você não gosta de usar, ng-clickaqui está uma solução alternativa. Ele usa a filterpara gerar a URL correta com base no estado atual. Meu exemplo usa ui.router .

O benefício é que o usuário verá onde o link fica pairando.

<a href="{{ 'my-element-id' | anchor }}">My element</a>

O filtro:

.filter('anchor', ['$state', function($state) {
    return function(id) {
        return '/#' + $state.current.url + '#' + id;
    };
}])
Reimund
fonte
2

Minha solução com ng-route foi esta diretiva simples:

   app.directive('scrollto',
       function ($anchorScroll,$location) {
            return {
                link: function (scope, element, attrs) {
                    element.click(function (e) {
                        e.preventDefault();
                        $location.hash(attrs["scrollto"]);
                        $anchorScroll();
                    });
                }
            };
    })

O html é parecido com:

<a href="" scrollTo="yourid">link</a>
MrFlo
fonte
Você não precisa especificar o atributo como scroll-to="yourid"e nomeie a directiva scrollTo(e acesso ao atributo como attrs["scrollTo"]? Além disso, sem a inclusão jQuery explicite o manipulador tem de ser preso com element.on('click', function (e) {..}).
Theodros Zelleke
1

Você pode tentar usar o anchorScroll .

Exemplo

Portanto, o controlador seria:

app.controller('MainCtrl', function($scope, $location, $anchorScroll, $routeParams) {
  $scope.scrollTo = function(id) {
     $location.hash(id);
     $anchorScroll();
  }
});

E a vista:

<a href="" ng-click="scrollTo('foo')">Scroll to #foo</a>

... e nenhum segredo para o ID da âncora:

<div id="foo">
  This is #foo
</div>
Edmar Miyake
fonte
1

Eu estava tentando fazer meu aplicativo Angular rolar para um carregamento de âncora e ver as regras de reescrita de URL de $ routeProvider.

Após longa experimentação, decidi sobre isso:

  1. registre um manipulador de eventos document.onload na seção .run () do módulo de aplicativo Angular.
  2. no manipulador, descubra o que o original possui tag de âncora, executando algumas operações de string.
  3. override location.hash com a tag de âncora despojada (que faz com que $ routeProvider a substitua imediatamente novamente com sua regra "# /". Mas tudo bem, porque o Angular agora está sincronizado com o que está acontecendo na URL 4) $ anchorScroll ().

angular.module("bla",[]).}])
.run(function($location, $anchorScroll){
         $(document).ready(function() {
	 if(location.hash && location.hash.length>=1)    		{
			var path = location.hash;
			var potentialAnchor = path.substring(path.lastIndexOf("/")+1);
			if ($("#" + potentialAnchor).length > 0) {   // make sure this hashtag exists in the doc.                          
			    location.hash = potentialAnchor;
			    $anchorScroll();
			}
		}	 
 });

Stoyan Kenderov
fonte
1

Não tenho 100% de certeza se isso funciona o tempo todo, mas no meu aplicativo isso me dá o comportamento esperado.

Digamos que você esteja na página SOBRE e tenha a seguinte rota:

yourApp.config(['$routeProvider', 
    function($routeProvider) {
        $routeProvider.
            when('/about', {
                templateUrl: 'about.html',
                controller: 'AboutCtrl'
            }).
            otherwise({
                redirectTo: '/'
            });
        }
]);

Agora, no seu HTML

<ul>
    <li><a href="#/about#tab1">First Part</a></li>
    <li><a href="#/about#tab2">Second Part</a></li>
    <li><a href="#/about#tab3">Third Part</a></li>                      
</ul>

<div id="tab1">1</div>
<div id="tab2">2</div>
<div id="tab3">3</div>

Em conclusão

Incluir o nome da página antes da âncora fez o truque para mim. Deixe-me saber sobre seus pensamentos.

Desvantagem

Isso renderizará novamente a página e depois role para a âncora.

ATUALIZAR

Uma maneira melhor é adicionar o seguinte:

<a href="#tab1" onclick="return false;">First Part</a>
Brian
fonte
1

Obtenha seu recurso de rolagem facilmente. Ele também suporta rolagem Animated / Smooth como um recurso adicional. Detalhes para a biblioteca de rolagem angular :

Github - https://github.com/oblador/angular-scroll

Bower :bower install --save angular-scroll

npm :npm install --save angular-scroll

Versão Minfied - apenas 9kb

Rolagem suave (rolagem animada) - sim

Scroll Spy - sim

Documentação - excelente

Demonstração - http://oblador.github.io/angular-scroll/

Espero que isto ajude.

Akash
fonte
1

Baseado em @Stoyan, eu vim com a seguinte solução:

app.run(function($location, $anchorScroll){
    var uri = window.location.href;

    if(uri.length >= 4){

        var parts = uri.split('#!#');
        if(parts.length > 1){
            var anchor = parts[parts.length -1];
            $location.hash(anchor);
            $anchorScroll();
        }
    }
});
ThatMSG
fonte
0

Na mudança de rota, ele rolará para o topo da página.

 $scope.$on('$routeChangeSuccess', function () {
      window.scrollTo(0, 0);
  });

coloque esse código no seu controlador.

Praveen MP
fonte
0

Na minha opinião, o @slugslog tinha, mas eu mudaria uma coisa. Eu usaria substituir, para que você não precise configurá-lo novamente.

$scope.scrollTo = function(id) {
    var old = $location.hash();
    $location.hash(id).replace();
    $anchorScroll();
};

Documentos Pesquisa por "Substituir método"

Jackie
fonte
0

Nenhuma das soluções acima funciona para mim, mas tentei isso e funcionou,

<a href="#/#faq-1">Question 1</a>

Então percebi que precisava notificar a página para começar com a página de índice e depois usar a âncora tradicional.

windmaomao
fonte
0

Em algum momento, a navegação por hash do aplicativo angularjs não funciona e as bibliotecas jquery javascript de bootstrap fazem uso extensivo desse tipo de navegação, para que ele funcione e seja adicionado target="_self"à tag anchor. por exemplo<a data-toggle="tab" href="#id_of_div_to_navigate" target="_self">

Kulbhushan Chaskar
fonte
0

Estou usando o AngularJS 1.3.15 e parece que não preciso fazer nada de especial.

https://code.angularjs.org/1.3.15/docs/api/ng/provider/ $ anchorScrollProvider

Então, o seguinte funciona para mim no meu html:

<ul>
  <li ng-repeat="page in pages"><a ng-href="#{{'id-'+id}}">{{id}}</a>
  </li>
</ul>
<div ng-attr-id="{{'id-'+id}}" </div>

Não precisei fazer nenhuma alteração no meu controlador ou JavaScript.

Digant C Kasundra
fonte