Conversation
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
🟢 Approval recommended
The change is small, scoped, and preserves existing datepicker behavior while addressing the missing validation trigger on calendar selection.
Pull request overview
This PR fixes required field validation for date/time inputs when users select a date via the jQuery UI datepicker, ensuring the submit button state updates even when a native change event is not emitted. It does so by wiring a datepicker onSelect wrapper that preserves any existing callback and then triggers the existing required-field validation logic.
Changes:
- Wires required text-input fields to also bind a datepicker
onSelecthandler when the input is a jQuery UI datepicker. - Preserves any pre-existing datepicker
onSelecthandler before running required-field revalidation. - Triggers revalidation asynchronously after date selection to keep submit-button state in sync.
File summaries
| File | Description |
|---|---|
| KTL.js | Adds datepicker onSelect wiring so required date fields revalidate correctly after calendar selection while preserving existing callbacks. |
Review details
- Files reviewed: 1/1 changed files
- Comments generated: 0
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
There was a problem hiding this comment.
🟢 Approval recommended
The change is narrowly scoped, preserves existing datepicker behavior, and safely triggers the existing validation path to address the missing revalidation on calendar selection.
Review details
- Files reviewed: 1/1 changed files
- Comments generated: 0 new
- Review effort level: Lite
cortexrd
left a comment
There was a problem hiding this comment.
Automated review (Claude Code, run by Norm). Summary: installing an onSelect on required date inputs suppresses the native change event jQuery UI would otherwise fire, which regresses persistent form, KTL.fieldValueChanged, the app onFieldValueChanged callback and Knack's to_date sync, while not changing behavior for stock Knack (calendar picks already fire change.ktl_req today). Details inline. Recommended direction: one load-time wrapper around $.datepicker._selectDate that re-triggers change after any onSelect, plus a decision on which date_time sub-inputs _req covers (the empty sibling time input is the likely actual cause of the reported symptom).
| || datePickerInput.data('ktlReqDatePickerBound')) return; | ||
|
|
||
| const existingOnSelect = datePickerInput.datepicker('option', 'onSelect'); | ||
| datePickerInput.datepicker('option', 'onSelect', function (dateText, instance) { |
There was a problem hiding this comment.
Regression: installing an onSelect suppresses jQuery UI's native change.
In Knack's bundled jQuery UI, _selectDate does onSelect ? onSelect.apply(...) : inst.input.trigger('change'). Knack's form init sets no onSelect, so today a calendar pick fires change. After this PR, on any required date field a calendar pick fires only this wrapper, so:
- the
.kn-input-date_timechange hook (~6754) never runs, sopersistentForm.ktlOnFieldValueChangednever saves the date (the input/focusout path ~8324 explicitly skips.knack-date, so there is no fallback: reload and the picked date is gone) KTL.fieldValueChangedand the publicktl.fields.onFieldValueChangedcallback never fire- ktlCond
.one('change')subscriptions go dead - Knack's own
$('#view-field').change(...)that copies the date intoinput[name=to_date]for calendar-format fields stops, so the submitted event has a stale end date
Side effect: with an onSelect installed, jQuery UI's Enter-key branch calls onSelect instead of _hideDatepicker(), so Enter no longer closes the calendar on required date fields.
Minimum fix: else datePickerInput.trigger('change') in the wrapper. Better: see the comment at 18433.
| * @param {HTMLInputElement} input - Datepicker input to wire. | ||
| * @returns {void} | ||
| */ | ||
| function wireDatePickerValidation(input) { |
There was a problem hiding this comment.
The premise doesn't hold for stock Knack. renderForm runs synchronously before knack-view-render and attaches no onSelect, so jQuery UI already triggers change on every calendar pick, and the existing change.ktl_req binding (18219-18223) plus the 6754 -> KTL.fieldValueChanged -> 18450 path already call validateNonEmptyTextField. The only case where change is suppressed is an app-installed onSelect. Was the symptom reproduced on a stock form? If the real trigger is an app onSelect or the empty time sibling (see 18232), this change doesn't address it.
If the goal is to make calendar picks fire change regardless of who set onSelect, the general fix is about six lines, once at load:
const origSelectDate = $.datepicker._selectDate;
$.datepicker._selectDate = function (id, dateStr) {
const inst = this._getInst($(id)[0]);
origSelectDate.apply(this, arguments);
if (inst && this._get(inst, 'onSelect')) inst.input.trigger('change');
};That restores change for every consumer and makes wireDatePickerValidation, the flag and the per-render wiring unnecessary.
| || datePickerInput.data('ktlReqDatePickerBound')) return; | ||
|
|
||
| const existingOnSelect = datePickerInput.datepicker('option', 'onSelect'); | ||
| datePickerInput.datepicker('option', 'onSelect', function (dateText, instance) { |
There was a problem hiding this comment.
datepicker('option', ...) is not a pure setter. _optionDatepicker calls _getDateDatepicker(el, true) -> _setDateFromField -> _setDate, which re-parses and rewrites the input value. Since fieldIsRequired awaits validateKtlCond before wiring, this lands after _cfdt / _sfv / persistent-form restores or app prefill: an ISO 2026-09-11 or a partially typed value in a required date field is silently blanked (then the initial pass marks it empty and disables Submit), and a parseable 9/1/2026 is rewritten to 09/01/2026. Both happen via .val() with no event, so nothing else notices.
It also does _curInst == inst && _hideDatepicker() and unconditionally _updateDatepicker(inst), which rebuilds the single shared #ui-datepicker-div and rebinds its day cells to the wired input. A calendar open on another field when a required date field is wired (late async wiring, refreshView, a second form rendering) switches to the required field's month and the next day click writes into the wrong input.
Side-effect-free alternative if the per-input override is kept: $.data(input, 'datepicker').settings.onSelect = fn (jQuery UI's _get reads settings dynamically).
| || typeof datePickerInput.datepicker !== 'function' | ||
| || datePickerInput.data('ktlReqDatePickerBound')) return; | ||
|
|
||
| const existingOnSelect = datePickerInput.datepicker('option', 'onSelect'); |
There was a problem hiding this comment.
Chaining existingOnSelect is order-dependent. Wiring runs after await Promise.all([processFieldKeywords(), processViewKeywords()]), i.e. after every synchronous knack-view-render / scene-render handler. _optionDatepicker uses extendRemove(inst.settings, opts), a plain overwrite, so any app code that sets onSelect later replaces KTL's wrapper, and ktlReqDatePickerBound then prevents re-wiring. Real app code does exactly this (OIT Project Portfolio Manager calls datepicker('option', 'onSelect', fn) from knack-scene-render.scene_147 with a 100 ms retry), so the motivating scenario is lost again on that path.
| }); | ||
| inputField.each((_, input) => wireDatePickerValidation(input)); | ||
| // Initial validation pass | ||
| inputField.each((_, el) => validateNonEmptyTextField(viewContainer, el)); |
There was a problem hiding this comment.
Likely actual root cause, left untouched. inputField is every non-hidden input under [data-input-id]. Knack's date_time template renders type="text" name="time" whenever time_format != 'Ignore Time', prefilled only when time_type != 'none'. With date-default None + time-default None (the configuration where the date is empty on a new record), the time input renders empty, keeps ktlNotValid_empty, and hasVisibleEmpty (~18420) keeps Submit disabled after the calendar pick. The wrapper revalidates only the date input, so this PR changes nothing for that case. The fix belongs in deciding which sub-inputs _req means for date_time (date only, or date+time per format).
|
|
||
| setTimeout(() => validateNonEmptyTextField(viewContainer, input), 0); | ||
| }); | ||
| datePickerInput.data('ktlReqDatePickerBound', true); |
There was a problem hiding this comment.
ktlReqDatePickerBound is never cleared and outlives the datepicker instance: datepicker('destroy') removes only the datepicker data and marker class. refreshView re-runs fieldIsRequired on the same DOM, where this guard skips wiring while change.ktl_req is rebound fresh, so the onSelect keeps pass-1 closures. Any destroy + re-create or later onSelect set leaves a fresh instance with no KTL onSelect and a flag that blocks re-wiring for the life of the element. A stateless check avoids the flag entirely: tag the wrapper function (wrapper.ktlReq = true) and skip when existingOnSelect?.ktlReq is set.
| if (typeof existingOnSelect === 'function') | ||
| existingOnSelect.call(this, dateText, instance); | ||
|
|
||
| setTimeout(() => validateNonEmptyTextField(viewContainer, input), 0); |
There was a problem hiding this comment.
Nits:
setTimeout(..., 0)buys nothing:_selectDatedoesinst.input.val(dateStr)before callingonSelect, so the value is already set. This makes it the only async validation path infieldIsRequired.typeof datePickerInput.datepicker !== 'function'(18436) is unreachable:hasDatepickeris only ever added by jQuery UI's_connectDatepicker/_inlineDatepicker.- 18230 and 18232 are two back-to-back
inputField.eachloops over the same set. - The JSDoc says jQuery UI calls
onSelect"without reliably emitting a native change event"; the actual behavior is deterministic (it fireschangeonly when noonSelectis set). Worth stating the observed symptom instead.
|
TL;DR for the review above (the seven inline comments are one issue seen from several angles, not seven separate tasks; no need to resolve each one). The problem in one sentence: jQuery UI's datepicker fires the input's native What to do:
Note on style: using jQuery here is fine. Vanilla JS is a preference in this repo, not a rule. |
This is just plain wrong as we had a date only field. We have had to implement this in multiple apps as Knack does not trigger a change on selecting a date. The reproduction is a date-only field, not date_time. On the tested KTL/Knack runtime, selecting a date does not trigger the field’s change handler. With wireDatePickerValidation removed, the suggested _selectDate wrapper has no effect unless an existing onSelect is present—which it is not in this reproduction. Please identify the existing onSelect you believe is suppressing change, or revise the recommendation to preserve the required-field revalidation path. |
Summary
Changelog
Validation