diff --git a/assets/controllers/birthday_controller.js b/assets/controllers/birthday_controller.js new file mode 100644 index 0000000..f9cc334 --- /dev/null +++ b/assets/controllers/birthday_controller.js @@ -0,0 +1,59 @@ +import { Controller } from '@hotwired/stimulus' + +/** + * Monitors birthday input fields (day, month, year) and dispatches a custom event + * only when all three fields are filled with valid values. + * + * This prevents premature HTMX refreshes when users are still entering the date, + * improving UX by waiting until the complete date is entered. + */ +export default class extends Controller { + static targets = ['day', 'month', 'year'] + + /** + * Checks if all birthday fields are filled and dispatches a completion event. + * + * Called on input/change events from the day, month, and year fields. + * Only dispatches the 'birthday:complete' event when all three fields + * contain valid values that could form a complete date. + */ + check() { + const day = this.dayTarget.value.trim() + const month = this.monthTarget.value.trim() + const year = this.yearTarget.value.trim() + + if (false === this.isComplete(day, month, year)) { + return + } + + this.element.dispatchEvent(new CustomEvent('birthday:complete', { bubbles: true })) + } + + /** + * Validates that all date components are filled with plausible values. + * + * @param {string} day The day value + * @param {string} month The month value + * @param {string} year The year value + * + * @returns {boolean} True if all fields contain complete values + */ + isComplete(day, month, year) { + // Day: 1-2 digits + if (0 === day.length || day.length > 2) { + return false + } + + // Month: 1-2 digits + if (0 === month.length || month.length > 2) { + return false + } + + // Year: exactly 4 digits for a complete year + if (4 !== year.length) { + return false + } + + return true + } +} diff --git a/templates/forms.html.twig b/templates/forms.html.twig index 464cfb7..5f9da02 100644 --- a/templates/forms.html.twig +++ b/templates/forms.html.twig @@ -253,8 +253,24 @@ 'year': 'Jahr' } -%} {%- set field_order = ['day', 'month', 'year'] -%} + + {# Check if HTMX attributes are present - if so, use birthday controller for deferred triggering #} + {%- set has_htmx = attr['hx-trigger'] is defined -%} + {%- set htmx_attrs = {} -%} + {%- if has_htmx -%} + {# Extract HTMX attributes and replace trigger with custom event #} + {%- set htmx_attrs = { + 'hx-trigger': 'birthday:complete', + 'hx-post': attr['hx-post']|default(''), + 'hx-target': attr['hx-target']|default('#main-content'), + 'hx-swap': attr['hx-swap']|default('innerHTML') + } -%} + {# Remove HTMX attrs from main attr to avoid duplication #} + {%- set attr = attr|filter((v, k) => k not in ['hx-trigger', 'hx-post', 'hx-target', 'hx-swap']) -%} + {%- endif -%} + {%- set attr = attr|merge({'class': (attr.class|default('') ~ ' flex items-center space-x-2')|trim}) -%} -
+
{%- for field_name in field_order -%} {%- set child = form[field_name] -%} {%- set child_attr = child.vars.attr|default({}) -%} @@ -262,6 +278,12 @@ 'placeholder': placeholders[field_name], 'class': (child_attr.class|default('') ~ ' form-field')|trim }) -%} + {%- if has_htmx -%} + {%- set child_attr = child_attr|merge({ + 'data-birthday-target': field_name, + 'data-action': 'change->birthday#check' + }) -%} + {%- endif -%} {{- form_widget(child, {'attr': child_attr}) -}} {% if not loop.last %}