Reputation: 3180
I've been trying to find a way where I can instantiate a service that will purely sit "in the background" and listen to events (and do stuff) - which I would like to be created when the app initializes, and be forgotten about.
Unfortunately, I would need to use dependency injection into a component, for the service to be instantiated - most paths I take lead to using the AppComponent
's constructor.
I won't be directly interacting with the service though (calling methods/properties), and want to keep it out of other components/services which don't have anything directly to do with it.
The service and the logic in it is pretty straightforward. My service is based on a Dynamic page titles in Angular 2 tutorial.
The service will listen to NavigationEnd
events from the Router
, grab the ActivatedRoute
, and then use the route's data to set the page title.
Unlike the example in the tutorial, I've created my own service instead of putting the logic within the AppComponent
; I want to keep my separation of concerns tip-top.
page-title.service.ts:
import { Injectable } from '@angular/core';
import { Router, NavigationEnd, ActivatedRoute } from '@angular/router';
import { Title } from '@angular/platform-browser';
import { filter, map, mergeMap } from 'rxjs/operators';
@Injectable()
export class PageTitleService {
constructor(
router: Router,
activatedRoute: ActivatedRoute,
titleService: Title
) {
router.events
.pipe(
filter((event) => event instanceof NavigationEnd),
map(() => activatedRoute),
map((route) => {
while (route.firstChild) {
route = route.firstChild;
}
return route;
}),
filter((route) => route.outlet === 'primary'),
mergeMap((route) => route.data)
)
.subscribe((routeData) => titleService.setTitle(routeData['title']));
}
}
Obviously, the service itself will rely on dependency injection to use the Router
, ActivatedRoute
, and Title
services.
The only way I currently know to instantiate this service is to use dependency injection into another component/service.
E.g. in app.component.ts
:
export class AppComponent implements OnInit {
constructor(
pageTitleService: PageTitleService, // inject the service to instantiate it
// ... other services
) { }
ngOnInit() {
// do stuff with other services, but not the pageTitleService
}
}
The problem is, I want to avoid doing this if at all possible.
Is it possible to instantiate the service somewhere other than a component/service?
I do have an app-load.module.ts
, which does some upfront initialization, before the rest of the app is loaded:
import { APP_INITIALIZER, NgModule } from '@angular/core';
import { OrganisationService } from './core/organisation/organisation.service';
export function initApp(organisationService: OrganisationService) {
return () =>
organisationService
.initialize()
.then(() => window.document.documentElement.classList.remove('app-loading'));
}
@NgModule({
imports: [],
declarations: [],
providers: [
OrganisationService,
{ provide: APP_INITIALIZER, useFactory: initApp, deps: [OrganisationService], multi: true }
]
})
export class AppLoadModule { }
Could I perhaps instantiate the PageTitleService
in here, somewhere?
Or, is there a better place/way to do it?
Thanks in advance.
Upvotes: 11
Views: 6453
Reputation: 5321
What you need is a Class
, which you can use to update page title [or some other stuff], but it should not be injectable.
You can define a plain JS Class
Not as @injectable
. You can then use the APP_INITIALIZER to instantiate the class manually. The instance thus created will not be accessible using DI, but can be saved and shared by using global/static variable.
Here is the sample stackblitz. I have only logged to console, you can update as needed.
TitleService
Class that updates title using updateTitle
method.
PageTitleService
Class that will subscribe to router and calls TitleService.updateTitle
.
pageTitleServiceInit
factory function to manually instantiate PageTitleService
.
import { APP_INITIALIZER, Injectable, NgModule } from "@angular/core";
import { BrowserModule } from "@angular/platform-browser";
import { FormsModule } from "@angular/forms";
import { AppComponent } from "./app.component";
import { HelloComponent } from "./hello/hello.component";
import {
ActivatedRoute,
NavigationEnd,
Router,
RouterModule
} from "@angular/router";
import { RandomComponent } from "./random/random.component";
import { filter } from "rxjs/operators";
// ================ TitleService ======================
@Injectable()
export class TitleService {
updateTitle(title: string) {
console.log("new title: ", title);
}
}
// ================ Initalizer Factory ======================
function pageTitleServiceInit(router, route, titleService) {
return () => {
new PageTitleService(router, route, titleService);
};
}
// ================ PageTitleService ======================
export class PageTitleService {
constructor(
router: Router,
activatedRoute: ActivatedRoute,
titleService: TitleService
) {
router.events
.pipe(filter(event => event instanceof NavigationEnd))
.subscribe((x: any) => {
console.log(activatedRoute);
titleService.updateTitle(x.url);
});
}
}
@NgModule({
imports: [
BrowserModule,
FormsModule,
RouterModule.forRoot([
{ path: "random", component: RandomComponent },
{ path: "hello", component: HelloComponent }
])
],
declarations: [AppComponent, HelloComponent, RandomComponent],
bootstrap: [AppComponent],
providers: [
TitleService,
{
provide: APP_INITIALIZER,
useFactory: pageTitleServiceInit,
deps: [Router, ActivatedRoute, TitleService],
multi: true
}
]
})
export class AppModule {}
Upvotes: 3
Reputation: 336
Solution
Instead of injecting the service via your app.component.ts
, you could inject the service via the app.module.ts
(or via your app-load.module.ts
)
Eg:
@NgModule({
...
})
export class AppModule {
constructor(pageTitleService: PageTitleService) {}
}
Upvotes: 2
Reputation: 19298
I think there are a few options possible.
new PageTitleService(router, activatedRoute, pageService)
However, this requires you to pass an instance of the objects defined in the constructor.injector.get(PageTitleService)
to get an instance. Unfortunately injector is also a service which should be inejcted in an component or service.In your case, you are going to need dependency injection. You could try option 1 and also manually create instances of router and activatedRoute, but they might have their own dependencies. And their dependencies might also have dependencies.
You could create a regular Singleton which has some kind of init function that does al the initial work. Something like this:
class Singleton
{
private constructor()
{
console.log("hello");
}
private hasStarted = false;
private static _singelton :Singleton
static get(): Singleton{
return Singleton._singleton || (Singleton._singleton = new Singleton())
}
pubic init(router: Router, activatedRoute: ActivatedRoute, titleService: Title): void{
if(!hasStarted){
//do whatever you want
hasStarted = true;
}
}
}
In your appcomponent:
constructor(router: Router, activatedRoute: ActivatedRoute, titleService: Title){
Singelton.getInstance().init(router: Router, activatedRoute: ActivatedRoute, titleService: Title);
}
This way there will only be one instance of your singleton an everyting in the init will only be executed once.
Note: nothing is realy private in typescipt. Something like this is still possible.
const singleton = Singelton.getInstance();
singelton['hasStarted'] = false;
singleton.init(...);
Upvotes: 1
Reputation: 1252
Just an observation why injecting in a component (App component) would not be such a bad idea:
init
method in the service which will be called from app component so that it can start listening to router event and update the title. This makes it very imperative, explicit too and you can move calling this init method to some other component if required.Upvotes: 3
Reputation: 1880
Use APP_INITIALIZER or APP_BOOTSTRAP_LISTENER. More details you can find here.
For use Router
in APP_INITIALIZER
set initialNavigation option to true
. Or use APP_BOOTSTRAP_LISTENER
(but you skip first navigation in this case)
for add initalizer you need add provider for it
{
provide: APP_INITIALIZER,
useFactory: (router) => yourFunction,
multi: true,
deps: [Router]
}
Upvotes: 0
Reputation: 1338
Is it possible for you to create a title.ts
in your root folder and do:
export const titleConfig = {
title: '',
}
and access that object from whenever you need it, and change it in your router
import { titleConfig } from 'src/app/title';
// ...more magic goes here
export class PageTitleService {
constructor(
private router: Router,
private activatedRoute: ActivatedRoute,
private titleService: Title
) {
this.router.events
.filter((event) => event instanceof NavigationEnd)
.map(() => this.activatedRoute)
.map((route) => {
while (route.firstChild) {
route = route.firstChild;
}
return route;
})
.filter((route) => route.outlet === 'primary')
.mergeMap((route) => route.data)
.subscribe((routeData) => titleConfig.title = routeData['title']); // change here
}
}
if you are using ChangeDetectionStrategy.Onpush
then you can do a { ...routerData['title'] }
or add a setTitle
method to the object. I'm not sure.
Upvotes: -1