Handling dates in single-page applications
Published on Wednesday, May 2, 2018 5:00:00 AM UTC in Programming
Working with dates in a single-page application always is a bit of a hassle. Well, dates in general are one of those things that look straight-forward at first but have a lot of nasty details to master, in particular when you're dealing with localization requirements. This is not distinct to JavaScript or TypeScript, but to all languages, simply because handling dates is a universal problem. There's a reason we have frameworks like Noda Time in .NET. For your single page applications, there are at least three things to take care of: getting date values from and to the backend, having an internal canonical representation that you can count on, e.g. for all technical handling like binding properties to controls, and of course displaying dates correctly to the user, i.e. in a format they expect and understand.
Exchanging dates with the backend
The problem with exchanging dates with a server is that serialized dates (typically JSON for your SPA) are simple strings, and are not automatically converted back to real date objects on the client - meaning you have to handle this manually. Luckily, this is not so difficult, as long as you follow some conventions consequently. For years, I've successfully used the following setup for this:
- Make sure your backend serializes dates into ISO format, i.e. formatted as "YYYY-MM-DDTHH:mm:ss.sssZ"
- Transparently parse these types of strings back into real dates on the client when deserializing the responses
- Serialize to the same ISO format when you're sending data to the backend
One of the highlighted terms above is "transparently", meaning that it's a good idea to integrate the respective logic into your central http client implementation in your app, so you never run into the problem of forgetting this additional step.
For this, first of all you need some deserialization logic for the formatted date strings. I've been using a generic JSON parser that simply extends the built-in JSON functions accordingly. You can find all kinds of variants of this on the internet. My personal flavor handles the time zone part; I've seen some implementations of this that don't do this and hence do not catch all potential values coming e.g. from a .NET backend.
export class JsonParser {
public static parseWithIsoDatesSupport<T>(json: string): T {
return JSON.parse(json, this.parseDates) as T;
}
private static parseDates(key: any, value: any): any {
const regexISO = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}(?:\.?\d*))(?:Z|(\+|-)([\d|:]*))?$/;
if (typeof value === "string") {
const regexParseResult = regexISO.exec(value);
if (regexParseResult != undefined) {
return new Date(value);
}
}
return value;
}
}
With this in place, you can plug it into your preferred http client implementation or even use it manually if you have to. Here's a sample for Axios:
// assumes $http points to an Axios instance
let response = await $http.get<T>(url, {
// other options...
transformResponse: (data, headers) => this.parseResponse(data, headers)
});
private parseResponse(data: any, headers: any): any {
// make sure to be nice and add special cases here, for data == undefined or empty or blob etc.
try {
let result = JsonParser.parseWithIsoDatesSupport(data);
return result;
}
catch {
return data;
}
}
And with that, all your dates will be type-safe correct date objects instead of plain strings. Of course you can plug the above logic into anything, like using fetch or plain old xhr.
A canonical form for internal representation
A seemingly natural answer to the question "how do I handle the peculiarities of dates gracefully?" would be to use one of the sophisticated libraries out there, for example Moment.js. But frankly, I don't use these libraries in my projects at all. Why not, you ask?
- I never found myself in the need of the vast feature set they provide. Ever.
- I even found those features counter-intuitive sometimes, for example, when dates are displayed in the local format for the particular user but the rest of the content is not (because a localized version is not available). I rather keep my content consistent and specifically target those cultures I support, with consistently falling back to an invariant one (see more on this below).
- Particular features of UI frameworks (like UI binding) sometimes seem to not fit those frameworks very well.
- Inspecting Moment instances in particular in the debugger has a potential to be quite confusing and for me even added frustration hunting down bugs in the past.
- It's another dependency I have to maintain, with little gain in exchange.
So what I do instead is use an own, canonical representation of the dates I'm working with. This is tailored to fit the requirements of particular UI frameworks, for example like Vuetify. I very much encourage you to create your own representation, but to give you an idea what I'm talking about, here's a slimmed-down version of a sample:
export class CustomDateTime {
private _value: Date;
constructor(value: Date) {
this._value = value;
}
public get value(): Date { return this._value; }
public set value(value: Date) {
this._value = value;
}
public get dateString(): string {
return this._value.toISOString().substr(0, 10);
}
public set dateString(value: string) {
let tempDate = new Date(value);
tempDate.setHours(this._value.getHours());
tempDate.setMinutes(this._value.getMinutes());
tempDate.setSeconds(this._value.getSeconds());
this._value = tempDate;
}
public get timeString(): string {
let hoursText = ("0" + this._value.getHours()).slice(-2);
let minutesText = ("0" + this._value.getMinutes()).slice(-2);
return hoursText + ":" + minutesText;
}
public set timeString(value: string) {
let tempDate = new Date(this._value);
let parts = value.split(":");
tempDate.setHours(parseInt(parts[0], 10));
tempDate.setMinutes(parseInt(parts[1], 10));
tempDate.setSeconds(0);
this._value = tempDate;
}
}
As you can see, at first this is nothing more than a wrapper around the underlying actual date, and you can both set it as well as get it out again, for example when you need to send it to your backend. Additionally, you can have any getters and setters you like to suit your needs. In the sample above, I have a dateString
property as well as a timeString
. This allows me to bind to a Vuetify date picker easily, for example:
<v-date-picker v-model="data.myDate.dateString"></v-date-picker>
Any changes in the control will be reflected in the actual date value and nicely integrate with the framework (and Vue.js behind the scenes), but at the same time I'm completely satisfying the control's requirement to have a specifically formatted date string as input (and output).
The same works for the time picker:
<v-time-picker v-model="data.myDate.timeString" format="24hr"></v-time-picker>
Like I said, this is a simple sample tailored to a particular use case and framework requirements. You would very likely create your own and implement it for your needs, and it might even look differently for different applications. The benefit however is that you have very tight control of what happens and are aware of the implementation details at all time - something that I found becomes an issue with lots of framework doing "black magic" for you without you noticing.
If you plug this type into your http client implementation (similar to the above hook for JSON conversion) you can even consistently and transparently wrap all your dates into one of these types, and trust you're dealing with the same data structure for dates throughout your application.
Displaying dates to the user
Again, major frameworks would solve this problem for you gracefully, but I prefer to have control over this part also. The question you should ask yourself is whether you really want to display dates to your users in their local format even if the rest of your content is not available locally, or whether it wouldn't be more consistent to fall back to an invariant culture in that case for all content. A descriptive sample: let's say you're posting articles, like I do here, and you provide them in German and English, with English being the invariant culture that you fall back to for visitors that come from France. Now, would it be better to have English articles displayed with French date formats, or wouldn't it be more consistent to use English date formats even for your French visitors? I very much prefer the latter, and that's the reason I can happily live with adding more supported date formats manually to my implementation once I support them.
By the way, during the last years things became much better to at least get some correct format out of modern browsers. So even if it's not the specific format you intended to get, using something like (Vue.js-style string interpolation):
<span>{{data.myLastModifiedDate.toLocaleString()}}</span>
... will result in local dates understandable to all your users without using external libraries, and is supported by all browsers. It will even become better with improvements like Intl.
Whether you would extend your custom date type (see above) to handle different formats for various cultures, or if you put that logic somewhere else and separated is of course up to you.
Tags: Date · JavaScript · TypeScript · Vuetify