Angular 2 Final Release Router Unit Test - javascript

How do I unit test routers in Angular version 2.0.0 with karma and jasmine?
Here's what my old unit test looks like in version 2.0.0-beta.14
import {
it,
inject,
injectAsync,
beforeEach,
beforeEachProviders,
TestComponentBuilder
} from 'angular2/testing';
import { RootRouter } from 'angular2/src/router/router';
import { Location, RouteParams, Router, RouteRegistry, ROUTER_PRIMARY_COMPONENT } from 'angular2/router';
import { SpyLocation } from 'angular2/src/mock/location_mock';
import { provide } from 'angular2/core';
import { App } from './app';
describe('Router', () => {
let location, router;
beforeEachProviders(() => [
RouteRegistry,
provide(Location, {useClass: SpyLocation}),
provide(Router, {useClass: RootRouter}),
provide(ROUTER_PRIMARY_COMPONENT, {useValue: App})
]);
beforeEach(inject([Router, Location], (_router, _location) => {
router = _router;
location = _location;
}));
it('Should be able to navigate to Home', done => {
router.navigate(['Home']).then(() => {
expect(location.path()).toBe('');
done();
}).catch(e => done.fail(e));
});
});

For testing we now create a testing module using TestBed. We can use the TestBed#configureTestingModule and pass a metadata object to it the same way we would pass to #NgModule
beforeEach(() => {
TestBed.configureTestingModule({
imports: [ /* modules to import */ ],
providers: [ /* add providers */ ],
declarations: [ /* components, directives, and pipes */ ]
});
});
For routing, instead of using the normal RouterModule, we would instead use RouterTestingModule. This sets up the Router and Location, so you don't need to yourself. You can also pass routes to it, by calling RouterTestingModule.withRoutes(Routes)
TestBed.configureTestingModule({
imports: [
RouterTestingModule.withRoutes([
{ path: 'home', component: DummyComponent }
])
]
})
To get the Location and Router in the test, the same thing works, as in your example.
let router, location;
beforeEach(() => {
TestBed...
});
beforeEach(inject([Router, Location], (_router: Router, _location: Location) => {
router = _router;
location = _location;
}));
You could also inject into each test as necessary
it('should go home',
async(inject([Router, Location], (router: Router, location: Location) => {
})));
The async above is used like done except we don't need to explicitly call done. Angular will actually do that for us after all asynchronous tasks are complete.
Another way to get the providers is from the test bed.
let location, router;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [RouterTestingModule.withRoutes([
{ path: 'home', component: DummyComponent }
])],
});
let injector = getTestBed();
location = injector.get(Location);
router = injector.get(Router);
});
Here's a complete test, refactoring your example
import { Component } from '#angular/core';
import { Location } from '#angular/common';
import { Router } from '#angular/router';
import { RouterTestingModule } from '#angular/router/testing';
import { fakeAsync, async, inject, TestBed, getTestBed } from '#angular/core/testing';
import { By } from '#angular/platform-browser';
#Component({
template: `
<router-outlet></router-outlet>
`
})
class RoutingComponent { }
#Component({
template: ''
})
class DummyComponent { }
describe('component: RoutingComponent', () => {
let location, router;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [RouterTestingModule.withRoutes([
{ path: 'home', component: DummyComponent }
])],
declarations: [RoutingComponent, DummyComponent]
});
});
beforeEach(inject([Router, Location], (_router: Router, _location: Location) => {
location = _location;
router = _router;
}));
it('should go home', async(() => {
let fixture = TestBed.createComponent(RoutingComponent);
fixture.detectChanges();
router.navigate(['/home']).then(() => {
expect(location.path()).toBe('/home');
console.log('after expect');
});
}));
});
UPDATE
Also, if you want to simply mock the router, which actually might be the better way to go in a unit test, you could simply do
let routerStub;
beforeEach(() => {
routerStub = {
navigate: jasmine.createSpy('navigate'),
};
TestBed.configureTestingModule({
providers: [ { provide: Router, useValue: routerStub } ],
});
});
And in your tests, all you want to do is test that the stub is called with the correct argument, when the component interacts with it
expect(routerStub.navigate).toHaveBeenCalledWith(['/route']);
Unless you actually want to test some routing, this is probably the preferred way to go. No need to set up any routing. In a unit test, if you are using real routing, you're involving unnecessary side effects that could affect what you are really trying to test, which is just the behavior of the component. And the behavior of the component is to simply call the navigate method. It doesn't need to test that the router works. Angular already guarantees that.

Good approach suggested by Paul
I have also configure my routing same way but additionally I have added service to update some data for routing and then check for current location.
so you can add service to update data on component which render some data and then will check about navigation.
configure below in TestBed.configureTestingModule
providers : [MyService]
then create get service in foreach
myService= TestBed.get(MyService);
update some data from service like
myService.someMethodCall();
This way you can play after some data rendering happen.

Instead of using useValue for routerStub, you can use useClass in the providers and it really worked for me.
export class RouterStub {
public url: string = '/';
constructor() { }
enter code here
navigateByUrl(url: any) {
this.url = url;
}
}
And in the beforeEach just instantiate the routerStub object like
routerStub = new RouterStub()
And in the test cases
component.router.navigateByUrl('/test');
fixture.detectChanges();

Related

How to unit test Angular's Meta service?

My Component is using Angular's Meta service to update a meta tag during ngOnInit. I'm using my RegionService to get an app-id and set it with Meta's updateTag method via a template literal. But my unit test is having problems getting the value set by my RegionService in the template literal. The test returns the following error:
Expected spy Meta.updateTag to have been called with:
[ Object({ name: 'apple-itunes-app', content: 'app-id=0123456789' }) ]
but actual calls were:
[ Object({ name: 'apple-itunes-app', content: 'app-id=undefined' }) ].
How can I modify my test so that it knows the value app-id, set by my template literal ${this.regionService.getAppId()} ?
my.component.ts
import { Component, OnInit } from '#angular/core';
import { RegionService } from 'src/services/region.service';
import { Meta } from '#angular/platform-browser';
export class MyComponent implements OnInit {
constructor(
private regionService: RegionService,
private meta: Meta
) {}
ngOnInit() {
this.meta.updateTag({name: 'apple-itunes-app', content: `app-id=${this.regionService.getAppId()}`});
}
}
my.component.spec.ts
import { ComponentFixture, TestBed, waitForAsync } from '#angular/core/testing';
import { NO_ERRORS_SCHEMA } from '#angular/core';
import { MyComponent } from './my.component';
import { RegionService } from 'src/services/region.service';
import { Meta } from '#angular/platform-browser';
import { RouterTestingModule } from '#angular/router/testing';
describe('MyComponent', () => {
let component: MyComponent;
let fixture: ComponentFixture<MyComponent>;
let regionServiceSpy: jasmine.SpyObj<RegionService>;
let metaServiceSpy: jasmine.SpyObj<Meta>;
beforeEach(
waitForAsync(() => {
const regionServiceSpyObj = jasmine.createSpyObj('RegionService', ['getAppId', 'retrieveABCRegions', 'retrieveDEFRegions']);
const metaServiceSpyObj = jasmine.createSpyObj('Meta', ['updateTag']);
TestBed.configureTestingModule({
declarations: [MyComponent],
imports: [RouterTestingModule],
providers: [
{ provide: RegionService, useValue: regionServiceSpyObj },
{ provide: Meta, useValue: metaServiceSpyObj },
],
schemas: [NO_ERRORS_SCHEMA],
}).compileComponents();
regionServiceSpy = TestBed.inject(RegionService) as jasmine.SpyObj<RegionService>;
metaServiceSpy = TestBed.inject(Meta) as jasmine.SpyObj<Meta>;
}),
);
beforeEach(() => {
fixture = TestBed.createComponent(MyComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should set app-id to 0123456789 if selectedRegion is FR', () => {
// arrange
// act
regionServiceSpy.selectedRegion = 'FR';
// assert
expect(metaServiceSpy.updateTag).toHaveBeenCalledWith({name: 'apple-itunes-app', content: 'app-id=0123456789'});
});
});
region.service.ts
import { retrieveABCRegions, retrieveDEFRegions} from 'src/regions';
#Injectable({
providedIn: 'root',
})
export class RegionService {
selectedRegion: Region;
getAppId(): string {
if (retrieveABCRegions().includes(this.selectedRegion)) {
return '111111111';
} else if (retrieveDEFRegions().includes(this.selectedRegion)) {
return '0123456789';
}
}
}
Since you've replaced the RegionService with a SpyObj mock:
{ provide: RegionService, useValue: regionServiceSpyObj }
the real service is no longer being used in your tests (which is the correct approach because you're not testing that service here).
So now you need to define what value the mock service's getAppId() method is going to return. You do that by creating a spy strategy for that method on your spy object.
There are different types of spy strategies you can use, but for your purposes here, probably the simplest is returnValue():
it('should set app-id to 0123456789 if selectedRegion is FR', () => {
// arrange
// act
// regionServiceSpy.selectedRegion = 'FR'; <--- not needed for this test since real service method not being called
regionServiceSpy.getAppId.and.returnValue('0123456789'); // <-- define what value mock service's getAppId returns
// assert
expect(metaServiceSpy.updateTag).toHaveBeenCalledWith({name: 'apple-itunes-app', content: 'app-id=0123456789'});
});
And note that setting regionServiceSpy.selectedRegion = 'FR' is not needed here since the actual service method is not being called.

Angular 10 Test: Component Resolver Data Subscription Error

I've spend the last few days trying to get up to speed with ng test and all the spec files #angular/cli creates when creating components and, well, pretty much else.
As I was working on my own portfolio website, I have come across an issue that I cannot seem to understand or fix.
I have this component (pretty vanilla stuff):
import { Component, OnInit } from '#angular/core';
import { ActivatedRoute } from '#angular/router';
import { Title } from '#angular/platform-browser'
import { ProjectDetails } from './project-details'
#Component({
selector: 'app-projects-details',
templateUrl: './projects-details.component.html',
styleUrls: ['./projects-details.component.sass']
})
export class ProjectsDetailsComponent implements OnInit {
// Class variables
currentContent: ProjectDetails
constructor(
private route : ActivatedRoute,
private title: Title
) { }
ngOnInit() {
// Assign the data to local variable for use
this.route.data.subscribe(content => {
this.currentContent = content.project.view //<-- This line causes the issue
// Set the title for the Projects view
this.title.setTitle(this.currentContent.view_title)
})
}
}
And this spec file (more vanilla stuff):
import { async, ComponentFixture, TestBed } from '#angular/core/testing';
import { RouterTestingModule } from '#angular/router/testing'
import { ProjectsDetailsComponent } from './projects-details.component';
import { ProjectDetails } from './project-details'
describe('ProjectsDetailsComponent', () => {
let component: ProjectsDetailsComponent;
let fixture: ComponentFixture<ProjectsDetailsComponent>;
const projectDetails : ProjectDetails = { /* valid object content */ }
beforeEach(async(() => {
TestBed.configureTestingModule({
imports:[
RouterTestingModule
],
declarations: [ ProjectsDetailsComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(ProjectsDetailsComponent);
component = fixture.componentInstance;
component.currentContent = projectDetails
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
When running the tests, I get this error:
TypeError: content.project is undefined in http://localhost:9876/_karma_webpack_/main.js (line 1576)
So, I'm not sure exactly what's going on here. No matter what I do, the error prevails.I have a similarly setup component that doesn't have this issue and a side by side comparison shows no differences in the spec.ts file aside from imports.
I tried changing the file to this:
import { async, ComponentFixture, TestBed } from '#angular/core/testing';
import { RouterTestingModule } from '#angular/router/testing'
import { ProjectsDetailsComponent } from './projects-details.component';
import { ProjectDetails } from './project-details'
import { ActivatedRoute } from '#angular/router';
describe('ProjectsDetailsComponent', () => {
let component: ProjectsDetailsComponent;
let fixture: ComponentFixture<ProjectsDetailsComponent>;
const projectDetails : ProjectDetails = {/* valid content */}
beforeEach(async(() => {
TestBed.configureTestingModule({
// imports:[
// RouterTestingModule
// ],
providers: [
{ provide: ActivatedRoute, useValue: projectDetails }
],
declarations: [ ProjectsDetailsComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(ProjectsDetailsComponent);
component = fixture.componentInstance;
component.currentContent = projectDetails
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
Which changes the error to this (which confuses me more):
TypeError: this.route.data is undefined in http://localhost:9876/_karma_webpack_/main.js (line 1575)
The question to the community: how do I fix this? What's the reason this error is coming up?
Instead of providing the raw projectDetails, provide an Observable in its data property:
import {of} from 'rxjs';
...
beforeEach(async(() => {
TestBed.configureTestingModule({
providers: [
// Properly provide the activated route mock object.
{ provide: ActivatedRoute, useValue: { data: of(projectDetails) } }
],
declarations: [ ProjectsDetailsComponent ]
})
.compileComponents();
}));
...
If you look at how you access the route data, you can see that it uses an Observable:
this.route.data.subscribe(content => {...});

Angular 8 testing component with karma fails

I'm trying to start testing my component. The first thing that I wanted to test is if the ngOnInit calls the correct services.
agreement.component.ts:
constructor(private agreementService: AgreementService,
private operatorService: OperatorService,
private accountService: AccountService,
private route: ActivatedRoute,
private router: Router,
private sessionService: SessionService,
private settingsService: SettingsService) {
this.agreementId = Number(this.route.snapshot.paramMap.get('agreementId'));
}
async ngOnInit() {
this.session = await this.sessionService.getSession();
this.settings = await this.settingsService.getSettings();
this.operatorService.getOperators(this.session.bic).subscribe(data => {
this.operators = data;
});
...
}
agreement.component.spec.ts
import {AgreementComponent} from './agreement.component';
import {async, TestBed} from '#angular/core/testing';
import {ActivatedRoute, convertToParamMap, Router} from '#angular/router';
import {RouterTestingModule} from '#angular/router/testing';
import {AgreementService} from '../../../services/agreement.service';
import {AccountService} from '../../../services/account.service';
import {SessionService} from '../../../services/session.service';
import {SettingsService} from '../../../services/settings.service';
describe('agreementComponent', () => {
let mockAgreementService: AgreementService;
let mockOperatorService;
let mockAccountService: AccountService;
let mockRoute: ActivatedRoute;
let mockRouter: Router;
let mockSessionService: SessionService;
let mockSettingsService: SettingsService;
let component: AgreementComponent;
beforeEach(async(() => {
mockAgreementService = jasmine.createSpyObj(['getAgreement']);
mockOperatorService = jasmine.createSpyObj(['getOperators']);
mockAccountService = jasmine.createSpyObj(['getFeeAccounts']);
mockRoute = jasmine.createSpyObj(['route']);
mockRouter = jasmine.createSpyObj(['router']);
mockSessionService = jasmine.createSpyObj(['getSession']);
mockSettingsService = jasmine.createSpyObj(['getSettings']);
TestBed.configureTestingModule({
declarations: [AgreementComponent],
imports: [
RouterTestingModule
],
providers: [
{
provide: ActivatedRoute, useValue:
{
snapshot: {
paramMap: convertToParamMap({agreementId: '0'})
}
}
},
]
});
component = new AgreementComponent(mockAgreementService, mockOperatorService, mockAccountService,
mockRoute, mockRouter, mockSessionService, mockSettingsService);
}));
it('should call operators service', () => {
component.ngOnInit();
expect(mockOperatorService).toHaveBeenCalled();
});
});
Currently I'm getting:
Failed: Cannot read property 'paramMap' of undefined
TypeError: Cannot read property 'ngOnInit' of undefined
I'm really sure this code lacks a lot of things in order to work fine, I just can't figure out what exactly is missing and what should be done differently, because googling my errors got me confused with ton of different solutions. I'm pretty new with angular testing so would like to have some pieces of advice how to write tests in the correct way.
Take a different approach by creating Stubs as explained in one of my articles.
Created reusable stubs as:
export class MockOperatorService{
getOperators(){
return of({data: "someVal"})
}
}
and so on for other other services.
Use RouterTestingModule as and when required in imports
Mock ActivatedRoute and other services in as below:
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [AgreementComponent],
imports: [
RouterTestingModule
],
providers: [
{
provide: ActivatedRoute, useValue:
{
snapshot: {
paramMap: convertToParamMap({agreementId: '0'})
}
}
},
{provide: OperatorService , useClass: MockOperatorService},
{....similarly for AgreementService etc etc}
]
});
}));
beforeEach(() => {
fixture = TestBed.createComponent(AgreementComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
I realized that there is a lack of proper set of articles to learn about angular testing so I wrote collection of articles which you can find on the bottom of this page. I hope it'll help
Update:
To spy as asked in comment, you can do:
it('should call getOperators service in ngOnInit', () => {
spyOn(component.operatorService,"getOperators").and.callThrough();
component.ngOnInit();
expect(component.operatorService.getOperators).toHaveBeenCalled();
// you can also be more specific by using ".toHaveBeenCalledWith()"
});

Getting TypeError pipe is not a function while testing angular component using observable with declarative approach using async

When run the tests i am getting i am getting the "TypeError: this.dashboardService.serviceAgents$.pipe is not a function" error.
ServiceDashboard page code as below :
import { Component } from '#angular/core';
import { ServiceDashboardService } from '../services/service-dashboard.service';
import { tap } from 'rxjs/operators';
import { ServiceAgent } from '../interfaces/service-agent';
#Component({
selector: 'app-service-dashboard',
templateUrl: './service-dashboard.page.html',
styleUrls: ['./service-dashboard.page.css'],
})
export class ServiceDashboardPage {
serviceAgentSlideOptions: any = {
slidesPerView: 4
};
serviceAgents$ = this.dashboardService.serviceAgents$
.pipe(
tap(serviceAgents => {
this.serviceAgentSlideOptions.slidesPerView = serviceAgents.length < 4 ? serviceAgents.length : 4;
})
);
constructor(private dashboardService: ServiceDashboardService) { }
}
Following is the unit test code for the ServiceDashboardPage.
import { CUSTOM_ELEMENTS_SCHEMA } from '#angular/core';
import { async, ComponentFixture, TestBed } from '#angular/core/testing';
import { ServiceDashboardPage } from './service-dashboard.page';
import { ServiceDashboardService } from '../services/service-dashboard.service';
import { ServiceAgent } from '../interfaces/service-agent';
import { of } from 'rxjs';
describe('ServiceDashboardPage', () => {
let component: ServiceDashboardPage;
let fixture: ComponentFixture<ServiceDashboardPage>;
let serviceDashboardServiceSpy: ServiceDashboardService;
beforeEach(async(() => {
serviceDashboardServiceSpy = jasmine.createSpyObj('ServiceDashboardService',
['serviceAgents$']);
TestBed.configureTestingModule({
declarations: [ServiceDashboardPage],
schemas: [CUSTOM_ELEMENTS_SCHEMA],
providers: [
{ provide: ServiceDashboardService, useValue: serviceDashboardServiceSpy }
]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(ServiceDashboardPage);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', async(() => {
(serviceDashboardServiceSpy.serviceAgents$ as unknown as
jasmine.Spy).and.returnValue(of([] as ServiceAgent[]));
expect(component).toBeTruthy();
}));
});
There are a number of issue with your code as written. As it was pointed out in the comments above, you clearly want an Observable in the service, but the command:
serviceDashboardServiceSpy = jasmine.createSpyObj('ServiceDashboardService', ['serviceAgents$']);
will create serviceAgents$ as a function, and not as an Observable.
But just changing that won't make your code testable, because you will want to change the value that is returned by that Observable and test to see that your component is reacting properly to those changes. For that you will need to refactor your code. The reason for the refactor is because the way you are setting up your Observable in the component by defining it immediately means that it is very hard to change the value and easily test. Simply moving the assignment to ngOnInit() however, will make this much more easily testable.
Then, you need to move fixture.detectChanges() out of the beforeEach() and into the spec itself (the it() function). The reason for this is because fixture.detectChanges() will execute ngOnInit() which we just set up, and we want to more carefully control when that is called.
Finally, you need to set up something to mock your service class - you were trying to use serviceDashboardServiceSpy object to do so, but in this case I prefer to use a mock class rather than a spy object. The reason for this is because you are defining serviceAgents$ within the real service class as a PROPERTY and not as a function. This makes it a little more tricky to test, and setting up a mock class instead of a spy object in my opinion makes this a little easier.
Taking all these things into account, I set up this StackBlitz to show your tests passing.
I also added a couple of tests to show how changing the value in the service Observable changes the associated value within your component.
Here is the .spec from that StackBlitz:
class MockServiceDashboardService {
get serviceAgents$() {
return of({length: 2});
}
}
describe('ServiceDashboardPage', () => {
let component: ServiceDashboardPage;
let fixture: ComponentFixture<ServiceDashboardPage>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ServiceDashboardPage],
schemas: [CUSTOM_ELEMENTS_SCHEMA],
providers: [
{ provide: ServiceDashboardService, useClass: MockServiceDashboardService }
]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(ServiceDashboardPage);
component = fixture.componentInstance;
});
it('should create', () => {
fixture.detectChanges();
expect(component).toBeTruthy();
});
it('should have length of 2', () => {
fixture.detectChanges();
expect(component.serviceAgentSlideOptions.slidesPerView).toEqual(2);
});
it('should have a length of 3', () => {
let dService = TestBed.get(ServiceDashboardService);
spyOnProperty(dService, 'serviceAgents$').and.returnValue(of({length: 3}))
fixture.detectChanges();
expect(component.serviceAgentSlideOptions.slidesPerView).toEqual(3);
});
});

How to redirect to an external URL from angular2 route without using component?

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.
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.
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/'
}
}
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'},
Hmm...
I think you can simply request the URL instead of calling ng2 Router...
For example...
External
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...?
just use:
{
path: 'external-link',
loadChildren: () => new Promise( () => { if(window.location.href.match(/external-link/) ) window.location.href = 'https://external-link.com/'; } )
},
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.
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...
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.
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.
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]);
}
}

Categories

Resources