diff --git a/src/Forms/Container.php b/src/Forms/Container.php
index 2392a7b28..10e4323f5 100644
--- a/src/Forms/Container.php
+++ b/src/Forms/Container.php
@@ -405,6 +405,36 @@ public function addFloat(string $name, $label = null): Controls\TextInput
}
+ /**
+ * Adds input for date selection.
+ * @param string|object|null $label
+ */
+ public function addDate(string $name, $label = null): Controls\DateTimeControl
+ {
+ return $this[$name] = new Controls\DateTimeControl($label, Controls\DateTimeControl::Date);
+ }
+
+
+ /**
+ * Adds input for time selection.
+ * @param string|object|null $label
+ */
+ public function addTime(string $name, $label = null, bool $withSeconds = false): Controls\DateTimeControl
+ {
+ return $this[$name] = new Controls\DateTimeControl($label, Controls\DateTimeControl::Time, $withSeconds);
+ }
+
+
+ /**
+ * Adds input for date and time selection.
+ * @param string|object|null $label
+ */
+ public function addDateTime(string $name, $label = null, bool $withSeconds = false): Controls\DateTimeControl
+ {
+ return $this[$name] = new Controls\DateTimeControl($label, Controls\DateTimeControl::DateTime, $withSeconds);
+ }
+
+
/**
* Adds control that allows the user to upload files.
* @param string|object|null $label
diff --git a/src/Forms/Controls/DateTimeControl.php b/src/Forms/Controls/DateTimeControl.php
new file mode 100644
index 000000000..b533a0ce5
--- /dev/null
+++ b/src/Forms/Controls/DateTimeControl.php
@@ -0,0 +1,164 @@
+mode = $mode;
+ $this->withSeconds = $withSeconds;
+ parent::__construct($label);
+ $this->control->step = $withSeconds ? 1 : null;
+ }
+
+
+ /**
+ * Format of returned value. Allowed values are string (ie 'Y-m-d'), self::FormatObject and self::FormatTimestamp.
+ * @return static
+ */
+ public function setFormat(string $format)
+ {
+ $this->format = $format;
+ return $this;
+ }
+
+
+ /**
+ * @param \DateTimeInterface|string|int|null $value
+ * @return static
+ */
+ public function setValue($value)
+ {
+ $this->value = $value === null ? null : $this->normalize($value);
+ return $this;
+ }
+
+
+ /**
+ * @return \DateTimeImmutable|string|int|null
+ */
+ public function getValue()
+ {
+ if ($this->format === self::FormatObject) {
+ return $this->value;
+ } elseif ($this->format === self::FormatTimestamp) {
+ return $this->value ? $this->value->getTimestamp() : null;
+ } else {
+ return $this->value ? $this->value->format($this->format) : null;
+ }
+ }
+
+
+ /**
+ * @param \DateTimeInterface|string|int $value
+ */
+ private function normalize($value): \DateTimeImmutable
+ {
+ if (is_numeric($value)) {
+ $dt = (new \DateTimeImmutable)->setTimestamp((int) $value);
+ } elseif (is_string($value)) {
+ $dt = new \DateTimeImmutable($value); // createFromFormat() must not be used because it allows invalid values
+ } elseif ($value instanceof \DateTime) {
+ $dt = \DateTimeImmutable::createFromMutable($value);
+ } elseif ($value instanceof \DateTimeImmutable) {
+ $dt = $value;
+ } elseif (!$value instanceof \DateTimeInterface) {
+ throw new Nette\InvalidArgumentException('Value must be DateTimeInterface or string or null, ' . gettype($value) . ' given.');
+ }
+
+ [$h, $m, $s] = [(int) $dt->format('H'), (int) $dt->format('i'), $this->withSeconds ? (int) $dt->format('s') : 0];
+ if ($this->mode === self::Date) {
+ return $dt->setTime(0, 0);
+ } elseif ($this->mode === self::Time) {
+ return $dt->setDate(0, 1, 1)->setTime($h, $m, $s);
+ } elseif ($this->mode === self::DateTime) {
+ return $dt->setTime($h, $m, $s);
+ }
+ }
+
+
+ public function loadHttpData(): void
+ {
+ $value = $this->getHttpData(Nette\Forms\Form::DataText);
+ try {
+ $this->value = is_string($value) && preg_match('~^(\d{4}-\d{2}-\d{2})?T?(\d{2}:\d{2}(:\d{2}(\.\d+)?)?)?$~', $value)
+ ? $this->normalize($value)
+ : null;
+ } catch (\Throwable $e) {
+ $this->value = null;
+ }
+ }
+
+
+ public function getControl(): Nette\Utils\Html
+ {
+ return parent::getControl()->addAttributes([
+ 'value' => $this->value ? $this->formatHtmlValue($this->value) : null,
+ 'type' => [self::Date => 'date', self::Time => 'time', self::DateTime => 'datetime-local'][$this->mode],
+ ]);
+ }
+
+
+ private function formatHtmlValue(\DateTimeInterface $dt): string
+ {
+ return $dt->format([
+ self::Date => 'Y-m-d',
+ self::Time => $this->withSeconds ? 'H:i:s' : 'H:i',
+ self::DateTime => $this->withSeconds ? 'Y-m-d\\TH:i:s' : 'Y-m-d\\TH:i',
+ ][$this->mode]);
+ }
+
+
+ /** @return static */
+ public function addRule($validator, $errorMessage = null, $arg = null)
+ {
+ if ($validator === Form::Min) {
+ $this->control->min = $arg = $this->formatHtmlValue($this->normalize($arg));
+ } elseif ($validator === Form::Max) {
+ $this->control->max = $arg = $this->formatHtmlValue($this->normalize($arg));
+ } elseif ($validator === Form::Range) {
+ $this->control->min = isset($arg[0])
+ ? $arg[0] = $this->formatHtmlValue($this->normalize($arg[0]))
+ : null;
+ $this->control->max = isset($arg[1])
+ ? $arg[1] = $this->formatHtmlValue($this->normalize($arg[1]))
+ : null;
+ }
+
+ return parent::addRule($validator, $errorMessage, $arg);
+ }
+}
diff --git a/tests/Forms/Controls.DateTimeControl.format.phpt b/tests/Forms/Controls.DateTimeControl.format.phpt
new file mode 100644
index 000000000..c596814ee
--- /dev/null
+++ b/tests/Forms/Controls.DateTimeControl.format.phpt
@@ -0,0 +1,44 @@
+addDate('date')
+ ->setValue('2023-10-22 10:30')
+ ->setFormat('j.n.Y');
+
+ Assert::same('22.10.2023', $input->getValue());
+});
+
+
+test('timestamp format', function () {
+ $form = new Form;
+ $input = $form->addDate('date')
+ ->setValue('2023-10-22 10:30')
+ ->setFormat(DateTimeControl::FormatTimestamp);
+
+ Assert::same(1697925600, $input->getValue());
+});
+
+
+test('object format', function () {
+ $form = new Form;
+ $input = $form->addDate('date')
+ ->setValue('2023-10-22 10:30')
+ ->setFormat(DateTimeControl::FormatObject);
+
+ Assert::equal(new DateTimeImmutable('2023-10-22 00:00'), $input->getValue());
+});
diff --git a/tests/Forms/Controls.DateTimeControl.loadData.phpt b/tests/Forms/Controls.DateTimeControl.loadData.phpt
new file mode 100644
index 000000000..26eb3c936
--- /dev/null
+++ b/tests/Forms/Controls.DateTimeControl.loadData.phpt
@@ -0,0 +1,110 @@
+addDate('unknown');
+ Assert::null($input->getValue());
+ Assert::false($input->isFilled());
+});
+
+
+test('invalid data', function () {
+ $_POST = ['malformed' => ['']];
+ $form = new Form;
+ $input = $form->addDate('malformed');
+ Assert::null($input->getValue());
+ Assert::false($input->isFilled());
+});
+
+
+test('invalid format', function () {
+ $_POST = ['text' => 'invalid'];
+ $form = new Form;
+ $input = $form->addDate('date');
+ Assert::null($input->getValue());
+ Assert::false($input->isFilled());
+});
+
+
+test('invalid date', function () {
+ $_POST = ['date' => '2023-13-22'];
+ $form = new Form;
+ $input = $form->addDate('date');
+ Assert::null($input->getValue());
+ Assert::false($input->isFilled());
+});
+
+
+test('invalid time', function () {
+ $_POST = ['time' => '10:60'];
+ $form = new Form;
+ $input = $form->addTime('time');
+ Assert::null($input->getValue());
+ Assert::false($input->isFilled());
+});
+
+
+test('valid date', function () {
+ $_POST = ['date' => '2023-10-22'];
+ $form = new Form;
+ $input = $form->addDate('date');
+ Assert::equal(new DateTimeImmutable('2023-10-22 00:00'), $input->getValue());
+ Assert::true($input->isFilled());
+});
+
+
+test('valid time', function () {
+ $_POST = ['time' => '10:22:33.44'];
+ $form = new Form;
+ $input = $form->addTime('time');
+ Assert::equal(new DateTimeImmutable('0000-01-01 10:22'), $input->getValue());
+ Assert::true($input->isFilled());
+});
+
+
+test('valid time with seconds', function () {
+ $_POST = ['time' => '10:22:33.44'];
+ $form = new Form;
+ $input = $form->addTime('time', null, true);
+ Assert::equal(new DateTimeImmutable('0000-01-01 10:22:33'), $input->getValue());
+ Assert::true($input->isFilled());
+});
+
+
+test('valid date-time', function () {
+ $_POST = ['date' => '2023-10-22T10:23:11.123'];
+ $form = new Form;
+ $input = $form->addDateTime('date');
+ Assert::equal(new DateTimeImmutable('2023-10-22 10:23:00'), $input->getValue());
+ Assert::true($input->isFilled());
+});
+
+
+test('valid date-time with seconds', function () {
+ $_POST = ['date' => '2023-10-22T10:23:11.123'];
+ $form = new Form;
+ $input = $form->addDateTime('date', null, true);
+ Assert::equal(new DateTimeImmutable('2023-10-22 10:23:11'), $input->getValue());
+ Assert::true($input->isFilled());
+});
diff --git a/tests/Forms/Controls.DateTimeControl.render.phpt b/tests/Forms/Controls.DateTimeControl.render.phpt
new file mode 100644
index 000000000..e268283b1
--- /dev/null
+++ b/tests/Forms/Controls.DateTimeControl.render.phpt
@@ -0,0 +1,95 @@
+addDate('date', 'label');
+
+ Assert::type(Html::class, $input->getControl());
+ Assert::same('', (string) $input->getControl());
+});
+
+
+test('required date', function () {
+ $form = new Form;
+ $input = $form->addDate('date')->setRequired('required');
+
+ Assert::same('', (string) $input->getControl());
+});
+
+
+test('date: min & max validator', function () {
+ $form = new Form;
+ $input = $form->addDate('date')
+ ->addRule($form::Min, null, new DateTime('2020-01-01 11:22:33'))
+ ->addRule($form::Max, null, new DateTime('2040-01-01 11:22:33'));
+
+ Assert::same('', (string) $input->getControl());
+});
+
+
+test('date: range validator', function () {
+ $form = new Form;
+ $input = $form->addDate('date')
+ ->addRule($form::Range, null, [new DateTime('2020-01-01 11:22:33'), new DateTime('2040-01-01 22:33:44')]);
+
+ Assert::same('', (string) $input->getControl());
+});
+
+
+test('time: range validator', function () {
+ $form = new Form;
+ $input = $form->addTime('time')
+ ->addRule($form::Range, null, [new DateTime('2020-01-01 11:22:33'), new DateTime('2040-01-01 22:33:44')]);
+
+ Assert::same('', (string) $input->getControl());
+});
+
+
+test('time with seconds: range validator', function () {
+ $form = new Form;
+ $input = $form->addTime('time', null, true)
+ ->addRule($form::Range, null, [new DateTime('2020-01-01 11:22:33'), new DateTime('2040-01-01 22:33:44')]);
+
+ Assert::same('', (string) $input->getControl());
+});
+
+
+test('date with value', function () {
+ $form = new Form;
+ $input = $form->addDate('date')
+ ->setValue(new Nette\Utils\DateTime('2023-10-22'));
+
+ Assert::same('', (string) $input->getControl());
+});
+
+
+test('time with value', function () {
+ $form = new Form;
+ $input = $form->addTime('time')
+ ->setValue(new Nette\Utils\DateTime('07:05'));
+
+ Assert::same('', (string) $input->getControl());
+});
+
+
+test('date-time with value', function () {
+ $form = new Form;
+ $input = $form->addDateTime('time')
+ ->setValue(new Nette\Utils\DateTime('2023-10-22 07:05'));
+
+ Assert::same('', (string) $input->getControl());
+});
diff --git a/tests/Forms/Controls.DateTimeControl.value.phpt b/tests/Forms/Controls.DateTimeControl.value.phpt
new file mode 100644
index 000000000..257b16142
--- /dev/null
+++ b/tests/Forms/Controls.DateTimeControl.value.phpt
@@ -0,0 +1,86 @@
+addDate('date');
+
+ Assert::exception(function () use ($input) {
+ $input->setValue([]);
+ }, Nette\InvalidArgumentException::class, 'Value must be DateTimeInterface or string or null, array given.');
+});
+
+
+test('date as string', function () {
+ $form = new Form;
+ $input = $form->addDate('date')
+ ->setValue('2013-07-05 10:30');
+
+ Assert::equal(new DateTimeImmutable('2013-07-05 00:00'), $input->getValue());
+});
+
+
+test('date as string timestamp', function () {
+ $form = new Form;
+ $input = $form->addDate('date')
+ ->setValue('254400000');
+
+ Assert::equal(new DateTimeImmutable('1978-01-23 00:00'), $input->getValue());
+});
+
+
+test('date as int timestamp', function () {
+ $form = new Form;
+ $input = $form->addDate('date')
+ ->setValue(254_400_000);
+
+ Assert::equal(new DateTimeImmutable('1978-01-23 00:00'), $input->getValue());
+});
+
+
+test('date as DateTime object', function () {
+ $form = new Form;
+ $input = $form->addDate('date')
+ ->setValue(new Nette\Utils\DateTime('2023-10-05 11:22:33.44'));
+
+ Assert::equal(new DateTimeImmutable('2023-10-05 00:00'), $input->getValue());
+});
+
+
+test('time as DateTime object', function () {
+ $form = new Form;
+ $input = $form->addTime('time')
+ ->setValue(new Nette\Utils\DateTime('2023-10-05 11:22:33.44'));
+
+ Assert::equal(new DateTimeImmutable('0000-01-01 11:22'), $input->getValue());
+});
+
+
+test('date-time as DateTime object', function () {
+ $form = new Form;
+ $input = $form->addDateTime('time')
+ ->setValue(new Nette\Utils\DateTime('2023-10-05 11:22:33.44'));
+
+ Assert::equal(new DateTimeImmutable('2023-10-05 11:22'), $input->getValue());
+});
+
+
+test('date-time with seconds as DateTime object', function () {
+ $form = new Form;
+ $input = $form->addDateTime('time', null, true)
+ ->setValue(new Nette\Utils\DateTime('2023-10-05 11:22:33.44'));
+
+ Assert::equal(new DateTimeImmutable('2023-10-05 11:22:33'), $input->getValue());
+});
diff --git a/tests/Forms/Controls.HiddenField.render.phpt b/tests/Forms/Controls.HiddenField.render.phpt
index 46ebfe265..bf6d5fec3 100644
--- a/tests/Forms/Controls.HiddenField.render.phpt
+++ b/tests/Forms/Controls.HiddenField.render.phpt
@@ -14,15 +14,6 @@ use Tester\Assert;
require __DIR__ . '/../bootstrap.php';
-class Translator implements Nette\Localization\ITranslator
-{
- public function translate($s, ...$parameters): string
- {
- return strtoupper($s);
- }
-}
-
-
test('', function () {
$form = new Form;
$input = $form->addHidden('hidden', 'value');