Angular 5 service replacement/override - javascript

I created a core library for my project containing some components and services. I built the library with ng-packagr. In the consuming project which references the library I built my webapp containing components provided by the library. Nothing special so far. But sometimes I want a component (coming from my lib) calling a method from a Service outside of the lib. Is this possible? Can I somehow inject a service to a component which is defined inside a library?
Cheers

I've achieved this before with something like this:
Your library's service(s) should be defined as an interface rather than as a concrete implementation (as is done in OO languages quite often). If your implementing application will only sometimes want to pass in its own version of the service then you should create a Default service in your library, and use it as so:
import { Component, NgModule, ModuleWithProviders, Type, InjectionToken, Inject, Injectable } from '#angular/core';
export interface ILibService {
aFunction(): string;
}
export const LIB_SERVICE = new InjectionToken<ILibService>('LIB_SERVICE');
export interface MyLibConfig {
myService: Type<ILibService>;
}
#Injectable()
export class DefaultLibService implements ILibService {
aFunction() {
return 'default';
}
}
#Component({
// whatever
})
export class MyLibComponent {
constructor(#Inject(LIB_SERVICE) libService: ILibService) {
console.log(libService.aFunction());
}
}
#NgModule({
declarations: [MyLibComponent],
exports: [MyLibComponent]
})
export class LibModule {
static forRoot(config?: MyLibConfig): ModuleWithProviders {
return {
ngModule: LibModule,
providers: [
{ provide: LIB_SERVICE, useClass: config && config.myService || DefaultLibService }
]
};
}
}
Then in your implementing application you have the ability to pass in the optional config via your library's forRoot method (note that forRoot should only be called once per application and at the highest level possible). Note that I've marked the config parameter as optional, so you should call forRoot even if you have no config to pass.
import { NgModule, Injectable } from '#angular/core';
import { LibModule, ILibService } from 'my-lib';
#Injectable()
export class OverridingService implements ILibService {
aFunction() {
return 'overridden!';
}
}
#NgModule({
imports: [LibModule.forRoot({ myService: OverridingService })]
})
export class ImplementingModule {
}
This was from memory as I don't have the code to hand at the moment so if it doesn't work for any reason let me know.

Related

Clean solution to ensure a singleton service is created on app initialization

I want to ensure a singleton service is created on application boot. I could add it as injection parameter to my AppComponent and not use it at all, but that looks a bit dirty. Right now I'm going with this solution:
import { APP_INITIALIZER, ModuleWithProviders, NgModule } from '#angular/core';
import { NavigationService } from './navigation.service';
#NgModule()
export class NavigationServiceModule {
public static forRoot(): ModuleWithProviders<NavigationServiceModule> {
return {
ngModule: NavigationServiceModule,
providers: [
{
provide: APP_INITIALIZER,
deps: [NavigationService],
multi: true,
useFactory: () => () => { }
}
]
}
}
}
But don't really love it, too. Any ideas how this could be achieved best?
Just use the default service setup and don't add it to the providers array - only one instance will be created (unless you explicitly provide it outside of constructors)
#Injectable({
providedIn: 'root'
})
export class NavigationService

Angular pass service with dependencies to module using forRoot

I have a service for authentication based on JWT. To reuse this service in all my projects i created a library which should be shipped with npm.
For this service to work i need some API-Calls. In every project the API could look completely different so i don't want to provide this functionality inside my library instead inject another service which handles my API-Calls.
My idea was to create a module which contains my service and provide an interface to describe the service for API-Calls and inject it forRoot. The Problem is that my api service has some dependencies like HttpClient and i cannot simple instantiate it in my app.module.
My library looks like:
auth.module.ts
import { NgModule, ModuleWithProviders, InjectionToken } from '#angular/core';
import { AuthService } from '../services/auth.service';
import { AuthAPI } from '../models/authAPI';
import { AuthapiConfigService } from '../services/authapi-config.service';
#NgModule()
export class AuthModule {
static forRoot(apiService: AuthAPI): ModuleWithProviders {
return {
ngModule: AuthModule,
providers: [
AuthService,
{
provide: AuthapiConfigService,
useValue: apiService
}
]
};
}
}
auth-api.interface.ts
import { Observable } from 'rxjs';
export interface AuthAPI {
reqLogin(): Observable<{ access_token: string; }>;
reqRegister(): Observable<{ access_token: string; }>;
}
auth-api-config.service.ts
import { InjectionToken } from '#angular/core';
import { AuthAPI } from '../models/authAPI';
/**
* This is not a real service, but it looks like it from the outside.
* It's just an InjectionTToken used to import the config object, provided from the outside
*/
export const AuthapiConfigService = new InjectionToken<AuthAPI>('API-Service');
auth.service.ts
constructor(#Inject(AuthapiConfigService) private apiService) {}
How i am trying to implement it:
auth-rest-service.ts
import { Injectable } from '#angular/core';
import { AuthAPI } from 'projects/library-project/src/lib/auth/models/authAPI';
import { Observable } from 'rxjs';
import { HttpClient } from '#angular/common/http';
#Injectable({
providedIn: 'root'
})
export class AuthRestService implements AuthAPI {
constructor(private http: HttpClient) {}
reqLogin(): Observable<{ access_token: string; }> {
return this.http.post<{access_token: string}>(`/login`, 'test');
}
reqRegister(): Observable<{ access_token: string; }> {
return this.http.post<{access_token: string}>(`/login`, 'test');
}
}
app.module.ts
import { AuthRestService } from './components/auth-service/auth-rest.service';
#NgModule({
declarations: [
...
],
imports: [
...
AuthModule.forRoot(AuthRestService),
...
],
providers: [AuthModule],
bootstrap: [AppComponent]
})
export class AppModule { }
I can't create an instance of AuthRestService because of the dependencies this service has (HttpClient). Is there any method to tell angular to provide me this service.
This is possible with usage of angular's Injector.
import { Injector, ModuleWithProviders, NgModule, Optional, Provider, SkipSelf } from '#angular/core';
import { isFunction } from 'lodash';
export function resolveService(cfg: SharedConfig, inj: Injector): IncompleteService {
const provider = cfg?.service;
// if service is an angular provider, use Injector, otherwise return service instance as simple value
const service = isFunction(service) ? inj.get(provider) : provider;
return service;
}
/**
* Service to be implemented from outside the module.
*/
#Injectable()
export abstract class IncompleteService {
abstract strategyMethod();
}
// Optional: A config object is optional of course, but usually it fits the needs.
export interface SharedConfig {
service: IncompleteService | Type<IncompleteService> | InjectionToken<IncompleteService>;
// other config properties...
}
/*
* Optional: If a Config interface is used, one might resolve the config itself
* using other dependencies (e.g. load JSON via HTTPClient). Hence an InjectionToken
* is necessary.
*/
export const SHARED_CONFIG = new InjectionToken<SharedConfig>('shared-config');
// Optional: If SharedConfig is resolved with dependencies, it must be provided itself.
export type ModuleConfigProvider = ValueProvider | ClassProvider | ExistingProvider | FactoryProvider;
/**
* One can provide the config as is, i.e. "{ service: MyService }" or resolved by
* injection, i.e.
* { provide: SHARED_CONFIG: useFactory: myConfigFactory, deps: [DependentService1, DependentService2] }
*/
#NgModule({
declarations: [],
imports: []
})
export class SharedModule {
static forRoot(config: SharedConfig | ModuleConfigProvider): ModuleWithProviders<SharedModule> {
// dynamic (config is Provider) or simple (config is SharedConfig)
return {
ngModule: SharedModule,
providers: [
(config as ModuleConfigProvider).provide ? (config as Provider) : { provide: SHARED_CONFIG, useValue: config },
{ provide: IncompleteService, useFactory: resolveService, deps: [SHARED_CONFIG, Injector] },
// ... provide additional things
],
};
}
/**
* In general not really useful, because usually an instance of IncompleteService
* need other dependencies itself. Hence you cannot provide this instance without
* creating it properly. But for the sake of completeness, it should work as well.
*/
#NgModule({
declarations: [],
imports: []
})
export class MostSimpleSharedModule {
static forRoot(service: IncompleteService): ModuleWithProviders<SharedModule> {
// dynamic (config is Provider) or simple (config is SharedConfig)
return {
ngModule: SharedModule,
providers: [
{ provide: IncompleteService, useValue: service },
// ... provide additional things
],
};
}
EDIT
If you really need an interface iso. an (injectable) abstract class IncompleteService, you just need to define another InjectionToken<IncompleteServiceInterface> and provide this token explicitly.

Angular: How to override the shared module service from different module

I have multiple modules say SchoolModule, UniversityModule, SharedModule
SharedModule has BaseService to which both SchoolModule and UniversityModule providers are extending
Now when I load my SchoolModule, I want BaseService should get the implementation of schoolService, and the same goes for UniversityModule
Structure
app
-- SharedModule
-- base.service
-- secret.service uses base.service
-- shared.component uses secret.service
-- SchoolModule
-- school.component uses shared.component
-- school.service
-- UniversityModule
-- university.component uses shared.component
-- university.service
StackBlitz
So how I can achieve this with Dependency Injection?
You have to declare your base service class as an abstract class with an abstract method getName()
export abstract class BaseService{
abstract getName(): string { return 'Base Service' }
}
export class SchoolService extends BaseService{
getName(): string { return 'School Service' }
}
Unfortunately, Angular2 can not inject a class from another module without get the import between modules, and if you need to lazy load it, things do not work.
There is a project that load dynamically a component from another module.
It is difficult to import this in to your project, but saves you to not code twice.
This project can be found here.
The problem is that SecretService provided is the one build with build in UniversityModule last imported module in App Module so if you want to have SecretService update with correctly BaseService you have to provide both BaseService and SecretService in university and school component something like this in school.component.ts:
import { SchoolService } from './school.service'
import { BaseService } from '../shared/base.service'
import { SecretService } from '../shared/secret.service'
#Component({
selector: 'app-school',
template: `SchoolName :: <app-shared></app-shared>`,
providers: [
{ provide: BaseService, useClass: SchoolService },
SecretService]
})
export class SchoolComponent implements OnInit {
constructor() { }
ngOnInit() {
}
}
and in university.component.ts
import {  UniversityService } from './university.service'
import { BaseService } from '../shared/base.service'
import { SecretService } from '../shared/secret.service'
#Component({
selector: 'app-university',
template: `UniversityName :: <app-shared></app-shared>`,
providers: [{ provide: BaseService, useClass: UniversityService },
SecretService]
})
export class UniversityComponent implements OnInit {
constructor() { }
ngOnInit() {
}
}
But why you wanna wrap base service in SecretService instead directly use BaseService in shared component ?
Using BaseService like this in shared.component.ts
import { Component, OnInit } from '#angular/core';
import { BaseService } from './base.service'
#Component({
selector: 'app-shared',
template: `{{name}}`
})
export class SharedComponent implements OnInit {
name: string
constructor(private ss: BaseService) {
this.name = ss.getName()
}
ngOnInit() {
}
}
Using BaseService you will solve this injection problem without any workaround on SecretService

Sending whole object from one component to another in angular 2

I have a problem. I don't know how to send object from one component to another.
In first component cinema.component.html I have following function call:
<a title="Reserve" (click)="openReservationPage(repertoire)">Reserve</a>
In cinema.component.ts file, for that .html I have something like:
openReservationPage(repertoire: UpcomingRepertoire) {
this.router.navigate(['/reserve', {repertoire: JSON.stringify(repertoire)}]);
}
My app.routes.ts file contains appropriate routing:
{ path: 'reserve', component: ReserveFormComponent }
How can I use this repertoire object in another page reserve-form.component.ts and reserve-form.component.html ?
As an answer for the question in the title, i would said create a service to pass data between components.
Since its a router implementation you can pass the repertoire as a route parameter.
Follow these steps:
1)Modify the route in app.routes.ts to take a param
{ path: 'reserve/:repertoire', component: ReserveFormComponent }
2)In cinema.component.ts pass the repertoire as param
this.router.navigate(['/reserve',JSON.stringify(repertoire)]‌​);
3)Extract the param in reserve-form.component.ts
First of all you need to import
import {ActivatedRoute } from "#angular/router";
Technique 1
repertoire:any;
constructor(private activatedRoute: ActivatedRoute) {
this.repertoire = JSON.parse(activatedRoute.snapshot.params["repertoire"]);
}
Technique 2
import { Subscription } from "rxjs/Rx";
private subscription: Subscription;
repertoire:any;
constructor(private activatedRoute: ActivatedRoute) {
this.subscription = activatedRoute.params.subscribe(
(param: any) => this.repertoire = JSON.parse(param['repertoire'])
);
}
ngOnDestroy() { // here we unsubscribe to the observable
this.subscription.unsubscribe();
}
Further Explanation :
Technique 1 is adopted when you are sure that the param will be passed every time you navigate to the component.
Technique 2 is a subscription to the observable once there a param published but don't forget to unsubscribe in the ngOnDestroy() component's life cycle method to prevent memory leak.
It is more preferable because some times there a scenario that a param is passed to a component after it was created where the snapshot method wouldn't capture and it more flexible with different scenario than the basic one in technique 1.
The link below explains how you can do this. I've recently used this to create a messaging service. The example below, shows the code for a simple messaging service. It allows you to pass a number between components, just change the to I guess. You can also write out to local storage, but It seems services are more popular. Once you've got your head around them, they're easy to re-use.
Hope this helps
Sharing Data Between Angular Components - Four Methods
Message Service (PmMessageService)
import { Injectable } from '#angular/core';
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
#Injectable()
export class PmMessageService
{
private pillMenuIndexBS = new BehaviorSubject <number> (null);
pillMenuIndex = this.pillMenuIndexBS.asObservable();
constructor() {}
setPillMenuIndex(index : number)
{
this.pillMenuIndexBS.next(index);
}
}
Component consuming message service, setting a value
import { Component, OnInit } from '#angular/core';
import { PmMessageService } from '../pm-message-service/pm-message.service'
import { BrowserModule } from '#angular/platform-browser';
import { platformBrowserDynamic } from '#angular/platform-browser-dynamic';
#Component({
selector: 'app-pm-configure',
templateUrl: './pm-configure.component.html',
styleUrls: ['./pm-configure.component.css']
})
export class PmConfigureComponent implements OnInit
{
constructor (private messageService : PmMessageService) {}
ngOnInit()
{
this.messageService.setPillMenuIndex(1);
}
}
Component consuming and subscribing.
import { Component, OnInit } from '#angular/core';
import { PmMessageService } from '../pm-message-service/pm-message.service'
import { BrowserModule } from '#angular/platform-browser';
import { platformBrowserDynamic } from '#angular/platform-browser-dynamic';
#Component({
selector: 'pm-bs-navbar',
templateUrl: './pm-bs-navbar.component.html',
styleUrls: ['./pm-bs-navbar.component.css']
})
export class PmBsNavbarComponent implements OnInit
{
tabActiveNumber;
constructor (private messageService : PmMessageService) {}
ngOnInit()
{
this.messageService.pillMenuIndex.subscribe(index => this.tabActiveNumber = index)
}
}

How SHOULD I inject document and window instances in angular2? [duplicate]

I have an Angular 2 application. For mocking the Document object in tests, I'd like to inject it to the service like:
import { Document } from '??'
#Injectable()
export class MyService {
constructor(document: Document) {}
}
The Title service of Angular uses the internal getDOM() method.
Is there any simple way to inject the Document to the service? Also, how should I reference it in the providers array?
This has been supported by Angular for a while.
You can use the DOCUMENT constant provided by the #angular/common package.
Description of the DOCUMENT constant (taken from the API documentation):
A DI Token representing the main rendering context. In a browser, this is the DOM Document.
An example is as shown below:
my-service.service.ts:
import { Inject, Injectable } from '#angular/core';
import { DOCUMENT } from '#angular/common';
#Injectable()
export class MyService {
constructor(#Inject(DOCUMENT) private document: Document) {}
}
my-service.service.spec.ts
import { provide } from '#angular/core';
import { DOCUMENT } from '#angular/common';
import { MyService } from './my-service';
class MockDocument {}
describe('MyService', () => {
beforeEachProviders(() => ([
provide(DOCUMENT, { useClass: MockDocument }),
MyService
]));
...
});
I'm unable to comment directly on adamdport's question (not yet 50 rep points), but here it is as stated in the angular docs.
Blockquote
#GünterZöchbauer it looks like DOCUMENT is deprecated. Any idea how to do this once it's gone? For example, how would I set the favicon dynamically?
Instead of importing from platform browser like so:
import { DOCUMENT } from '#angular/platform-browser';
Import it from angular common:
import {DOCUMENT} from '#angular/common';
in addition to #Günter Zöchbauer's answer.
Angular define DOCUMENT as an InjectionToken
export const DOCUMENT = new InjectionToken<Document>('DocumentToken');
dom_tokens.ts
And inject it with document in browser.ts
{provide: DOCUMENT, useFactory: _document, deps: []}
export function _document(): any {
return document;
}
Therefore, when we use it, we just need to inject #Inject(DOCUMENT)
or use the token directly in deps:[DOCUMENT]
import { Inject, Injectable } from '#angular/core';
import { DOCUMENT } from '#angular/common';
#Injectable()
export class MyService {
constructor(#Inject(DOCUMENT) private document) {}
}
It's the ": Document" that's causing the problem.

Categories