Skip to content

Conversation

evnchn
Copy link
Collaborator

@evnchn evnchn commented May 31, 2025

Motivation

Addresses NiceGUI Wishlist, about how NiceGUI does not have native ui.date_input and ui.time_input, and need to do https://nicegui.io/documentation/date#input_element_with_date_picker and https://nicegui.io/documentation/time#input_element_with_time_picker

Implementation

Progress

  • I chose a meaningful title that completes the sentence: "If applied, this PR will..."
  • The implementation is complete.
  • Pytests have been added (or are not necessary).
  • Documentation has been added (or is not necessary).

I need some help for the pytest...

Review with care, particularly to the situation of ui.date_input with range=True

@evnchn evnchn added feature Type/scope: New feature or enhancement in progress Status: Someone is working on it ⚪️ minor Priority: Low impact, nice-to-have 🌱 beginner Difficulty: Good for first-time contributors labels May 31, 2025
@falkoschindler falkoschindler self-requested a review May 31, 2025 14:22
@platinops
Copy link
Contributor

Hi @evnchn, while you're at it, would you mind also adding a DateTimeInput as mentioned here in the Quasar docs (codepen)?

@evnchn
Copy link
Collaborator Author

evnchn commented Jun 2, 2025

@platinops So this is my first time making a new UI element, so I'd like to take it slowly and get these two merged before working on that.

Also, I don't think a combined Date and Time input would be as urgent as the pair ui.date_input and ui.time_input, since you can achieve the same functionality with two separate inputs. Combining the inputs also creates many edge cases, such as what should happen when only choosing the date, not the time (or vice versa)

Not only that, personal opinion, it's totally not obvious that you click the calendar icon on the left to edit the date. I'd assume if you use this in production, users are going to be confused and ring up customer support asking "how can I change the date", so I am not sure if I should endorse sub-optimal UI design in the first place.

{46EAA1CC-B0D3-4F1A-8FD9-5E0FA719C5AB}

You can open a separate feature request, if you think a combined ui.date_time_input is still worth developing.

@platinops
Copy link
Contributor

Hi @evnchn.

See below implementation:

  • Alternative InputDate and InputTime using Quasar's mask and NiceGUI's bind_value, so no need for sync_value() (range property is not supported, but can maybe be implemented using NiceGUI's _value_to_model_value).
  • Replaced ui.button with ui.icon.
  • InputDateTime edge cases where no date/time is filled in are gracefully handled by Quasar.
  • I have been using a similar implementation (using NiceGUI's predecessor JustPy) for years and have yet to see the first customer support ticket about this :)

I could open a separate feature request / PR for this, but for a consistent API/implementation this seemed an appropriate place to discuss.

from typing import Optional, Any

from nicegui import ui

from nicegui.events import Handler, ValueChangeEventArguments, GenericEventArguments
from nicegui.elements.button import Button as button
from nicegui.elements.date import Date as date
from nicegui.elements.time import Time as time
from nicegui.elements.menu import Menu as menu
from nicegui.elements.icon import Icon as icon
from nicegui.elements.mixins.disableable_element import DisableableElement
from nicegui.elements.mixins.label_element import LabelElement
from nicegui.elements.mixins.value_element import ValueElement


class DateInput(LabelElement, ValueElement, DisableableElement):
    def __init__(
        self,
        label: Optional[str] = None,
        *,
        placeholder: Optional[str] = None,
        value: Optional[str] = "",
        on_change: Optional[Handler[ValueChangeEventArguments]] = None,
    ) -> None:
        """Date Input
        This element extends Quasar's `QInput <https://quasar.dev/vue-components/input>`_ component with a time picker.
        :param label: displayed label for the time input
        :param placeholder: text to show if no time is selected
        :param value: the current time value
        :param on_change: callback to execute when the value changes
        """
        super().__init__(
            tag="q-input", label=label, value=value, on_value_change=on_change
        )
        if placeholder is not None:
            self._props["placeholder"] = placeholder
        with self.add_slot("append"):
            with (
                icon("event")
                .on("click", lambda: self.date_menu.open())
                .classes("cursor-pointer")
            ):
                with menu() as self.date_menu:
                    date(mask="YYYY-MM-DD").bind_value(self)

class TimeInput(LabelElement, ValueElement, DisableableElement):
    def __init__(
        self,
        label: Optional[str] = None,
        *,
        with_seconds: Optional[bool] = False,
        placeholder: Optional[str] = None,
        value: str = "",
        on_change: Optional[Handler[ValueChangeEventArguments]] = None,
    ) -> None:
        """Time Input
        This element extends Quasar's `QInput <https://quasar.dev/vue-components/input>`_ component with a date picker.

        :param label: displayed label for the time input
        :param with_seconds: include seconds in time picker
        :param placeholder: text to show if no time is selected
        :param value: the current time value
        :param on_change: callback to execute when the value changes
        """
        super().__init__(
            tag="q-input", label=label, value=value, on_value_change=on_change
        )
        if placeholder is not None:
            self._props["placeholder"] = placeholder
        mask = "HH:mm"
        time_picker_props = "format24h"
        if with_seconds:
            mask += ":ss"
            time_picker_props += " with-seconds"
        with self.add_slot("append"):
            with (
                icon("access_time")
                .on("click", lambda: self.time_menu.open())
                .classes("cursor-pointer")
            ):
                with menu() as self.time_menu:
                    time(mask=mask).props(time_picker_props).bind_value(self)


class DateTimeInput(LabelElement, ValueElement, DisableableElement):
    def __init__(
        self,
        label: Optional[str] = None,
        *,
        with_seconds: Optional[bool] = False,
        placeholder: Optional[str] = None,
        value: Optional[str] = "",
        on_change: Optional[Handler[ValueChangeEventArguments]] = None,
    ) -> None:
        """Date+Time Input
        This element extends Quasar's `QInput <https://quasar.dev/vue-components/input>`_ component with a date and time picker.

        :param label: displayed label for the time input
        :param with_seconds: include seconds in time picker
        :param placeholder: text to show if no time is selected
        :param value: the current time value
        :param on_change: callback to execute when the value changes
        """
        super().__init__(
            tag="q-input", label=label, value=value, on_value_change=on_change
        )
        if placeholder is not None:
            self._props["placeholder"] = placeholder
        mask = "YYYY-MM-DD HH:mm"
        time_picker_props = "format24h"
        if with_seconds:
            mask += ":ss"
            time_picker_props += " with-seconds"
        with self.add_slot("prepend"):
            with (
                icon("event")
                .on("click", lambda: self.date_menu.open())
                .classes("cursor-pointer")
            ):
                with menu() as self.date_menu:
                    date(mask=mask).bind_value(self)
        with self.add_slot("append"):
            with (
                icon("access_time")
                .on("click", lambda: self.time_menu.open())
                .classes("cursor-pointer")
            ):
                with menu() as self.time_menu:
                    time(mask=mask).props(time_picker_props).bind_value(self)


date_input = DateInput(label="Date", placeholder="Enter date")
ui.label().bind_text_from(date_input, "value")

time_input = TimeInput(label="Time", placeholder="Enter time")
ui.label().bind_text_from(time_input, "value")

time_with_seconds_input = TimeInput(
    label="Time", placeholder="Enter time (with seconds)", with_seconds=True
)
ui.label().bind_text_from(time_with_seconds_input, "value")

datetime_input = DateTimeInput(label="DateTime", placeholder="Enter datetime")
ui.label().bind_text_from(datetime_input, "value")

datetimesec_input = DateTimeInput(
    label="DateTimeSec", placeholder="Enter datetime (with seconds)", with_seconds=True
)
ui.label().bind_text_from(datetimesec_input, "value")

ui.run()

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
🌱 beginner Difficulty: Good for first-time contributors feature Type/scope: New feature or enhancement in progress Status: Someone is working on it ⚪️ minor Priority: Low impact, nice-to-have
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants