From c836bc8329523375ba318870559dca8a6e725e68 Mon Sep 17 00:00:00 2001 From: Gaspard Merten Date: Sun, 19 Jul 2020 16:49:06 +0200 Subject: [PATCH] Adding support for time --- CHANGELOG.md | 16 +- example/main.dart | 2 +- lib/date_field.dart | 421 ++++++++++++++++++++++++++++++++++++++------ 3 files changed, 380 insertions(+), 59 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1721be3..8667562 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,4 +31,18 @@ ## 0.2.2 -* Auto-formatting with dart-fm to meet pub.dev requirements \ No newline at end of file +* Auto-formatting with dart-fm to meet pub.dev requirements + +## 0.3.0 + +Breaking changes: +* No more const constructor. + +Deprecated: +* DateField and DateFormField are now deprecated and will be removed in the next version, please consider switching to +DateTimeField and DateTimeFormField. + +Improvements: +* Adding support for time. Now you can ask the user for a time, a date or both. +* Improving performances by setting default value in the constructor. +* Adding .time constructor for the DateField widget only. diff --git a/example/main.dart b/example/main.dart index a62eb65..eb9e75f 100644 --- a/example/main.dart +++ b/example/main.dart @@ -29,7 +29,7 @@ class _HomeWidgetState extends State { children: [ Column( children: [ - DateField( + DateTimeField( selectedDate: selectedDate, onDateSelected: (DateTime date) { diff --git a/lib/date_field.dart b/lib/date_field.dart index 86b4cde..527bedc 100644 --- a/lib/date_field.dart +++ b/lib/date_field.dart @@ -2,31 +2,32 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; -/// A [FormField] that contains a [DateField]. +/// A [FormField] that contains a [DateTimeField]. /// -/// This is a convenience widget that wraps a [DateField] widget in a +/// This is a convenience widget that wraps a [DateTimeField] widget in a /// [FormField]. /// /// A [Form] ancestor is not required. The [Form] simply makes it easier to /// save, reset, or validate multiple fields at once. To use without a [Form], /// pass a [GlobalKey] to the constructor and use [GlobalKey.currentState] to /// save or reset the form field. -class DateFormField extends FormField { - DateFormField( - {Key key, - FormFieldSetter onSaved, - FormFieldValidator validator, - DateTime initialValue, - bool autovalidate = false, - bool enabled = true, - this.onDateSelected, - this.firstDate, - this.lastDate, - this.label = 'Select date', - this.dateFormat, - this.decoration, - this.initialDatePickerMode = DatePickerMode.day}) - : super( +class DateTimeFormField extends FormField { + DateTimeFormField({ + Key key, + FormFieldSetter onSaved, + FormFieldValidator validator, + DateTime initialValue, + bool autovalidate = false, + bool enabled = true, + this.onDateSelected, + this.firstDate, + this.lastDate, + this.label = 'Select date', + this.dateFormat, + this.decoration, + this.initialDatePickerMode = DatePickerMode.day, + this.mode = DateFieldPickerMode.date, + }) : super( key: key, initialValue: initialValue, onSaved: onSaved, @@ -43,18 +44,18 @@ class DateFormField extends FormField { field.didChange(value); } - return DateField( - label: label, - firstDate: firstDate, - lastDate: lastDate, - decoration: decoration, - initialDatePickerMode: initialDatePickerMode, - dateFormat: dateFormat, - errorText: state.errorText, - onDateSelected: onChangedHandler, - selectedDate: state.value, - enabled: enabled, - ); + return DateTimeField( + label: label, + firstDate: firstDate, + lastDate: lastDate, + decoration: decoration, + initialDatePickerMode: initialDatePickerMode, + dateFormat: dateFormat, + errorText: state.errorText, + onDateSelected: onChangedHandler, + selectedDate: state.value, + enabled: enabled, + mode: mode); }, ); @@ -80,30 +81,55 @@ class DateFormField extends FormField { /// (optional) Let you choose the [DatePickerMode] for the date picker! (default is [DatePickerMode.day] final DatePickerMode initialDatePickerMode; + /// Whether to ask the user to pick only the date, the time or both. + final DateFieldPickerMode mode; + @override _DateFormFieldState createState() => _DateFormFieldState(); } class _DateFormFieldState extends FormFieldState {} -/// [DateField] +/// [DateTimeField] /// -/// Shows an [_InputDropdown] that'll trigger [DateField._selectDate] whenever the user +/// Shows an [_InputDropdown] that'll trigger [DateTimeField._selectDate] whenever the user /// clicks on it ! The date picker is **platform responsive** (ios date picker style for ios, ...) -class DateField extends StatelessWidget { +class DateTimeField extends StatelessWidget { /// Default constructor - const DateField({ + DateTimeField({ + Key key, @required this.onDateSelected, @required this.selectedDate, - this.firstDate, - this.lastDate, this.initialDatePickerMode = DatePickerMode.day, this.decoration, this.errorText, - this.dateFormat, this.label = 'Select date', this.enabled = true, - }); + this.mode = DateFieldPickerMode.dateAndTime, + DateTime firstDate, + DateTime lastDate, + DateFormat dateFormat, + }) : dateFormat = dateFormat ?? getDateFormatFromDateFieldPickerMode(mode), + firstDate = firstDate ?? DateTime(1900), + lastDate = lastDate ?? DateTime(2100), + super(key: key); + + DateTimeField.time({ + Key key, + this.onDateSelected, + this.selectedDate, + this.label, + this.errorText, + this.decoration, + this.enabled, + DateTime firstDate, + DateTime lastDate, + }) : initialDatePickerMode = null, + mode = DateFieldPickerMode.time, + dateFormat = DateFormat.jm(), + firstDate = firstDate ?? DateTime(2000), + lastDate = lastDate ?? DateTime(2001), + super(key: key); /// Callback for whenever the user selects a [DateTime] final ValueChanged onDateSelected; @@ -135,18 +161,23 @@ class DateField extends StatelessWidget { /// (optional) Whether the field is usable. If false the user won't be able to select any date final bool enabled; + /// Whether to ask the user to pick only the date, the time or both. + final DateFieldPickerMode mode; + /// Shows a dialog asking the user to pick a date ! Future _selectDate(BuildContext context) async { + final DateTime initialDateTime = selectedDate ?? lastDate ?? DateTime.now(); + if (Theme.of(context).platform == TargetPlatform.iOS) { showModalBottomSheet( context: context, builder: (BuildContext builder) { return Container( - height: MediaQuery.of(context).size.height / 4, + height: 216, child: CupertinoDatePicker( - mode: CupertinoDatePickerMode.date, + mode: _cupertinoModeFromPickerMode(mode), onDateTimeChanged: onDateSelected, - initialDateTime: selectedDate ?? lastDate ?? DateTime.now(), + initialDateTime: initialDateTime, minimumDate: firstDate, maximumDate: lastDate, ), @@ -154,16 +185,40 @@ class DateField extends StatelessWidget { }, ); } else { - final DateTime _selectedDate = await showDatePicker( - context: context, - initialDatePickerMode: initialDatePickerMode, - initialDate: selectedDate ?? lastDate ?? DateTime.now(), - firstDate: firstDate ?? DateTime(1900), - lastDate: lastDate ?? DateTime(2100)); + DateTime _selectedDateTime = initialDateTime; - if (_selectedDate != null) { - onDateSelected(_selectedDate); + if ([DateFieldPickerMode.dateAndTime, DateFieldPickerMode.date] + .contains(mode)) { + final DateTime _selectedDate = await showDatePicker( + context: context, + initialDatePickerMode: initialDatePickerMode, + initialDate: initialDateTime, + firstDate: firstDate, + lastDate: lastDate); + + if (_selectedDate != null) { + _selectedDateTime = _selectedDate; + } } + + if ([DateFieldPickerMode.dateAndTime, DateFieldPickerMode.time] + .contains(mode)) { + final TimeOfDay _selectedTime = await showTimePicker( + initialTime: TimeOfDay.fromDateTime(initialDateTime), + context: context, + ); + + if (_selectedTime != null) { + _selectedDateTime = DateTime( + _selectedDateTime.year, + _selectedDateTime.month, + _selectedDateTime.day, + _selectedTime.hour, + _selectedTime.minute); + } + } + + onDateSelected(_selectedDateTime); } } @@ -171,30 +226,55 @@ class DateField extends StatelessWidget { Widget build(BuildContext context) { String text; - if (selectedDate != null) - text = (dateFormat ?? DateFormat.yMMMd()).format(selectedDate); + if (selectedDate != null) text = dateFormat.format(selectedDate); return _InputDropdown( text: text ?? label, label: text == null ? null : label, errorText: errorText, decoration: decoration, - onPressed: enabled - ? () { - _selectDate(context); - } - : null, + onPressed: enabled ? () => _selectDate(context) : null, ); } } +/// Those values are used by the [DateTimeField] widget to determine whether to ask +/// the user for the time, the date or both. +enum DateFieldPickerMode { time, date, dateAndTime } + +/// Returns the [CupertinoDatePickerMode] corresponding to the selected +/// [DateFieldPickerMode]. This exists to prevent redundancy in the [DateTimeField] +/// widget parameters. +CupertinoDatePickerMode _cupertinoModeFromPickerMode(DateFieldPickerMode mode) { + switch (mode) { + case DateFieldPickerMode.time: + return CupertinoDatePickerMode.time; + case DateFieldPickerMode.date: + return CupertinoDatePickerMode.date; + default: + return CupertinoDatePickerMode.dateAndTime; + } +} + +/// Returns the corresponding default [DateFormat] for the selected [DateFieldPickerMode] +DateFormat getDateFormatFromDateFieldPickerMode(DateFieldPickerMode mode) { + switch (mode) { + case DateFieldPickerMode.time: + return DateFormat.jm(); + case DateFieldPickerMode.date: + return DateFormat.yMMMMd(); + default: + return DateFormat.yMd().add_jm(); + } +} + /// /// [_InputDropdown] /// /// Shows a field with a dropdown arrow ! /// It does not show any popup menu, it'll just trigger onPressed whenever the /// user does click on it ! -class _InputDropdown extends StatelessWidget { + class _InputDropdown extends StatelessWidget { const _InputDropdown({ Key key, @required this.text, @@ -254,3 +334,230 @@ class _InputDropdown extends StatelessWidget { ); } } + + + +/// Deprecated! Use [DateTimeFormField] instead +@Deprecated('This widget will be removed in the next version of the date_field package, consider switching to DateTimeFormField') +class DateFormField extends FormField { + DateFormField({ + Key key, + FormFieldSetter onSaved, + FormFieldValidator validator, + DateTime initialValue, + bool autovalidate = false, + bool enabled = true, + this.onDateSelected, + this.firstDate, + this.lastDate, + this.label = 'Select date', + this.dateFormat, + this.decoration, + this.initialDatePickerMode = DatePickerMode.day, + this.mode = DateFieldPickerMode.date, + }) : super( + key: key, + initialValue: initialValue, + onSaved: onSaved, + validator: validator, + autovalidate: autovalidate, + enabled: enabled, + builder: (FormFieldState field) { + final _DateFormFieldState state = field; + + void onChangedHandler(DateTime value) { + if (onDateSelected != null) { + onDateSelected(value); + } + field.didChange(value); + } + + return DateTimeField( + label: label, + firstDate: firstDate, + lastDate: lastDate, + decoration: decoration, + initialDatePickerMode: initialDatePickerMode, + dateFormat: dateFormat, + errorText: state.errorText, + onDateSelected: onChangedHandler, + selectedDate: state.value, + enabled: enabled, + mode: mode); + }, + ); + + /// (optional) A callback that will be triggered whenever a new + /// DateTime is selected + final ValueChanged onDateSelected; + + /// (optional) The first date that the user can select (default is 1900) + final DateTime firstDate; + + /// (optional) The last date that the user can select (default is 2100) + final DateTime lastDate; + + /// (optional) The label to display for the field (default is 'Select date') + final String label; + + /// (optional) Custom [InputDecoration] for the [InputDecorator] widget + final InputDecoration decoration; + + /// (optional) How to display the [DateTime] for the user (default is [DateFormat.yMMMD]) + final DateFormat dateFormat; + + /// (optional) Let you choose the [DatePickerMode] for the date picker! (default is [DatePickerMode.day] + final DatePickerMode initialDatePickerMode; + + /// Whether to ask the user to pick only the date, the time or both. + final DateFieldPickerMode mode; + + @override + _DateFormFieldState createState() => _DateFormFieldState(); +} + +/// Deprecated! Use [DateField] instead +@Deprecated('This widget will be removed in the next version of the date_field package, consider switching to DateTimeField') +class DateField extends StatelessWidget { + /// Default constructor + DateField({ + Key key, + @required this.onDateSelected, + @required this.selectedDate, + this.initialDatePickerMode = DatePickerMode.day, + this.decoration, + this.errorText, + this.label = 'Select date', + this.enabled = true, + this.mode = DateFieldPickerMode.dateAndTime, + DateTime firstDate, + DateTime lastDate, + DateFormat dateFormat, + }) : dateFormat = dateFormat ?? getDateFormatFromDateFieldPickerMode(mode), + firstDate = firstDate ?? DateTime(1900), + lastDate = lastDate ?? DateTime(2100), + super(key: key); + + DateField.time({ + Key key, + this.onDateSelected, + this.selectedDate, + this.label, + this.errorText, + this.decoration, + this.enabled, + DateTime firstDate, + DateTime lastDate, + }) : initialDatePickerMode = null, + mode = DateFieldPickerMode.time, + dateFormat = DateFormat.jm(), + firstDate = firstDate ?? DateTime(2000), + lastDate = lastDate ?? DateTime(2001), + super(key: key); + + /// Callback for whenever the user selects a [DateTime] + final ValueChanged onDateSelected; + + /// The current selected date to display inside the field + final DateTime selectedDate; + + /// (optional) The first date that the user can select (default is 1900) + final DateTime firstDate; + + /// (optional) The last date that the user can select (default is 2100) + final DateTime lastDate; + + /// Let you choose the [DatePickerMode] for the date picker! (default is [DatePickerMode.day] + final DatePickerMode initialDatePickerMode; + + /// The label to display for the field (default is 'Select date') + final String label; + + /// (optional) The error text that should be displayed under the field + final String errorText; + + /// (optional) Custom [InputDecoration] for the [InputDecorator] widget + final InputDecoration decoration; + + /// (optional) How to display the [DateTime] for the user (default is [DateFormat.yMMMD]) + final DateFormat dateFormat; + + /// (optional) Whether the field is usable. If false the user won't be able to select any date + final bool enabled; + + /// Whether to ask the user to pick only the date, the time or both. + final DateFieldPickerMode mode; + + /// Shows a dialog asking the user to pick a date ! + Future _selectDate(BuildContext context) async { + final DateTime initialDateTime = selectedDate ?? lastDate ?? DateTime.now(); + + if (Theme.of(context).platform == TargetPlatform.iOS) { + showModalBottomSheet( + context: context, + builder: (BuildContext builder) { + return Container( + height: 216, + child: CupertinoDatePicker( + mode: _cupertinoModeFromPickerMode(mode), + onDateTimeChanged: onDateSelected, + initialDateTime: initialDateTime, + minimumDate: firstDate, + maximumDate: lastDate, + ), + ); + }, + ); + } else { + DateTime _selectedDateTime = initialDateTime; + + if ([DateFieldPickerMode.dateAndTime, DateFieldPickerMode.date] + .contains(mode)) { + final DateTime _selectedDate = await showDatePicker( + context: context, + initialDatePickerMode: initialDatePickerMode, + initialDate: initialDateTime, + firstDate: firstDate, + lastDate: lastDate); + + if (_selectedDate != null) { + _selectedDateTime = _selectedDate; + } + } + + if ([DateFieldPickerMode.dateAndTime, DateFieldPickerMode.time] + .contains(mode)) { + final TimeOfDay _selectedTime = await showTimePicker( + initialTime: TimeOfDay.fromDateTime(initialDateTime), + context: context, + ); + + if (_selectedTime != null) { + _selectedDateTime = DateTime( + _selectedDateTime.year, + _selectedDateTime.month, + _selectedDateTime.day, + _selectedTime.hour, + _selectedTime.minute); + } + } + + onDateSelected(_selectedDateTime); + } + } + + @override + Widget build(BuildContext context) { + String text; + + if (selectedDate != null) text = dateFormat.format(selectedDate); + + return _InputDropdown( + text: text ?? label, + label: text == null ? null : label, + errorText: errorText, + decoration: decoration, + onPressed: enabled ? () => _selectDate(context) : null, + ); + } +} \ No newline at end of file