In this article we'll take a quick look at how injection tokens can be used through implementing an rxjs based wrapper around navigator.geolocation.watchPosition()
.
What is an injection token?
These tokens can be used in the DI provider. It helps you represent dependencies which doesn't have a runtime representation, but they get set during module setups. This means we could eventually disable functionalities if these tokens are setup properly.
Examples
You can find the docs here: angular.io/api/core/InjectionToken
Let's create our first token which will be called GEOLOCATION
import { DOCUMENT } from '@angular/common';
import { inject, InjectionToken } from '@angular/core';
export const GEOLOCATION = new InjectionToken<Geolocation>(
'Token for window.navigator.geolocation object', // debugging purposes
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
{ factory: () => inject(DOCUMENT).defaultView!.navigator.geolocation }
);
With factory: () => ...
we can define where the values are coming from by default. With these lines we are trying to make sure that the geolocation API is there.
Let's try to break it up more this factory:
- we inject the
document
- grab the
defaultView
, it is nullable, but in this case we know it won't be null - then we can pretty much just access the
geolocation
through thenavigator
object
Let's look at a much much much smaller example
import { InjectionToken } from '@angular/core';
export const POSITION_OPTIONS = new InjectionToken<PositionOptions>(
'Token for position options',
{ factory: () => ({}) }
);
Nothing fancy happens here, in this case the default value is an empty object, we just provide capabilities to change these options per module for example:
In the providers array we can just say:
{
provide: POSITION_OPTIONS,
useValue: {
enableHighAccuracy: true,
} as PositionOptions,
},
And this way when we use the tokens we can change the options for our service.
We can also inject a token into a token. Let's try to check if the geolocation
object is present or not, that means if geolocation
is supported or not.
import { inject, InjectionToken } from '@angular/core';
import { GEOLOCATION } from './geolocation.token';
export const GEOLOCATION_SUPPORT = new InjectionToken<boolean>(
'Is Geolocation API supported?',
{
factory: () => !!inject(GEOLOCATION),
}
);
We were able to simply inject our token into it and simply return if it is present or not.
Service implementation
With these tokens set up we can setup a fairly configurable service and wrap around rxjs Observable
.
Let's first inject our tokens
constructor(
@Inject(POSITION_OPTIONS) options: PositionOptions,
@Inject(GEOLOCATION) geolocationRef: Geolocation,
@Inject(GEOLOCATION_SUPPORT) geolocationSupported: boolean
)
If the tokens are not set during our module setup the value will be resolved what is inside the factory method.
So based on our previous steps just above; we can assume the options is {}
, geolocationRef
is a geolocation
object and geolocationSupported
is true.
Let's quickly implement an Observable
wrapper around this:
import { Inject, Injectable } from '@angular/core';
import { finalize, Observable } from 'rxjs';
import { GEOLOCATION } from './geolocation.token';
import { POSITION_OPTIONS } from './geolocation-options.token';
import { GEOLOCATION_SUPPORT } from './geolocation-support.token';
@Injectable({
providedIn: 'root',
})
export class GeolocationService extends Observable<
Parameters<PositionCallback>[0]
> {
constructor(
@Inject(POSITION_OPTIONS) options: PositionOptions,
@Inject(GEOLOCATION) geolocationRef: Geolocation,
@Inject(GEOLOCATION_SUPPORT) geolocationSupported: boolean
) {
let watchPositionId: number;
super((sub) => {
if (!geolocationSupported) {
sub.error('Geolocation is not supported in your browser');
}
watchPositionId = geolocationRef.watchPosition(
(position) => sub.next(position),
(positionError) => sub.error(positionError),
{
...options,
}
);
});
return this.pipe(
finalize(() => geolocationRef.clearWatch(watchPositionId))
) as GeolocationService;
}
}
If the geolocation API is not supported our service will give as an error, the watchPosition
callbacks are fairly straightforward those are just pushing values into the right stream and since we have options as well we just copy them into the function call third parameter. One thing you would have to do manually is to clear the watching, now if the Observable completes or unsubscribes it will auto-magically stop the watching.
Lastly you can provide these tokens in the provider array as previously mentioned, but another really cool win with these kinda stuff is the ease of testing. If we want to test what happens when the user doesn't allow the geolocation API we can just do:
{
provide: GEOLOCATION_SUPPORT,
useValue: false
},
This makes testing edge cases super easy.
Repo link
Example repo with full code: github.com/benceHornyak/geolocation-service Npm package: npmjs.com/package/@bencehornyak/geolocation..
As you can see on the issues it is not fully ready to be consumed, but it is a great start if you want to play with it. If you are up to the challenge and want to improve your knowledge you can contribute to the repo 😊
Resources
Here are some resources which were useful for me when I started to learn this topic:
- angular.io/api/core/InjectionToken
- angular.io/guide/dependency-injection-provi..
- angular.io/guide/lightweight-injection-tokens
Final thoughts
I hope you have found this short article about my example useful, let know if I've missed anything