mirror of
https://github.com/Iconica-Development/flutter_address_form.git
synced 2025-05-19 18:53:47 +02:00
Functionally working Adressform
This commit is contained in:
parent
e00b961414
commit
1107729c79
5 changed files with 229 additions and 147 deletions
|
@ -25,19 +25,61 @@ class MyApp extends StatelessWidget {
|
||||||
// is not restarted.
|
// is not restarted.
|
||||||
primarySwatch: Colors.blue,
|
primarySwatch: Colors.blue,
|
||||||
),
|
),
|
||||||
home: const AddressFormExample(),
|
home: AddressFormExample(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class AddressFormExample extends StatelessWidget {
|
class AddressFormExample extends StatelessWidget {
|
||||||
const AddressFormExample({Key? key}) : super(key: key);
|
AddressFormExample({Key? key}) : super(key: key);
|
||||||
|
|
||||||
|
final RegExp zipcodeRegExp = RegExp(r'^[1-9][0-9]{3}\s?[a-zA-Z]{2}$');
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(),
|
appBar: AppBar(),
|
||||||
body: AddressForm(),
|
body: AddressForm(
|
||||||
|
zipCodeValidator: (text) {
|
||||||
|
if (text.isEmpty) {
|
||||||
|
return 'Can\'t be empty';
|
||||||
|
}
|
||||||
|
if (!zipcodeRegExp.hasMatch(text)) {
|
||||||
|
return 'Invalid zipcode';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
housenumberValidator: (text) {
|
||||||
|
if (text.isEmpty) {
|
||||||
|
return 'Can\'t be empty';
|
||||||
|
}
|
||||||
|
if (text.length >= 3 || int.tryParse(text) == null) {
|
||||||
|
return 'Invalid number';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
suffixValidator: (text) {
|
||||||
|
if (text.isNotEmpty && RegExp(r'/^[a-z]*$/').hasMatch(text)) {
|
||||||
|
return 'Invalid prefix';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
streetValidator: (text) {
|
||||||
|
if (text.isEmpty) {
|
||||||
|
return 'Can\'t be empty';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
cityValidator: (text) {
|
||||||
|
if (text.isEmpty) {
|
||||||
|
return 'Can\'t be empty';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
controller: AddressController(onAutoComplete: (address) {
|
||||||
|
return address;
|
||||||
|
}),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,40 +0,0 @@
|
||||||
import 'package:flutter/foundation.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter_address_form/src/models/address.dart';
|
|
||||||
|
|
||||||
class AddressController extends ChangeNotifier {
|
|
||||||
Address address = const Address();
|
|
||||||
|
|
||||||
final TextEditingController zipcode = TextEditingController();
|
|
||||||
final TextEditingController houseNumber = TextEditingController();
|
|
||||||
final TextEditingController suffix = TextEditingController();
|
|
||||||
final TextEditingController street = TextEditingController();
|
|
||||||
final TextEditingController city = TextEditingController();
|
|
||||||
|
|
||||||
final Function()? onChangeInputCallback;
|
|
||||||
|
|
||||||
AddressController(this.onChangeInputCallback);
|
|
||||||
|
|
||||||
Address get getAddress => address;
|
|
||||||
|
|
||||||
void setAddress(
|
|
||||||
String zipcode,
|
|
||||||
String street,
|
|
||||||
int housenumber,
|
|
||||||
String suffix,
|
|
||||||
String city,
|
|
||||||
) {
|
|
||||||
address = address.copyWith(
|
|
||||||
zipcode: zipcode,
|
|
||||||
street: street,
|
|
||||||
housenumber: housenumber,
|
|
||||||
suffix: suffix,
|
|
||||||
city: city,
|
|
||||||
);
|
|
||||||
notifyListeners();
|
|
||||||
}
|
|
||||||
|
|
||||||
void onChangeInput() {
|
|
||||||
onChangeInputCallback?.call();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,111 +1,83 @@
|
||||||
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
|
||||||
import 'package:flutter_address_form/src/address_controller.dart';
|
|
||||||
import 'package:flutter_address_form/src/models/address.dart';
|
|
||||||
|
|
||||||
class AddressForm extends StatefulWidget {
|
import 'package:flutter_address_form/src/models/address_model.dart';
|
||||||
const AddressForm({Key? key}) : super(key: key);
|
|
||||||
|
|
||||||
@override
|
class AddressForm extends StatelessWidget {
|
||||||
State<AddressForm> createState() => _AddressFormState();
|
AddressForm({
|
||||||
}
|
Key? key,
|
||||||
|
this.zipCodeLabel = const Text('Zipcode'),
|
||||||
class _AddressFormState extends State<AddressForm> {
|
this.housenumberLabel = const Text('Housenumber'),
|
||||||
final AddressController _addressController = AddressController(() {});
|
this.suffixLabel = const Text('Suffix'),
|
||||||
|
this.streetLabel = const Text('Street'),
|
||||||
final RegExp _zipcodeRegExp = RegExp(r'^[1-9][0-9]{3}\s?[a-zA-Z]{2}$');
|
this.cityLabel = const Text('City'),
|
||||||
|
required this.zipCodeValidator,
|
||||||
@override
|
required this.housenumberValidator,
|
||||||
void initState() {
|
required this.suffixValidator,
|
||||||
_addressController;
|
required this.streetValidator,
|
||||||
super.initState();
|
required this.cityValidator,
|
||||||
|
AddressController? controller,
|
||||||
|
}) {
|
||||||
|
_addressController =
|
||||||
|
controller ?? AddressController(onAutoComplete: (model) => model);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final Widget zipCodeLabel;
|
||||||
|
final Widget housenumberLabel;
|
||||||
|
final Widget suffixLabel;
|
||||||
|
final Widget streetLabel;
|
||||||
|
final Widget cityLabel;
|
||||||
|
|
||||||
|
final String? Function(String) zipCodeValidator;
|
||||||
|
final String? Function(String) housenumberValidator;
|
||||||
|
final String? Function(String) suffixValidator;
|
||||||
|
final String? Function(String) streetValidator;
|
||||||
|
final String? Function(String) cityValidator;
|
||||||
|
|
||||||
|
late final AddressController _addressController;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Column(
|
return Column(
|
||||||
children: [
|
children: [
|
||||||
AddressFormTextField(
|
AddressFormTextField(
|
||||||
controller: _addressController.zipcode,
|
controller: _addressController._zipcodeController,
|
||||||
validator: (text) {
|
validator: zipCodeValidator,
|
||||||
if (text.isEmpty) {
|
label: zipCodeLabel,
|
||||||
return 'Can\'t be empty';
|
|
||||||
}
|
|
||||||
if (!_zipcodeRegExp.hasMatch(text)) {
|
|
||||||
return 'Invalid zipcode';
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
label: const Text('Postcode'),
|
|
||||||
),
|
),
|
||||||
Flexible(
|
Flexible(
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
AddressFormTextField(
|
AddressFormTextField(
|
||||||
controller: _addressController.houseNumber,
|
controller: _addressController._housenumberController,
|
||||||
validator: (text) {
|
validator: housenumberValidator,
|
||||||
if (text.isEmpty) {
|
label: housenumberLabel,
|
||||||
return 'Can\'t be empty';
|
|
||||||
}
|
|
||||||
if (text.length >= 3 || int.tryParse(text) == null) {
|
|
||||||
return 'Invalid number';
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
label: const Text('Huisnummer'),
|
|
||||||
),
|
),
|
||||||
AddressFormTextField(
|
AddressFormTextField(
|
||||||
controller: _addressController.suffix,
|
controller: _addressController._suffixController,
|
||||||
validator: (text) {
|
validator: suffixValidator,
|
||||||
if (text.isEmpty) {
|
label: suffixLabel,
|
||||||
return 'Can\'t be empty';
|
|
||||||
}
|
|
||||||
if (RegExp(r'/^[a-z]*$/').hasMatch(text) &&
|
|
||||||
text.length != 1) {
|
|
||||||
return 'Invalid prefix';
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
label: const Text('Toevoeging'),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
AddressFormTextField(
|
AddressFormTextField(
|
||||||
controller: _addressController.street,
|
controller: _addressController._streetController,
|
||||||
validator: (text) {
|
validator: streetValidator,
|
||||||
if (text.isEmpty) {
|
label: streetLabel,
|
||||||
return 'Can\'t be empty';
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
label: const Text('Straatnaam'),
|
|
||||||
),
|
),
|
||||||
AddressFormTextField(
|
AddressFormTextField(
|
||||||
controller: _addressController.city,
|
controller: _addressController._cityController,
|
||||||
validator: (text) {
|
validator: cityValidator,
|
||||||
if (text.isEmpty) {
|
label: cityLabel,
|
||||||
return 'Can\'t be empty';
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
label: const Text('Woonplaats'),
|
|
||||||
),
|
),
|
||||||
TextButton(
|
|
||||||
onPressed: () {},
|
|
||||||
child: Text('Test'),
|
|
||||||
)
|
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class AddressFormTextField extends StatefulWidget {
|
class AddressFormTextField extends StatelessWidget {
|
||||||
final Widget label;
|
final Widget label;
|
||||||
final TextEditingController controller;
|
final TextEditingController controller;
|
||||||
final String? Function(String) validator;
|
final String? Function(String) validator;
|
||||||
|
@ -116,33 +88,20 @@ class AddressFormTextField extends StatefulWidget {
|
||||||
required this.validator,
|
required this.validator,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
@override
|
String? get _errorText => validator(controller.value.text);
|
||||||
State<AddressFormTextField> createState() => _AddressFormTextFieldState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _AddressFormTextFieldState extends State<AddressFormTextField> {
|
|
||||||
String? get _errorText {
|
|
||||||
final text = widget.controller.value.text;
|
|
||||||
return widget.validator(text);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return ValueListenableBuilder<TextEditingValue>(
|
return ValueListenableBuilder<TextEditingValue>(
|
||||||
valueListenable: widget.controller,
|
valueListenable: controller,
|
||||||
builder: (context, value, _) {
|
builder: (context, value, _) {
|
||||||
return Flexible(
|
return Flexible(
|
||||||
child: Container(
|
child: Container(
|
||||||
margin: const EdgeInsets.all(10),
|
margin: const EdgeInsets.all(10),
|
||||||
child: TextField(
|
child: TextField(
|
||||||
controller: widget.controller,
|
controller: controller,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
label: widget.label,
|
label: label,
|
||||||
border: const OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
errorText: _errorText),
|
errorText: _errorText),
|
||||||
),
|
),
|
||||||
|
@ -151,3 +110,77 @@ class _AddressFormTextFieldState extends State<AddressFormTextField> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class AddressController extends ChangeNotifier {
|
||||||
|
final AddressModel? initialValue;
|
||||||
|
final FutureOr<AddressModel> Function(AddressModel)? onAutoComplete;
|
||||||
|
|
||||||
|
AddressController({this.initialValue, this.onAutoComplete}) {
|
||||||
|
_model = initialValue ??
|
||||||
|
const AddressModel(
|
||||||
|
zipcode: null,
|
||||||
|
street: null,
|
||||||
|
housenumber: null,
|
||||||
|
suffix: null,
|
||||||
|
city: null,
|
||||||
|
);
|
||||||
|
|
||||||
|
_zipcodeController.addListener(_update);
|
||||||
|
_streetController.addListener(_update);
|
||||||
|
_housenumberController.addListener(_update);
|
||||||
|
_suffixController.addListener(_update);
|
||||||
|
_cityController.addListener(_update);
|
||||||
|
}
|
||||||
|
|
||||||
|
late AddressModel _model;
|
||||||
|
|
||||||
|
late final _zipcodeController =
|
||||||
|
TextEditingController(text: initialValue?.zipcode);
|
||||||
|
late final _streetController =
|
||||||
|
TextEditingController(text: initialValue?.street);
|
||||||
|
late final _housenumberController =
|
||||||
|
TextEditingController(text: initialValue?.housenumber.toString());
|
||||||
|
late final _suffixController =
|
||||||
|
TextEditingController(text: initialValue?.suffix);
|
||||||
|
late final _cityController = TextEditingController(text: initialValue?.city);
|
||||||
|
|
||||||
|
AddressModel get model => _model;
|
||||||
|
|
||||||
|
void _update() async {
|
||||||
|
AddressModel updatedModel = _model.copyWith(
|
||||||
|
zipcode: _zipcodeController.text,
|
||||||
|
street: _streetController.text,
|
||||||
|
housenumber: int.tryParse(_housenumberController.text),
|
||||||
|
suffix: _suffixController.text,
|
||||||
|
city: _cityController.text);
|
||||||
|
_model = await onAutoComplete?.call(updatedModel) ?? updatedModel;
|
||||||
|
|
||||||
|
if (_model.zipcode != updatedModel.zipcode) {
|
||||||
|
_zipcodeController.text = _model.zipcode ?? '';
|
||||||
|
}
|
||||||
|
if (_model.street != updatedModel.street) {
|
||||||
|
_streetController.text = _model.street ?? '';
|
||||||
|
}
|
||||||
|
if (_model.housenumber != updatedModel.housenumber) {
|
||||||
|
_housenumberController.text = _model.housenumber?.toString() ?? '';
|
||||||
|
}
|
||||||
|
if (_model.suffix != updatedModel.suffix) {
|
||||||
|
_suffixController.text = _model.suffix ?? '';
|
||||||
|
}
|
||||||
|
if (_model.city != updatedModel.city) {
|
||||||
|
_cityController.text = _model.city ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
super.dispose();
|
||||||
|
_zipcodeController.dispose();
|
||||||
|
_streetController.dispose();
|
||||||
|
_housenumberController.dispose();
|
||||||
|
_suffixController.dispose();
|
||||||
|
_cityController.dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import 'package:flutter/material.dart' show immutable;
|
import 'package:flutter/material.dart' show immutable;
|
||||||
|
|
||||||
@immutable
|
@immutable
|
||||||
class Address {
|
class AddressModel {
|
||||||
const Address({
|
const AddressModel({
|
||||||
this.zipcode,
|
this.zipcode,
|
||||||
this.street,
|
this.street,
|
||||||
this.housenumber,
|
this.housenumber,
|
||||||
|
@ -16,14 +16,14 @@ class Address {
|
||||||
final String? suffix;
|
final String? suffix;
|
||||||
final String? city;
|
final String? city;
|
||||||
|
|
||||||
Address copyWith({
|
AddressModel copyWith({
|
||||||
String? zipcode,
|
String? zipcode,
|
||||||
String? street,
|
String? street,
|
||||||
int? housenumber,
|
int? housenumber,
|
||||||
String? suffix,
|
String? suffix,
|
||||||
String? city,
|
String? city,
|
||||||
}) =>
|
}) =>
|
||||||
Address(
|
AddressModel(
|
||||||
zipcode: zipcode ?? this.zipcode,
|
zipcode: zipcode ?? this.zipcode,
|
||||||
street: street ?? this.street,
|
street: street ?? this.street,
|
||||||
housenumber: housenumber ?? this.housenumber,
|
housenumber: housenumber ?? this.housenumber,
|
|
@ -1,12 +1,59 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
||||||
import 'package:flutter_address_form/flutter_address_form.dart';
|
import 'package:flutter_address_form/flutter_address_form.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
test('adds one to input values', () {
|
testWidgets('Render App with AddressForm Widget', (tester) async {
|
||||||
final calculator = Calculator();
|
final RegExp zipcodeRegExp = RegExp(r'^[1-9][0-9]{3}\s?[a-zA-Z]{2}$');
|
||||||
expect(calculator.addOne(2), 3);
|
await tester.pumpWidget(
|
||||||
expect(calculator.addOne(-7), -6);
|
MaterialApp(
|
||||||
expect(calculator.addOne(0), 1);
|
home: Scaffold(
|
||||||
|
appBar: AppBar(),
|
||||||
|
body: AddressForm(
|
||||||
|
zipCodeValidator: (text) {
|
||||||
|
if (text.isEmpty) {
|
||||||
|
return 'Can\'t be empty';
|
||||||
|
}
|
||||||
|
if (!zipcodeRegExp.hasMatch(text)) {
|
||||||
|
return 'Invalid zipcode';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
housenumberValidator: (text) {
|
||||||
|
if (text.isEmpty) {
|
||||||
|
return 'Can\'t be empty';
|
||||||
|
}
|
||||||
|
if (text.length >= 3 || int.tryParse(text) == null) {
|
||||||
|
return 'Invalid number';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
suffixValidator: (text) {
|
||||||
|
if (text.isNotEmpty && RegExp(r'/^[a-z]*$/').hasMatch(text)) {
|
||||||
|
return 'Invalid prefix';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
streetValidator: (text) {
|
||||||
|
if (text.isEmpty) {
|
||||||
|
return 'Can\'t be empty';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
cityValidator: (text) {
|
||||||
|
if (text.isEmpty) {
|
||||||
|
return 'Can\'t be empty';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
controller: AddressController(onAutoComplete: (address) {
|
||||||
|
return address;
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await tester.pump();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue