Reporting The User's Timezone Offset To The Server Using An API Client In Angular 7.2.10
In the recent episode of Coding Block - Why Date-ing is Hard - Allen Underwood, Joe Zack, and Michael Outlaw went over many of the reasons as to why working with Dates in computer programming is a fun any cozy nightmare. Not the least of which is having to account for the various Timezones in which the users may be accessing the system. One of the ways in which we've dealt with this problem at InVision is to automatically report the user's Timezone Offset as a custom HTTP header whenever the user communicates with the server. This isn't build into the Angular HTTP Client; as such, I wanted to put together a quick demo of how this can be done in Angular 7.2.10.
Run this demo in my JavaScript Demos project on GitHub.
View this code in my JavaScript Demos project on GitHub.
In the early days of Angular 2, I talked about how powerful it can be to create specialized HTTP Clients on top of the core HttpClient that Angular provides. Doing so allows us to bake-in a lot of application-specific domain knowledge without altering the functionality of the underlying HttpClient service. Using this same pattern, we can create an API Client that reports the user's current Timezone Offset as part of each HTTP request that gets sent to the server.
Using the native Date object, we can use the .getTimezoneOffset() method to get the number of minutes the user has to add to their local date in order to calculate the current UTC time. We will then take this value and send it to the server using the custom HTTP header, "X-Timezone-Offset":
// Import the core angular services.
import { HttpClient } from "@angular/common/http";
import { Injectable } from "@angular/core";
import { Observable } from "rxjs";
// ----------------------------------------------------------------------------------- //
// ----------------------------------------------------------------------------------- //
interface StringMap {
[ key: string ]: string;
}
export interface RequestConfig {
method: string;
url: string;
params?: StringMap;
headers?: StringMap;
body?: any;
}
@Injectable({
providedIn: "root"
})
export class ApiHttpClient {
private httpClient: HttpClient;
// I initialize the api-client.
constructor( httpClient: HttpClient ) {
this.httpClient = httpClient;
}
// ---
// PUBLIC METHODS.
// ---
// I make an HTTP request to the API and return an observable response.
public makeRequest<T>( requestConfig: RequestConfig ) : Observable<T> {
// The point of having a specialized API HttpClient is that you can bake-in logic
// that is specific to this API, but not necessarily needed for any other
// HttpClient in the application. There is a LOT you can do with this pattern;
// but, for the PURPOSES OF THIS DEMO, we're only going to be sending the current
// browser's timezone offset (in minutes).
var headers: StringMap = {
...requestConfig.headers,
// Pass the timezone offset as a special HTTP header. This way, the server
// can record this value if it has been changed (based on the user's locale).
"X-Timezone-Offset": this.getTimezoneOffset()
};
var httpStream = this.httpClient.request<T>(
requestConfig.method,
requestConfig.url,
{
responseType: "json",
headers: headers
}
);
return( httpStream );
}
// ---
// PRIVATE METHODS.
// ---
// I return the timezone offset (in minutes) for the current Browser platform. This
// is the number of minutes that the current timezone would have to add to a Date in
// order to calculated the current UTC time.
private getTimezoneOffset() : string {
return( String( new Date().getTimezoneOffset() ) );
}
}
As you can see, the ApiHttpClient service is exposing a .makeRequest() method that functions like a simplified version of the native HttpClient. But, with each request, the ApiHttpClient service is adding API-specific meta-data like the "X-Timezone-Offset" header. Obviously, in a production application, there would be more API-specific data manipulation (like error normalization); but, for the purposes of this demo, I'm keeping it simple.
ASIDE: At InVision, we actually send this header through as negative value as it is persisted with the intent to be added to UTC times in order to generate user-local times. Essentially, we use it as the reverse of the native .getTimezoneOffset() intention.
Going up a layer of abstraction, we can then create an ApiService class that provides domain-level access using the ApiHttpClient. For the sake of this demo, our ApiService is only going to expose one method for accessing the currently-authenticated user account:
// Import the core angular services.
import { Injectable } from "@angular/core";
import { Observable } from "rxjs";
// Import the application components and services.
import { ApiHttpClient } from "./api.http-client";
// ----------------------------------------------------------------------------------- //
// ----------------------------------------------------------------------------------- //
export interface Account {
id: number;
name: string;
email: string;
}
@Injectable({
providedIn: "root"
})
export class ApiService {
private apiClient: ApiHttpClient;
// I initialize the api service.
constructor( apiClient: ApiHttpClient ) {
this.apiClient = apiClient;
}
// ---
// PUBLIC METHODS.
// ---
// I get the account for the currently-authenticated user.
public getAccount() : Observable<Account> {
var stream = this.apiClient.makeRequest<Account>({
method: "GET",
url: "./api/account.json"
});
return( stream );
}
}
As you can see, this ApiService doesn't know anything about the Timezone Offset. It only knows that it has to communicate with the remote API using the specialized ApiHttpClient.
And finally, we can consume this ApiService from our App component, which will simply load the Account after the application has been bootstrapped:
// Import the core angular services.
import { Component } from "@angular/core";
// Import the application components and services.
import { Account } from "./api.service";
import { ApiService } from "./api.service";
// ----------------------------------------------------------------------------------- //
// ----------------------------------------------------------------------------------- //
@Component({
selector: "my-app",
styleUrls: [ "./app.component.less" ],
template:
`
<ng-template [ngIf]="( ! account )">
<p>
<em>Loading....</em>
</p>
</ng-template>
<ng-template [ngIf]="account">
<h2>
Welcome {{ account.name }}
</h2>
<p>
I'm Johnny Cab - where can I take you tonight?
</p>
</ng-template>
`
})
export class AppComponent {
public account: Account | null;
private apiService: ApiService;
// I initialize the app component.
constructor( apiService: ApiService ) {
this.apiService = apiService;
this.account = null;
}
// ---
// PUBLIC METHODS.
// ---
// I get called once after the inputs have been bound for the first time.
public ngOnInit() : void {
this.apiService.getAccount().subscribe(
( account ) => {
console.group( "Account Response" );
console.log( account );
console.groupEnd();
this.account = account;
}
);
}
}
Now, if we run this Angular application in the browser, we will get the following browser output:
As you can see, the "X-Timezone-Offset" HTTP Header is automatically sent with each API call made by the specialized HttpClient - ApiHttpClient. The server can now look for this HTTP Header during the request processing and use it to update server-side data as needed. And, if the server ever has to calculate user-local time during a server-side operation (such as generating an Email body), it can subtract this offset from the current UTC time.
Working with dates is not easy. And, working with Timezones is a special kind of nightmare. One way to make life a little easier is to let the browser report the user's local timezone offset. This way, instead of worrying about what Timezone the user is actually in, all you have to do is consume the reported offsets. In Angular 7.2.10, we can create a specialized HttpClient to report this value during communication with our back-end API.
Want to use code from this post? Check out the license.
Reader Comments
why you don't use interceptor?
@YY,
Good question. I have a lot of feelings about HTTP Interceptors; so, I've tried to answer you in a subsequent blog post:
www.bennadel.com/blog/3589-http-interceptors-are-an-anti-pattern-that-create-hidden-dependencies-and-unnecessary-complexity-in-angular.htm
Your mileage may vary; but, I find that HTTP Interceptors are fine at first; but, break-down and create friction over time.
How can I display current date and time using rest api?
@Durvesh,
In those cases, I usually return the Date/Time stamp from the server as a UTC-millisecond value. Basically, what you would get from something like:
new Date().getTime()
Then, on the client-side, I would use that UTC-millisecond value to instantiate a
Date
object and do the formatting from there. As a simple example, it would be something like:new Date( utc_milliseconds_from_server ).toTimeString()