Roteamento Angular2 com Hashtag para âncora de página


120

Desejo adicionar alguns links em minha página Angular2, que quando clicados, irão pular para posições específicas dentro daquela página, como o que as hashtags normais fazem. Portanto, os links seriam algo como

/users/123#userInfo
/users/123#userPhoto
/users/123#userLikes

etc.

Não acho que preciso de HashLocationStrategy, já que estou bem com a forma normal do Angular2, mas se eu adicionar diretamente, o link vai realmente pular para a raiz, não para algum lugar na mesma página. Qualquer direção é apreciada, obrigado.

Respostas:


130

Atualizar

Isso agora é suportado

<a [routerLink]="['somepath']" fragment="Test">Jump to 'Test' anchor </a>
this._router.navigate( ['/somepath', id ], {fragment: 'test'});

Adicione o código abaixo ao seu componente para rolar

  import {ActivatedRoute} from '@angular/router'; // <-- do not forget to import

  private fragment: string;

  constructor(private route: ActivatedRoute) { }

  ngOnInit() {
    this.route.fragment.subscribe(fragment => { this.fragment = fragment; });
  }

  ngAfterViewInit(): void {
    try {
      document.querySelector('#' + this.fragment).scrollIntoView();
    } catch (e) { }
  }

Original

Este é um problema conhecido e monitorado em https://github.com/angular/angular/issues/6595


1
@invot uma variável com uma string (por exemplo, o que 123está em questão) assumindo que o caminho da rota espera um parâmetro como{ path: 'users/:id', ....}
Günter Zöchbauer

2
Se você quiser rolar até a âncora, consulte esta postagem: github.com/angular/angular/issues/6595
Pere Pages

12
Observação: isso ainda não rola
Martín Coll

2
sim, funciona com setTimeout, se eu encontrar a melhor solução, avisarei você
Amr Ibrahim

1
Para aqueles que lutam para rolar para ids semelhantes a números, lembre-se de que 01ou 100não são seletores CSS válidos. Você pode querer adicionar uma letra ou algo para torná-lo um seletor válido. Portanto, você ainda passaria 01por um fragmento, mas o idprecisaria ser algo semelhante d01e, portanto document.querySelector('#d'+id), corresponderia.
pop de

52

Embora a resposta de Günter esteja correta, ela não cobre o "salto para" a parte da etiqueta de âncora .

Portanto, além de:

<a [routerLink]="['somepath']" fragment="Test">Jump to 'Test' anchor </a>
this._router.navigate( ['/somepath', id ], {fragment: 'test'});

... no componente (pai) onde você precisa de um comportamento "pular para", adicione:

import { Router, NavigationEnd } from '@angular/router';

class MyAppComponent {
  constructor(router: Router) {

    router.events.subscribe(s => {
      if (s instanceof NavigationEnd) {
        const tree = router.parseUrl(router.url);
        if (tree.fragment) {
          const element = document.querySelector("#" + tree.fragment);
          if (element) { element.scrollIntoView(true); }
        }
      }
    });

  }
}

Observe que esta é uma solução alternativa ! Siga esta edição do github para atualizações futuras. Créditos a Victor Savkin por fornecer a solução!


Olá, estou criando uma página de FAQ onde você pode pular para a resposta clicando em uma pergunta definida em uma lista no topo da página. Assim, o usuário já está na página atual quando pula para a âncora. Se eu quiser que o atributo routerLink funcione, tenho que fornecer "['../faq']"o valor ou então ele tentará pular para / faq / faq / # anchor, insteaf de / faq / # anchor. Esta é a maneira correta de fazer isso ou existe uma maneira mais elegante de se referir à página atual no routerlink? Além disso, document.querySelector("#" + tree.fragment);me dá um erro de seletor inválido. Tem certeza de que está correto? Obrigado
Maurice

2
quando você clica novamente no mesmo link, ele não funciona. Alguém fez isso funcionar se o usuário clicar no mesmo link de âncora <a [routerLink]="['/faq']" fragment="section6">?
Junior Mayhé

@JuniorM Você já descobriu isso? Estou tendo o mesmo problema.
The Muffin Man


1
Isso precisa de mais exposição. Esta é uma resposta melhor IMO. A maioria das pessoas vai querer pular para a seção.
iamtravisw

44

Desculpe por responder um pouco tarde; Há uma função predefinida na Documentação de Roteamento Angular que nos ajuda a rotear com uma hashtag para a âncora da página, ou seja, anchorScrolling: 'enabled'

Etapa 1: - Primeiro importe o RouterModule no arquivo app.module.ts: -

imports:[ 
    BrowserModule, 
    FormsModule,
    RouterModule.forRoot(routes,{
      anchorScrolling: 'enabled'
    })
  ],

Passo 2: - Vá para a página HTML, crie a navegação e adicione dois atributos importantes como [routerLink] e fragmento para combinar os respectivos Div IDs : -

<ul>
    <li> <a [routerLink] = "['/']"  fragment="home"> Home </a></li>
    <li> <a [routerLink] = "['/']"  fragment="about"> About Us </a></li>
  <li> <a [routerLink] = "['/']"  fragment="contact"> Contact Us </a></li>
</ul>

Etapa 3: - Crie uma seção / div combinando o nome do ID com o fragmento : -

<section id="home" class="home-section">
      <h2>  HOME SECTION </h2>
</section>

<section id="about" class="about-section">
        <h2>  ABOUT US SECTION </h2>
</section>

<section id="contact" class="contact-section">
        <h2>  CONTACT US SECTION </h2>
</section>

Para sua referência, adicionei o exemplo abaixo criando uma pequena demonstração que ajuda a resolver seu problema.

Demo: https://routing-hashtag-page-anchors.stackblitz.io/


Muito obrigado por isso. Limpo, conciso e funciona!
Belmiris

2
Sim, obrigado, para scoll automático no carregamento da página com Angular 7, basta adicionar a scrollPositionRestoration: 'enabled',opção anchorScrolling :)
Mickaël

Isso anexa o hash ao final do meu url de maneira adequada, mas não se ancora ao div com o mesmo nome. Não tenho certeza do que estou perdendo. Eu segui os três passos acima.
oaklandrichie de

@oaklandrichie: Você pode compartilhar o código jsfiddle / stackblitz aqui. Posso ajudá-lo
Naheed Shareef

esta resposta definitivamente deve ser aceita, funciona como um encanto!
Kiss Koppány

25

Um pouco tarde, mas aqui está uma resposta que achei que funciona:

<a [routerLink]="['/path']" fragment="test" (click)="onAnchorClick()">Anchor</a>

E no componente:

constructor( private route: ActivatedRoute, private router: Router ) {}

  onAnchorClick ( ) {
    this.route.fragment.subscribe ( f => {
      const element = document.querySelector ( "#" + f )
      if ( element ) element.scrollIntoView ( element )
    });
  }

O exemplo acima não rola automaticamente para a visualização se você acessar uma página já com uma âncora, então usei a solução acima no meu ngInit para que ele pudesse funcionar com isso também:

ngOnInit() {
    this.router.events.subscribe(s => {
      if (s instanceof NavigationEnd) {
        const tree = this.router.parseUrl(this.router.url);
        if (tree.fragment) {
          const element = document.querySelector("#" + tree.fragment);
          if (element) { element.scrollIntoView(element); }
        }
      }
    });
  }

Certifique-se de importar Router, ActivatedRoute e NavigationEnd no início de seu componente e deve estar tudo pronto para ir.

Fonte


4
Funciona para mim! Caso você queira navegar na mesma página em que já está, use [routerLink] = "['.']"
Raoul

1
você poderia explicar melhor? esta parte document.querySelector ( "#" + f )me dá um erro porque espera um seletor, não uma string.
Maurice

1
@Maurice pra mim isso funciona: element.scrollIntoView()(sem passar elementpara a função). Para torná-lo suave, use este: element.scrollIntoView({block: "end", behavior: "smooth"}).
Sr. B.

Intellisense aqui mostra que dentro onAnchorClick(), temos de passar um booleano para scrollIntoView: if (element) { element.scrollIntoView(true); }. Agora posso clicar duas vezes no mesmo link e rolar as obras
Junior Mayhé

18

Nenhuma das respostas anteriores funcionou para mim. Em um último esforço, tentei em meu modelo:

<a (click)="onClick()">From Here</a>
<div id='foobar'>To Here</div>

Com isso em meu .ts:

onClick(){
    let x = document.querySelector("#foobar");
    if (x){
        x.scrollIntoView();
    }
}

E funciona conforme o esperado para links internos. Na verdade, isso não usa marcas de âncora, portanto, não afetaria a URL de forma alguma.


1
simples e fácil
WasiF

6

As soluções acima não funcionaram para mim ... Esta funcionou:

Primeiro, prepare-se MyAppComponentpara a rolagem automática em ngAfterViewChecked () ...

import { Component, OnInit, AfterViewChecked } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { Subscription } from 'rxjs';

@Component( {
   [...]
} )
export class MyAppComponent implements OnInit, AfterViewChecked {

  private scrollExecuted: boolean = false;

  constructor( private activatedRoute: ActivatedRoute ) {}

  ngAfterViewChecked() {

    if ( !this.scrollExecuted ) {
      let routeFragmentSubscription: Subscription;

      // Automatic scroll
      routeFragmentSubscription =
        this.activatedRoute.fragment
          .subscribe( fragment => {
            if ( fragment ) {
              let element = document.getElementById( fragment );
              if ( element ) {
                element.scrollIntoView();

                this.scrollExecuted = true;

                // Free resources
                setTimeout(
                  () => {
                    console.log( 'routeFragmentSubscription unsubscribe' );
                    routeFragmentSubscription.unsubscribe();
                }, 1000 );

              }
            }
          } );
    }

  }

}

Em seguida, navegue até o my-app-routeenvio de prodIDhashtag

import { Component } from '@angular/core';
import { Router } from '@angular/router';

@Component( {
   [...]
} )
export class MyOtherComponent {

  constructor( private router: Router ) {}

  gotoHashtag( prodID: string ) {
    this.router.navigate( [ '/my-app-route' ], { fragment: prodID } );
  }

}

Votação sem comentários ou explicação ... não é uma boa prática ...
JavierFuentes

Isso funcionou para mim! Por que usar ngAfterViewChecked em vez de ngInit?
Antoine Boisier-Michaud

Obrigado @ AntoineBoisier-Michaud por seu feedback positivo. ngInit não dispara sempre que preciso no meu projeto ... ngAfterViewChecked dispara. Você pode aprovar esta solução, por favor? Obrigado.
JavierFuentes de

6

Todas as outras respostas funcionarão na versão Angular <6.1. Mas se você tiver a versão mais recente, não precisará fazer esses hacks horríveis, pois o Angular corrigiu o problema.

aqui está o link para o problema

Tudo o que você precisa fazer é definir scrollOffseta opção do segundo argumento do RouterModule.forRootmétodo.

@NgModule({
  imports: [
    RouterModule.forRoot(routes, {
      scrollPositionRestoration: 'enabled',
      anchorScrolling: 'enabled',
      scrollOffset: [0, 64] // [x, y]
    })
  ],
  exports: [RouterModule]
})
export class AppRoutingModule {}

2
isso funcionará para links externos? Digamos que de outro site eu clique em www.abc.com # sectionToScrollTo
Sushmit Sagar

anchorScrolling não está funcionando, se você faz uso extensivo de * ngIf, porque ele salta para o início :-(
Jojo.Lechelt

O único problema que tive com isso é o tempo - ele tende a pular para a âncora antes que o estilo de algum elemento chegue à renderização completa, fazendo com que o posicionamento fique errado. Seria bom se você pudesse adicionar um atraso :)
Charly

5

Use isso para o módulo do roteador em app-routing.module.ts:

@NgModule({
  imports: [RouterModule.forRoot(routes, {
    useHash: true,
    scrollPositionRestoration: 'enabled',
    anchorScrolling: 'enabled',
    scrollOffset: [0, 64]
  })],
  exports: [RouterModule]
})

Isso estará em seu HTML:

<a href="#/users/123#userInfo">

4

No arquivo html:

<a [fragment]="test1" [routerLink]="['./']">Go to Test 1 section</a>

<section id="test1">...</section>
<section id="test2">...</section>

No arquivo ts:

export class PageComponent implements AfterViewInit, OnDestroy {

  private destroy$$ = new Subject();
  private fragment$$ = new BehaviorSubject<string | null>(null);
  private fragment$ = this.fragment$$.asObservable();

  constructor(private route: ActivatedRoute) {
    this.route.fragment.pipe(takeUntil(this.destroy$$)).subscribe(fragment => {
      this.fragment$$.next(fragment);
    });
  }

  public ngAfterViewInit(): void {
    this.fragment$.pipe(takeUntil(this.destroy$$)).subscribe(fragment => {
      if (!!fragment) {
        document.querySelector('#' + fragment).scrollIntoView();
      }
    });
  }

  public ngOnDestroy(): void {
    this.destroy$$.next();
    this.destroy$$.complete();
  }
}

O querySelector pode ser facilmente colocado em uma diretiva chamada scrolllTo. O URL seria <a scrollTo="test1" [routerLink]="['./']"> Vá para a seção Teste 1 </a>
John Peters

3

Como a propriedade fragment ainda não fornece rolagem de âncora, esta solução alternativa funcionou para mim:

<div [routerLink]="['somepath']" fragment="Test">
  <a href="#Test">Jump to 'Test' anchor </a>
</div>

3

Acrescentando à resposta de Kalyoyan , esta assinatura está vinculada ao roteador e permanecerá ativa até que a página seja totalmente atualizada. Ao assinar eventos de roteador em um componente, certifique-se de cancelar a assinatura em ngOnDestroy:

import { OnDestroy } from '@angular/core';
import { Router, NavigationEnd } from '@angular/router';
import { Subscription } from "rxjs/Rx";

class MyAppComponent implements OnDestroy {

  private subscription: Subscription;

  constructor(router: Router) {
    this.subscription = router.events.subscribe(s => {
      if (s instanceof NavigationEnd) {
        const tree = router.parseUrl(router.url);
        if (tree.fragment) {
          const element = document.querySelector("#" + tree.fragment);
          if (element) { element.scrollIntoView(element); }
        }
      }
    });
  }

  public ngOnDestroy() {
    this.subscription.unsubscribe();
  }
}

Achei que as assinaturas para eventos de rota fossem automaticamente canceladas.

3

Acabei de fazer isso funcionar em meu próprio site, então achei que valeria a pena postar minha solução aqui.

<a [routerLink]="baseUrlGoesHere" fragment="nameOfYourAnchorGoesHere">Link Text!</a>

<a name="nameOfYourAnchorGoesHere"></a>
<div>They're trying to anchor to me!</div>

E então, em seu componente, certifique-se de incluir isto:

 import { ActivatedRoute } from '@angular/router';

 constructor(private route: ActivatedRoute) { 
     this.route.fragment.subscribe ( f => {
         const element = document.querySelector ( "#" + f )
         if ( element ) element.scrollIntoView ( element )
     });
 }

Eu acho que é melhor escrever apenas element.scrollIntoView()ou element.scrollIntoView(true). Sua versão não compilou para mim (talvez por causa de strictNullChecks?).
David L.

3

Depois de ler todas as soluções, procurei um componente e encontrei um que faz exatamente o que a pergunta original pedia: rolar para ancorar links. https://www.npmjs.com/package/ng2-scroll-to

Ao instalá-lo, você usa uma sintaxe como:

// app.awesome.component.ts
@Component({
   ...
   template: `...
        <a scrollTo href="#main-section">Scroll to main section</a>
        <button scrollTo scrollTargetSelector="#test-section">Scroll to test section</a>
        <button scrollTo scrollableElementSelector="#container" scrollYTarget="0">Go top</a>
        <!-- Further content here -->
        <div id="container">
            <section id="main-section">Bla bla bla</section>
            <section id="test-section">Bla bla bla</section>
        <div>
   ...`,
})
export class AwesomeComponent {
}

Funcionou muito bem para mim.


Use a roda, não invente de novo;)
Yogen Rai

Você já olhou o código por trás desse componente? Parece muito frágil - o projeto também tem 14 problemas abertos - que incluem coisas como elemento não existente, destinos nulos, não rolar para o elemento, problemas de suporte do navegador.
Drenai de

não funciona quando você tem filho (o filho tem entidades ancoradas e / ou nomes de âncora) no componente pai, apenas atualiza a página
Sasha Bond

3

Uma solução simples que funciona para páginas sem nenhum parâmetro de consulta é compatível com o navegador para trás / para a frente, roteador e link direto.

<a (click)="jumpToId('anchor1')">Go To Anchor 1</a>


ngOnInit() {

    // If your page is dynamic
    this.yourService.getWhatever()
        .then(
            data => {
            this.componentData = data;
            setTimeout(() => this.jumpToId( window.location.hash.substr(1) ), 100);
        }
    );

    // If your page is static
    // this.jumpToId( window.location.hash.substr(1) )
}

jumpToId( fragment ) {

    // Use the browser to navigate
    window.location.hash = fragment;

    // But also scroll when routing / deep-linking to dynamic page
    // or re-clicking same anchor
    if (fragment) {
        const element = document.querySelector('#' + fragment);
        if (element) element.scrollIntoView();
    }
}

O tempo limite é simplesmente para permitir que a página carregue quaisquer dados dinâmicos "protegidos" por um * ngIf. Isso também pode ser usado para rolar para o topo da página ao alterar a rota - basta fornecer uma marca âncora superior padrão.


2

se não importa ter esses ids de elemento anexados ao url, você deve considerar dar uma olhada neste link:

Angular 2 - Links de âncora para elemento na página atual

// html
// add (click) event on element
<a (click)="scroll({{any-element-id}})">Scroll</a>

// in ts file, do this
scroll(sectionId) {
let element = document.getElementById(sectionId);

  if(element) {
    element.scrollIntoView(); // scroll to a particular element
  }
 }


1

Aqui está outra solução alternativa com referência à resposta JavierFuentes:

<a [routerLink]="['self-route', id]" fragment="some-element" (click)="gotoHashtag('some-element')">Jump to Element</a>

no script:

import {ActivatedRoute} from "@angular/router";
import {Subscription} from "rxjs/Subscription";

export class Links {
    private scrollExecuted: boolean = false;

    constructor(private route: ActivatedRoute) {} 

    ngAfterViewChecked() {
            if (!this.scrollExecuted) {
              let routeFragmentSubscription: Subscription;
              routeFragmentSubscription = this.route.fragment.subscribe(fragment => {
                if (fragment) {
                  let element = document.getElementById(fragment);
                  if (element) {
                    element.scrollIntoView();
                    this.scrollExecuted = true;
                    // Free resources
                    setTimeout(
                      () => {
                        console.log('routeFragmentSubscription unsubscribe');
                        routeFragmentSubscription.unsubscribe();
                      }, 0);
                  }
                }
              });
            }
          }

        gotoHashtag(fragment: string) {
            const element = document.querySelector("#" + fragment);
            if (element) element.scrollIntoView(element);
        }
}

Isso permite ao usuário rolar diretamente para o elemento, se o usuário acessar diretamente a página com hashtag no url.

Mas, neste caso, subscrevi a rota Fragment in, ngAfterViewCheckedmas ngAfterViewChecked()é chamada continuamente a cada ngDoChecke não permite que o usuário role de volta para o topo, então routeFragmentSubscription.unsubscribeé chamada após um tempo limite de 0 milis após a exibição ser rolada para o elemento.

Além disso, o gotoHashtagmétodo é definido para rolar para o elemento quando o usuário clica especificamente na tag âncora.

Atualizar:

Se url tiver querystrings, [routerLink]="['self-route', id]"na âncora não preservará as querystrings. Tentei seguir a solução alternativa para o mesmo:

<a (click)="gotoHashtag('some-element')">Jump to Element</a>

constructor( private route: ActivatedRoute,
              private _router:Router) {
}
...
...

gotoHashtag(fragment: string) {
    let url = '';
    let urlWithSegments = this._router.url.split('#');

    if(urlWithSegments.length){
      url = urlWithSegments[0];
    }

    window.location.hash = fragment;
    const element = document.querySelector("#" + fragment);
    if (element) element.scrollIntoView(element);
}

1

Este funciona para mim !! Este ng; para que ele ancore dinamicamente a tag, você precisa esperá-los renderizar

HTML:

<div #ngForComments *ngFor="let cm of Comments">
    <a id="Comment_{{cm.id}}" fragment="Comment_{{cm.id}}" (click)="jumpToId()">{{cm.namae}} Reply</a> Blah Blah
</div>

Meu arquivo ts:

private fragment: string;
@ViewChildren('ngForComments') AnchorComments: QueryList<any>;

ngOnInit() {
      this.route.fragment.subscribe(fragment => { this.fragment = fragment; 
   });
}
ngAfterViewInit() {
    this.AnchorComments.changes.subscribe(t => {
      this.ngForRendred();
    })
}

ngForRendred() {
    this.jumpToId()
}

jumpToId() { 
    let x = document.querySelector("#" + this.fragment);
    console.log(x)
    if (x){
        x.scrollIntoView();
    }
}

Não se esqueça de importar isso ViewChildren, QueryListetc. e adicionar algum construtor ActivatedRoute!!


1

Ao contrário de outras respostas, eu também adicionaria focus()junto com scrollIntoView(). Também estou usando, setTimeoutpois salta para o topo de outra forma ao alterar a URL. Não sei qual foi a razão para isso, mas parece setTimeoutque a solução alternativa.

Origem:

<a [routerLink] fragment="some-id" (click)="scrollIntoView('some-id')">Jump</a>

Destino:

<a id="some-id" tabindex="-1"></a>

Texto datilografado:

scrollIntoView(anchorHash) {
    setTimeout(() => {
        const anchor = document.getElementById(anchorHash);
        if (anchor) {
            anchor.focus();
            anchor.scrollIntoView();
        }
    });
}

1

Eu tive o mesmo problema. A solução: usando a porta View Scroller https://angular.io/api/common/ViewportScroller#scrolltoanchor

- código app-routing.module.ts:

import { PageComponent } from './page/page.component';

const routes: Routes = [
   path: 'page', component: PageComponent },
   path: 'page/:id', component: PageComponent }
];

- Componente HTML

  <a (click) = "scrollTo('typeExec')">
    <mat-icon>lens</mat-icon>
  </a>

- Código do componente:

    import { Component } from '@angular/core';
    import { ViewportScroller } from '@angular/common';


    export class ParametrageComponent {

      constructor(private viewScroller: ViewportScroller) {}

      scrollTo(tag : string)
      {
        this.viewScroller.scrollToAnchor(tag);
      }

    }

0

Acabei de testar um plugin muito útil disponível em nmp - ngx-scroll-to , que funciona muito bem para mim. No entanto, ele foi projetado para Angular 4+, mas talvez alguém ache esta resposta útil.


0

Tentei a maioria dessas soluções, mas tive problemas ao sair e voltar com outro fragmento que não funcionaria, então fiz algo um pouco diferente que funciona 100% e me livrei do hash feio na URL.

tl; dr aqui está uma maneira melhor do que a que vi até agora.

import { Component, OnInit, AfterViewChecked, OnDestroy } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { Subscription } from 'rxjs/Subscription';

@Component({
    selector: 'app-hero',
    templateUrl: './hero.component.html',
    styleUrls: ['./hero.component.scss']
})
export class HeroComponent implements OnInit, AfterViewChecked, OnDestroy {
    private fragment: string;
    fragSub: Subscription;

    constructor(private route: ActivatedRoute) { }

    ngOnInit() {
        this.fragSub = this.route.fragment.subscribe( fragment => { this.fragment = fragment; })
    }

    ngAfterViewChecked(): void {
        try {
            document.querySelector('#' + this.fragment).scrollIntoView({behavior: 'smooth'});
            window.location.hash = "";
          } catch (e) { }
    }

    ngOnDestroy() {
        this.fragSub.unsubscribe();
    }
}
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.