36

I need for the same anchor link to be pointed conditionally locally or to external resource. I tried

<a [href]="outside?externalUrl:null"  [routerLink]="outside?[]:['/route',id]" >test</a>

But it doesn't work. I don't get any errors, but it points to the same local page and ignores the external URL. Any ideas?

Another option would be to construct the link, but I can't find any docs how to access routerLink inside a service

Edit: I know I can clone the whole link with *ngIf but I don't want to do it, my link contains a video tag with a buch of options

Liam
  • 27,717
  • 28
  • 128
  • 190
Toolkit
  • 10,779
  • 8
  • 59
  • 68

6 Answers6

40

The simplest way would be to use *ngIf / else:

<ng-container *ngIf="outside; else internalBlock">
  <a [href]="externalUrl">External</a>
</ng-container>

<ng-template #internalBlock>
  <a [routerLink]="['/route', id]">Internal</a>
</ng-template>

EDIT#1: (Ugly workaround)

Since you don't want to use *ngIf (I still don't understand why), you can do this:

Template:

<a href="javascript:void(0)" (click)="handleClick(outside, '/route', id, externalUrl)">Link</a>

Component:

handleClick(outside: boolean, internalUrl: string, internalId: string, externalUrl: string): void {
  if (outside) {
    window.location.href = externalUrl;
    // You can also use Location class of Angular
  } else {
    this.router.navigate([`${internalUrl}/${internalId}`]);
  }
}
developer033
  • 24,267
  • 8
  • 82
  • 108
24

For a conditional href, prepending on attr. before the href worked for me using null as a value, like this:

[attr.href]="!item.subMenu ? item.url : null"
Liam
  • 27,717
  • 28
  • 128
  • 190
ohjeeez
  • 559
  • 4
  • 6
  • 6
    For some use cases this is the perfect solution. If your a-tag is around an entire element for example using *ngIf you'll need to repeat the entire child. This solution prevents this when only for some cases the a-tag needs to link to something. – NewVigilante Sep 01 '20 at 06:46
  • For the sake of completeness: You don't need to repeat the entire child. You can use `...` and `` like @The.Wolfgang.Grimmer answered below. – Florian Gössele Oct 21 '22 at 08:40
2

You can access routerLink instance by injecting RouterLinkWithHref in the directive.

Directive:

import { ElementRef, Optional, Input, Directive, OnChanges } from '@angular/core';
import { RouterLinkWithHref } from '@angular/router';

@Directive({
  selector: '[externalLink]'
})
export class ExternalLinkDirective implements OnChanges {

  @Input() externalLink: string;

  constructor(
    private el: ElementRef,
    @Optional() private link: RouterLinkWithHref
  ) {}

  ngOnChanges(){

    if (!this.link || !this.externalLink) {
      return;
    }
    this.el.nativeElement.href=this.link.href=this.externalLink;

    // Replace onClick
    this.link.onClick = (...args: any[]) => {
      return true;
    }

  }

}

Usage:

<!-- Ignore router link and use external link -->
<a routerLink="/some/path" externalLink="https://google.com">Link</a>

ivlasov
  • 3
  • 2
taras-d
  • 1,789
  • 1
  • 12
  • 29
1

For my use case, the content inside the <a> tags have some nested html within it so i have to create a custom componet for this.

Basically what the component does is

  • use content transclusion
  • use ngTemplateOutlet with context
  • conditionally display either <a> tag with either routerLink directive or href.

  • route-or-redirect.component.ts
import {
  Component,
  ContentChild,
  Directive,
  Input,
  OnInit,
  TemplateRef,
} from '@angular/core';
import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser';

export interface Bookmark {
  id: string;
  title: string;
  imgUrl: string;
}

@Directive({
  selector: '[appLinkContent]',
})
export class RouteOrRedirectLinkContentDirective {}

@Component({
  selector: 'app-router-or-redirect',
  templateUrl: './router-or-redirect.component.html',
  styleUrls: ['./router-or-redirect.component.scss'],
})
export class RouteOrRedirectComponent implements OnInit {
  @Input() route = '/';
  @Input() set externalLink(link: string) {
    this.safeUrl = this.sanitizer.bypassSecurityTrustResourceUrl(link);
  }
  @Input() redirect = false;
  @Input() bookmark!: Bookmark;
  @ContentChild(RouteOrRedirectLinkContentDirective, { read: TemplateRef })
  linkContent: TemplateRef<RouteOrRedirectLinkContentDirective> | undefined;

  safeUrl: SafeResourceUrl | undefined;

  constructor(private sanitizer: DomSanitizer) {}

  ngOnInit(): void {}
}
  • route-or-redirect.component.html
<a [routerLink]="route" *ngIf="!redirect; else redirectLink">
  <ng-container
    *ngTemplateOutlet="linkContent; context: { $implicit: bookmark }"
  ></ng-container>
</a>

<ng-template #redirectLink>
  <a [attr.href]="safeUrl">
    <ng-container
      *ngTemplateOutlet="linkContent; context: { $implicit: bookmark }"
    ></ng-container>
  </a>
</ng-template>
  • route-or-redirect.component.ts
import { Component, OnInit, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { RouterLinkWithHref, RouterModule } from '@angular/router';
import { RouterTestingModule } from '@angular/router/testing';

import {
  Bookmark,
  RouteOrRedirectComponent,
  RouteOrRedirectLinkContentDirective,
} from './router-or-redirect.component';

@Component({
  selector: 'app-test',
  template: `
    <app-router-or-redirect class="default-no-content"></app-router-or-redirect>

    <app-router-or-redirect class="default-with-content">
      <div *appLinkContent>test link</div>
    </app-router-or-redirect>

    <app-router-or-redirect
      class="use-route"
      route="/test"
      externalLink="localhost:4200"
    >
      <div *appLinkContent>test link</div>
    </app-router-or-redirect>

    <app-router-or-redirect
      class="use-redirect"
      route="/test"
      externalLink="localhost:4200"
      [redirect]="true"
    >
      <div *appLinkContent>test link</div>
    </app-router-or-redirect>

    <app-router-or-redirect
      class="link-with-context"
      route="/test"
      externalLink="localhost:4200"
      [redirect]="true"
      [bookmark]="bookmark"
    >
      <div *appLinkContent="let bookmark">
        <img [attr.src]="bookmark.imgUrl" />
        <h3>{{ bookmark.title }}</h3>
      </div>
    </app-router-or-redirect>
  `,
  styles: [],
})
export class TestRouterOrRedirectComponent implements OnInit {
  bookmark: Bookmark = {
    id: '1',
    title: 'Test Link',
    imgUrl: 'https://placeimg.com/640/480/any',
  };

  constructor() {}

  ngOnInit(): void {}
}

describe('RouterOrRedirectComponent', () => {
  let component: TestRouterOrRedirectComponent;
  let fixture: ComponentFixture<TestRouterOrRedirectComponent>;

  let defaultWithNoContent: RouteOrRedirectComponent;
  let defaultWithContent: RouteOrRedirectComponent;
  let useRoute: RouteOrRedirectComponent;
  let useRedirect: RouteOrRedirectComponent;

  beforeEach(async(() => {
    TestBed.configureTestingModule({
      schemas: [CUSTOM_ELEMENTS_SCHEMA],
      imports: [RouterModule, RouterTestingModule],
      declarations: [
        TestRouterOrRedirectComponent,
        RouteOrRedirectComponent,
        RouteOrRedirectLinkContentDirective,
      ],
    }).compileComponents();
  }));

  beforeEach(() => {
    fixture = TestBed.createComponent(TestRouterOrRedirectComponent);
    component = fixture.componentInstance;

    defaultWithNoContent = fixture.debugElement.children[0].componentInstance;
    defaultWithContent = fixture.debugElement.children[1].componentInstance;
    useRoute = fixture.debugElement.children[2].componentInstance;
    useRedirect = fixture.debugElement.children[3].componentInstance;

    fixture.detectChanges();
  });

  it('should create', () => {
    expect(component).toBeTruthy();
  });

  it('should default to link with route point to /', () => {
    const link = fixture.debugElement
      .query(By.css('.default-no-content'))
      .query(By.directive(RouterLinkWithHref));

    expect(link).toBeTruthy();
    expect(link.attributes.href).toBe('/');
  });

  it('should default to link with content and route points to /', () => {
    const link = fixture.debugElement
      .query(By.css('.default-with-content'))
      .query(By.directive(RouterLinkWithHref));

    expect(link.attributes.href).toBe('/');
    expect(link.nativeElement.textContent).toBe('test link');
  });

  it('should use routerLink for <a> tag if "redirect" binding is not specified', () => {
    const link = fixture.debugElement
      .query(By.css('.use-route'))
      .query(By.directive(RouterLinkWithHref));

    expect(link.attributes.href).toBe('/test');
    expect(link.nativeElement.textContent).toBe('test link');
  });

  it('should default "redirect" binding to false', () => {
    expect(useRoute.redirect).toBe(false);
  });

  it('should use href for <a> tag if "redirect" is true', () => {
    const link = fixture.debugElement
      .query(By.css('.use-redirect'))
      .query(By.css('a'));

    expect(useRedirect.redirect).toBe(true);
    expect(link.query(By.directive(RouterLinkWithHref))).toBeNull();

    expect(link.nativeElement.href).toBe('localhost:4200');
    expect(link.nativeElement.textContent).toBe('test link');
    expect(useRoute.redirect).toBe(false);
  });

  it('should use the bound value as the link template context', () => {
    const link = fixture.debugElement
      .query(By.css('.link-with-context'))
      .query(By.css('a'));

    expect(link.query(By.css('h3')).nativeElement.textContent).toContain(
      component.bookmark.title
    );
    expect(
      link.query(By.css('img')).nativeElement.getAttribute('src')
    ).toContain(component.bookmark.imgUrl);
    expect(useRoute.redirect).toBe(false);
  });
});
The.Wolfgang.Grimmer
  • 1,172
  • 2
  • 9
  • 32
  • Great suggestion for this problem. I found I didn't need a directive and it was simpler if I did it with a shared template as seen here: https://github.com/angular/angular/issues/22972#issuecomment-407358396 – Roobot May 02 '22 at 23:46
0

After a lot of investigation, I implemented a different approach to this issue, since my <a href>...</a>contains code inside (a clickable div for example). For instance, I don't want to use ngIf because that forces me to duplicate the div's inside code. So this is my solution:

Component HTML

<div>
   <a [href]="getRouterLink()" >
     <div class="section">
     <!-- stuff inside the div -->
     </div>   
   </a>
</div>

Component JS

Component({
selector: 'app-home-section',
templateUrl: './home-section.component.html',
styleUrls: ['./home-section.component.scss']
})
export class HomeSectionComponent implements OnInit {

@Input() link: string;

constructor(private router: Router) { }

ngOnInit() {
}

isRouterLink() {

  if (this.link) {
    let firstLinkChar = this.link.charAt(0);
    let isSlash = firstLinkChar == '/';
    return isSlash;
  }

  return false;
}

getRouterLink() {

  let url = this.isRouterLink() ? window.location.origin + this.link : 'http://' + this.link;  
return url;
 }
}

This was the only way to make it work simplier, because even I put the "www.example.com" directly to the href (with or without the [ ]), it always append the base url. It's not pretty, but is functional.

Liam
  • 27,717
  • 28
  • 128
  • 190
Tito Leiva
  • 892
  • 1
  • 12
  • 29
  • 1
    doesn't this still uses `href` attribute. There's a subtle difference between `routerLink` and `href`. You can read it [here](https://stackoverflow.com/a/61588147) – The.Wolfgang.Grimmer Oct 15 '20 at 05:58
0
[attr.href]="link || null"