Forms และ Input
ใน React form inputs เป็นแบบ “controlled” — คุณผูก value prop เข้ากับ state และอัปเดต state นั้นผ่าน onChange handler Flutter ใช้แนวทางที่แตกต่างกันเล็กน้อย: TextEditingController ทำหน้าที่เหมือน ref ที่เก็บค่าข้อความปัจจุบัน แล้วคุณแนบ controller เข้ากับ TextField การอ่านค่าคือ controller.text แทนที่จะเป็น event.target.value และไม่จำเป็นต้อง re-render เพื่อดักจับ input
สำหรับการ validate form แบบสมบูรณ์ Flutter มี Form widget คู่กับ GlobalKey<FormState> แต่ละ TextFormField ภายในรับ validator callback การเรียก formKey.currentState!.validate() จะ trigger ทุก validator พร้อมกันและคืนค่า true เฉพาะเมื่อทุกตัวผ่าน — เทียบเท่ากับการสะสม error state แบบ per-field และตรวจสอบด้วยตนเองใน React
Controlled Input vs TextField
หัวข้อที่มีชื่อว่า “Controlled Input vs TextField”import { useState } from 'react';
function LoginForm() { const [email, setEmail] = useState(''); const [password, setPassword] = useState('');
const handleSubmit = (e) => { e.preventDefault(); if (!email.includes('@')) { alert('Invalid email'); return; } if (password.length < 6) { alert('Password too short'); return; } console.log('Login:', email); };
return ( <form onSubmit={handleSubmit}> <input value={email} onChange={e => setEmail(e.target.value)} placeholder="Email" /> <input value={password} onChange={e => setPassword(e.target.value)} type="password" placeholder="Password" /> <button type="submit">Login</button> </form> );}class LoginForm extends StatefulWidget { const LoginForm({super.key}); @override State<LoginForm> createState() => _LoginFormState();}
class _LoginFormState extends State<LoginForm> { final _formKey = GlobalKey<FormState>(); final _emailController = TextEditingController(); final _passwordController = TextEditingController();
@override void dispose() { _emailController.dispose(); _passwordController.dispose(); super.dispose(); }
@override Widget build(BuildContext context) { return Form( key: _formKey, child: Column( children: [ TextFormField( controller: _emailController, decoration: const InputDecoration(labelText: 'Email'), validator: (v) => v != null && v.contains('@') ? null : 'Invalid email', ), TextFormField( controller: _passwordController, obscureText: true, decoration: const InputDecoration(labelText: 'Password'), validator: (v) => v != null && v.length >= 6 ? null : 'Min 6 characters', ), ElevatedButton( onPressed: () { if (_formKey.currentState!.validate()) { // submit } }, child: const Text('Login'), ), ], ), ); }}ตัวอย่างที่รันได้จริง
หัวข้อที่มีชื่อว่า “ตัวอย่างที่รันได้จริง”import 'package:flutter/material.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(
home: LoginScreen(),
);
}
}
class LoginScreen extends StatefulWidget {
const LoginScreen({super.key});
@override
State<LoginScreen> createState() => _LoginScreenState();
}
class _LoginScreenState extends State<LoginScreen> {
final _formKey = GlobalKey<FormState>();
final _emailController = TextEditingController();
final _passwordController = TextEditingController();
@override
void dispose() {
_emailController.dispose();
_passwordController.dispose();
super.dispose();
}
void _submit() {
if (_formKey.currentState!.validate()) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Login successful! Welcome ${_emailController.text}'),
backgroundColor: Colors.green,
),
);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Login Form Demo')),
body: Padding(
padding: const EdgeInsets.all(24),
child: Form(
key: _formKey,
child: Column(
children: [
TextFormField(
controller: _emailController,
decoration: const InputDecoration(
labelText: 'Email',
border: OutlineInputBorder(),
),
keyboardType: TextInputType.emailAddress,
validator: (value) {
if (value == null || value.isEmpty) return 'Email is required';
if (!value.contains('@')) return 'Enter a valid email';
return null;
},
),
const SizedBox(height: 16),
TextFormField(
controller: _passwordController,
decoration: const InputDecoration(
labelText: 'Password',
border: OutlineInputBorder(),
),
obscureText: true,
validator: (value) {
if (value == null || value.isEmpty) return 'Password is required';
if (value.length < 6) return 'Password must be at least 6 characters';
return null;
},
),
const SizedBox(height: 24),
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: _submit,
child: const Text('Login'),
),
),
],
),
),
),
);
}
}