เนื้อหาต่อไปนี้ จะมาดูต่อเกี่ยวกับการใช้งานฟอร์ม ต่อจาก
เนื้อหาตอนที่แล้ว ที่เราพูดถึงเกี่ยวกับการใช้งาน TextFormField
เป็นส่วนใหญ่ ยังมี widget เพิ่มเติมที่ใช้งามร่วมกับฟอร์ม รวมไปถึง
การจัดการกับข้อมูลที่ได้จากฟอร์ม เพื่อนำไปใช้งานต่อ
ทบทวนตอนที่แล้วได้ที่บทความ
การใช้งาน Form และ Form Validation ใน Flutter http://niik.in/1048
https://www.ninenik.com/content.php?arti_id=1048 via @ninenik
*เนื้อหานี้ใช้เนื้อหาต่อเนื่องจากบทความ http://niik.in/1048
การใช้งาน Checkbox
เราสามารถกำหนด checkbox ให้กับฟอร์มด้วย 2 widget คือ Checkbox กับ CheckboxListTile
แต่เราจะแนะนำเป็น CheckboxListTile() ที่จะใช้งานได้งายและสะดวกกว่า เพราะเป็นการนำเอา ListTile กับ
Checkbox มารวมกัน สามารถกดที่พื้นที่ของ ListTile หรือข้อความแทนการกดที่ตัว checkbox โดยตรง
สามารถจัดตำแหน่งไม่ว่าจะไว้ด้านหน้าข้อความ หรือด้านหลังข้อความก็ทำได้ง่าย
กำหนด State property ที่เกี่ยวข้อง
bool _termsChecked = false;
ดูตัวอย่างการใช้งาน checkbox ทั้งสองแบบ
ListTile( title: Text('This is title'), trailing: Checkbox( value: _termsChecked, onChanged: (bool? value) { setState(() { _termsChecked = value!; }); }, ), ), CheckboxListTile( value: _termsChecked, onChanged: (value) { setState(() { _termsChecked = value!; }); }, subtitle: !_termsChecked ? Text( 'Required', style: TextStyle(color: Colors.red, fontSize: 12.0), ) : null, title: new Text( 'I agree to the terms and condition', ), controlAffinity: ListTileControlAffinity.leading, ),
ผลลัพธ์ที่ได้
ตัวแรกเราต้องจัดรูปแบบใน ListTile อีกที แต่ตัวที่สองเราสามารถใช้งานคล้าย ListTile ได้เลย
ในที่นี้จะพูดถึง CheckboxListTile
ตัว checkbox จะรองรับค่าหรือ value ที่เป็น boolean เวลาเราจะใช้งาน ต้องกำหนดตัวแปร boolean
เพื่อรับค่ามาใช้งาน ใช้สำหรับตอบรับ หรือปฏิเสธในกรณีเงื่อนไขให้เลือก 1 รายการ อย่างในตัวอย่าง
เป็นการให้เลือก ตอบรับ ข้อกำหนดของการใช้งาน
ในกรณีใช้เป็นตัวเลือกหลายๆ รายการ จะหมายถึง ตอบรับกับรายการตัวเลือกนั้นๆ หรือไม่ ดูตัวอย่าง
// กำหนดตัวแปร ลิสรายการ checkbox List<Map<String, bool>> hobbies = [ {'อ่านหนังสือ': true}, {'วาดรูป': false}, {'ดูหนัง': true}, {'ช้อปปิ้ง': true}, ]; // กำหนดตัวแปร เก็บค่าของแต่ละ checkbxo List<bool> _checkHobby = [];
ต่อไปส่วนของการวนลูปแสดงข้อมูล และใช้งาน
Divider(), // ตัว widget แบ่ง Builder(builder: (context) { // เราใช้ Builder เพื่อที่จะใช้งานฟังก์ชั่นสร้าง widget ได้ List<Widget> list = <Widget>[]; hobbies.asMap().forEach((index, hobby){ // วนลูปสร้างลิสรายการ var key = hobby.keys.toList(); // แปลงเป็น list ของ key var val = hobby.values.toList(); // แปลงเป็น list ของ value _checkHobby.add(val[0]); // เก็บค่า value ขแงแต่ละรายการ list.add(CheckboxListTile( value: _checkHobby[index], // ใช้ค่า value ของแต่ละรายการ onChanged: (value) { setState(() { _checkHobby[index] = value!; // เปลี่ยนค่าเมื่อมีการเลือกหรือไม่เลือก }); }, title: Text( '${key[0]}', ), controlAffinity: ListTileControlAffinity.leading, )); }); return Column( // คืนค่าเป็นรายการ checkbox ในคอลัมน์ children: list, ); }),
ผลลัพธ์ที่ได้
กรณีมีตัวเลือกหลายรายการ จะเป็นลักษณะ ว่าแต่ละรายการเราเลือกหรือไม่
การใช้งาน Radio
รูปแบบการใช้งาน radio ร่วมกับฟอร์ม ก็สามารถทำได้คล้ายๆ กับ checkbox โดยเราสามารถใช้ได้ทั้ง
Radio กับ RadioListTile และวิธีการที่สะดวกและง่ายก็แนะนำเป็น RadioListTile
radio จะใช้สำหรับให้เลือกอย่างใดอย่างหนึ่งเพียงอย่างเดียว จากรายการที่แสดงให้เลือก โดยค่าที่กำหนดให้
กับ radio จะเป็น object แตกต่างจาก checkbox ซึ่ง object หรือ class ที่เหมาะจะมาใช้เป็นข้อมูล radio ก็คือ
enum ( มีอธิบายไว้ในบทความ http://niik.in/1044 )
อย่างสมมติเช่น เรากำหนดสีตัวเลือก ให้ผู้ใช้ระบุ ก็จะกำหนดเป็น
enum ColorOption { red, green, blue }
อย่าลืมว่า enum เป็น class หนึ่ง ดังนั้นเวลาระบุ ก็ต้องกำหนดไว้ด้านนอกของ class อื่นๆ
ปกติจะใช้ radio ในการกำหนดตัวเลือกที่ไม่มากนัก ดูตัวอย่าง การเลือกเพศ ชาย หญิง
สิ่งแรกก็คือกำหนด class หรือ object ของค่าข้อมูลที่จะใช้งาน
enum Gender { male, female }
จากนั้นเราก็กำหนดตัวแปรค่าเริ่มต้น
// กำหนดตัวแปรค่าเริ่มต้นของรายการที่่ถูกเลือก Gender? _selectedGender = Gender.male; // กำหนดตัวแปรสำหรับใช้เก็บข้อความอ้างอิง String? _selectedGenderText = 'ชาย';
เรากำหนดตัวแปรค่าเริ่มต้นสำหรับรายการที่ถูกเลือก และกำหนดตัวแปร
ข้อมูลเพิ่มเติม สำหรับนำไปใช้งาน อย่างข้างต้น ให้ค่าเริ่มต้นเป็น male และข้อความ
ที่สัมพันธ์ก็คือ เพศ 'ชาย'
ตัวอย่างการเรียกใช้งาน RadioListTile
Column( children: <Widget>[ RadioListTile( title: const Text('Male'), value: Gender.male, // ค่าของตัวเล็อก male groupValue: _selectedGender, // ใช้กลุ่มค่าที่ถูกเลือกเป็นตัวแปรเดียวกัน onChanged: (Gender? value) { setState(() { _selectedGender = value; _selectedGenderText = (_selectedGender == Gender.male) ? 'ชาย' : 'หญิง'; }); }, controlAffinity: ListTileControlAffinity.leading, ), RadioListTile( title: const Text('Female'), value: Gender.female, // ค่าของตัวเล็อก female groupValue: _selectedGender, // ใช้กลุ่มค่าที่ถูกเลือกเป็นตัวแปรเดียวกัน onChanged: (Gender? value) { setState(() { _selectedGender = value; _selectedGenderText = (_selectedGender == Gender.male) ? 'ชาย' : 'หญิง'; }); }, controlAffinity: ListTileControlAffinity.leading, ), ], ),
ผลลัพธ์ที่ได้
ค่าเริ่มต้นที่ถูกเลือกเป็น male เมื่อเรากดปุ่ม submit ก็จะแสดงในส่วนของข้อความที่เรากำหนดไว้ใช้งาน
ให้สัมพันธ์กับข้อมูลที่เลือก
สมมติเราอยากสร้างรายการ radio รองรับจำนวนมากขึ้นมาหน่อย ก็สามารถใช้เป็นแบบนี้ได้
enum ColorOption { red, green, blue }
จากนั้นเราก็กำหนดตัวแปรค่าเริ่มต้น
// กำหนดตัวแปรค่าเริ่มต้นของรายการที่่ถูกเลือก ColorOption? _selectedColorOption = ColorOption.red; // กำหนดตัวแปรสำหรับใช้เก็บข้อความอ้างอิง String _selectedColorOptionText = 'สีแดง'; // กำหนดตัวแปรสำหรับใช้เก็บข้อความอ้างอิงในลูป List<String> _listColorOptionText = ['สีแดง', 'สีเขียว', 'สีน้ำเงิน'];
ตัวอย่างการเรียกใช้งาน RadioListTile
Divider(), Builder(builder: (context) { List<Widget> list = <Widget>[]; ColorOption.values.asMap().forEach((index, val){ list.add( RadioListTile( title: Text(_listColorOptionText[index]), value: val, // ค่าของตัวเล็อก female groupValue: _selectedColorOption, // ใช้กลุ่มค่าที่ถูกเลือกเป็นตัวแปรเดียวกัน onChanged: (ColorOption? value) { setState(() { _selectedColorOption = value; _selectedColorOptionText = _listColorOptionText[index]; }); }, controlAffinity: ListTileControlAffinity.leading, ), ); }); return Column( children: list, ); }),
ผลลัพธ์ที่ได้
วิธีนี้เหมาะกับรายการตัวเลือกที่มีจำนวนมากๆ เวลากำหนดก็จะทำได้ง่ายขึ้น กว่าการเพิ่มทีละตัว
การใช้งาน Dropdown
ใช้สำหรับแสดงลิสรายการเพื่อให้ผู้ใช้เลือกหรือกำหนดค่าที่ต้องการ คล้ายกับการเลือกของ radio ที่จะ
สามารถเลือกได้เพียงอันเดียว จากรายการทั้งหมด การใช้งาน DropdownButtonFormField จะรองรับสำหรับ
ฟอร์มมากกว่าการใช้งาน DropdownButton ธรรมดา ดูตัวอย่างทั้งสองรูปแบบ เบื้องต้น
กำหนด State property ที่เกี่ยวข้อง
String _dropdownValue = '';
DropdownButton<String>( value: null, onChanged: (String? newValue) { setState(() { _dropdownValue = newValue!; }); }, isExpanded: true, items: <String>['One', 'Two', 'Three', 'Four'] .map<DropdownMenuItem<String>>((String value) { return DropdownMenuItem<String>( value: value, child: Text(value), ); }).toList(), ), SizedBox(height: 5.0,), DropdownButtonFormField<String>( value: null, onChanged: (value) { setState(() { _dropdownValue = value!; }); }, hint: Text('Rating'), isExpanded: true, items: <String>['One', 'Two', 'Three', 'Four'] .map<DropdownMenuItem<String>>((String value) { return DropdownMenuItem<String>( value: value, child: Text(value), ); }).toList(), ),
ผลลัพธ์ที่ได้
มาดูวิธีการกำหนดและใช้งานสำหรับฟอร์ม
// กำหนดตัวแปรสำหรับลิสรายการ List<String> maritalStatus = ['โสด','แต่งงาน','หย่า','หม้าย']; // กำหนดตัวแปรสำหรับเก็บค่าที่เลือก เริ่มต้นเป็นค่าว่าง String _seslectedMaritalStatus = '';
ต่อไปเรียกใช้งานเป็นดังนี้
Divider(), DropdownButtonFormField<String>( value: null, autovalidateMode: AutovalidateMode.always, // validator: (value) => (value == null) ? 'เลือกสถานะการแต่งงาน' : null, validator: Validators.required('เลือกสถานะการแต่งงาน'), onChanged: (value) { setState(() { _seslectedMaritalStatus = value!; }); }, hint: Text('สถานะการแต่งงาน'), isExpanded: true, items: maritalStatus.map<DropdownMenuItem<String>>((String value) { return DropdownMenuItem<String>( value: value, child: Text(value), ); }).toList(), ),
ผลลัพธ์ที่ได้
จะเห็นว่า DropdownButtonFormField รองรับการตรวจสอบข้อมูลด้วย validator เหมือนกับ TextFormField
เราสามารถสร้าง List<String> เพื่อวนลูปสร้างรายการตัวเลือกให้กับ dropbox ได้ง่าย
ตอนนี้เราได้รู้จัก element ที่ใช้งานร่วมกับฟอร์มเพิ่มเติม รวมถึง TextFormFiled จากบทความตอนที่แล้ว เราได้
รู้จักวิธีการสร้างลิสรายการสำหรับแต่ละ widget รู้จักกำหนดตัวแปรสำหรับรับค่าเพื่อนำไปใช้งานต่อ
เราจะลองสร้างฟอร์มสมมติ โดยรวม element ต่างๆ มาไว้ด้วยกันในฟอร์ม ตามตัวอย่างข้างล่าง และรูปแบบการ
กำหนดสำหรับเป็นข้อมูลของฟอร์ม เมื่อกดส่งข้อมูล จะจำลองการแสดงข้อมูลที่เป็น Map
ไฟล์ contact.dart
import 'package:flutter/material.dart'; import 'package:intl/date_symbol_data_local.dart'; import 'package:intl/intl.dart'; import 'package:flutter/services.dart'; import '../validations/validation.dart'; class Contact extends StatefulWidget { static const routeName = '/contact'; const Contact({Key? key}) : super(key: key); @override State<StatefulWidget> createState() { return _ContactState(); } } class _ContactState extends State<Contact> with Validators { // กำหนดข้อมูลฟิลด์ สำหรับบันทึก final Map<String, dynamic> formData = { 'email': null, 'password': null, 'birthday': null, 'gender': null, 'hobby': null, 'maritalstatus': null, }; // สร้างฟอร์ม key หรือ id ของฟอร์มสำหรับอ้างอิง final _formKey = GlobalKey<FormState>(); late DateFormat dateFormat; // รูปแบบการจัดการวันที่และเวลา // กำหนดตัวแปรรับค่า final _text1 = TextEditingController(); final _text2 = TextEditingController(); final _text3 = TextEditingController(); final _text4 = TextEditingController(); // กำหนดตัวแปร ลิสรายการ checkbox List<Map<String, bool>> hobbies = [ {'อ่านหนังสือ': true}, {'วาดรูป': false}, {'ดูหนัง': true}, {'ช้อปปิ้ง': true}, ]; // กำหนดตัวแปร เก็บค่าของแต่ละ checkbxo List<bool> _checkHobby = []; List<String> _checkedHobby = []; // ค่าสำหรับส่งไปใช้งาน // กำหนดตัวแปรค่าเริ่มต้นของรายการที่่ถูกเลือก Gender? _selectedGender = Gender.male; // กำหนดตัวแปรสำหรับใช้เก็บข้อความอ้างอิง String? _selectedGenderText = 'ชาย'; // กำหนดตัวแปรสำหรับใช้เก็บข้อความอ้างอิงในลูป List<String> _listGenderText = ['ชาย', 'หญิง']; // กำหนดตัวแปรสำหรับลิสรายการ List<String> maritalStatus = ['โสด','แต่งงาน','หย่า','หม้าย']; // กำหนดตัวแปรสำหรับเก็บค่าที่เลือก เริ่มต้นเป็นค่าว่าง String _seslectedMaritalStatus = ''; // กำหนดสถานะการแสดงแบบรหัสผ่าน bool _isHidden = true; bool _termsChecked = false; void _selectDate() async { final DateTime now = DateTime.now(); final DateTime firstDate = DateTime(2017, 7, 1); // ช่วงเริ่มต้น final DateTime lastDate = DateTime(2023, 7, 1); // ช่วงสิ้นสิน final DateTime initialDate = now.isAfter(lastDate) ? lastDate : now; final DateTime? newDate = await showDatePicker( context: context, initialDate: initialDate, firstDate: firstDate, lastDate: lastDate, helpText: 'Select a date', ); if (newDate != null) { setState(() { _text2.value = TextEditingValue(text: dateFormat.format(newDate).toString()); }); } } // เกียวกับการใช้เวลา /// แปลงเวลาจากวันที่ TimeOfDay.fromDateTime(DateTime.now()) /// เวลาปัจจุบัน TimeOfDay.now() /// แบบกำหนดเอง TimeOfDay(hour: 7, minute: 15), void _selectTime() async { final TimeOfDay? newTime = await showTimePicker( context: context, initialTime: TimeOfDay.now(), ); if (newTime != null) { setState(() { _text2.value = TextEditingValue(text: newTime.format(context)); }); } } @override void initState() { super.initState(); // กำหนดรูปแบบการจัดการวันที่และเวลา Intl.defaultLocale = 'en'; initializeDateFormatting(); dateFormat = DateFormat('d/MM/y','en'); } @override void dispose() { _text1.dispose(); // ยกเลิกการใช้งานที่เกี่ยวข้องทั้งหมดถ้ามี _text2.dispose(); _text3.dispose(); _text4.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('Contact Us'), ), body: SingleChildScrollView( child: Form( // ใช้งาน Form key: _formKey, // กำหนด key child: Padding( padding: const EdgeInsets.all(10.0), child: Column( children: <Widget>[ // กำหนด widget ที่จะใช้งานกับฟอร์ม TextFormField( autovalidateMode: AutovalidateMode.always, decoration: InputDecoration( hintText: 'อีเมล', icon: Icon(Icons.email_outlined), ), controller: _text1, // ผูกกับ TextFormField ที่จะใช้ validator: Validators.compose([ Validators.required('กรุณาระบุอีเมล'), Validators.email('กรุณาใส่อีเมลให้ถูกต้อง') ]), ), SizedBox(height: 5.0,), TextFormField( autovalidateMode: AutovalidateMode.always, decoration: InputDecoration( icon: Icon(Icons.vpn_key), hintText: 'รหัสผ่าน', suffixIcon: IconButton( onPressed: (){ setState(() { _isHidden = !_isHidden; // เมื่อกดก็เปลี่ยนค่าตรงกันข้าม }); }, icon: Icon( _isHidden // เงื่อนไขการสลับ icon ? Icons.visibility_off : Icons.visibility ), ), ), controller: _text3, // ผูกกับ TextFormField ที่จะใช้ validator: Validators.required('กรุณาระบุรห้สผ่าน'), obscureText: _isHidden, // การซ่อนหรือแสดงข้อความในรูปแบบรหัสผ่าน ), SizedBox(height: 5.0,), TextFormField( autovalidateMode: AutovalidateMode.always, decoration: InputDecoration( hintText: 'วันเกิด', icon: Icon(Icons.date_range), ), controller: _text2, // ผูกกับ TextFormField ที่จะใช้ validator: Validators.required('กรุณาระบุวันเกิด'), onTap: _selectDate, readOnly: true, ), Divider(), Builder(builder: (context) { List<Widget> list = <Widget>[]; Gender.values.asMap().forEach((index, val){ list.add( RadioListTile( title: Text(_listGenderText[index]), value: val, // ค่าของตัวเล็อก female groupValue: _selectedGender, // ใช้กลุ่มค่าที่ถูกเลือกเป็นตัวแปรเดียวกัน onChanged: (Gender? value) { setState(() { _selectedGender = value; _selectedGenderText = _listGenderText[index]; }); }, controlAffinity: ListTileControlAffinity.leading, ), ); }); return Column( children: list, ); }), Divider(), // ตัว widget แบ่ง Builder(builder: (context) { // เราใช้ Builder เพื่อที่จะใช้งานฟังก์ชั่นสร้าง widget ได้ List<Widget> list = <Widget>[]; _checkedHobby.clear(); hobbies.asMap().forEach((index, hobby){ // วนลูปสร้างลิสรายการ var key = hobby.keys.toList(); // แปลงเป็น list ของ key var val = hobby.values.toList(); // แปลงเป็น list ของ value _checkHobby.add(val[0]); // เก็บค่า value ขแงแต่ละรายการ if(_checkHobby[index]) _checkedHobby.add(key[0]); // เก็บรายการที่เลือก list.add(CheckboxListTile( value: _checkHobby[index], // ใช้ค่า value ของแต่ละรายการ onChanged: (value) { setState(() { _checkHobby[index] = value!; // เปลี่ยนค่าเมื่อมีการเลือกหรือไม่เลือก }); }, title: Text( '${key[0]}', ), controlAffinity: ListTileControlAffinity.leading, )); }); return Column( // คืนค่าเป็นรายการ checkbox ในคอลัมน์ children: list, ); }), Divider(), DropdownButtonFormField<String>( value: null, autovalidateMode: AutovalidateMode.always, decoration: InputDecoration( icon: Icon(Icons.family_restroom_outlined), ), validator: Validators.required('เลือกสถานะการแต่งงาน'), onChanged: (value) { setState(() { _seslectedMaritalStatus = value!; }); }, hint: Text('สถานะการแต่งงาน'), isExpanded: true, items: maritalStatus.map<DropdownMenuItem<String>>((String value) { return DropdownMenuItem<String>( value: value, child: Text(value), ); }).toList(), ), Divider(), CheckboxListTile( value: _termsChecked, onChanged: (value) { setState(() { _termsChecked = value!; }); }, subtitle: !_termsChecked ? Text( 'ต้องระบุ', style: TextStyle(color: Colors.red, fontSize: 12.0), ) : null, title: new Text( 'ยอมรับเงื่อนไขและข้อตกลงการใช้งาน', ), controlAffinity: ListTileControlAffinity.leading, ), ElevatedButton( onPressed: () { // อ้างอิงฟอร์มที่กำลังใช้งาน ตรวจสอบความถูกต้องข้อมูลในฟอร์ม if (_formKey.currentState!.validate()) { //หากผ่าน formData['email'] = _text1.text; formData['password'] = _text2.text; formData['birthday'] = _text3.text; formData['gender'] = _selectedGenderText; formData['hobby'] = _checkedHobby; formData['maritalstatus'] = _seslectedMaritalStatus; // print(formData); // แสดงข้อความจำลอง ใน snackbar ScaffoldMessenger.of(context).showSnackBar( // นำค่าข้อมูลไปแสดงหรือใช้งานผ่าน controller SnackBar(content: Text('Process Data...${formData}')), ); } }, child: const Text('Submit'), ), ], ), ), ), ), ); } } // กำหนดข้อมูลสำหรับ radio enum Gender { male, female }
ผลลัพธ์ที่ได้
เมื่อทำการ submit หรือ validate ฟอร์มผ่านแล้ว เราทำการเก็บค่าข้อมูลทั้งหมด ไว้ใน Map ที่ชื่อ
formData เพื่อนำไปใช้งานต่อ ข้างต้น เราแค่แสดงผลข้อมูลด้วย snackBar
สำหรับเนื้อหาเกี่ยวกับการใช้งาน element ของฟอร์มเพิ่มเติมในตอนนี้ก็มีประมาณนี้ หวังว่าจะเป็แแนวทาง
นำไปปรับใช้งานต่อไป เนื้อหาตอนหน้า เราจะนำสิ่งที่ได้เรียนรู้เกี่ยวกับฟอร์มทั้งสองตอนนี้ ไปประยุกต์
กับการใช้งานฟอร์มที่บันทึกลงฐานข้อมูลหนังสือของบทความก่อนหน้า รอติดตาม