material-ui LocalizationProvider for a remote time zone
Asked Answered
I

1

7

My app needs material-ui date and time pickers to operate on a remote time zone specified by the server. I'd like the today circle on the date picker to actually indicate today in the remote time zone, and I'd like to translate the datetimes in the remote time zone to seconds since 1970-01-01T00:00:00Z.

I'm using the material-ui v5 alphas. The docs say that you specify a @date-io adapter for your time library. Looks like there are four obvious options:

  • @date-io/date-fns (based on date-fns and date-fns-tz has a broken remote time zone design. It uses Javascript Dates to represent that date and time in a remote time zone, but if the local time zone has a "spring forward" hour, there are times you can't represent. issue.
  • @date-io/dayjs (based on dayjs) doesn't handle daylight saving time correctly. issue
  • @date-io/luxon (based on luxon) looks promising
  • @date-io/moment (based on moment and moment-timezone) looks promising

so I want to specify an adapter for either luxon or moment that constructs dates in a particular zone.

Either library supports setting a global default time zone (luxon, moment), but I'd prefer to set a time zone when constructing a particular date adapter. Messing with global state based on the server response is gross.

I found a date-io issue that says:

You can pass moment-time zone right to the libInstance in this case it will use time zone set of the moment instance or global one

That's what I want! But I'm confused about exactly what this instance is supposed to be. It doesn't help that I'm pretty new to Javascript.

The @date-io/luxon constructor doesn't seem to allow overriding instance like this today.

Trying to get the moment one to work:

$ mkdir tztest
$ cd tztest
$ npm init -y
$ npm install --save moment moment-timezone '@date-io/moment'
$ node
> let MomentUtils = require('@date-io/moment');
undefined
> let moment = require('moment');
undefined
> let _ = require('moment-timezone');
undefined

> // Operations including the following should all work similarly to when using the default instance:
> (new MomentUtils()).date();
Moment<2021-03-18T11:57:30-07:00>
> (new MomentUtils()).date('2021-01-01T00:00:00');
Moment<2021-01-01T00:00:00-08:00>
> (new MomentUtils()).getCurrentLocaleCode();
'en'

> // Here's some garbage I tried
> (new MomentUtils({instance: moment().tz('America/New_York')})).date();
Uncaught TypeError: _this.moment is not a function
    at MomentUtils.date (/Users/slamb/git/tztest/node_modules/@date-io/moment/build/index.js:78:32)
> (new MomentUtils({instance: moment.tz('America/New_York')})).date();
Uncaught TypeError: _this.moment is not a function
    at MomentUtils.date (/Users/slamb/git/tztest/node_modules/@date-io/moment/build/index.js:78:32)
> (new MomentUtils({instance: () => moment.tz('America/New_York')})).date();
Moment<2021-03-18T14:44:07-04:00>
> (new MomentUtils({instance: () => moment.tz('America/New_York')})).date('2021-01-01T00:00:00');
Moment<2021-03-18T14:44:19-04:00>
> (new MomentUtils({instance: (arg1, arg2, arg3, arg4) => moment.tz(arg1, arg2, arg3, arg4, 'America/New_York')})).date('2021-01-01T00:00:00');
Moment<2021-01-01T00:00:00-05:00>
> (new MomentUtils({instance: (arg1, arg2, arg3, arg4) => moment.tz(arg1, arg2, arg3, arg4, 'America/New_York')})).getCurrentLocaleCode();
Uncaught TypeError: _this.moment.locale is not a function
    at MomentUtils.getCurrentLocaleCode (/private/tmp/tztest/node_modules/@date-io/moment/build/index.js:63:49)
> (new MomentUtils({instance: (arg1, arg2, arg3, arg4) => moment.tz(arg1, arg2, arg3, arg4, 'America/New_York')})).date();
Moment<2021-03-18T14:44:36-04:00>
> (new MomentUtils({instance: function() { return moment(arguments).tz('America/New_York'); } })).date()
...here the interpreter started making fun of me...

From @date-io/moment source, as quoted below, I see it uses it in several different ways. Naturally, I want all those to work properly.

export default class MomentUtils implements IUtils<defaultMoment.Moment> {
  ...
  constructor({ locale, formats, instance }: Opts = {}) {
    this.moment = instance || defaultMoment;
  ...
    return /A|a/.test(this.moment().localeData().longDateFormat("LT"));
  ...
          return this.moment.localeData().longDateFormat(token as LongDateFormatKey);
  ...
    return this.locale || this.moment.locale();
  ...
      return this.moment(value, format, this.locale, true);
  ...
    return this.moment(value, format, true);
  ...
    const moment = this.moment(value);
  ...
    return this.moment.weekdaysShort(true);
Ingenue answered 18/3, 2021 at 19:7 Comment(0)
S
6

The issue is material-ui has different docs and date adapter integration api for different major versions.

I got moment-timezone working with @material-ui/[email protected]

dependencies

"@material-ui/core": "4.11.3",
"@material-ui/pickers": "4.0.0-alpha.11",
"moment": "2.29.1",
"moment-timezone": "0.5.33",
"react": "17.0.2",
"react-dom": "17.0.2"

working demo

https://codesandbox.io/s/material-ui-starter-template-forked-pvpmc

code example

import React from "react";
import { render } from "react-dom";
import momentTimezone from "moment-timezone";
import { TextField } from "@material-ui/core";
import { DatePicker, LocalizationProvider } from "@material-ui/pickers";
import MomentAdapter from "@material-ui/pickers/adapter/moment";

const App = () => {
  const timeZoneFromServer = "Asia/Tokyo";
  const { moment } = new MomentAdapter({ instance: momentTimezone });
  const dateWithTimeZone = moment().tz(timeZoneFromServer);

  const handleDateChange = () => {};

  return (
    <LocalizationProvider dateAdapter={MomentAdapter}>
      <DatePicker
        renderInput={(props) => <TextField {...props} />}
        value={dateWithTimeZone}
        onChange={handleDateChange}
      />
    </LocalizationProvider>
  );
};

render(<App />, document.getElementById("root"));
Soak answered 31/3, 2021 at 4:51 Comment(3)
That's close! But I'd like the today indicator to be correct even when nothing is selected / value is null. So I need to pass the time zone through the dateAdapter somehow, not through value. (Also, I'm using v5 rather than v4, but I think it's almost the same: your demo link adapted to v5)Ingenue
in moment if you do moment().tz('Some/Place') passing in no arguments to the moment() call automatically sets it to the current date. Is that your expected output? I edited my example to remove the .add(5, 'days') call...I don't remember why I added that.Soak
You can also do moment.tz.setDefault('America/New_York'). momentjs.com/timezone/docs/#/using-timezones/default-timezoneSoak

© 2022 - 2024 — McMap. All rights reserved.