formwise 1.1.0
formwise: ^1.1.0 copied to clipboard
Smart form fields for Flutter with auto-validation, auto-formatting, typo detection, and animated error feedback. Supports email, phone, credit card, URL, and custom fields.
import 'package:flutter/material.dart';
import 'package:formwise/formwise.dart';
void main() => runApp(const FormwiseExampleApp());
class FormwiseExampleApp extends StatelessWidget {
const FormwiseExampleApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Formwise Demo',
debugShowCheckedModeBanner: false,
theme: ThemeData(
colorSchemeSeed: const Color(0xFF6750A4),
useMaterial3: true,
),
darkTheme: ThemeData(
colorSchemeSeed: const Color(0xFF6750A4),
useMaterial3: true,
brightness: Brightness.dark,
),
home: const DemoHomePage(),
);
}
}
class DemoHomePage extends StatelessWidget {
const DemoHomePage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Formwise Demo')),
body: ListView(
padding: const EdgeInsets.all(16),
children: [
_DemoCard(
title: 'Sign Up Form',
subtitle: 'Email typo detection, password strength, validation',
onTap: () => Navigator.push(
context,
MaterialPageRoute(builder: (_) => const SignUpDemo()),
),
),
_DemoCard(
title: 'Payment Form',
subtitle: 'Credit card auto-detect, formatting, Luhn validation',
onTap: () => Navigator.push(
context,
MaterialPageRoute(builder: (_) => const PaymentDemo()),
),
),
_DemoCard(
title: 'Address Form',
subtitle: 'Postal code validation & formatting by country',
onTap: () => Navigator.push(
context,
MaterialPageRoute(builder: (_) => const AddressDemo()),
),
),
_DemoCard(
title: 'All Formatters',
subtitle: 'Phone, date, currency, mask, titleCase, trimmed',
onTap: () => Navigator.push(
context,
MaterialPageRoute(builder: (_) => const FormattersDemo()),
),
),
_DemoCard(
title: 'Async Validation',
subtitle: 'Simulated server check with animated spinner',
onTap: () => Navigator.push(
context,
MaterialPageRoute(builder: (_) => const AsyncValidationDemo()),
),
),
],
),
);
}
}
class _DemoCard extends StatelessWidget {
final String title;
final String subtitle;
final VoidCallback onTap;
const _DemoCard({
required this.title,
required this.subtitle,
required this.onTap,
});
@override
Widget build(BuildContext context) {
return Card(
margin: const EdgeInsets.only(bottom: 12),
child: ListTile(
title: Text(title),
subtitle: Text(subtitle),
trailing: const Icon(Icons.chevron_right),
onTap: onTap,
),
);
}
}
// ---------------------------------------------------------------------------
// Sign Up Demo
// ---------------------------------------------------------------------------
class SignUpDemo extends StatefulWidget {
const SignUpDemo({super.key});
@override
State<SignUpDemo> createState() => _SignUpDemoState();
}
class _SignUpDemoState extends State<SignUpDemo> {
final _controller = SmartFormController();
String? _emailSuggestion;
double _passwordStrength = 0;
String _passwordLabel = '';
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Sign Up')),
body: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
SmartTextFormField.email(
name: 'email',
formController: _controller,
onTypoDetected: (suggestion) {
setState(() => _emailSuggestion = suggestion);
},
),
if (_emailSuggestion != null)
Padding(
padding: const EdgeInsets.only(top: 4, left: 12),
child: GestureDetector(
onTap: () {
_controller.setValue('email', _emailSuggestion!);
setState(() => _emailSuggestion = null);
},
child: Text(
'Did you mean $_emailSuggestion? Tap to fix.',
style: TextStyle(
color: Theme.of(context).colorScheme.primary,
fontSize: 13,
),
),
),
),
const SizedBox(height: 16),
SmartTextFormField(
name: 'name',
formController: _controller,
labelText: 'Full Name',
hintText: 'John Doe',
prefixIcon: const Icon(Icons.person_outline),
validator: SmartValidators.compose([
SmartValidators.required(errorMessage: 'Name is required'),
SmartValidators.minLength(length: 2),
]),
inputFormatters: [SmartFormatters.titleCase()],
textCapitalization: TextCapitalization.words,
),
const SizedBox(height: 16),
SmartTextFormField.password(
name: 'password',
formController: _controller,
minLength: 8,
requireUppercase: true,
requireDigit: true,
onChanged: (value) {
setState(() {
_passwordStrength = PasswordStrength.calculate(value);
_passwordLabel = PasswordStrength.label(_passwordStrength);
});
},
),
const SizedBox(height: 8),
StrengthIndicator(
strength: _passwordStrength,
label: _passwordLabel,
),
const SizedBox(height: 16),
SmartTextFormField.phone(
name: 'phone',
formController: _controller,
mask: '(###) ###-####',
),
const SizedBox(height: 24),
FilledButton(
onPressed: () {
if (_controller.validate()) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Success! Values: ${_controller.values}'),
),
);
}
},
child: const Text('Sign Up'),
),
],
),
),
);
}
}
// ---------------------------------------------------------------------------
// Payment Demo
// ---------------------------------------------------------------------------
class PaymentDemo extends StatefulWidget {
const PaymentDemo({super.key});
@override
State<PaymentDemo> createState() => _PaymentDemoState();
}
class _PaymentDemoState extends State<PaymentDemo> {
final _controller = SmartFormController();
CardBrand _brand = CardBrand.unknown;
@override
void dispose() {
_controller.dispose();
super.dispose();
}
IconData _brandIcon(CardBrand brand) {
switch (brand) {
case CardBrand.visa:
case CardBrand.mastercard:
case CardBrand.discover:
return Icons.credit_card;
case CardBrand.amex:
return Icons.credit_score;
case CardBrand.unknown:
return Icons.credit_card_outlined;
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Payment')),
body: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
SmartTextFormField.creditCard(
name: 'card',
formController: _controller,
suffixIcon: Icon(_brandIcon(_brand)),
onChanged: (value) {
setState(() {
_brand = SmartValidators.detectCardBrand(value);
});
},
),
if (_brand != CardBrand.unknown)
Padding(
padding: const EdgeInsets.only(top: 4, left: 12),
child: Text(
'Detected: ${_brand.name.toUpperCase()}',
style: TextStyle(
color: Theme.of(context).colorScheme.secondary,
fontSize: 13,
),
),
),
const SizedBox(height: 16),
Row(
children: [
Expanded(
child: SmartTextFormField(
name: 'expiry',
formController: _controller,
labelText: 'Expiry',
hintText: 'MM/YY',
keyboardType: TextInputType.number,
inputFormatters: [
SmartFormatters.mask('##/##'),
],
validator: SmartValidators.pattern(
regex: RegExp(r'^(0[1-9]|1[0-2])/\d{2}$'),
errorMessage: 'Invalid expiry (MM/YY)',
),
),
),
const SizedBox(width: 16),
Expanded(
child: SmartTextFormField(
name: 'cvv',
formController: _controller,
labelText: 'CVV',
hintText: '123',
obscureText: true,
keyboardType: TextInputType.number,
maxLength: 4,
validator: SmartValidators.compose([
SmartValidators.required(errorMessage: 'Required'),
SmartValidators.pattern(
regex: RegExp(r'^\d{3,4}$'),
errorMessage: '3 or 4 digits',
),
]),
),
),
],
),
const SizedBox(height: 16),
SmartTextFormField(
name: 'amount',
formController: _controller,
labelText: 'Amount',
hintText: '\$0.00',
prefixIcon: const Icon(Icons.attach_money),
keyboardType:
const TextInputType.numberWithOptions(decimal: true),
inputFormatters: [SmartFormatters.currency()],
validator: SmartValidators.numericRange(
min: 0.01,
errorMessage: 'Enter an amount',
),
),
const SizedBox(height: 24),
FilledButton(
onPressed: () {
if (_controller.validate()) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Payment submitted: ${_controller.values}'),
),
);
}
},
child: const Text('Pay Now'),
),
],
),
),
);
}
}
// ---------------------------------------------------------------------------
// Address Demo
// ---------------------------------------------------------------------------
class AddressDemo extends StatefulWidget {
const AddressDemo({super.key});
@override
State<AddressDemo> createState() => _AddressDemoState();
}
class _AddressDemoState extends State<AddressDemo> {
final _controller = SmartFormController();
PostalCountry _country = PostalCountry.us;
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Address')),
body: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
DropdownButtonFormField<PostalCountry>(
initialValue: _country,
decoration: const InputDecoration(
labelText: 'Country',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.flag_outlined),
),
items: PostalCountry.values.map((c) {
return DropdownMenuItem(
value: c,
child: Text('${c.code} — ${c.name}'),
);
}).toList(),
onChanged: (value) {
if (value != null) setState(() => _country = value);
},
),
const SizedBox(height: 16),
SmartTextFormField(
name: 'street',
formController: _controller,
labelText: 'Street Address',
hintText: '123 Main St',
prefixIcon: const Icon(Icons.home_outlined),
validator: SmartValidators.required(),
textCapitalization: TextCapitalization.words,
),
const SizedBox(height: 16),
SmartTextFormField(
name: 'city',
formController: _controller,
labelText: 'City',
prefixIcon: const Icon(Icons.location_city_outlined),
validator: SmartValidators.required(),
inputFormatters: [SmartFormatters.titleCase()],
textCapitalization: TextCapitalization.words,
),
const SizedBox(height: 16),
SmartTextFormField.postalCode(
key: ValueKey(_country),
name: 'postal',
country: _country,
formController: _controller,
),
const SizedBox(height: 8),
Text(
'Format: ${_country.example}',
style: Theme.of(context).textTheme.bodySmall,
),
const SizedBox(height: 24),
FilledButton(
onPressed: () {
if (_controller.validate()) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content:
Text('Address saved: ${_controller.values}'),
),
);
}
},
child: const Text('Save Address'),
),
],
),
),
);
}
}
// ---------------------------------------------------------------------------
// Formatters Demo
// ---------------------------------------------------------------------------
class FormattersDemo extends StatelessWidget {
const FormattersDemo({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Formatters')),
body: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
SmartTextFormField.phone(
name: 'phone_us',
labelText: 'US Phone',
mask: '(###) ###-####',
hintText: '(555) 123-4567',
),
const SizedBox(height: 16),
SmartTextFormField.phone(
name: 'phone_jp',
labelText: 'Japan Phone',
mask: PhoneFormat.japan.mask,
hintText: '090-1234-5678',
),
const SizedBox(height: 16),
SmartTextFormField(
name: 'date_us',
labelText: 'Date (MM/DD/YYYY)',
hintText: '12/31/2025',
keyboardType: TextInputType.number,
inputFormatters: [SmartFormatters.smartDate(DateFormat.mmddyyyy)],
),
const SizedBox(height: 16),
SmartTextFormField(
name: 'date_eu',
labelText: 'Date (DD/MM/YYYY)',
hintText: '31/12/2025',
keyboardType: TextInputType.number,
inputFormatters: [
SmartFormatters.smartDate(DateFormat.ddmmyyyy),
],
),
const SizedBox(height: 16),
SmartTextFormField(
name: 'currency',
labelText: 'Currency (USD)',
hintText: '\$1,234.56',
prefixIcon: const Icon(Icons.attach_money),
keyboardType:
const TextInputType.numberWithOptions(decimal: true),
inputFormatters: [SmartFormatters.currency()],
),
const SizedBox(height: 16),
SmartTextFormField(
name: 'currency_eu',
labelText: 'Currency (EUR)',
hintText: '1.234,56',
prefixIcon: const Icon(Icons.euro),
keyboardType:
const TextInputType.numberWithOptions(decimal: true),
inputFormatters: [
SmartFormatters.currency(
symbol: '',
separator: '.',
decimal: ',',
),
],
),
const SizedBox(height: 16),
SmartTextFormField(
name: 'ssn',
labelText: 'SSN',
hintText: '123-45-6789',
keyboardType: TextInputType.number,
inputFormatters: [
SmartFormatters.mask('###-##-####'),
],
),
const SizedBox(height: 16),
SmartTextFormField(
name: 'title_case',
labelText: 'Title Case',
hintText: 'Type something',
inputFormatters: [SmartFormatters.titleCase()],
textCapitalization: TextCapitalization.words,
),
const SizedBox(height: 16),
SmartTextFormField(
name: 'uppercase',
labelText: 'Uppercase',
hintText: 'AUTO UPPERCASED',
inputFormatters: [SmartFormatters.uppercase()],
),
const SizedBox(height: 16),
SmartTextFormField(
name: 'trimmed',
labelText: 'Trimmed (no double spaces)',
hintText: 'Extra spaces collapse',
inputFormatters: [SmartFormatters.trimmed()],
),
],
),
),
);
}
}
// ---------------------------------------------------------------------------
// Async Validation Demo
// ---------------------------------------------------------------------------
class AsyncValidationDemo extends StatelessWidget {
const AsyncValidationDemo({super.key});
Future<String?> _checkUsername(String? value) async {
if (value == null || value.isEmpty) return null;
await Future.delayed(const Duration(seconds: 2));
final taken = ['admin', 'user', 'test', 'root'];
if (taken.contains(value.toLowerCase())) {
return 'Username "$value" is already taken';
}
return null;
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Async Validation')),
body: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
'Try typing "admin", "user", "test", or "root" to see '
'the async validator reject them after a simulated server check.',
style: Theme.of(context).textTheme.bodyMedium,
),
const SizedBox(height: 16),
SmartTextFormField(
name: 'username',
labelText: 'Username',
hintText: 'Pick a username',
prefixIcon: const Icon(Icons.alternate_email),
validator: SmartValidators.compose([
SmartValidators.required(),
SmartValidators.minLength(length: 3),
SmartValidators.pattern(
regex: RegExp(r'^[a-zA-Z0-9_]+$'),
errorMessage: 'Letters, numbers, and underscores only',
),
]),
asyncValidator: _checkUsername,
validationDebounce: const Duration(milliseconds: 500),
),
const SizedBox(height: 16),
SmartTextFormField.url(
name: 'website',
labelText: 'Website (optional)',
),
],
),
),
);
}
}