|
| 1 | +# WFormDatePicker |
| 2 | + |
| 3 | +A form-integrated date picker that wraps [WDatePicker](./w-date-picker.md) with Flutter's `FormField<DateTime>`. Provides native form validation, automatic `error:` state styling, and optional label, hint, and error message display. |
| 4 | + |
| 5 | +- [Basic Usage](#basic-usage) |
| 6 | +- [Constructor](#constructor) |
| 7 | +- [Props](#props) |
| 8 | +- [Form Validation](#form-validation) |
| 9 | +- [Range Mode in Forms](#range-mode-in-forms) |
| 10 | +- [Event Handling](#event-handling) |
| 11 | +- [State Variants](#state-variants) |
| 12 | +- [Styling Examples](#styling-examples) |
| 13 | +- [All Supported Classes](#all-supported-classes) |
| 14 | +- [Customizing Theme](#customizing-theme) |
| 15 | +- [Related Documentation](#related-documentation) |
| 16 | + |
| 17 | +<x-preview path="forms/form_date_picker_basic" size="md" source="example/lib/pages/forms/form_date_picker_basic.dart"></x-preview> |
| 18 | + |
| 19 | +```dart |
| 20 | +WFormDatePicker( |
| 21 | + label: 'Birth Date', |
| 22 | + hint: 'We need your date of birth for verification', |
| 23 | + className: 'p-3 border border-gray-300 rounded-lg error:border-red-500', |
| 24 | + validator: (value) => value == null ? 'Please select a date' : null, |
| 25 | + onChanged: (date) => print('Selected: $date'), |
| 26 | +) |
| 27 | +``` |
| 28 | + |
| 29 | +## Basic Usage |
| 30 | + |
| 31 | +`WFormDatePicker` works inside a Flutter `Form` widget. It renders a vertical layout with an optional label above the date picker and error/hint text below. |
| 32 | + |
| 33 | +```dart |
| 34 | +final _formKey = GlobalKey<FormState>(); |
| 35 | +
|
| 36 | +Form( |
| 37 | + key: _formKey, |
| 38 | + child: Column( |
| 39 | + children: [ |
| 40 | + WFormDatePicker( |
| 41 | + label: 'Event Date', |
| 42 | + className: 'p-3 border rounded-lg error:border-red-500', |
| 43 | + validator: (date) => date == null ? 'Date is required' : null, |
| 44 | + onChanged: (date) => _eventDate = date, |
| 45 | + ), |
| 46 | + WButton( |
| 47 | + className: 'mt-4 bg-blue-500 text-white px-4 py-2 rounded-lg', |
| 48 | + onPressed: () { |
| 49 | + if (_formKey.currentState!.validate()) { |
| 50 | + _formKey.currentState!.save(); |
| 51 | + } |
| 52 | + }, |
| 53 | + child: WText('Submit'), |
| 54 | + ), |
| 55 | + ], |
| 56 | + ), |
| 57 | +) |
| 58 | +``` |
| 59 | + |
| 60 | +When validation fails, the widget automatically adds the `error` state to the inner `WDatePicker`, activating any `error:` prefixed classes. |
| 61 | + |
| 62 | +## Constructor |
| 63 | + |
| 64 | +```dart |
| 65 | +WFormDatePicker({ |
| 66 | + Key? key, |
| 67 | + // FormField params |
| 68 | + FormFieldValidator<DateTime>? validator, |
| 69 | + FormFieldSetter<DateTime>? onSaved, |
| 70 | + AutovalidateMode? autovalidateMode, |
| 71 | + bool enabled = true, |
| 72 | + // WDatePicker params |
| 73 | + DateTime? initialValue, |
| 74 | + DateRange? initialRange, |
| 75 | + DatePickerMode mode = DatePickerMode.single, |
| 76 | + ValueChanged<DateTime>? onChanged, |
| 77 | + ValueChanged<DateRange>? onRangeChanged, |
| 78 | + DateTime? minDate, |
| 79 | + DateTime? maxDate, |
| 80 | + String? className, |
| 81 | + String? placeholder, |
| 82 | + Set<String>? states, |
| 83 | + DateDisplayFormat? displayFormat, |
| 84 | + // Form wrapper params |
| 85 | + String? label, |
| 86 | + String labelClassName = 'text-sm font-medium text-gray-700 dark:text-gray-300 mb-1', |
| 87 | + bool showError = true, |
| 88 | + String errorClassName = 'text-red-500 text-xs mt-1', |
| 89 | + String? hint, |
| 90 | + String hintClassName = 'text-gray-500 text-xs mt-1', |
| 91 | +}) |
| 92 | +``` |
| 93 | + |
| 94 | +## Props |
| 95 | + |
| 96 | +### FormField Props |
| 97 | + |
| 98 | +| Prop | Type | Default | Description | |
| 99 | +|:-----|:-----|:--------|:------------| |
| 100 | +| `validator` | `FormFieldValidator<DateTime>?` | `null` | Validation function, receives `DateTime?` | |
| 101 | +| `onSaved` | `FormFieldSetter<DateTime>?` | `null` | Called when form is saved | |
| 102 | +| `autovalidateMode` | `AutovalidateMode?` | `null` | When to auto-validate | |
| 103 | +| `enabled` | `bool` | `true` | Whether the picker is interactive | |
| 104 | +| `initialValue` | `DateTime?` | `null` | Initial selected date (single mode) | |
| 105 | + |
| 106 | +### Date Picker Props |
| 107 | + |
| 108 | +| Prop | Type | Default | Description | |
| 109 | +|:-----|:-----|:--------|:------------| |
| 110 | +| `mode` | `DatePickerMode` | `single` | Selection mode: `single` or `range` | |
| 111 | +| `initialRange` | `DateRange?` | `null` | Initial date range (range mode) | |
| 112 | +| `onChanged` | `ValueChanged<DateTime>?` | `null` | Called on date selection (single mode) | |
| 113 | +| `onRangeChanged` | `ValueChanged<DateRange>?` | `null` | Called on range selection (range mode) | |
| 114 | +| `minDate` | `DateTime?` | `null` | Earliest selectable date | |
| 115 | +| `maxDate` | `DateTime?` | `null` | Latest selectable date | |
| 116 | +| `className` | `String?` | `null` | Wind utility classes for the trigger | |
| 117 | +| `placeholder` | `String?` | `null` | Placeholder text (defaults to `'Select date'`) | |
| 118 | +| `states` | `Set<String>?` | `null` | Custom states for dynamic styling | |
| 119 | +| `displayFormat` | `DateDisplayFormat?` | `null` | Custom date display format function | |
| 120 | + |
| 121 | +### Layout Props |
| 122 | + |
| 123 | +| Prop | Type | Default | Description | |
| 124 | +|:-----|:-----|:--------|:------------| |
| 125 | +| `label` | `String?` | `null` | Label text displayed above the picker | |
| 126 | +| `labelClassName` | `String` | `'text-sm font-medium text-gray-700 dark:text-gray-300 mb-1'` | Wind classes for the label | |
| 127 | +| `showError` | `bool` | `true` | Whether to display validation error text | |
| 128 | +| `errorClassName` | `String` | `'text-red-500 text-xs mt-1'` | Wind classes for error message | |
| 129 | +| `hint` | `String?` | `null` | Hint text displayed below the picker | |
| 130 | +| `hintClassName` | `String` | `'text-gray-500 text-xs mt-1'` | Wind classes for hint text | |
| 131 | + |
| 132 | +> [!NOTE] |
| 133 | +> When both an error and hint are present, the error message takes priority and the hint is hidden. |
| 134 | +
|
| 135 | +## Form Validation |
| 136 | + |
| 137 | +The `validator` function receives the currently selected `DateTime?` value. Return a `String` to show an error, or `null` if valid. |
| 138 | + |
| 139 | +```dart |
| 140 | +WFormDatePicker( |
| 141 | + label: 'Departure Date', |
| 142 | + className: 'p-3 border rounded-lg error:border-red-500 error:bg-red-50', |
| 143 | + validator: (date) { |
| 144 | + if (date == null) return 'Please select a departure date'; |
| 145 | + if (date.isBefore(DateTime.now())) return 'Date must be in the future'; |
| 146 | + return null; |
| 147 | + }, |
| 148 | +) |
| 149 | +``` |
| 150 | + |
| 151 | +### Auto-Validation |
| 152 | + |
| 153 | +Use `autovalidateMode` to validate as the user interacts: |
| 154 | + |
| 155 | +```dart |
| 156 | +WFormDatePicker( |
| 157 | + label: 'Start Date', |
| 158 | + autovalidateMode: AutovalidateMode.onUserInteraction, |
| 159 | + className: 'p-3 border rounded-lg error:border-red-500', |
| 160 | + validator: (date) => date == null ? 'Required' : null, |
| 161 | +) |
| 162 | +``` |
| 163 | + |
| 164 | +### Using onSaved |
| 165 | + |
| 166 | +```dart |
| 167 | +DateTime? _savedDate; |
| 168 | +
|
| 169 | +WFormDatePicker( |
| 170 | + label: 'Meeting Date', |
| 171 | + className: 'p-3 border rounded-lg', |
| 172 | + onSaved: (date) => _savedDate = date, |
| 173 | + validator: (date) => date == null ? 'Required' : null, |
| 174 | +) |
| 175 | +``` |
| 176 | + |
| 177 | +## Range Mode in Forms |
| 178 | + |
| 179 | +In range mode, the `FormField<DateTime>` internally tracks the range's **start date** for validation purposes. This means a simple null check still works as a "required" validator. |
| 180 | + |
| 181 | +<x-preview path="forms/form_date_picker_range" size="md" source="example/lib/pages/forms/form_date_picker_range.dart"></x-preview> |
| 182 | + |
| 183 | +```dart |
| 184 | +WFormDatePicker( |
| 185 | + label: 'Trip Dates', |
| 186 | + mode: DatePickerMode.range, |
| 187 | + initialRange: null, |
| 188 | + className: 'p-3 border rounded-lg error:border-red-500', |
| 189 | + placeholder: 'Select check-in / check-out', |
| 190 | + validator: (date) => date == null ? 'Please select your trip dates' : null, |
| 191 | + onRangeChanged: (range) { |
| 192 | + if (range.isComplete) { |
| 193 | + _calculateTripDuration(range); |
| 194 | + } |
| 195 | + }, |
| 196 | +) |
| 197 | +``` |
| 198 | + |
| 199 | +> [!NOTE] |
| 200 | +> The `validator` receives the range's start `DateTime` — not a `DateRange` object. For more complex range validation (e.g., minimum stay duration), use the `onRangeChanged` callback to manage validation externally. |
| 201 | +
|
| 202 | +## Event Handling |
| 203 | + |
| 204 | +`onChanged` and `onRangeChanged` fire alongside the form state updates. The `FormFieldState` is updated automatically — you don't need to call `didChange` yourself. |
| 205 | + |
| 206 | +```dart |
| 207 | +WFormDatePicker( |
| 208 | + label: 'Appointment', |
| 209 | + onChanged: (date) { |
| 210 | + // FormField state is already updated at this point |
| 211 | + _loadAvailableSlots(date); |
| 212 | + }, |
| 213 | +) |
| 214 | +``` |
| 215 | + |
| 216 | +## State Variants |
| 217 | + |
| 218 | +`WFormDatePicker` inherits all state variants from `WDatePicker` and adds the `error` state automatically when validation fails. |
| 219 | + |
| 220 | +| State | Activated When | |
| 221 | +|:------|:---------------| |
| 222 | +| `hover:` | Mouse hovers over the trigger | |
| 223 | +| `focus:` | Calendar popover is open | |
| 224 | +| `open:` | Calendar popover is open (alias) | |
| 225 | +| `disabled:` | `enabled` is `false` | |
| 226 | +| `selected:` | A date or range has been selected | |
| 227 | +| `error:` | Form validation has failed | |
| 228 | + |
| 229 | +```dart |
| 230 | +WFormDatePicker( |
| 231 | + label: 'Date', |
| 232 | + className: 'p-3 border border-gray-300 rounded-lg ' |
| 233 | + 'hover:border-blue-400 ' |
| 234 | + 'focus:border-blue-500 focus:ring-2 focus:ring-blue-200 ' |
| 235 | + 'error:border-red-500 error:ring-2 error:ring-red-200 ' |
| 236 | + 'disabled:opacity-50 disabled:bg-gray-100', |
| 237 | + validator: (date) => date == null ? 'Required' : null, |
| 238 | +) |
| 239 | +``` |
| 240 | + |
| 241 | +## Styling Examples |
| 242 | + |
| 243 | +### Standard Form Field |
| 244 | + |
| 245 | +```dart |
| 246 | +WFormDatePicker( |
| 247 | + label: 'Date of Birth', |
| 248 | + hint: 'You must be at least 18 years old', |
| 249 | + className: 'w-full p-3 border border-gray-300 rounded-lg ' |
| 250 | + 'focus:border-blue-500 focus:ring-2 focus:ring-blue-200 ' |
| 251 | + 'error:border-red-500', |
| 252 | + maxDate: DateTime.now().subtract(const Duration(days: 365 * 18)), |
| 253 | + validator: (date) => date == null ? 'Date of birth is required' : null, |
| 254 | +) |
| 255 | +``` |
| 256 | + |
| 257 | +### Dark Mode Ready |
| 258 | + |
| 259 | +```dart |
| 260 | +WFormDatePicker( |
| 261 | + label: 'Schedule', |
| 262 | + labelClassName: 'text-sm font-medium text-gray-700 dark:text-gray-300', |
| 263 | + className: 'p-3 bg-white dark:bg-gray-800 ' |
| 264 | + 'border border-gray-300 dark:border-gray-600 rounded-lg ' |
| 265 | + 'hover:border-blue-400 dark:hover:border-blue-500 ' |
| 266 | + 'error:border-red-500 dark:error:border-red-400', |
| 267 | + errorClassName: 'text-red-500 dark:text-red-400 text-xs mt-1', |
| 268 | + validator: (date) => date == null ? 'Required' : null, |
| 269 | +) |
| 270 | +``` |
| 271 | + |
| 272 | +### Minimalist |
| 273 | + |
| 274 | +```dart |
| 275 | +WFormDatePicker( |
| 276 | + label: 'When', |
| 277 | + labelClassName: 'text-xs text-gray-500 uppercase tracking-wider', |
| 278 | + className: 'border-b border-gray-200 py-2 rounded-none ' |
| 279 | + 'focus:border-blue-500 error:border-red-500', |
| 280 | + hintClassName: 'text-gray-400 text-xs mt-0.5', |
| 281 | + hint: 'Optional', |
| 282 | +) |
| 283 | +``` |
| 284 | + |
| 285 | +## All Supported Classes |
| 286 | + |
| 287 | +### Trigger (className) |
| 288 | + |
| 289 | +All Wind utility classes work on the trigger container. The most commonly used: |
| 290 | + |
| 291 | +| Category | Examples | |
| 292 | +|:---------|:---------| |
| 293 | +| Background | `bg-white`, `bg-gray-50`, `dark:bg-gray-800` | |
| 294 | +| Border | `border`, `border-gray-300`, `rounded-lg` | |
| 295 | +| Padding | `p-3`, `px-4`, `py-2` | |
| 296 | +| Sizing | `w-full`, `w-64` | |
| 297 | +| Ring | `ring-2`, `ring-blue-200` | |
| 298 | +| Shadow | `shadow-sm` | |
| 299 | +| State prefixes | `hover:`, `focus:`, `disabled:`, `selected:`, `error:`, `dark:` | |
| 300 | + |
| 301 | +### Label (labelClassName) |
| 302 | + |
| 303 | +Default: `text-sm font-medium text-gray-700 dark:text-gray-300 mb-1` |
| 304 | + |
| 305 | +### Error (errorClassName) |
| 306 | + |
| 307 | +Default: `text-red-500 text-xs mt-1` |
| 308 | + |
| 309 | +### Hint (hintClassName) |
| 310 | + |
| 311 | +Default: `text-gray-500 text-xs mt-1` |
| 312 | + |
| 313 | +## Customizing Theme |
| 314 | + |
| 315 | +`WFormDatePicker` inherits all theme customization from `WDatePicker`. Override Tailwind color scales in `WindThemeData` to change selection colors, error states, and calendar chrome: |
| 316 | + |
| 317 | +```dart |
| 318 | +WindTheme( |
| 319 | + data: WindThemeData( |
| 320 | + colors: { |
| 321 | + ...WindThemeData.defaultColors, |
| 322 | + 'red': { |
| 323 | + 500: Color(0xFFEF4444), // Error border color |
| 324 | + 200: Color(0xFFFECACA), // Error ring color |
| 325 | + }, |
| 326 | + }, |
| 327 | + ), |
| 328 | + child: MyApp(), |
| 329 | +) |
| 330 | +``` |
| 331 | + |
| 332 | +## Related Documentation |
| 333 | + |
| 334 | +- [WDatePicker](./w-date-picker.md) - The base date picker widget |
| 335 | +- [WFormInput](./w-form-input.md) - Form-integrated text input |
| 336 | +- [WFormSelect](./w-form-select.md) - Form-integrated dropdown |
| 337 | +- [WFormCheckbox](./w-form-checkbox.md) - Form-integrated checkbox |
| 338 | +- [WPopover](./w-popover.md) - The underlying overlay engine |
0 commit comments