Skip to content

Date System Revamp Proposal

Adam Shaw edited this page Apr 26, 2018 · 11 revisions

Goals:

  • Dates exposed by a calendar should all be zoned similarly so that date-math is convenient (#2981)
  • The concept of an ambiguously-zoned moment should be removed (#2981)
  • The concept of an ambiguously-timed moment should be removed (#2981)
  • Should be possible to compute timezone offset on the client-side (#2981, #3188)
  • When the user mutates the dates of an event, the dates should not be drastically transformed (#2780)
  • A way forward to support other calendar systems that have different computations for years/months. Nepali, Hijri, Jalaali.

Constraints:

  • IE11 should still be supported. However, IE9/10 can be dropped.
  • Should NOT create a new dependency on a client-side timezone lib that requires timezone data to be downloaded. Too heavy-weight. Should be optional if anything. This includes polyfills. This rules out making moment-timezone, Intl.js, and date-time-format-timezone hard dependencies.

Nice-to-haves:

  • Remove reliance on MomentJS (#3937)

A Note on Browser Support

Once we drop IE9 and IE10 support, we can start relying on the native Intl API, specifically the DateTimeFormat functionality. Then, we don't need to rely on translation files for date formatting, the browser will do all the work. However, we will still need translation files for generic strings like "Today", "Next", and "Prev" which appear in buttons of the calendar's header.

The Intl API also kind-of exposes timezone capabilities. In the past, browsers would only support the client's current local timezone as well as UTC, but no others. With the Intl API, it is now possible to compute timezone offsets for arbitrary named timezones like "America/Chicago" through a hack with formatToParts, which is what the new Luxon date lib does. However, IE11 does not yet support this. Only Edge. So, we cannot rely on this technique yet.

Proposal Summary

  • remove the false option for timezones. only have "UTC", "local", and named timezones like "America/Chicago"
  • if you want to use named timezones like "America/Chicago" you must choose one of the following:
    • use a plugin that's responsible for computing timezone offset for arbitrary timezones. there will be a fullcalendar-moment-timezone plugin for example.
    • use what I refer to as "UTC coersion". explained below.
  • expose native Date objects in all parts of the API instead of moment objects
  • for parts in the API that need to express a whole-day versus a datetime, such as in agenda view when the user clicks a 00:00 timeslot versus the "all-day" area, this will be indicated by a new boolean flag that will be passed to the callback. this flag will be separate from the datetime.

Where Dates Matter

Where in FullCalendar's API do dates and timezones matter?

  1. dates that are generated by the view
    • View::start, View::end
    • dates given to dayClick and select callbacks
    • dates given to event sources for a request. both event JSON feeds and event functions.
  2. when event data is received by an event source. how should the start/end input for an event be translated into an Event Object's start/end properties?
  3. when event data is mutated by the end-user. when they drag-and-drop or resize an event to a different place. how will the resulting start/end be computed? how will the new timezone offset be computed?

The 'local' and 'UTC' timezones

When the timezone is set to 'local', then ALL the dates within the API will be expressed in local time. So, to address the above parts of the API that matter:

  1. If the user is in month view, and clicks the rectangle for Sept 1st 2018, and their local computer is in United States Eastern Daylight Time (GMT-4), then the dayClick callback will report a Date whose toString() value will be "Sat Sep 01 2018 00:00:00 GMT-0400 (EDT)".
  2. For received event data:
    • if an ISO8601 string is given without a timezone offset such as "2018-09-15T12:00:00", it will be assumed to be in the client's local timezone. Doing a eventObj.start.toString() would result in "Sat Sep 15 2018 12:00:00 GMT-0400 (EDT)".
    • if an ISO8601 string DOES have a timezone offset, it will be parsed literally and will show up on the calendar with different day/hour values if the timezone offset is different than the local one. For example, if the client's local timezone is GMT-4, but an event was declared to begin at '2018-09-15T09:00:00+0900', then the result of eventObj.start.toString() would be "Fri Sep 14 2018 20:00:00 GMT-0400 (EDT)". Because the timezone offset was converted, the date/hour values were converted to compensate.
  3. When an event's dates are mutated, the client can easily compute the timezone offset of the new dates because the client always understands every date in local time.

When the timezone is set to 'UTC', then ALL the dates within the API will be expressed in UTC! If this is the case, all the native Date methods that deal with local time such as toString, getDate, and getHours will be useless! Instead, the UTC-versions of these Dates will be important such as toUTCString, getUTCDate, and getUTCHours.

Here's how dates throughout the API will behave when the timezone is UTC:

  1. If the user is in month view, and clicks the rectangle for Sept 1st 2018, then the dayClick callback will report a Date whose toUTCString() value will be "Sat, 01 Sep 2018 00:00:00 GMT".
  2. For received event data:
    • if an ISO8601 string is given without a timezone offset such as "2018-09-15T12:00:00", it will be assumed to be in UTC. Doing a eventObj.start.toUTCString() would result in "Sat Sep 15 2018 12:00:00 GMT".
    • if an ISO8601 string DOES have a timezone offset, it will be parsed literally and will show up on the calendar with different day/hour values if the timezone offset is not zero. For example, if an event is declared to begin at '2018-09-15T12:00:00+0900', then the result of eventObj.start.toUTCString() would be "Sat, 15 Sep 2018 03:00:00 GMT". Because the timezone offset was converted, the date/hour values were converted to compensate.
  3. When an event's dates are mutated, the client can easily compute the timezone offset of the new dates because the client always understands every date in UTC.

Named Timezones

A named timezone is an arbitrary timezone in the world that is represented by a string identifier, like "America/Chicago". Not all browers know how to compute timezone offsets for arbitrary named timezones unfortunately. We must resort to other techniques. So, when a named timezone is specified, a new settings called timezoneImplementation will come into play.

The easiest case to explain is when timezoneImplementation is set to use a plugin. A fullcalendar-moment-timezone plugin will be available as well as a fullcalendar-luxon plugin, which knows how to handle timezone offsets in very modern browsers. These plugins know how to compute the timezone offset for any date in nearly any timezone.

So, when timezoneImplementation is set to use a plugin, here's how dates throughout the API will behave:

(Please imagine that there's a utility method on the Calendar object called dateToString. This serves the same purpose as the Date object's toString and toUTCString methods but will format the date in whatever timezone the calendar was declared to be in.)

(Also, please imagine the timezone has been set to 'America/Chicago')

  1. If the user is in month view, and clicks the rectangle for Sept 1st 2018, then regardless of what their local computer's timezone is in, the dayClick callback will report a Date whose calendar.dateToString(date) value will be "2018-09-01T00:00:00-05:00". This is because -5:00 is the timezone offset for that date in the America/Chicago timezone.
  2. For received event data:
    • if an ISO8601 string is given without a timezone offset such as "2018-09-15T12:00:00", it will be assumed to be in calendar's named offset timezone. Doing a calendar.dateToString(date) would result in "2018-09-15T12:00:00-05:00" for the America/Chicago timezone.
    • if an ISO8601 string DOES have a timezone offset, it will be parsed literally and will show up on the calendar with different day/hour values if the date's offset does not match the current timezone's offset. For example, if an event is declared to begin at '2018-09-15T12:00:00+0900', then the result of calendar.dateToString(eventObj.start) would be "2018-09-14T22:00:00-05:00". Because the timezone offset was converted, the date/hour values were converted to compensate.
  3. When an event's dates are mutated, the client can compute the timezone offset of the new dates because the timezone plugin knows how to do this.

UTC Coercion for Named Timezones

If the developer using FullCalendar doesn't want to use a plugin that provides client-side timezone data nor a plugin that depends on dropping IE11 support, then we must provide an alternative. (See @Jaicob's comment). This is what the preexisting timezone: false setting sort-of does.

The main point of this approach is to compute the timezone offset for each event's dates ON THE SERVER, and then send those timezone offset down as part of the ISO8601 string for each date.

This technique will entail setting timezoneImplementation to 'UTC-coercion'. It will only be available when a named timezone is provided.

We could encourage developers to use timezone: 'UTC', and when transmitting datetimes, express them as ISO8601 date strings with the timezone's offset omitted or set to -00:00 (see @lukasz-karolewski's comment). However, these dates aren't really in UTC, UTC is just simply the best way the browser knows how to express them. They are in a different timezone that the browser simply does not know how to compute timezone offsets for. So, expressing dates in UTC is a bit of a lie and a bit of hack. It's essentially what timezoneImplementation: 'UTC-coercion' does, but I'd rather make this hack more explicit to the developer by calling it "coercion". I'd also like a way to retain the timezone offset information if it needs to be displayed in the UI.

So, when timezoneImplementation is set to 'UTC-coercion', here's how dates throughout the API will behave:

  1. If the user is in month view, and clicks the rectangle for Sept 1st 2018, then the dayClick callback will report a Date whose toUTCString() value will be "Sat, 01 Sep 2018 00:00:00 GMT". A call to calendar.dateToString(date) will omit the timezone information however and produce "2018-09-15T00:00:00".
  2. For received event data:
    • if an ISO8601 string is given without a timezone offset such as "2018-09-15T12:00:00", it will be parsed as UTC. Doing a eventObj.start.toUTCString() would result in "Sat Sep 15 2018 12:00:00 GMT". A call to calendar.dateToString(eventObj.start) will omit the timezone information however and produce "2018-09-15T12:00:00".
    • if an ISO8601 string DOES have a timezone offset, for the most part it will be ignored! This is what's referred to as "UTC coercion". Only the y/m/d/h/m/s values will be used. So, if a value of '2018-09-15T12:00:00+0900' is received, the timezone part will be ignored and eventObj.start.toUTCString() will produce "Sat Sep 15 2018 12:00:00 GMT". It won't retain the timezone offset. It's milliseconds since the epoch will be the same as new Date(Date.UTC(2018, 8, 15, 12, 0, 0)). A call to calendar.dateToString(date) will omit the timezone information however and produce "2018-09-15T12:00:00". The only case for which the original timezone information is retained is for date-formatted text on each event element. If the timezone token is specified for event text formatting, it will display +0900.
  3. When an event's dates are mutated, the client will not definitively know how to compute the new timezone offset. It will probably stay the same, but it might also change if the user dragged the event over a daylight-savings-time cutover. It will be up to the developer to update the event's timezone offsets themself if they so choose. A method on Event Object will be available to do this.

Date Formatting

When we do away with MomentJS as a dependency, we can longer accept date formatting strings. That's okay because the native Intl API offers a more international-friendly way to format dates anyway. It accepts an options object that defines which pieces of date information should end up in the date text, and FullCalendar will accept the same such objects. Example:

titleFormat: {
  year: 'numeric', // 2018
  month: 'long', // January
  day: 'numeric' // 1
}

For more information see the DateTimeFormat docs.

So, any place that previously accepted a formatting string will accept one of these objects. For more arbitrary programmatic text generation, they will also accept a function:

titleFormat: function(date) {
  return 'asdfasdfasdf';
}

However, it's understandable that developers might want to use formatting strings still. If that's the case, they can import a plugin that hooks-in this functionality. They can then start using format strings. Example:

import { Calendar, addPlugin } from 'fullcalendar';
import { MomentPlugin } from 'fullcalendar-moment';

addPlugin(MomentPlugin);

new Calendar({
  titleFormat: 'YYYY m,d' // a MomentJS formatting string
})

Maybe we'll provide one for dateformat as well.

Date Range Formatting

The same date formatting input (object/method/string) will be accepted for date range formatting. I've already figured out hacky but dependable technique to derive a range string from DateTimeFormat.

For the fullcalendar-moment plugin as well as other such plugins, similar hacks will needed. We already have something like it.

Durations

Since we will no longer use MomentJS, we can no longer expose Duration objects. But we DO plan on accepting the same type of input for a duration. A string such as '10:00' or a plain object like:

{
  day: 5,
  hour: 5
}

We'll want the names of these props to be consistent with DateTimeFormat.

Date Math

If we switch to using solely native Date objects, we won't have any built-in date math utilities like we did with MomentJS. However, it will be possible to port those date objects to a library of your choice:

new Calendar({
  timezone: 'local', // or 'UTC'
  dayClick: function(date) {
    moment(date).addDays(1);
    // or if 'UTC' ...
    moment.utc(date).addDays(1);
  }
})

However, it's a bit awkward to use different constructors based on whether the calendar is in the local timezone or UTC. So, we'll make a converter util for moment and others. This will also do the work of initializing the moment to the calendar's timezone and locale:

import { Calendar } from 'fullcalendar';
import { dateToMoment } from 'fullcalendar-moment';

let calendar = new Calendar({
  timezone: 'UTC',
  dayClick: function(date) {
    dateToMoment(calendar, date).addDays(1);
  }
})

What about date-fns?

date-fns is a popular library for working with dates. But it won't go well with the new date system because it has a shortcoming: it only operates on the local timezone of each date. It's not possible to do date math on the UTC values. Upvote this issue if you care.

Other Calendar Systems

Other calendar systems like Nepali, Hijri, and Jalaali will not be supported on the initial release of v4, but things will be architected in such a way where they can be added in later, via plugins. This will be a lot easier now that we will maintain our own internal date utils instead of relying on Moment's.

You can’t perform that action at this time.