← Back to Timezone Handling ← Back to JavaScript Development

10.3.2 Timezone Input controller

app/assets/js/controllers/timezone-input-controller.js registers as timezone-input. It is an opt-in form enhancement that displays stored UTC instants in one IANA timezone and submits hidden ISO UTC values.

Use it only when the endpoint has an explicit client-UTC submission contract. Ordinary forms can instead render local values with the PHP Timezone helper and convert them with PHP TimezoneHelper::toUtc().

Targets and values

Name Kind Required Contract
datetimeInput target One or more for useful behavior datetime-local input; optional data-utc-value supplies the stored instant
notice target No Receives “Times shown in …” text
timezone String value No IANA zone; falls back to browser detection
showNotice Boolean value No Defaults to true

Complete form

<?php
$timezone = TimezoneHelper::getGatheringTimezone(
    $gathering,
    $this->getRequest()->getAttribute('identity'),
);
?>

<?= $this->Form->create($gathering, [
    'data-controller' => 'timezone-input',
    'data-timezone-input-timezone-value' => $timezone,
    'data-timezone-input-show-notice-value' => 'true',
]) ?>

<?= $this->Form->control('start_date', [
    'type' => 'datetime-local',
    'data-timezone-input-target' => 'datetimeInput',
    'data-utc-value' => $gathering->start_date?->toIso8601String() ?? '',
]) ?>

<?= $this->Form->control('end_date', [
    'type' => 'datetime-local',
    'data-timezone-input-target' => 'datetimeInput',
    'data-utc-value' => $gathering->end_date?->toIso8601String() ?? '',
]) ?>

<p class="form-text" data-timezone-input-target="notice"></p>

<?= $this->Form->button(__('Save')) ?>
<?= $this->Form->end() ?>

Resolve the zone on the server. Do not let a client value select a tenant or override an event’s authorized timezone.

Lifecycle

Connect

connect():

  1. selects the Stimulus timezone value or KMP_Timezone.detectTimezone();
  2. converts every non-empty data-utc-value with toLocalInput();
  3. records data-original-utc and data-local-value;
  4. fills notice targets when enabled; and
  5. attaches bound submit and reset listeners to the controller element.

The controller element should be the form. Attaching it to a wrapper means a submit event must bubble through that wrapper and hidden fields will be appended there rather than directly to the form.

Submit

For each non-empty target, handleSubmit():

  1. calls KMP_Timezone.toUTC(input.value, timezone);
  2. saves the local value in data-submitted-local;
  3. removes a previous generated hidden input for the same field;
  4. creates a same-name hidden input with data-timezone-converted="true";
  5. disables the visible input; and
  6. appends the hidden input to the controller element.

If conversion returns an empty string, the visible input stays enabled and gets data-timezone-conversion-failed="true". The current controller does not cancel submission or announce that failure, so server validation is mandatory. A workflow that requires client-side blocking must implement and test accessible error handling rather than silently dropping the value.

Repeated submits skip an already-disabled visible input and do not create duplicate hidden fields.

Reset

handleReset() removes generated hidden fields, re-enables target inputs, clears the failure marker, and restores each initial converted local value on the next task with setTimeout(..., 0).

Disconnect

disconnect() removes the exact bound submit and reset listeners. Preserve this cleanup because Turbo Frame replacement can reconnect the controller many times.

Public methods

Method Contract
convertUtcToLocal() Rebuild target values from each target’s original data-utc-value
updateNotice() Replace notice content with a clock icon and zone/abbreviation text
handleSubmit(event) Perform the hidden-field submission conversion
handleReset(event) Restore the connected state
updateTimezone(newTimezone) Set the controller’s internal zone, reconvert original UTC values, and refresh notices
getTimezone() Return the current internal zone

updateTimezone() expects a timezone string, not a DOM event. Call it from coordinating JavaScript or an outlet; do not wire it directly as a Stimulus action without an adapter that extracts and validates the selected value.

Data flow

stored UTC instant
  ↓ data-utc-value
connect → UTC instant to local datetime-local text
  ↓ user edits
submit → local text to hidden UTC ISO value
  ↓
server validates tenant/context/timezone and persists UTC

The visible input’s name is reused by the hidden input. Every target therefore needs a non-empty, unique form field name.

DST and accessibility

Verification

Focused tests are in app/tests/js/controllers/timezone-input-controller.test.js. Run:

cd app
npm run test:js -- timezone-input-controller.test.js
npm run dev

Use Playwright for a real form round trip, DST behavior, Turbo reconnection, and validation/focus announcements.