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 useful 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 at times 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 is running on the same process and has 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 prevent 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 issue, but now we must manually check every link in our application to make sure if it is external that it has the appropriate attributes. Luckily for us, we can leverage Angular Directives to automate this for us.

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 the directive is attached. In this use case, we access the rel, target abd 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 use a @HostBinding on the href attribute to reassign href attribute so its 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 = '';
  @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);
  }
}


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 = '';
  @HostBinding('attr.target') targetAttr = '';
  @HostBinding('attr.href') hrefAttr = '';
  @Input() href: string;

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

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

  private isLinkExternal() {
    return isPlatformBrowser(this.platformId) && !this.href.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.

Support this Blog View Code Demo