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:
<div>
<a [href]="getRouterLink()" >
<div class="section">
<!-- stuff inside the div -->
</div>
</a>
</div>
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.
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}`]);
}
}
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
ngTemplateOutlet
with context<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);
});
});
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>
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"