Cory Rylan

My name is , Google Developer Expert, Speaker, Software Developer. Building Design Systems and Web Components.

Follow @coryrylan
Angular

Managing External Links Safely in Angular

Cory Rylan

- 4 minutes

Updated

This article has been updated to the latest version Angular 17 and tested with Angular 16. The content is likely still applicable for all Angular 2 + versions.

When building large enterprise Angular applications, there are times where the user may need to leave the application temporarily to complete some task. Often when we provide a link to a separate domain, we can apply a particular attribute target="_blank" to open that link in a new tab. Opening a link in a new tab can be helpful to keep the application running and not lose work in progress.

<a href="https://example.com" target="_blank">
  Example External Link
</a>

While opening a link in a new tab can be convenient, it can pose a security and performance risk. When using target="_blank" the referring domain runs on the same process as your application which causes two issues. First, by being on the same process, the referring domain can access details about your application via the window.opener property and bypasses the default isolated behavior of browser tabs. The other issue is a performance problem as now your application and the referring link are running on the same process and have to share resources.

To solve both of these issues, any link using target="_blank" should also have a rel="noopener" attribute as well. The rel="noopener" attribute makes sure that the new browser tab does not run on the same process and prevents it from accessing window.opener.

<a href="https://example.com" target="_blank" rel="noopener">
  Example External Link
</a>

This attribute fixes our performance and security issues. Still, now we must manually check every link in our application to ensure that it is external that it has the appropriate attributes. Luckily for us, we can leverage Angular Directives to automate this.

Angular Directive

Angular Directives typically are attribute-based selectors. With Directives, however, you can use any valid CSS selector.

import { Directive } from '@angular/core';

@Directive({
  selector: 'a[href]'
})
export class ExternalLinkDirective {}

In the example Directive, we can select all anchor tags with an href attribute. This selector allows us to inspect all links in our application and determine how we should update them.

import { Directive, HostBinding, Input } from '@angular/core';
import { isPlatformBrowser } from '@angular/common';

@Directive({
  selector: 'a[href]'
})
export class ExternalLinkDirective {
  @HostBinding('attr.rel') relAttr = '';
  @HostBinding('attr.target') targetAttr = '';
  @HostBinding('attr.href') hrefAttr = '';
  @Input() href: string;

  ngOnChanges() {
    this.hrefAttr = this.href;

    if (this.isLinkExternal()) {
      this.relAttr = 'noopener';
      this.targetAttr = '_blank';
    }
  }

  private isLinkExternal() {
    return !this.href.includes(location.hostname);
  }
}

Using the HostBinding decorator, we can access the attributes of the host element, which is the link to the directive is attached. In this use case, we access the rel, target, and href attributes.

Using the @Input decorator, we can get the value of the href on the link.Because we set the href as an input, then whenever the href changes the ngOnChanges life cycle will be called. We need to hook into ngOnChanges that way if the link id dynamically created, our directive knows to check the href value and update.


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

@Component({
  selector: 'my-app',
  template: `<a href="https://{{dynamic}}">Dynamic Link</a>`
})
export class AppComponent  {
  dynamic = 'example.com';
}

When taking in the href as an @Input Angular stops reflecting or updating the DOM href attribute on our links. Because of this, we have to reassign href attribute, so it's updated in the DOM.

export class ExternalLinkDirective {
  ...
  @HostBinding('attr.href') hrefAttr = '';
  @Input() href: string;

  ngOnChanges() {
    this.hrefAttr = this.href;

    ...
  }
}

Now anytime the input changes, we update the href attribute in the DOM as well. In the ngOnChanges, we check if the link is external or not by checking the current domain using the native browser API location.hostname.

export class ExternalLinkDirective {
  @HostBinding('attr.rel') relAttr = null;
  @HostBinding('attr.target') targetAttr = null;
  @Input() href: string;

  ngOnChanges() {
    this.elementRef.nativeElement.href = this.href;

    if (this.isLinkExternal()) {
      this.relAttr = 'noopener';
      this.targetAttr = '_blank';
    } else {
      this.relAttr = '';
      this.targetAttr = '';
    }
  }

  private isLinkExternal() {
    return !this.elementRef.nativeElement.hostname.includes(location.hostname);
  }
}

If the link is external to our application, then we set the rel and target attributes appropriately. Now all links in our application are external when clicked open into a separate tab safely. There is a little more work to make this code safe for server-side rendering with Angular Universal.

import {
  Directive,
  HostBinding,
  PLATFORM_ID,
  Inject,
  Input
} from '@angular/core';
import { isPlatformBrowser } from '@angular/common';

@Directive({
  selector: 'a[href]'
})
export class ExternalLinkDirective {
  @HostBinding('attr.rel') relAttr = null;
  @HostBinding('attr.target') targetAttr = null;
  @Input() href: string;

  constructor(
    @Inject(PLATFORM_ID) private platformId: string,
    private elementRef: ElementRef) {}

  ngOnChanges() {
    this.hrefAttr = this.href;

    if (this.isLinkExternal()) {
      this.relAttr = 'noopener';
      this.targetAttr = '_blank';
    }
  }

  private isLinkExternal() {
    return isPlatformBrowser(this.platformId) && !this.elementRef.nativeElement.hostname.includes(location.hostname);
  }
}

If this is running in a server-side application, we need to check that way we do not try to access the browser only APIs like location. To check if we are running on the server, we inject the platformId. This gives us a token value that we can pass to the isPlatformBrowser() function. Now our code only runs on the client and prevents and errors on the server.

To see the full working example check out the demo link below. To get more information about the security and performance risks of using target="_blank" check out the Google Developer Documentation.

Special thanks to @robertvhoesel for the improved parsing of the hostname url!

View Demo Code   
Twitter Facebook LinkedIn Email
 

No spam. Short occasional updates on Web Development articles, videos, and new courses in your inbox.

Related Posts

Angular

Creating Dynamic Tables in Angular

Learn how to easily create HTML tables in Angular from dynamic data sources.

Read Article
Web Components

Reusable Component Patterns - Default Slots

Learn about how to use default slots in Web Components for a more flexible API design.

Read Article
Web Components

Reusable Component Anti-Patterns - Semantic Obfuscation

Learn about UI Component API design and one of the common anti-patterns, Semantic Obfuscation.

Read Article