Repository navigation
[feature] Add on_change="ignore" to datetime_input - #17267
Conversation
Let datetime_input buffer a committed value until the next rerun, matching date_input and time_input. Co-authored-by: Cursor <cursoragent@cursor.com>
✅ Snyk checks have passed. No issues have been found so far.
💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse. |
✅ PR preview is ready!
|
|
There was a problem hiding this comment.
Summary
st.datetime_input now accepts on_change="ignore", matching st.date_input and st.time_input. A committed datetime stays in the browser until the next rerun. With bind="query-params", the URL updates when that value is committed. Inside st.form, ignore leaves form batching unchanged.
The implementation is a proto flag (ignore_rerun = 16), the same validate_on_change_mode path the other time widgets use, and triggerRerun: false on the existing updateWidgetMgrState commit helper. Every user commit (Enter, popover close, paste, clear, blur) already goes through that helper.
Product Alignment
This PR is user-facing. It changes the public on_change parameter of st.datetime_input ("ignore", "rerun", a callback, or None).
The product decisions are already approved in specs/2026-04-14-on-change-modes/product-spec.md, which is on develop and lists st.datetime_input as an affected widget. This PR does not edit that spec. The PR body points at the spec and at #5827; it does not claim a separate approval, and it does not need one. This is a rollout of that spec to a widget the spec already names, not a new API.
The implementation follows the spec and the date/time inputs that already shipped under it:
- The default is
"rerun".Noneremains an alias. Omittingon_changestill reruns the app. "ignore"setsignore_rerun. The frontend buffers the committed value and does not schedule a rerun. Python sees the value on the next rerun, from the return value or session state.- A bound widget updates the URL when the value is committed. Typing a segment leaves the URL unchanged. That is the same commit boundary
st.date_inputandst.time_inputuse for segmented fields under this spec. - Inside
st.form, the form still owns commit timing. The docstring says ignore has no extra effect there. - A callback still reruns the script and runs before the rest of the app.
- The docstring lists each literal, the lost-on-refresh caveat, and the
bind/ form interaction, in the same shape as the date and time docstrings.
That matches Consistency, Same Name Same Behavior, and Patterns Are Sacred: one parameter name, the same modes, the same defaults. Ignore stays opt-in (Sensible Defaults, Progressive Disclosure, Minimize Migration Distance) and composes with bind="query-params" and st.form (Design for Composition). No new command or parameter is added.
Code Quality
The change stays on the existing commit path. DateTimeInput updates widget state through useBasicWidgetState → updateWidgetMgrState, and the form-commit helper calls that same function. One spread, ...(element.ignoreRerun ? { triggerRerun: false } : {}), covers every commit gesture. WidgetStateManager already buffers triggerRerun: false and still syncs a bound URL.
The backend matches time_input and date_input: validate_on_change_mode(..., supported_modes=("rerun", "ignore")), widget policies see the normalized callback, and ignore_rerun is set only when the original argument is the string "ignore". Field number 16 is the next free number on DateTimeInput. Generated protobufs are gitignored; CI compiles them with make protobuf.
Complexity is proportionate. There is no new abstraction, state, or dependency.
Performance
No meaningful performance impact. The flag is read once per datetime commit, which is a user gesture, not a per-message or per-rerun hot path. The change does not add effects, remounts, full-state scans, copies, or extra serialization. Rerun suppression uses the existing triggerRerun buffer in WidgetStateManager. No new performance coverage is needed.
Test Coverage
Coverage matches the date and time input rollout.
- Python unit tests set
ignore_rerunfor"ignore","rerun",None, and a callback; invalid modes and a non-callable raiseStreamlitValueError;"ignore"is allowed inside a form. - Typing tests cover
on_change=None,"rerun","ignore", andvalue=Nonewith"ignore". - Frontend tests assert
triggerRerun: falsefor blur, calendar close, clear, and paste; they assert the flag is omitted and a rerun is sent whenignoreRerunis false; keystrokes do not commit; a form still only records a pending form. A direct helper test covers the flag inupdateWidgetMgrState. - The e2e scenario covers typing without a commit, Escape commit, delivery on the next rerun, type-then-click, calendar close (time preserved), paste, query-param updates, and reload.
NUM_DATETIME_INPUTSis 21. Existing snapshots target labeled widgets, so the new widget does not change those images.
Enter outside a form and form submit share updateWidgetMgrState and the existing form batching in WidgetStateManager. The form unit test checks that ignore still only marks the form pending.
Backwards Compatibility
Existing calls keep their behavior. Omitting on_change, passing None, or passing a callback still reruns. The signature default changes from None to "rerun", which the merged spec treats as a signature clarification: the omitted argument still reruns. on_change is not part of the element id, so keyed widgets do not reset. The new proto field defaults to false, so a message without the flag still reruns.
Security & Risk
No new endpoint, HTML rendering, script execution, file handling, cookie, or auth path. A bound ignore-mode datetime is written through the existing query-param sync as the user's datetime string, which bind="query-params" already does for this widget.
External test recommendation
- Recommend external_test: No
- Triggered categories: None
- Evidence:
proto/streamlit/proto/DateTimeInput.proto: addsignore_rerunon the widget message. No route, header, cookie, or session-transport change.lib/streamlit/elements/widgets/time_widgets.py: sets that flag fromon_change="ignore". No server URL, auth, or storage change.frontend/lib/src/components/widgets/DateTimeInput/dateTimeInputUtils.ts: passestriggerRerun: falseinto the existingsetStringArrayValuepath. URL updates stay inWidgetStateManager.maybeSyncValueToUrl.e2e_playwright/st_datetime_input_test.py: asserts the query param for a bound ignore-mode widget. That uses the existing client-side query-param sync.
- Suggested external_test focus areas:
- None. Local widget and e2e coverage exercises commit, rerun suppression, and query-param update.
- Confidence: High
- Assumptions and gaps: Query-param writes continue to use the existing
history.replaceStatepath. This PR does not change routing, iframe/postMessagebehavior, cookies,localStorage/sessionStorage, CORS, CSP, or SiS/Snowflake session handling. Embedded hosts that already runbind="query-params"on other ignore-mode widgets see the same URL-sync mechanism.
Accessibility
The widget's roles, accessible names, and keyboard model are unchanged. Ignore mode uses the same commit gestures (Enter, Escape, blur, paste, clear). The e2e Apply control is a standard st.button in the sidebar.
Readability
frontend/lib/src/components/widgets/DateTimeInput/DateTimeInput.test.tsx (around lines 1847–1850)
The new comment explains a real focus race, and it opens with the focus-move mechanics. Lead with the rule the next reader has to follow.
// The popover stays open after a date click. Wait until the calendar
// cell is focused before editing the time, or the next key changes the
// highlighted day instead of the hour.PR title
[feature] Add on_change ignore to datetime_input is within the [type] Description limit and stands alone. Quoting the mode makes the API value obvious in a changelog:
[feature] Add on_change="ignore" to datetime_input
PR description
The body leads with the behavior and the two non-obvious rules (the URL updates on commit; a form already waits). The testing section names the files and what they cover. Drop the Made with Cursor line; it adds no review information.
Recommendations
No code changes are required before merge. Optional wording tweaks are in Readability.
Verdict
APPROVED: on_change="ignore" on st.datetime_input follows the merged spec and the date/time input implementation, with unit and e2e coverage of commit, query-param, and rerun behavior.
This is an automated AI review by grok-4.7-xhigh. Please verify the feedback and use your judgment.
Lead with the rule the next reader has to follow: wait for the calendar cell to take focus before editing the time. Co-authored-by: Cursor <cursoragent@cursor.com>
|
Addressed the optional readability notes in the AI review:
There were no inline review threads to resolve. |
There was a problem hiding this comment.
Summary
st.datetime_input accepts on_change="ignore", the mode already shipped for st.date_input and st.time_input. A committed datetime stays in frontend widget state and does not schedule a rerun; the next rerun delivers it to Python. With bind="query-params", the URL updates on commit. Inside st.form, the flag does not change form batching.
The change is a proto flag (DateTimeInput.ignore_rerun = 16), backend wiring through validate_on_change_mode(..., supported_modes=("rerun", "ignore")), and triggerRerun: false on the existing setStringArrayValue path in updateWidgetMgrState. The default stays a rerun ("rerun", with None still accepted). Tests cover the proto flag, typing, frontend commit paths, and one aggregated Playwright scenario.
All four expected reviews completed (gpt-5.6-sol-high, claude-opus-5-5-high, gemini-3.8-flash-high, grok-4.7-xhigh). None failed. All four approve. They agree the implementation follows the merged product spec and the sibling widgets, and that there is no security, compatibility, performance, or coverage blocker. They differ on two non-blocking points: optional wording in the e2e comment and PR description, and whether bound query-param updates require @pytest.mark.external_test. The external-test checklist does not hit a category here, so the recommendation is No.
Product Alignment
This PR is user-facing: it extends the public on_change parameter on st.datetime_input (accepted values, default, docstring, and commit/URL behavior).
specs/2026-04-14-on-change-modes/product-spec.md is already on develop and lists st.datetime_input among the widgets that should accept on_change="ignore". Those decisions are approved, and every reviewer found the implementation follows them:
"rerun"is the default;Noneremains an alias and still reruns."ignore"updates the UI and frontend state, does not rerun, and sends the value on the next rerun.- A callable still reruns and runs as the callback.
bind="query-params"updates the URL when the value is committed, before Python receives it. Unbound ignored values are lost on refresh.- Inside
st.form, ignore has no extra effect because the form already defers commit.
The docstring lists datetime-specific commit actions (Enter, popover close, paste, clear) and states that a typed segment alone does not commit. That matches st.date_input and st.time_input and principles 10 (same name, same behavior) and 11 (patterns are sacred) in specs/AGENTS.md. The PR body points at the spec and issue #5827 and does not introduce a product decision outside the spec.
One review noted that other widgets in that spec (for example st.pills and st.segmented_control) still need the same mode. That is follow-up work, not a gap in this PR.
Code Quality
Reviewers agree the change is small and matches the existing widget pattern. validate_on_change_mode separates the raw mode from the callback passed to check_widget_policies and register_widget, so "ignore" inside a form does not raise the form-callback error. Every datetime commit already goes through updateWidgetMgrState, so one conditional covers blur, Enter, calendar close, paste, clear, and in-form commits. Complexity is proportionate to the feature.
The frontend unit test that types a time after a calendar click now waits until the calendar cell is focused. That stabilizes existing keyboard behavior and does not change production focus handling.
Performance
No meaningful runtime impact. The new work is a boolean on the datetime proto and a conditional when a user commits a value. It does not sit on element creation, ForwardMsg hashing, delta queueing, session-state scans, or shared renderers. triggerRerun: false avoids a rerun. Dedicated performance coverage is not warranted.
Test Coverage
Coverage matches the sibling widgets and is sufficient. Reviewers agree on this.
- Python unit tests check the proto flag for
"ignore","rerun",None, and a callback; invalid modes and non-callables raiseStreamlitValueError;"ignore"is allowed inside a form. - Typing tests cover
"rerun","ignore",None, andvalue=Nonewith"ignore". - Frontend tests assert
triggerRerun: falseon blur, calendar close, clear, and paste; keystrokes do not commit; form batching is unchanged;ignoreRerun: falsestill reruns. - The e2e scenario checks no rerun on type, Escape, calendar selection, and paste; query-param updates; the buffered value on the next button rerun; type-then-click; and reload via the URL.
NUM_DATETIME_INPUTSis updated to 21.
Backwards Compatibility
Existing apps are unchanged. Omitting on_change, passing None, or passing a callback still reruns, and a callback still runs. The signature default changes from None to "rerun"; the spec treats that as a non-breaking clarification, and both values normalize to the same handler. ignore_rerun defaults to false in proto3. Widget identity does not include on_change.
Security & Risk
No new routes, auth, file handling, HTML rendering, dependencies, or executable code. Ignored values stay on the existing widget-state and query-param binding path. The URL receives the widget's ISO datetime string only when bind="query-params" is set. The flag is opt-in, and the commit path is shared with the default rerun mode.
External test recommendation
- Recommend external_test: No
- Triggered categories: None
- Evidence:
proto/streamlit/proto/DateTimeInput.proto: addsignore_rerun. No route, handshake, cookie, storage, CSP, or asset-serving change.lib/streamlit/elements/widgets/time_widgets.py: sets that flag fromon_change="ignore"and reusesvalidate_on_change_mode. Query-param binding still uses the existingquery_param_keyfield.frontend/lib/src/components/widgets/DateTimeInput/dateTimeInputUtils.ts: passestriggerRerun: falseintosetStringArrayValue. URL sync stays in the existingWidgetStateManagerhistory.replaceStatepath. This diff does not change Starlette routes,server.baseUrlPath, redirects, cookies,localStorage,sessionStorage, iframe messaging, or host-page discovery.e2e_playwright/st_datetime_input_test.py: asserts the bound query string in the local Playwright app.
- Suggested external_test focus areas:
- None. Local unit and e2e coverage already exercises commit-without-rerun and the query-param round-trip. The same URL sync already serves
st.date_input,st.time_input, and form binding.
- None. Local unit and e2e coverage already exercises commit-without-rerun and the query-param round-trip. The same URL sync already serves
- Confidence: High
- Assumptions and gaps: One review recommended Yes under "Routing and URL behavior" because a bound ignored commit updates the browser URL. That category applies when a change introduces or modifies Starlette routes,
server.baseUrlPath, catch-alls, request methods, URL resolution, redirects, or status codes. Those files are untouched here, so the category is not hit. The other three reviews recommended No. Generated protobufs are gitignored and compiled in CI; this review assumes that compile matches the proto source.
Accessibility
No new controls, labels, focus behavior, or keyboard paths in production. Ignore mode uses the same commit gestures (Enter, Escape, blur, calendar, paste, clear). The calendar-focus wait in the unit test documents existing focus behavior.
Readability
Production comments, docstrings, and names in time_widgets.py, dateTimeInputUtils.ts, and DateTimeInput.proto match st.date_input and st.time_input. All four reviews left that wording as-is.
The e2e sidebar comment in e2e_playwright/st_datetime_input.py is the one wording disagreement. One review asked for a plainer description of why Apply sits in the sidebar; the others found the current comment sufficient. A suggested rewrite is posted inline. Either wording is acceptable.
PR title. [feature] Add on_change="ignore" to datetime_input stands alone, uses the [type] Description form, and is within the length limit. No change.
PR description. The body leads with the user-visible behavior and calls out URL updates and forms. Reviewers offered optional polish only: mention that this mirrors date and time input and that an existing popover test was stabilized; shorten the testing-plan file list; or make the opening sentence active. The current description is clear enough for review.
Recommendations
- No changes are required before merge.
- Optional: apply the inline rewrite of the sidebar comment in
e2e_playwright/st_datetime_input.pyif you want that note to spell out the click sequence. - Optional follow-up: the remaining widgets in
specs/2026-04-14-on-change-modes/product-spec.mdcan take the same mode in later PRs.
Verdict
APPROVED: All four reviews approve this spec-aligned extension of on_change="ignore" to st.datetime_input, and the only disagreements are optional wording and an external-test classification that the checklist does not support.
This is an automated consolidated AI review by grok-4.7-xhigh. Please verify the feedback and use your judgment.
This review also includes 1 inline comment(s) on specific code lines.
Co-authored-by: Cursor <cursoragent@cursor.com>
## Describe your changes Adds keyword-only `required` on `st.datetime_input` so empty (`None`) values cannot be committed or submitted in a form. Follows the client-side commit and form-submit path of `st.text_input` / `st.number_input` / `st.date_input` / `st.time_input` (#16956, #17023, #17165, #17420). Next wave-1 widget from the [required-widgets spec](https://github.com/streamlit/streamlit/blob/develop/specs/2026-08-18-required-widgets/product-spec.md) (#16555), after `st.text_input`, `st.number_input`, `st.date_input`, and `st.time_input`. - Outside a form, an empty commit keeps the last accepted value and shows `This field is required.` (no rerun, no query-param write). Form submit (button or Enter) runs every required field's validator and does not `clear_on_submit` on failure. - Visible labels get a muted `(required)` marker, and the clear X for `value=None` fields is hidden. - Only `None` counts as empty. A date typed without a time is incomplete, not empty: it reverts on blur as it does today, and a required form still refuses to submit while no datetime is committed. - Composes with `on_change="ignore"` (#17267, which landed after the spec said datetime had no ignore mode): a blocked empty value never overwrites a value that `ignore` mode is holding back from rerunning. - Toggling `required` remounts an unkeyed field and keeps the stored value when a `key` is set (`required` is hashed only for unkeyed identity). Internal changes worth a look: - `SingleDateTimeInput` drops its `formCommit` prop. The parent's single commit function now also does the synchronous in-form write, so each commit path makes one call (existing write-count tests unchanged). - Enter-to-submit now gates on the commit result instead of the `error` prop. That prop is from before the commit, so a required error the commit just cleared swallowed the first Enter. Range errors already make the commit fail, so optional fields behave the same. - A `session_state` value equal to the committed one remounts the field (via the renamed `resetKey`) only when the field shows something else, e.g. after a blocked clear. Apps that assign session state on every rerun do not remount. - Paste remembers a value for commit dedup only when the parent accepts it, like the other commit paths. A rejected out-of-range paste no longer skips the commit on a later blur once `min_value` / `max_value` allow it. ## GitHub Issue Link (if applicable) - Related to #7165 - Spec: #16555 ## Testing Plan - [x] Unit Tests (JS and/or Python) - [x] E2E Tests - `lib/tests/streamlit/elements/datetime_input_test.py` — proto marshal, widget identity, defaults, `disabled` / `on_change="ignore"` composition, AppTest - `lib/tests/streamlit/typing/datetime_input_types.py` — return types do not narrow - `frontend/lib/src/components/widgets/DateTimeInput/DateTimeInput.test.tsx` — commit gating on every exit path, form submit, Enter-to-submit, incomplete values, popover time, session_state resync, leftover required error, ignore + query-param composition - `e2e_playwright/st_datetime_input_test.py` — form submit, standalone empty clear, hidden label, snapshots (new Linux snapshots come from CI) Follow-ups shared with `st.date_input` (not in this PR): `aria-required` sits on the field wrapper and segment group, where screen readers may not announce it; the spec's accessible-name fallback would cover hidden labels. --- **Contribution License Agreement** By submitting this pull request you agree that all contributions to this project are made under the Apache 2.0 license. 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Touches form submit timing, commit/dedup paths, and session_state resync in a complex datetime widget; behavior is heavily tested but regressions in forms or Enter-to-submit are the main concern. > > **Overview** > Adds **`required`** to **`st.datetime_input`**, aligned with other required widgets: empty values are blocked on commit outside forms (last value kept, no rerun), form submit runs validators and fails without `clear_on_submit`, labels show **`(required)`**, and the clear control is hidden when the default is empty. > > **Frontend:** `DateTimeInput` centralizes commits in **`commitIsoValue`** (required + range checks, sync form writes), registers **form submit validators**, and tracks pending vs last-written ISO via refs. **`SingleDateTimeInput`** drops separate **`formCommit`**; **`onChange`** returns whether the parent accepted the commit; **`allowEmptyCommit`** / **`onEdit`** support required empty-clear behavior; Enter-to-submit keys off the commit result, not stale **`error`**; **`formResetKey`** becomes **`resetKey`** for form clear and programmatic resync remounts. > > **Tests & tooling:** Large **`DateTimeInput.test.tsx`** **`required`** suite; Playwright demo widgets and tests (shared **`clear_date_segments`** moved from date tests); proto field **`required = 17`**; agent docs updated. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 27b1a13. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY --> <!-- agent-metrics-start --> <details> <summary>Agent metrics</summary> | Type | Name | Count | |------|------|------:| | skill | checking-changes | 2 | | skill | debugging-streamlit | 1 | | skill | finalizing-pr | 1 | | skill | sharing-pr-agent-artifacts | 1 | | skill | updating-internal-docs | 1 | | subagent | fixing-pr | 5 | | subagent | general-purpose | 7 | | subagent | qa-testing-feature | 1 | | subagent | reviewing-local-changes | 1 | | subagent | simplifying-local-changes | 1 | </details> <!-- agent-metrics-end --> --------- Co-authored-by: lukasmasuch <lukas.masuch@gmail.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Streamlit Bot <core+streamlitbot-github@streamlit.io> Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Lukas Masuch <sfc-gh-lmasuch@users.noreply.github.com>
Describe your changes
st.datetime_inputacceptson_change="ignore", so a committed datetime is held until the next rerun instead of rerunning the app immediately.bind="query-params", the URL updates when the value is committed. Typing a segment alone does not.st.form, ignore has no extra effect: the form already waits until submit.GitHub Issue Link (if applicable)
specs/2026-04-14-on-change-modes/product-spec.mdTesting Plan
lib/tests/streamlit/elements/datetime_input_test.py— ignore mode sets the proto flag, invalid modes raise, and ignore is allowed inside a formfrontend/lib/src/components/widgets/DateTimeInput/— commits passtriggerRerun: falsee2e_playwright/st_datetime_input_test.py— no rerun on commit, the query param updates, and the buffered value arrives on the next rerun