formwise 1.1.0 copy "formwise: ^1.1.0" to clipboard
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.

example/lib/main.dart

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)',
            ),
          ],
        ),
      ),
    );
  }
}
0
likes
160
points
30
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

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.

Repository (GitHub)
View/report issues

Topics

#form #validation #input #formatting #widget

License

MIT (license)

Dependencies

flutter, tflite_flutter

More

Packages that depend on formwise