Jeroen1984
Jeroen1984

Reputation: 1686

Add element with RouterLink dynamically

When I put an anchor element in somewhere in an Angular component like this:

<a [routerLink]="['/LoggedIn/Profile']">Static Link</a>

everything is working fine. When clicking the link, the Angular router navigates to the target component.

Now, I would like to add the same link dynamically. Somewhere in my app I have a "notification component", its single responsibility is to display notifications.

The notification component does something like this:

<div [innerHTML]="notification.content"></div>

Where notification.content is a public string variable in the NotificationComponent class that contains the HTML to display.

The notification.content variable can contain something like:

<div>Click on this <a [routerLink]="['/LoggedIn/Profile']">Dynamic Link</a> please</div>

Everything works fine and shows up on my screen, but nothing happens when I click the dynamic link.

Is there a way to let the Angular router work with this dynamically added link?

PS: I know about DynamicComponentLoader, but I really need a more unrestricted solution where I can send all kinds of HTML to my notification component, with all kind of different links.

Upvotes: 24

Views: 16243

Answers (4)

MrWedders
MrWedders

Reputation: 156

Combining some of the other answers - I wanted this as a Directive so I could target specific elements that are being innerHTML'd, but to avoid using querySelector (etc) to keep everything Angulary.

I also found an issue with the approaches above, in that if the href is a full URL (i.e, https://www.example.com/abc) feeding that whole thing to the router would result in navigating to /https.

I also needed checks to ensure we only router'd hrefs that were within our domain.

@Directive({
  selector: '[hrefToRouterLink]'
})
export class HrefToRouterLinkDirective {
  constructor(private _router: Router){}

  private _baseHref = quotemeta(environment.root_url.replace(`^https?://`, ''));
  private _hrefRe: RegExp = new RegExp(`^(https?:)?(\\/+)?(www\\.)?${this._baseHref}`, `i`);

  @HostListener('click', ['$event'])
  onClick(e) {
    // Is it a link?
    if (!(e.target instanceof HTMLAnchorElement)) 
      return;

    let href: string = e.target?.getAttribute('href')
      .replace(/(^\s+|\s+$)/gs, '');

    // Is this a URL in our site?
    if (!this._hrefRe.test(href))
      return;
      
    // If we're here, it's a link to our site, stop normal navigation
    e.preventDefault();
    e.stopPropagation();

    // Feed the router.
    this._router.navigateByUrl(
      href.replace(this._hrefRe, '')
    );
  }
}

In the above environment.root_url describes our base domain, and quotemeta is a rough implementation of a Perl-ish quotemeta function just to escape special characters.

YMMV and I've definitely missed some edge cases, but this seems to work fine.

Upvotes: 0

Abdo Driowya
Abdo Driowya

Reputation: 156

Since angular 9, AOT is the default recommended way to compile angular projects. Unlike JIT, AOT doesn't hold an instance for the compiler at runtime, which means you can't dynamically compile angular code. It's possible to disable AOT in angular 9, but it's not recommended as your bundle size will be bigger and your application slower.

The way I solve this is by adding a click listener at runtime using renderer api, preventing the default behavior of urls and calling angular router

import { Directive, ElementRef, OnDestroy, OnInit, Renderer2 } from '@angular/core';
import { Router } from '@angular/router';

@Directive({
  selector: '[hrefToRouterLink]'
})
export class HrefToRouterLinkDirective implements OnInit, OnDestroy {
  private _listeners: { destroy: () => void }[] = [];

  constructor(private _router: Router, 
  private _el: ElementRef, 
  private _renderer: Renderer2) {
  }

  ngOnInit() {
    // TODO how to guarantee this directive running after all other directives without setTimeout?
    setTimeout(() => {
      const links = this._el.nativeElement.querySelectorAll('a');
      links.forEach(link => {
        this._renderer.setAttribute(link, 'routerLink', link?.getAttribute('href'));
        const destroyListener = this._renderer.listen(link, 'click',
          (_event) => {
            _event.preventDefault();
            _event.stopPropagation();
            this._router.navigateByUrl(link?.getAttribute('href'));
          });
        this._listeners.push({ destroy: destroyListener });
      });
    }, 0);
  }

  ngOnDestroy(): void {
    this._listeners?.forEach(listener => listener.destroy());
    this._listeners = null;
  }

}

You can find an example here : https://stackblitz.com/edit/angular-dynamic-routerlink-2

Obviously the method explained above work for both JIT & AOT, but If you are still using JIT and want to dynamically compile component (which may help solve other problems) . You can find an example here : https://stackblitz.com/edit/angular-dynamic-routerlink-1

Used resources :

https://stackoverflow.com/a/35082441/6209801

https://indepth.dev/here-is-what-you-need-to-know-about-dynamic-components-in-angular

Upvotes: 1

SquarePeg
SquarePeg

Reputation: 1781

routerLink cannot be added after the content is already rendered but you can still achieve the desired result:

  1. Create a href with dynamic data and give it a class:

    `<a class="routerlink" href="${someDynamicUrl}">${someDynamicValue}</a>`
    
  2. Add a HostListener to app.component that listens for the click and uses the router to navigate

    @HostListener('document:click', ['$event'])
    public handleClick(event: Event): void {
     if (event.target instanceof HTMLAnchorElement) {
       const element = event.target as HTMLAnchorElement;
       if (element.className === 'routerlink') {
         event.preventDefault();
         const route = element?.getAttribute('href');
         if (route) {
           this.router.navigate([`/${route}`]);
         }
       }
     }
    

    }

Upvotes: 22

G&#252;nter Z&#246;chbauer
G&#252;nter Z&#246;chbauer

Reputation: 657108

routerLink is a directive. Directives and Components are not created for HTML that is added using [innerHTML]. This HTML is not process by Angular in any way.

The recommended way is to not use [innerHTML] but DynamicComponentLoaderViewContainerRef.createComponent where you wrap the HTML in a component and add it dynamically.

For an example see Angular 2 dynamic tabs with user-click chosen components

Upvotes: 2

Related Questions