Enrutamiento Angular2 con Hashtag al ancla de página


120

Deseo agregar algunos enlaces en mi página de Angular2, que al hacer clic, saltarán a posiciones específicas dentro de esa página, como hacen los hashtags normales. Entonces los enlaces serían algo como

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

etc.

No creo que necesite HashLocationStrategy, ya que estoy bien con la forma normal de Angular2, pero si agrego directamente, el enlace en realidad saltaría a la raíz, no a algún lugar de la misma página. Se agradece cualquier dirección, gracias.

Respuestas:


130

Actualizar

Esto ahora es compatible

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

Agregue el código de abajo a su componente para desplazarse

  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 es un problema conocido y se rastrea en https://github.com/angular/angular/issues/6595


1
@invot una variable con una cadena (por ejemplo, lo que 123está en la pregunta) asumiendo que la ruta de la ruta espera un parámetro como{ path: 'users/:id', ....}
Günter Zöchbauer

2
Si desea desplazarse hasta el ancla, vea esta publicación: github.com/angular/angular/issues/6595
Pere Pages

12
Nota: esto todavía no se desplaza
Martín Coll

2
sí, funciona con setTimeout, si encuentro una mejor solución, te lo haré saber
Amr Ibrahim

1
Para aquellos que luchan por desplazarse a identificadores en forma de números, tenga en cuenta que eso 01o 100no son selectores de CSS válidos. Es posible que desee agregar una letra o algo para que sea un selector válido. Por lo tanto, aún pasaría 01como un fragmento, pero idtendría que ser algo así d01y, por document.querySelector('#d'+id)lo tanto , coincidiría.
pop

52

Aunque la respuesta de Günter es correcta, no cubre el "salto a" la parte de la etiqueta de anclaje .

Por tanto, además de:

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

... en el componente (padre) donde necesita un comportamiento de "salto a", agregue:

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

  }
}

¡Tenga en cuenta que esta es una solución ! Siga este problema de github para futuras actualizaciones. ¡Créditos a Victor Savkin por brindar la solución!


hola, estoy creando una página de preguntas frecuentes en la que puede saltar a la respuesta haciendo clic en una pregunta definida en una lista en la parte superior de la página. Entonces, el usuario ya está en la página actual cuando salta al ancla. Si quiero que el atributo routerLink funcione, tengo que darlo "['../faq']"como valor o de lo contrario intentará saltar a / faq / faq / # anchor, en lugar de / faq / # anchor. ¿Es esta la forma correcta de hacerlo o hay una forma más elegante de hacer referencia a la página actual en routerlink? Además, document.querySelector("#" + tree.fragment);me da un error de selector no válido. ¿Estás seguro de que esto es correcto? Gracias
Maurice

2
cuando vuelve a hacer clic en el mismo enlace, no funciona. ¿Alguien hizo que esto funcione si el usuario hace clic en el mismo enlace de anclaje <a [routerLink]="['/faq']" fragment="section6">?
Junior Mayhé

@JuniorM ¿Alguna vez te diste cuenta de esto? Me encuentro con el mismo problema.
The Muffin Man


1
Esto necesita más exposición. Esta es una mejor respuesta en mi opinión. La mayoría de la gente querrá saltar a la sección.
iamtravisw

44

Perdón por responderla un poco tarde; Hay una función predefinida en la Documentación de enrutamiento angular que nos ayuda a enrutar con un hashtag al ancla de la página, es decir, anchorScrolling: 'habilitado'

Paso 1: - Primero importe el RouterModule en el archivo app.module.ts: -

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

Paso 2: - Vaya a la página HTML, cree la navegación y agregue dos atributos importantes como [routerLink] y un fragmento para hacer coincidir las respectivas Div ID : -

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

Paso 3: - Cree una sección / div haciendo coincidir el nombre de ID con el 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 su referencia, agregué el ejemplo a continuación creando una pequeña demostración que ayuda a resolver su problema.

Demostración: https://routing-hashtag-page-anchors.stackblitz.io/


Muchas gracias por todo. ¡Limpio, conciso y funciona!
Belmiris

2
Sí, gracias, por el rastreo automático en la carga de la página con Angular 7, solo tiene que agregar scrollPositionRestoration: 'enabled',debajo de la opción anchorScrolling :)
Mickaël

Esto agrega el hash al final de mi URL correctamente, pero no se ancla al div con el mismo nombre. No estoy seguro de lo que me estoy perdiendo. Seguí los tres pasos anteriores.
Oaklandrichie

@oaklandrichie: ¿Puedes compartir el código jsfiddle / stackblitz aquí? Puedo ayudarte
Naheed Shareef

esta respuesta definitivamente debe ser aceptada, ¡funciona a la perfección!
Kiss Koppány

25

Un poco tarde, pero aquí hay una respuesta que encontré que funciona:

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

Y en el componente:

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

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

Lo anterior no se desplaza automáticamente a la vista si aterriza en una página que ya tiene un ancla, así que usé la solución anterior en mi ngInit para que también pudiera funcionar con eso:

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

Asegúrese de importar Router, ActivatedRoute y NavigationEnd al comienzo de su componente y debería estar listo.

Fuente


4
¡Funciona para mi! En caso de que desee navegar dentro de la misma página en la que ya está, use [routerLink] = "['.']"
Raoul

1
¿Podrías explicarme más? esta parte document.querySelector ( "#" + f )me da un error porque espera un selector, no una cadena.
Maurice

1
@Maurice para mí esto funciona: element.scrollIntoView()(sin pasar elementa la función). Para que sea suave, utilice la siguiente: element.scrollIntoView({block: "end", behavior: "smooth"}).
Sr. B.

Intellisense aquí muestra que dentro onAnchorClick(), debemos pasar a un valor lógico: scrollIntoView if (element) { element.scrollIntoView(true); }. Ahora puedo hacer clic dos veces en el mismo enlace y el desplazamiento funciona
Junior Mayhé

18

Ninguna de las respuestas anteriores funcionó para mí. En un último esfuerzo, probé en mi plantilla:

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

Con esto en mi .ts:

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

Y funciona como se esperaba para los enlaces internos. En realidad, esto no usa etiquetas de anclaje, por lo que no tocaría la URL en absoluto.


1
simple y fácil
WasiF

6

Las soluciones anteriores no funcionaron para mí ... Esta lo hizo:

Primero, prepárese MyAppComponentpara el desplazamiento automático en 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 );

              }
            }
          } );
    }

  }

}

Luego, navega hasta my-app-routeenviar 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 } );
  }

}

Votar en contra sin comentarios ni explicaciones ... no es una buena práctica ...
JavierFuentes

¡Esto funcionó para mí! Sin embargo, ¿por qué usar ngAfterViewChecked en lugar de ngInit?
Antoine Boisier-Michaud

Gracias @ AntoineBoisier-Michaud por sus comentarios positivos. ngInit no se activa siempre que necesito en mi proyecto ... ngAfterViewChecked lo hace. ¿Puedes votar esta solución por favor? Gracias.
JavierFuentes

6

Todas las demás respuestas funcionarán en la versión Angular <6.1. Pero si tiene la última versión, entonces no necesitará hacer estos horribles trucos ya que Angular ha solucionado el problema.

aquí está el enlace para emitir

Todo lo que necesita hacer es configurar scrollOffsetcon la opción del segundo argumento del RouterModule.forRootmétodo.

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

2
¿Funcionará esto para enlaces externos? digamos que desde otro sitio web
hago

anchorScrolling no funciona, si hace un uso extensivo de * ngIf, porque salta a principios :-(
Jojo.Lechelt

El único problema que he tenido con esto es el tiempo: tiende a saltar al ancla antes de que el estilo de algún elemento se haya renderizado por completo, lo que hace que el posicionamiento esté desactivado. Sería bueno si pudiera agregar un retraso :)
Charly

5

Utilizar esto para el módulo de enrutador en app-routing.module.ts:

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

Esto estará en tu HTML:

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

4

En archivo html:

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

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

En archivo 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();
  }
}

El querySelector podría colocarse fácilmente en una directiva llamada scrolllTo. Url habría <a scrollTo="test1" [routerLink]="['./']"> Ir a la sección de prueba 1 </a>
John Peters

3

Dado que la propiedad del fragmento aún no proporciona desplazamiento de anclaje, esta solución me funcionó:

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

3

Además de la respuesta de Kalyoyan , esta suscripción está vinculada al enrutador y estará activa hasta que la página se actualice por completo. Al suscribirse a eventos de enrutador en un componente, asegúrese de cancelar la suscripción en 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();
  }
}

Pensé que las suscripciones a los eventos de ruta se eliminarían automáticamente.

3

Acabo de hacer que esto funcione en mi propio sitio web, así que pensé que valdría la pena publicar mi solución aquí.

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

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

Y luego, en su componente, asegúrese de incluir esto:

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

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

Creo que es mejor escribir solo element.scrollIntoView()o element.scrollIntoView(true). Su versión no se compiló para mí (¿tal vez debido a estrictosNullChecks?)
David L.

3

Después de leer todas las soluciones, busqué un componente y encontré uno que hace exactamente lo que pedía la pregunta original: desplazarse para anclar enlaces. https://www.npmjs.com/package/ng2-scroll-to

Cuando lo instala, usa una sintaxis 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 {
}

Me ha funcionado muy bien.


Usa la rueda, no la vuelvas a inventar;)
Yogen Rai

¿Ha mirado el código detrás de ese componente? Parece muy frágil, el proyecto también tiene 14 problemas abiertos, que incluyen cosas como elementos que no existen, objetivos nulos, que no se desplazan al elemento, problemas de soporte del navegador.
Drenai

no funciona cuando tiene un niño (el niño tiene entidades ancladas y / o nombres de ancla) en el componente principal, simplemente actualiza la página
Sasha Bond

3

Una solución simple que funciona para páginas sin ningún parámetro de consulta, es compatible con el navegador hacia atrás / adelante, el enrutador y los enlaces profundos.

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

El tiempo de espera es simplemente para permitir que la página cargue cualquier dato dinámico "protegido" por un * ngIf. Esto también se puede usar para desplazarse a la parte superior de la página al cambiar de ruta; solo proporcione una etiqueta de anclaje superior predeterminada.


2

Si no importa tener esos ID de elementos adjuntos a la URL, debería considerar echar un vistazo a este enlace:

Angular 2: vínculos de anclaje al elemento en la página actual

// 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

Aquí hay otra solución alternativa con referencia a la respuesta de JavierFuentes:

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

en guión:

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

Esto permite al usuario desplazarse directamente al elemento, si el usuario aterriza directamente en la página que tiene un hashtag en la URL.

Pero en este caso, me he suscrito a la ruta Fragmento en, ngAfterViewCheckedpero ngAfterViewChecked()se llama continuamente por cada ngDoChecky no permite al usuario desplazarse hacia arriba, por lo que routeFragmentSubscription.unsubscribese llama después de un tiempo de espera de 0 milis después de que la vista se desplaza al elemento.

Además, el gotoHashtagmétodo se define para desplazarse al elemento cuando el usuario hace clic específicamente en la etiqueta de anclaje.

Actualizar:

Si la URL tiene cadenas de consulta, [routerLink]="['self-route', id]"en anchor no conservará las cadenas de consulta. Intenté seguir la solución alternativa para lo mismo:

<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 mí! Este ngFor para que ancle dinámicamente la etiqueta, debe esperar a que se procesen

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>

Mi archivo 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();
    }
}

¡No olvides importar eso ViewChildren, QueryListetc. y agregar algún constructor ActivatedRoute!


1

A diferencia de otras respuestas, además, también me gustaría añadir focus()junto con scrollIntoView(). También lo estoy usando, setTimeoutya que salta a la parte superior de lo contrario al cambiar la URL. No estoy seguro de cuál fue la razón de eso, pero parece setTimeoutque la solución.

Origen:

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

Destino:

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

Mecanografiado:

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

1

Tuve el mismo problema. La solución: usar View port 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 del 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

Acabo de probar un complemento muy útil disponible en nmp - ngx-scroll-to , que funciona muy bien para mí. Sin embargo, está diseñado para Angular 4+, pero tal vez alguien encuentre útil esta respuesta.


0

Probé la mayoría de estas soluciones, pero tuve problemas para salir y volver con otro fragmento que no funcionaría, así que hice algo un poco diferente que funciona al 100% y me deshago del hash feo en la URL.

tl; dr aquí hay una mejor manera de lo que he visto hasta ahora.

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();
    }
}
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.