Can't render routerLink in innerHTML in Angular
Asked Answered
C

3

5

In the component code I have a field called taggy containing a full anchor tag. At first, in my HTML, I rendered it to the screen, which, of course didn't work out because I only saw contents of taggy in the verbatim version. The text said literally <a href="blopp/1337">hazaa<a>.

One googling away, I discovered my mistake. I'm supposed to use innerHTML. It works but causes the browser to reload, since we're using a href and not routerLink. Naturally, I changed the value of taggy so it says routerLink instead of href. However, when I do so, it works kind of but not quite. In my HTML I have the following.

{{taggy}}
<div [innerHTML]=taggy></div>
<div innerHTML=taggy></div>

None of those produce a working link. The closest is the middle one, as it creates a div and inside it an anchor. However, there's no attributes on the anchor tag and it only contains the text that the user's supposed to read. The routing information is gone.

What to do?

I've found a suggestion but it doesn't feel as a good way to go (even author indicates that it's not the best approach). There's also this but it's not relevant in my case as I'm passing the string to another component.

Crush answered 12/1, 2020 at 18:14 Comment(9)
This seems to go against Angular practice. You should not be manipulating the DOM directly in this way. Is there a reason you're not simply putting a tag in your template and binding to its href?Retentivity
@WillAlexander Yes, there is. I have a component that takes a @Input() footer:string. In that footer, I want to be able to show some info. On occasion, that info will contain a link. Since the passed data is a string, I can't specify anything more, and so I need to send stuff to innerHTML, which, as I mentioned in the question, does work. The problem is that when I use href, the page will reload on clicking the footer, whereas I would like to be router to avoid the reload. Hence, I'm passing routerLink instead. And that's not being rendered. Got suggestion on an alternative approach?Crush
@WillAlexander I totally agree with your statement, by the way. I'd be delighted to get educated on how to approach my thing in a Angularish way.Crush
You need to extract the required data from the string you're passing as an @Input. How you go about it is up to you, but you should 100% definitely not be passing in pure HTML, especially if it comes from a user.Retentivity
@WillAlexander I'm not sure how your comment is useful. The whole point of my question is how to achieve what you're saying. I fully understand if you don't know how to do that in Angular. Let's see if someone else comes along with something useful and learn from that. Happy coding.Crush
What are the contents of taggy?Sisyphus
What I’m saying is you shouldn’t be trying to pass it straight into your template. You can use simple TypeScript string manipulation or RegEx to find and extract any links, but because we don’t know exactly what form your incoming data has, it’s impossible to give an exact answer. Use the TS side to extract the data, then put an anchor tag in your template and bind to its href and use string interpolation for the tag’s contents. Do you see what I mean?Retentivity
@NicholasK It can be anything but in this case, which is the reason that have to rely on passing a string and hope that the receiving component renders it as supposed to. In this particular case, it's <a href="blopp/1337">hazaa<a> but I want it to be <a routerLink="blopp/1337">hazaa<a>. The former renders to my liking, while the latter doesn't. It gives me the anchor tag with no href and no routerLink. Suggestions?Crush
@WillAlexander I fail to see how it addresses my issue. I can't know in advance what input will be provided to the component, so assuming it contains a link is not correct. Goofing around with regexes trying to figure out if/what URL is contained seems like a path of thousand sights. Instead of your solution, it's probably wiser to declare a config interface and pass that to the component. In it I can specify anything needed for rendition. However, what the question asks is how to render routerLink. Perhaps it's simply not possible. But there's a difference between shouldn't and can't.Crush
A
11

As you've discovered, you can't have a dynamic template in Angular because the templates are bundled with components as javascript. If you want to learn more, check out this issue on github. The upshot of it is: dynamic templates would break AOT compilation.

You have also discovered that you can set the innerHTML of an object to arbitrary HTML, but routerLink won't work in that context. Why? Because routerlink is an Angular directive, not an HTML attribute. Since we are just setting the innerHTML, it's not a compiled template.

So, what's the solution? Let's back up and think about what routerLink is doing in the first place. As it turns out, not much. Take a look at the source. Specifically, this is what it's doing when an element with the routerLink directive is clicked:

@HostListener('click')
onClick(): boolean {
  const extras = {
    skipLocationChange: attrBoolValue(this.skipLocationChange),
    replaceUrl: attrBoolValue(this.replaceUrl),
  };
  this.router.navigateByUrl(this.urlTree, extras);
  return true;
}

It's just using HostListener to monitor clicks, and routing accordingly. We can do something similar in our component. I've called it FooterComponent since you mentioned that this was a footer with dynamic HTML:

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

@Component({
  selector: 'app-footer',
  template: '<div [innerHTML]="footerHtml"></div>'
})
export class FooterComponent {
  @Input() footerHtml: string = "";

  constructor(private router: Router) { }
  // Watch for clicks in our component
  @HostListener("click", ['$event'])
  onClick(event: MouseEvent) {
    // If we don't have an anchor tag, we don't need to do anything.
    if (event.target instanceof HTMLAnchorElement === false) { 
      return;
    }
    // Prevent page from reloading
    event.preventDefault();
    let target = <HTMLAnchorElement>event.target;
    // Navigate to the path in the link
    this.router.navigate([target.pathname]);
  }
}

As you can see, it takes in input called footerHtml, which is of type string. To use it, we will add this somewhere:

<app-footer [footerHtml]="footerHtml"></app-footer>

Where the footerHtml property in our component is defined as follows:

footerHtml = '<p>This is the footer with a <a href="/admin">link!</a></p>';

When the element is clicked, we check to make sure the user clicked on a hyperlink. If they did, we navigate using the router and prevent the default behavior. Here's a working StackBlitz example. Hope this helps!

Antiseptic answered 17/1, 2020 at 2:8 Comment(1)
Wonderful! I just added if (!target.host.startsWith(window.location.host)) { return; } to serve my needs.Ingles
A
1

Based on the question, #Dean solution works fine however, it does not work for external link. That is, it can only work for angular route path. Below is my solution to solve both internal and external url/link.

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

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

  @Component({
    selector: 'app-footer',
    template: '<div [innerHTML]="footerHtml"></div>'
  })
  export class FooterComponent {
    footerHtml = '<p>This is the innerHTML content with angular valid path  <a href="/admin">internal link!</a> and external link <a target="_blank" href="https://google.com">external link</a></p>';

    // Watch for clicks in our component
    @HostListener('click', ['$event'])
    onClick(event: MouseEvent) {
    this.routingService.urlLocator(event);
    // Do nothing if no anchor tag.
    if (event.target instanceof HTMLAnchorElement === false) {
      return;
    }

    // Prevent normal html anchor behaviour
    event.preventDefault();

    // Checks if the anchor has a full url name or if target is "_blank"
    // That is, target external url
    if (event.target.target === '_blank') {
      // open it in new window (You can decide to use window.location.href)
      window.open(event.target.href); 
    } else {
      // handle valid angular route path
      const target = <HTMLAnchorElement>event.target;
      this.router.navigate([target.pathname]);
    }
  }
}
Ardine answered 4/5, 2020 at 12:5 Comment(0)
P
1

If you don't mind installing a library for this, have a look at ngx-dynamic-hooks. It can replace any piece of text in a dynamic string of content with an actual Angular component - including link-elements.

Funnily enough, I've written an example based on the exact idea of automatically replacing internal links with [RouterLinks]. Have a look at it here (or in this Stackblitz).

Posthaste answered 26/8, 2020 at 17:41 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.