ข้ามไปยังเนื้อหา

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

React
import { useState } from 'react';
function EmailInput() {
const [email, setEmail] = useState('');
return (
<div>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="Enter email"
/>
<p>Current value: {email}</p>
</div>
);
}
Flutter
class EmailInput extends StatefulWidget {
const EmailInput({super.key});
@override
State<EmailInput> createState() => _EmailInputState();
}
class _EmailInputState extends State<EmailInput> {
// Controller is like a ref — it holds the live text value.
late final TextEditingController _controller;
@override
void initState() {
super.initState();
_controller = TextEditingController();
}
@override
void dispose() {
// Always dispose controllers to free resources.
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Column(
children: [
TextField(
controller: _controller,
decoration: const InputDecoration(
labelText: 'Enter email',
),
),
// ValueListenableBuilder rebuilds only this Text widget
// when the controller value changes.
ValueListenableBuilder<TextEditingValue>(
valueListenable: _controller,
builder: (_, value, __) =>
Text('Current value: ${value.text}'),
),
],
);
}
}
React
import { useState } from 'react';
function LoginForm() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [errors, setErrors] = useState({});
function validate() {
const next = {};
if (!email.includes('@')) next.email = 'Invalid email';
if (password.length < 6) next.password = 'Min 6 characters';
setErrors(next);
return Object.keys(next).length === 0;
}
function handleSubmit(e) {
e.preventDefault();
if (validate()) {
console.log('Login successful!');
}
}
return (
<form onSubmit={handleSubmit}>
<input
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="Email"
/>
{errors.email && <span>{errors.email}</span>}
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="Password"
/>
{errors.password && <span>{errors.password}</span>}
<button type="submit">Log in</button>
</form>
);
}
Flutter
// GlobalKey<FormState> is the handle Flutter uses to
// call validate() on the whole form at once.
final _formKey = GlobalKey<FormState>();
class LoginForm extends StatefulWidget {
const LoginForm({super.key});
@override
State<LoginForm> createState() => _LoginFormState();
}
class _LoginFormState extends State<LoginForm> {
late final TextEditingController _emailController;
late final TextEditingController _passwordController;
@override
void initState() {
super.initState();
_emailController = TextEditingController();
_passwordController = TextEditingController();
}
@override
void dispose() {
_emailController.dispose();
_passwordController.dispose();
super.dispose();
}
void _submit() {
// Triggers every validator — returns true when all pass.
if (_formKey.currentState!.validate()) {
print('Login successful!');
}
}
@override
Widget build(BuildContext context) {
return Form(
key: _formKey,
child: Column(
children: [
TextFormField(
controller: _emailController,
decoration: const InputDecoration(labelText: 'Email'),
validator: (value) {
if (value == null || !value.contains('@')) {
return 'Invalid email';
}
return null; // null = valid
},
),
TextFormField(
controller: _passwordController,
obscureText: true,
decoration: const InputDecoration(labelText: 'Password'),
validator: (value) {
if (value == null || value.length < 6) {
return 'Min 6 characters';
}
return null;
},
),
ElevatedButton(
onPressed: _submit,
child: const Text('Log in'),
),
],
),
);
}
}
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: Scaffold(
        body: Center(
          child: Padding(
            padding: EdgeInsets.all(24),
            child: LoginForm(),
          ),
        ),
      ),
    );
  }
}

class LoginForm extends StatefulWidget {
  const LoginForm({super.key});

  @override
  State<LoginForm> createState() => _LoginFormState();
}

class _LoginFormState extends State<LoginForm> {
  // GlobalKey<FormState> is the handle that lets you call
  // formKey.currentState!.validate() to trigger all validators.
  final _formKey = GlobalKey<FormState>();

  // Controllers are created in initState and disposed in dispose —
  // the idiomatic Flutter equivalent of React refs for input values.
  late final TextEditingController _emailController;
  late final TextEditingController _passwordController;

  @override
  void initState() {
    super.initState();
    _emailController = TextEditingController();
    _passwordController = TextEditingController();
  }

  @override
  void dispose() {
    _emailController.dispose();
    _passwordController.dispose();
    super.dispose();
  }

  void _submit() {
    // validate() calls every validator, shows inline errors,
    // and returns true only when all fields pass.
    if (_formKey.currentState!.validate()) {
      ScaffoldMessenger.of(context).showSnackBar(
        const SnackBar(content: Text('Login successful!')),
      );
    }
  }

  @override
  Widget build(BuildContext context) {
    return Form(
      key: _formKey,
      child: Column(
        mainAxisSize: MainAxisSize.min,
        children: [
          Text(
            'Login',
            style: Theme.of(context).textTheme.headlineMedium,
          ),
          const SizedBox(height: 24),
          TextFormField(
            controller: _emailController,
            keyboardType: TextInputType.emailAddress,
            decoration: const InputDecoration(
              labelText: 'Email',
              hintText: '[email protected]',
              border: OutlineInputBorder(),
            ),
            // validator returns a String error message, or null when valid.
            validator: (value) {
              if (value == null || value.isEmpty) {
                return 'Email is required';
              }
              if (!value.contains('@')) {
                return 'Enter a valid email address';
              }
              return null;
            },
          ),
          const SizedBox(height: 16),
          TextFormField(
            controller: _passwordController,
            obscureText: true,
            decoration: const InputDecoration(
              labelText: 'Password',
              border: OutlineInputBorder(),
            ),
            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('Log in'),
            ),
          ),
        ],
      ),
    );
  }
}
วิธีอ่านค่าปัจจุบันจาก TextField ใน Flutter คืออะไร?
validator ควร return อะไรเมื่อ input valid?
`GlobalKey<FormState>` มีจุดประสงค์อะไรใน Flutter?
เพราะเหตุใดจึงต้องเรียก `controller.dispose()` เมื่อ `StatefulWidget` ถูกลบออก?