just_form 0.0.1
just_form: ^0.0.1 copied to clipboard
A Flutter package for building forms and validation.
Just Form #
A powerful and flexible Flutter form management package that provides automatic field registration, validation, state management using BLoC pattern, and built-in field widgets.
Table of Contents #
- Overview
- Features
- Getting Started
- Installation
- Basic Usage
- Validation
- Form Fields
- Advanced Features
- Examples
- API Reference
Overview #
Just Form simplifies form building in Flutter by:
- Automatic Field Management - Fields register themselves with the form controller
- Type-Safe - Full generic type support for any value type
- Performance Optimized - Selective rebuild control and fine-grained state tracking
- Validation Ready - Built-in validators and cross-field validation support
- BLoC Powered - Uses Flutter BLoC for predictable state management
- Extensible - Easily create custom field widgets
- Attribute Tracking - Store and track custom metadata alongside field values
Features #
✅ Automatic form field registration and lifecycle management
✅ Real-time field value and error tracking
✅ Cross-field validation support
✅ Custom field widget creation
✅ Flexible rebuild triggers (value, error, attributes)
✅ Field attribute manipulation
✅ External and internal controller management
✅ Built-in field widgets (TextField, DateField, DropdownField, etc.)
✅ Selective field rebuilding with JustBuilder
✅ Debouncing and validation optimization
Getting Started #
Installation #
Add just_form to your pubspec.yaml:
dependencies:
just_form: ^0.0.1
flutter_bloc: ^9.1.1
Basic Usage #
The simplest form requires just a JustFormBuilder and some fields:
import 'package:flutter/material.dart';
import 'package:just_form/just_form_builder.dart';
import 'package:just_form/field/just_text_field.dart';
class MyForm extends StatelessWidget {
@override
Widget build(BuildContext context) {
return JustFormBuilder(
builder: (context) {
return Column(
children: [
// Email field
JustTextField(
name: 'email',
builder: (context, controller) {
return TextField(
onChanged: (value) => controller.value = value,
decoration: InputDecoration(
hintText: 'Enter email',
errorText: controller.error,
),
);
},
),
// Password field
JustTextField(
name: 'password',
builder: (context, controller) {
return TextField(
onChanged: (value) => controller.value = value,
decoration: InputDecoration(
hintText: 'Enter password',
errorText: controller.error,
),
);
},
),
// Submit button
ElevatedButton(
onPressed: () {
final formController = context.read<JustFormController>();
print('Form values: ${formController.getValues()}');
print('Form errors: ${formController.getErrors()}');
},
child: Text('Submit'),
),
],
);
},
);
}
}
Validation #
Built-in Validators #
Just Form supports any FormFieldValidator<T> from Flutter. Use packages like validatorless or create your own:
JustTextField(
name: 'email',
validators: [
(value) {
if (value == null || value.isEmpty) {
return 'Email is required';
}
if (!value.contains('@')) {
return 'Please enter a valid email';
}
return null;
},
],
builder: (context, controller) {
return TextField(
onChanged: (value) => controller.value = value,
decoration: InputDecoration(errorText: controller.error),
);
},
)
Custom Validators #
Create reusable custom validators:
class EmailValidator {
static String? validate(String? value) {
if (value == null || value.isEmpty) {
return 'Email is required';
}
final regex = RegExp(r'^[^@]+@[^@]+\.[^@]+$');
if (!regex.hasMatch(value)) {
return 'Invalid email format';
}
return null;
}
}
// Usage
JustTextField(
name: 'email',
validators: [EmailValidator.validate],
builder: (context, controller) => TextField(...),
)
Cross-Field Validation #
Validate multiple fields together using JustValidator:
JustFormBuilder(
validators: [
JustValidator(
triggers: ['password', 're-password'],
validator: (formValues) {
final password = formValues?['password'];
final confirmPassword = formValues?['re-password'];
if (password != confirmPassword) {
return 'Passwords do not match';
}
return null;
},
targets: [
JustTargetError(
field: 're-password',
message: (error) => 'Passwords do not match',
),
],
),
],
builder: (context) {
return Column(
children: [
JustTextField(
name: 'password',
builder: (context, controller) => TextField(...),
),
JustTextField(
name: 're-password',
builder: (context, controller) => TextField(...),
),
],
);
},
)
Form Fields #
Available Fields #
Just Form provides pre-built field widgets for common use cases:
1. JustTextField
Text input field with optional validation display.
JustTextField(
name: 'username',
initialValue: 'John',
validators: [/* validators */],
builder: (context, controller) {
return TextField(
controller: TextEditingController(text: controller.value),
onChanged: (value) => controller.value = value,
decoration: InputDecoration(errorText: controller.error),
);
},
)
2. JustDateField
Date picker field.
JustDateField(
name: 'birth-date',
initialValue: DateTime(2000, 1, 1),
builder: (context, controller) {
return TextField(
readOnly: true,
onTap: () async {
final date = await showDatePicker(
context: context,
initialDate: controller.value ?? DateTime.now(),
firstDate: DateTime(1900),
lastDate: DateTime.now(),
);
if (date != null) controller.value = date;
},
decoration: InputDecoration(
hintText: 'Select date',
suffixIcon: Icon(Icons.calendar_today),
),
);
},
)
3. JustTimeField
Time picker field.
JustTimeField(
name: 'appointment-time',
builder: (context, controller) {
return TextField(
readOnly: true,
onTap: () async {
final time = await showTimePicker(
context: context,
initialTime: controller.value ?? TimeOfDay.now(),
);
if (time != null) controller.value = time;
},
decoration: InputDecoration(
hintText: 'Select time',
suffixIcon: Icon(Icons.access_time),
),
);
},
)
4. JustDropDownButton
Dropdown selection field.
JustDropDownButton<String>(
name: 'country',
initialValue: 'US',
builder: (context, controller) {
return DropdownButton<String>(
value: controller.value,
onChanged: (value) => controller.value = value,
items: ['US', 'UK', 'CA', 'AU']
.map((c) => DropdownMenuItem(value: c, child: Text(c)))
.toList(),
);
},
)
5. JustCheckbox
Boolean checkbox field.
JustCheckbox(
name: 'agree-terms',
initialValue: false,
builder: (context, controller) {
return Checkbox(
value: controller.value ?? false,
onChanged: (value) => controller.value = value,
);
},
)
6. JustSwitch
Toggle switch field.
JustSwitch(
name: 'notifications-enabled',
initialValue: true,
builder: (context, controller) {
return Switch(
value: controller.value ?? false,
onChanged: (value) => controller.value = value,
);
},
)
7. JustSlider
Slider field for numeric values.
JustSlider(
name: 'brightness',
initialValue: 0.5,
builder: (context, controller) {
return Slider(
value: controller.value ?? 0.5,
onChanged: (value) => controller.value = value,
min: 0,
max: 1,
);
},
)
8. JustRangeSlider
Range slider for selecting min/max values.
JustRangeSlider(
name: 'price-range',
initialValue: RangeValues(10, 100),
builder: (context, controller) {
final range = controller.value ?? RangeValues(10, 100);
return RangeSlider(
values: range,
onChanged: (value) => controller.value = value,
min: 0,
max: 500,
);
},
)
9. JustRadioGroup
Radio button group field.
JustRadioGroup<String>(
name: 'plan',
initialValue: 'basic',
builder: (context, controller) {
return Column(
children: ['basic', 'pro', 'enterprise'].map((plan) {
return RadioListTile<String>(
value: plan,
groupValue: controller.value,
onChanged: (value) => controller.value = value,
title: Text(plan),
);
}).toList(),
);
},
)
10. JustCheckboxListTile
Checkbox list tile field.
JustCheckboxListTile(
name: 'marketing-emails',
initialValue: true,
builder: (context, controller) {
return CheckboxListTile(
value: controller.value ?? false,
onChanged: (value) => controller.value = value,
title: Text('Receive marketing emails'),
);
},
)
11. JustSwitchListTile
Switch list tile field.
JustSwitchListTile(
name: 'dark-mode',
initialValue: false,
builder: (context, controller) {
return SwitchListTile(
value: controller.value ?? false,
onChanged: (value) => controller.value = value,
title: Text('Dark Mode'),
);
},
)
12. JustFieldList
Dynamic list of fields.
JustFieldList(
name: 'phone-numbers',
initialValue: [],
builder: (context, controller) {
return Column(
children: [
...List.generate(
controller.value?.length ?? 0,
(index) => JustTextField(
name: 'phone-numbers.$index',
builder: (context, fieldController) => TextField(),
),
),
ElevatedButton(
onPressed: () {
final numbers = controller.value ?? [];
controller.value = [...numbers, ''];
},
child: Text('Add Phone'),
),
],
);
},
)
13. JustPickerField
Generic picker field for custom selection.
JustPickerField<Color>(
name: 'favorite-color',
initialValue: Colors.blue,
builder: (context, controller) {
return ListTile(
title: Text('Favorite Color'),
trailing: Container(
width: 50,
color: controller.value ?? Colors.blue,
),
onTap: () async {
// Show color picker
},
);
},
)
14. JustNestedBuilder
Nested form fields.
JustNestedBuilder(
name: 'address',
initialValue: {},
builder: (context, controller) {
return Column(
children: [
JustTextField(
name: 'address.street',
builder: (context, fieldController) => TextField(),
),
JustTextField(
name: 'address.city',
builder: (context, fieldController) => TextField(),
),
],
);
},
)
Creating Custom Fields #
Create your own field widgets by extending JustField<T>:
class MyCustomField extends StatelessWidget {
final String name;
final String? initialValue;
final void Function(String?)? onChanged;
const MyCustomField({
required this.name,
this.initialValue,
this.onChanged,
});
@override
Widget build(BuildContext context) {
return JustField<String>(
name: name,
initialValue: initialValue,
builder: (context, controller) {
return Container(
padding: EdgeInsets.all(8),
decoration: BoxDecoration(
border: Border.all(color: Colors.grey),
borderRadius: BorderRadius.circular(8),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TextField(
onChanged: (value) {
controller.value = value;
onChanged?.call(value);
},
decoration: InputDecoration(
hintText: 'Enter value',
border: InputBorder.none,
errorText: controller.error,
),
),
],
),
);
},
);
}
}
// Usage
MyCustomField(
name: 'custom-field',
initialValue: 'Hello',
onChanged: (value) => print('Changed: $value'),
)
Advanced Features #
Manipulating Attributes #
Attributes are custom metadata you can attach to fields:
JustTextField(
name: 'email',
initialAttributes: {
'touched': false,
'validating': false,
'lastValidated': null,
},
builder: (context, controller) {
return TextField(
onChanged: (value) {
controller.value = value;
// Mark field as touched
controller.setAttribute('touched', true);
},
onFocus: (hasFocus) {
if (hasFocus) {
controller.setAttribute('validating', true);
}
},
decoration: InputDecoration(
hintText: 'Enter email',
errorText: controller.error,
filled: controller.attributes['touched'] == true,
fillColor: Colors.blue.withOpacity(0.1),
),
);
},
)
Access attributes in JustBuilder:
JustBuilder(
fields: ['email'],
rebuildOnAttributeChanged: true,
builder: (context, state) {
final email = state['email'];
final touched = email?.attributes['touched'] ?? false;
return Column(
children: [
if (touched) Text('Field was touched'),
if (email?.attributes['validating'] == true)
CircularProgressIndicator(),
],
);
},
)
Form Control #
Access and manipulate the entire form:
ElevatedButton(
onPressed: () {
final formController = context.read<JustFormController>();
// Get all values
print('Values: ${formController.values}');
// Get all errors
print('Errors: ${formController.errors}');
// Validate the form
final isValid = await formController.validate();
// Set field value
formController.fields()['email']?.value = 'new@email.com';
// Reset form
formController.reset();
// Get specific field
final emailField = formController.fields()['email'];
print('Email value: ${emailField?.value}');
},
child: Text('Check Form'),
)
Selective Rebuilds #
Use JustBuilder to rebuild only when specific fields change:
JustBuilder(
fields: ['email', 'password'],
rebuildOnValueChanged: true,
rebuildOnErrorChanged: true,
builder: (context, state) {
final email = state['email'];
final password = state['password'];
return Column(
children: [
if (email?.error != null)
Text('Email error: ${email?.error}'),
if (password?.error != null)
Text('Password error: ${password?.error}'),
// Rebuild only when email or password changes
],
);
},
)
Examples #
Check the /example folder for complete working examples:
- basic_example.dart - Complete form with various field types
- basic_example_nested_form.dart - Nested forms and complex structures
- todo.dart - Dynamic list of fields example
To run the example:
cd example
flutter run
API Reference #
JustFormBuilder #
The main widget for building forms.
Properties:
builder- Required. Widget builder functioncontroller- Optional. External form controllerinitialValues- Map of initial field valuesvalidators- List of form-level validatorsonFieldRegistered- Callback when field registersonValuesChanged- Callback when any field value changesonErrorsChanged- Callback when errors change
JustField #
Generic field widget for managing individual form fields.
Properties:
name- Required. Unique field identifierbuilder- Required. Field widget builderinitialValue- Initial field valuevalidators- Field-level validatorskeepValueOnDestroy- Preserve value on widget removalrebuildOnValueChanged- Rebuild on value changesrebuildOnErrorChanged- Rebuild on error changesrebuildOnAttributeChanged- Rebuild on attribute changesinitialAttributes- Custom metadata
JustBuilder #
Selective field listener and rebuilder.
Properties:
fields- List of field names to monitorallFields- Monitor all fieldsbuilder- Required. Widget builder functionrebuildOnValueChanged- Rebuild on value changesrebuildOnErrorChanged- Rebuild on error changesrebuildOnAttributeChanged- Rebuild on attribute changes
JustFieldController #
Controller for individual field management.
Properties:
value- Get/set field valueerror- Get/set field errorattributes- Get field attributes
Methods:
setValue(T value)- Set field valuesetError(String? error)- Set field errorsetAttribute(String key, dynamic value)- Set attributegetState()- Get current field state
License #
This package is licensed under the MIT License. See LICENSE file for details.
Contributing #
Contributions are welcome! Please feel free to submit a Pull Request.
Support #
For issues, questions, or suggestions, please open an issue on the GitHub repository.