21

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.

Johannes
  • 828
  • 12
  • 29
Jeroen1984
  • 1,616
  • 1
  • 19
  • 32

4 Answers4

20

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

    }

AngularBoy
  • 1,715
  • 2
  • 25
  • 35
2

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

Community
  • 1
  • 1
Günter Zöchbauer
  • 623,577
  • 216
  • 2,003
  • 1,567
  • 1
    Hmm this was not the answer i was hoping for. I really do not want to create a component for every different notification. I was hoping there was a way to let angular scan the dom for any newly added directives or something... – Jeroen1984 Mar 30 '16 at 13:37
  • You can pass data to the component and bind to it in the components template to make reusable for similar scenarios. This limitation is usually only a problem if the HTML is provided by the user and it's impossible to know at build time. – Günter Zöchbauer Mar 30 '16 at 13:39
  • The component added dynamically can be provided by the other components, they don't need to be listed in `directives: [...]` – Günter Zöchbauer Mar 30 '16 at 13:48
  • Thanks :) See http://stackoverflow.com/questions/36325212/angular-2-dynamic-tabs-with-user-click-chosen-components for an example of such a wrapper. – Günter Zöchbauer Mar 31 '16 at 06:32
  • @GünterZöchbauer Thanks for the answer but the link you have provided does not work. – Magani Felix Feb 19 '17 at 15:03
  • `DynamicComponentLoader` is long gone. See the link in the comment. just before xours for more information. – Günter Zöchbauer Feb 19 '17 at 15:05
1

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

Abdo Driowya
  • 129
  • 3
  • 11
0

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.

MrWedders
  • 156
  • 8