88

I would like to create an external redirect, but to make all routes consistent I think it would be nice to do everything(including external redirects) under Router States configuration.

so:

const appRoutes: Routes = [
  {path: '', component: HomeComponent},
  {path: 'first', component: FirstComponent},
  {path: 'second', component: SecondComponent},
  {path: 'external-link', /*would like to have redirect here*/}      
];

UPD: and I don't want to use empty component for this case like @koningdavid suggested. This solution looks really weird for me. It should be something really easy to implement for such case, without virtual components.

Stepan Suvorov
  • 25,118
  • 26
  • 108
  • 176
Yaroslav Polubedov
  • 1,352
  • 2
  • 11
  • 13
  • 4
    looks like something you should be doing on the server instead, a sort of redirect rule – Ayyash May 10 '17 at 07:57
  • TL;DR `window.open(url, '_blank');` or build-in dedicated external routing to your app via router in a few different ways below. – Ben Racicot Jan 31 '23 at 15:26

10 Answers10

98

You can create a RedirectGuard:

import {Injectable} from '@angular/core';
import {CanActivate, ActivatedRouteSnapshot, Router, RouterStateSnapshot} from '@angular/router';

@Injectable({
    providedIn: 'root'
})
export class RedirectGuard implements CanActivate {

  constructor(private router: Router) {}

  canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean {

      window.location.href = route.data['externalUrl'];
      return true;

  }
}

Import it in app.module:

providers: [RedirectGuard],

And define your route:

{
     path: 'youtube',
     canActivate: [RedirectGuard],
     component: RedirectGuard,
     data: {
       externalUrl: 'https://www.youtube.com/'
     }
 }
Gargoyle
  • 9,590
  • 16
  • 80
  • 145
Vitalii Shevchuk
  • 1,111
  • 7
  • 4
  • 4
    This is actually much better than the accepted answer. Using a Guard/CanActivate means you can cancel angular navigation. If you're opening the external link in a new window this is important, because you enable tapping the link multiple times and don't have to show an empty/noop comopnent. full example: https://gist.github.com/SimplGy/64f9e64afd4d7af2e6e9befced7878c1 – SimplGy Sep 14 '18 at 18:39
  • 4
    May be `return false` is more logical return statement? – Vlad Sep 22 '18 at 14:28
  • What if the externalUrl relies on config variables? i.e yoursitedev.com vs yoursitestage.com? Can you reliably import dynamic config variables for use in app.module? – Smooth Feb 12 '19 at 19:10
  • 2
    Great it works nice with me . but i want to put a bearer token in header because i need it to the other side thank you ? – nasri_thamer Jul 09 '19 at 14:45
  • Regarding `component: RedirectGuard` this will lead to error `VM422 vendor.js:75026 ERROR Error: Uncaught (in promise): Error: No component factory found for RedirectGuard Did you add it to @NgModule.entryComponents?` so better use a real component here rather than `RedirectGuard` – hlovdal Jan 28 '20 at 11:47
  • @hlovdal You need to use the `providedIn` parameter to the `@Injectable`. I updated this post to show that. – Gargoyle May 11 '20 at 18:28
  • how can i redirect to external link with header like this {'token : "XXXXXX"'} – saber tabatabaee yazdi Sep 17 '20 at 12:24
  • 2
    @Gargoyle Thanks for your reply - it helped. I was not sure why you had both ***providers*** of **AppModule** and used ***providedIn***. It works without adding it to AppModule. Thanks again! – WineGoddess Nov 29 '21 at 05:28
  • Thanks for this solution. To use config variables like additional parameters we can use routes with params with router navigate function. – Niroshan K Mar 30 '22 at 14:46
94

You can achieve what you want with a trick using the resolve option of a route. Resolve is some data value that Angular2 will obtain for the route to be initialized. More details you can find here in the official documentation.

I have tried this approach and it does work. Example:

Add this to the provider section (plus import the required classes from Routing)

@NgModule({
    providers: [
        {
            provide: 'externalUrlRedirectResolver',
            useValue: (route: ActivatedRouteSnapshot, state: RouterStateSnapshot) =>
            {
                window.location.href = (route.data as any).externalUrl;
            }
        }
    ]
})

Then you can define your route like this:

{
        path: 'test',
        component: AnyRandomComponent,
        resolve: {
            url: 'externalUrlRedirectResolver'
        },
        data: {
            externalUrl: 'http://www.google.com'
        }
    }

This will redirect to the external URL. It's a bit of a hackish way really. I tried to achieve the result without using the component at all, but you have to use either redirectTo or component or children or loadChildren. redirectTo won't trigger the resolve and I am not sure about children, though you can experiment.

You can implement it in a nice class rather than direct function in provider. More info in the documentation (see reference above).

P.S. I would really rather use a redirect component myself I think. Just use the trick with the data and getting the state from the router with externalUrl to get this as a parameter.

Ilya Chernomordik
  • 27,817
  • 27
  • 121
  • 207
  • Maybe it is a little hacky, but this solution works with AOT compilation while Sam's did not (I tried it) – Joshua Kemmerer Feb 27 '18 at 15:01
  • Is there any need for `state` in the redirect function? Seems to be working fine without it. – John Montgomery Aug 09 '18 at 23:41
  • Also consider using a CanActivate Router guard. (https://stackoverflow.com/a/51059505/111243) This is useful so you can open in a new window and prevent angular navigation, tap the link multiple times, not show the AnyRandomComponent. – SimplGy Sep 14 '18 at 18:41
  • I can see AnyRandomComponent is blinking before the redirection. I prefer to use the NavigationEnd event instead – Dmitry Grinko May 03 '20 at 20:36
26

As far as I know NG2 router doesn't support external redirecting. You could create a redirect component as a workaround.

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

@Component({
  selector: 'redirect',
  template: 'redirecting...'
})
export class RedirectComponent implements OnInit {
  constructor() { }

  ngOnInit() {
    window.location.href = 'http://www.redirecturl.com'
  }
}

And use that in your routing

{ path: 'login', component: RedirectComponent, pathmath: 'full'},
koningdavid
  • 7,903
  • 7
  • 35
  • 46
  • 3
    Thank you, but I need without using component. The main idea created without component. – Yaroslav Polubedov Oct 20 '16 at 10:06
  • 2
    The Router can't redirect externally. It doesn't make sense, as an external resource can't actually be a state of the app. – Fiddles Nov 03 '16 at 02:33
  • why not to have it consistent? You can manage all the redirects via Router config. And you should not care about it on component level. For example: we had login like a state of our app and after we migrate it to another application – Stepan Suvorov Nov 04 '16 at 10:02
  • @stepan-suvorov Angular 2 offers the **client-side Component Router** for single page apps. Always redirecting to a component is consistent. When you redirect to a component Angular router adds the URL fragment to the original URL, and redirecting to external URL would not allow to do so. – Yakov Fain Nov 04 '16 at 12:20
18

Hmm...

I think you can simply request the URL instead of calling ng2 Router...


For example...

<a href="http://example.com">External</a>

instead of

<a routerLink="/someRoute" routerLinkActive="active">External</a>

OR

window.location.href = 'http://www.example.com'

instead of

this.router.navigate( [ '/someRoute', 'someParam' ] );

Right...?

shramee
  • 5,030
  • 1
  • 22
  • 46
  • 1
    please read this part of the question "but to make all routes consistent". We would like to keep all URL/state changes under router configuration – Stepan Suvorov Nov 03 '16 at 08:54
6

just use:

{
    path: 'external-link',
    loadChildren: () => new Promise( () => { if(window.location.href.match(/external-link/) ) window.location.href = 'https://external-link.com/'; } ) 
  },
  • 1
    While this code may answer the question, providing additional context regarding why and/or how this code answers the question improves its long-term value. – taha Jul 14 '20 at 20:01
  • This code does not work with the root path `path: ''` as it will always redirect when trying to load the children – Cyril Duchon-Doris Jul 26 '22 at 15:15
3

The Router can't redirect externally. An external resource can't be a state of the app.

If it's only for clarity, keeping all the routes visible in the one spot, you could define another constant array with all the external paths in the same file as the routes.

Fiddles
  • 2,790
  • 1
  • 32
  • 35
3

You can use the NavigationEnd event.

import { NavigationEnd, Router } from '@angular/router';

app.component.ts

this.router.events.subscribe(event => {
  if (event instanceof NavigationEnd) {
    if (event.url.includes('faq')) {
      // open in the same tab:
      window.location.href = 'https://faq.website.com';

      // open a new tab:
      // window.open('https://faq.website.com', '_blank');

      // and redirect the current page:
      // this.router.navigate(['/']);
    }
  }
});

P.S. Don't forget to remove your route from the AppRoutingModule.

Dmitry Grinko
  • 13,806
  • 14
  • 62
  • 86
2

I assume you don't wanna create a component for every single url, which is why you are looking to do it without a component...

So you can try creating a function that generates the component object for you...

For example...

function generateLinkingComponent( url ) {
  // Generate your component using koningdavid's code
  // replace 'http://www.redirecturl.com' with url param
  // and return it...
}

And add it like this in your router config...

const appRoutes: Routes = [
  {path: '', component: HomeComponent},
  {path: 'first', component: FirstComponent},
  {path: 'second', component: SecondComponent},
  {path: 'external-link', component: generateLinkingComponent( 'http://example.com' )},
  {path: 'client-login', component: generateLinkingComponent( 'http://client-login.example.com' )},
  {path: 'admin-login', component: generateLinkingComponent( 'http://admin.example.com' )},
];

This will be easy with JS... but not sure how one can return a class in a function in typeScript...

Hope that helps...

Dexygen
  • 12,287
  • 13
  • 80
  • 147
shramee
  • 5,030
  • 1
  • 22
  • 46
2

Wrapping up Ilya's answer:

Add this module.

import { Component, Injectable, NgModule } from '@angular/core';
import { ActivatedRouteSnapshot, Resolve } from '@angular/router';

@Component({
  template: ''
})
class ExternalLinkComponent {
  constructor() {
  }
}

@Injectable()
class ExternalLinkResolver implements Resolve<any> {
  resolve(route: ActivatedRouteSnapshot): any {
    window.location.href = route.data.targetUri;
    return true;
  }
}

export class ExternalRoute {
  data: {
    targetUri: string;
  };
  path: string;
  pathMatch = 'full';
  resolve = { link: ExternalLinkResolver };
  component = ExternalLinkComponent;

  constructor(path: string, targetUri: string) {
    this.path = path;
    this.data = { targetUri: targetUri };
  }

}

@NgModule({
  providers: [ ExternalLinkResolver ],
  declarations: [ExternalLinkComponent]
})
export class ExternalRoutesModule { }

Then import ExternalRoutesModule and add instances of ExternalRoute.

const childRoutes: Routes = [
  new ExternalRoute('', '/settings/account'),
  { path: 'staff-profiles', component:  StaffProfilesComponent},
  { path: 'staff-assignments', component:  StaffAssignmentsComponent}
];

const routes: Routes = [
  { path: '', component: BaseComponent, children: childRoutes }
];

@NgModule({
  imports: [ ExternalRoutesModule, RouterModule.forChild(routes) ],
  exports: [ RouterModule ]
})
export class SettingsRoutingModule { }

Note I'm mounting the submodule routes via loadChildren in this example.

Sam
  • 1,725
  • 1
  • 17
  • 28
0

Here is a code that should work for you without a lot of issues. FYI the router events error handler can be put anywhere irrespective of placement in the component.

app.component.html

Angular Port is in 4200
<a routerLink="/test">Main Link - 1</a> |

<a [routerLink]="getLinkWithExternal({url: '/test', external:false})">Other Link - 1a</a> |
<a [routerLink]="getLinkWithExternal({url: 'http://localhost:4211', external:true})">Other Link - 1b</a> |

<a [routerLink]="getLink({url: '/test'})">Other Link - 1a</a> |
<a [routerLink]="getLink({url: 'http://localhost:4211'})">Other Link - 1b</a> |


<a style="cursor: pointer; text-decoration: underline;" (click)="routeLink('/test')">Other Link - 1c</a> |
<a style="cursor: pointer; text-decoration: underline;" (click)="routeLink('http://localhost:4211')">Other Link - 1d</a>

<router-outlet></router-outlet>

app.component.ts

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

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  title = 'app';

  constructor(private router: Router) { }

  // RECOMMENDATION - Add following in menus of each microservice (individual and different)
  //    external: true
  // Will make it a better menu structure for each microservice
  // If Single and same menu for all microservices then remove external === true
  // Logic One
  getLinkWithExternal(sidebarnavLink: any) {
    this.router.errorHandler = function (error: any) {
      if (!sidebarnavLink.url.includes(window.location.origin.toString()) && sidebarnavLink.url.includes("http") && sidebarnavLink.external === true) {
        window.location.href = sidebarnavLink.url.toString();
        return true;
      }
      return null;
    }.bind(sidebarnavLink);
    return [sidebarnavLink.url];
  }

  getLinkWithExternalWithEventSubscribe(sidebarnavLink: any) {
    this.router.events.subscribe(function (event) {
      if (event instanceof NavigationEnd) {
        if (event.url.includes('http')) {
          if (!sidebarnavLink.url.includes(window.location.origin.toString()) && sidebarnavLink.url.includes("http") && sidebarnavLink.external === true) {
            window.location.href = sidebarnavLink.url.toString();
            return true;
          }
          return this.router.navigateByUrl(sidebarnavLink.url);
          // return this.router.navigate([sidebarnavLink.url]);
        }
        return this.router.navigateByUrl(sidebarnavLink.url);
        // return this.router.navigate([sidebarnavLink.url]);
      }
    }.bind(sidebarnavLink))
  }

  getLinkWithExternalImplementationTwoWithNoRouteError(sidebarnavLink: any) {
    if (!sidebarnavLink.url.includes(window.location.origin.toString()) && sidebarnavLink.url.includes("http") && sidebarnavLink.external === true) {
      window.location.href = sidebarnavLink.url.toString();
      return true;
    }
    return [sidebarnavLink.url];
  }

  // Logic Two
  getLink(sidebarnavLink: any) {
    this.router.errorHandler = function (error: any) {
      if (!sidebarnavLink.url.includes(window.location.origin.toString()) && sidebarnavLink.url.includes("http")) {
        window.location.href = sidebarnavLink.url.toString();
        return true;
      }
      return null;
    }.bind(sidebarnavLink);
    return [sidebarnavLink.url];
  }

  // Logic Three
  routeLink(lnk: any) {
    if (lnk.includes("http")) {
      console.log("Test");
      window.location.href = lnk;
      return true;
    }
    return this.router.navigateByUrl(lnk);
    // return this.router.navigate([lnk]);
  }

}
Gary
  • 2,293
  • 2
  • 25
  • 47