mirror of
https://github.com/Iconica-Development/flutter_address_form.git
synced 2025-05-19 10:43:45 +02:00
Merge pull request #1 from Iconica-Development/feature/first_release
Basic layout
This commit is contained in:
commit
a2e2be34e8
8 changed files with 400 additions and 84 deletions
|
@ -1,4 +1,5 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_address_form/flutter_address_form.dart';
|
||||
|
||||
void main() {
|
||||
runApp(const MyApp());
|
||||
|
@ -24,92 +25,88 @@ class MyApp extends StatelessWidget {
|
|||
// is not restarted.
|
||||
primarySwatch: Colors.blue,
|
||||
),
|
||||
home: const MyHomePage(title: 'Flutter Demo Home Page'),
|
||||
home: AddressFormExample(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class MyHomePage extends StatefulWidget {
|
||||
const MyHomePage({super.key, required this.title});
|
||||
class AddressFormExample extends StatelessWidget {
|
||||
AddressFormExample({super.key});
|
||||
|
||||
// This widget is the home page of your application. It is stateful, meaning
|
||||
// that it has a State object (defined below) that contains fields that affect
|
||||
// how it looks.
|
||||
// final RegExp zipcodeRegExp = RegExp(r'^[1-9][0-9]{3}\s?[a-zA-Z]{2}$');
|
||||
|
||||
// This class is the configuration for the state. It holds the values (in this
|
||||
// case the title) provided by the parent (in this case the App widget) and
|
||||
// used by the build method of the State. Fields in a Widget subclass are
|
||||
// always marked "final".
|
||||
|
||||
final String title;
|
||||
|
||||
@override
|
||||
State<MyHomePage> createState() => _MyHomePageState();
|
||||
}
|
||||
|
||||
class _MyHomePageState extends State<MyHomePage> {
|
||||
int _counter = 0;
|
||||
|
||||
void _incrementCounter() {
|
||||
setState(() {
|
||||
// This call to setState tells the Flutter framework that something has
|
||||
// changed in this State, which causes it to rerun the build method below
|
||||
// so that the display can reflect the updated values. If we changed
|
||||
// _counter without calling setState(), then the build method would not be
|
||||
// called again, and so nothing would appear to happen.
|
||||
_counter++;
|
||||
});
|
||||
final _addressController = AddressController(
|
||||
zipCodeValidator: (text) {
|
||||
if (text != null) {
|
||||
if (text.isEmpty) {
|
||||
return 'Can\'t be empty';
|
||||
}
|
||||
if (!RegExp(r'^[1-9][0-9]{3}\s?[a-zA-Z]{2}$').hasMatch(text)) {
|
||||
return 'Invalid zipcode';
|
||||
}
|
||||
}
|
||||
return null;
|
||||
},
|
||||
housenumberValidator: (text) {
|
||||
if (text != null) {
|
||||
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 != null) {
|
||||
if (text.isNotEmpty && RegExp(r'/^[a-z]*$/').hasMatch(text)) {
|
||||
return 'Invalid prefix';
|
||||
}
|
||||
}
|
||||
return null;
|
||||
},
|
||||
streetValidator: (text) {
|
||||
if (text != null) {
|
||||
if (text.isEmpty) {
|
||||
return 'Can\'t be empty';
|
||||
}
|
||||
}
|
||||
return null;
|
||||
},
|
||||
cityValidator: (text) {
|
||||
if (text != null) {
|
||||
if (text.isEmpty) {
|
||||
return 'Can\'t be empty';
|
||||
}
|
||||
}
|
||||
return null;
|
||||
},
|
||||
onAutoComplete: (address) {
|
||||
return address;
|
||||
},
|
||||
);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// This method is rerun every time setState is called, for instance as done
|
||||
// by the _incrementCounter method above.
|
||||
//
|
||||
// The Flutter framework has been optimized to make rerunning build methods
|
||||
// fast, so that you can just rebuild anything that needs updating rather
|
||||
// than having to individually change instances of widgets.
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
// Here we take the value from the MyHomePage object that was created by
|
||||
// the App.build method, and use it to set our appbar title.
|
||||
title: Text(widget.title),
|
||||
title: const Text('Address Form'),
|
||||
),
|
||||
body: Center(
|
||||
// Center is a layout widget. It takes a single child and positions it
|
||||
// in the middle of the parent.
|
||||
child: Column(
|
||||
// Column is also a layout widget. It takes a list of children and
|
||||
// arranges them vertically. By default, it sizes itself to fit its
|
||||
// children horizontally, and tries to be as tall as its parent.
|
||||
//
|
||||
// Invoke "debug painting" (press "p" in the console, choose the
|
||||
// "Toggle Debug Paint" action from the Flutter Inspector in Android
|
||||
// Studio, or the "Toggle Debug Paint" command in Visual Studio Code)
|
||||
// to see the wireframe for each widget.
|
||||
//
|
||||
// Column has various properties to control how it sizes itself and
|
||||
// how it positions its children. Here we use mainAxisAlignment to
|
||||
// center the children vertically; the main axis here is the vertical
|
||||
// axis because Columns are vertical (the cross axis would be
|
||||
// horizontal).
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
const Text(
|
||||
'You have pushed the button this many times:',
|
||||
),
|
||||
Text(
|
||||
'$_counter',
|
||||
style: Theme.of(context).textTheme.headline4,
|
||||
body: Column(
|
||||
children: [
|
||||
AddressForm(
|
||||
onSubmit: (value) => value,
|
||||
controller: _addressController,
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
_addressController.validate();
|
||||
},
|
||||
child: const Text('Validate'),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
floatingActionButton: FloatingActionButton(
|
||||
onPressed: _incrementCounter,
|
||||
tooltip: 'Increment',
|
||||
child: const Icon(Icons.add),
|
||||
), // This trailing comma makes auto-formatting nicer for build methods.
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -55,6 +55,13 @@ packages:
|
|||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
flutter_address_form:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
path: ".."
|
||||
relative: true
|
||||
source: path
|
||||
version: "0.0.1"
|
||||
flutter_lints:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
|
@ -158,3 +165,4 @@ packages:
|
|||
version: "2.1.2"
|
||||
sdks:
|
||||
dart: ">=2.18.2 <3.0.0"
|
||||
flutter: ">=1.17.0"
|
||||
|
|
|
@ -32,6 +32,9 @@ dependencies:
|
|||
flutter:
|
||||
sdk: flutter
|
||||
|
||||
flutter_address_form:
|
||||
path: ../
|
||||
|
||||
|
||||
# The following adds the Cupertino Icons font to your application.
|
||||
# Use with the CupertinoIcons class for iOS style icons.
|
||||
|
|
|
@ -1,7 +1,4 @@
|
|||
library flutter_address_form;
|
||||
|
||||
/// A Calculator.
|
||||
class Calculator {
|
||||
/// Returns [value] plus 1.
|
||||
int addOne(int value) => value + 1;
|
||||
}
|
||||
export 'src/address_form.dart';
|
||||
export 'src/models/address_model.dart';
|
||||
|
|
214
lib/src/address_form.dart
Normal file
214
lib/src/address_form.dart
Normal file
|
@ -0,0 +1,214 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:flutter_address_form/src/models/address_model.dart';
|
||||
|
||||
/// A widget that creates a form with different address fields widgets.
|
||||
/// Returns a `AddressModel` Object from a `AddressController`.
|
||||
class AddressForm extends StatefulWidget {
|
||||
AddressForm({
|
||||
super.key,
|
||||
AddressController? controller,
|
||||
this.zipCodeDecoration = const InputDecoration(label: Text('Zipcode')),
|
||||
this.housenumberDecoration =
|
||||
const InputDecoration(label: Text('Housenumber')),
|
||||
this.suffixDecoration = const InputDecoration(label: Text('Suffix')),
|
||||
this.streetDecoration = const InputDecoration(label: Text('Street')),
|
||||
this.cityDecoration = const InputDecoration(label: Text('City')),
|
||||
required this.onSubmit,
|
||||
}) {
|
||||
_addressController = controller ??
|
||||
AddressController(
|
||||
onAutoComplete: (model) => model,
|
||||
zipCodeValidator: (text) => null,
|
||||
cityValidator: (text) => null,
|
||||
housenumberValidator: (text) => null,
|
||||
streetValidator: (text) => null,
|
||||
suffixValidator: (text) => null,
|
||||
);
|
||||
}
|
||||
|
||||
final InputDecoration zipCodeDecoration;
|
||||
final InputDecoration housenumberDecoration;
|
||||
final InputDecoration suffixDecoration;
|
||||
final InputDecoration streetDecoration;
|
||||
final InputDecoration cityDecoration;
|
||||
|
||||
final ValueChanged<String> onSubmit;
|
||||
|
||||
/// Controls the `AddressModel`
|
||||
late final AddressController _addressController;
|
||||
|
||||
@override
|
||||
State<AddressForm> createState() => _AddressFormState();
|
||||
}
|
||||
|
||||
class _AddressFormState extends State<AddressForm> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Form(
|
||||
key: widget._addressController._formKey,
|
||||
child: Column(
|
||||
children: [
|
||||
AddressFormTextField(
|
||||
validator: widget._addressController.zipCodeValidator,
|
||||
controller: widget._addressController._zipcodeController,
|
||||
fieldDecoration: widget.zipCodeDecoration,
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: AddressFormTextField(
|
||||
validator: widget._addressController.housenumberValidator,
|
||||
controller: widget._addressController._housenumberController,
|
||||
fieldDecoration: widget.housenumberDecoration,
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: AddressFormTextField(
|
||||
validator: widget._addressController.suffixValidator,
|
||||
controller: widget._addressController._suffixController,
|
||||
fieldDecoration: widget.suffixDecoration,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
AddressFormTextField(
|
||||
validator: widget._addressController.streetValidator,
|
||||
controller: widget._addressController._streetController,
|
||||
fieldDecoration: widget.streetDecoration,
|
||||
),
|
||||
AddressFormTextField(
|
||||
validator: widget._addressController.cityValidator,
|
||||
controller: widget._addressController._cityController,
|
||||
fieldDecoration: widget.cityDecoration,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class AddressFormTextField extends StatelessWidget {
|
||||
const AddressFormTextField({
|
||||
super.key,
|
||||
required this.fieldDecoration,
|
||||
required this.controller,
|
||||
this.validator,
|
||||
});
|
||||
|
||||
final TextEditingController controller;
|
||||
final InputDecoration fieldDecoration;
|
||||
final String? Function(String?)? validator;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.all(10),
|
||||
child: TextFormField(
|
||||
controller: controller,
|
||||
decoration: fieldDecoration,
|
||||
validator: validator,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class AddressController extends ChangeNotifier {
|
||||
/// An optional value to initialize the form field to, or null otherwise.
|
||||
final AddressModel? initialValue;
|
||||
|
||||
final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
|
||||
|
||||
/// When the form changes, the function passes the current `AddressModel` as an argument and gives the possibility to manipulate and return a `AddressModel`.
|
||||
final FutureOr<AddressModel> Function(AddressModel)? onAutoComplete;
|
||||
|
||||
AddressController(
|
||||
{this.initialValue,
|
||||
this.onAutoComplete,
|
||||
this.zipCodeValidator,
|
||||
this.housenumberValidator,
|
||||
this.suffixValidator,
|
||||
this.streetValidator,
|
||||
this.cityValidator}) {
|
||||
_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;
|
||||
|
||||
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 _zipcodeController =
|
||||
TextEditingController(text: initialValue?.zipcode);
|
||||
late final _streetController =
|
||||
TextEditingController(text: initialValue?.street);
|
||||
late final _housenumberController =
|
||||
TextEditingController(text: initialValue?.housenumber);
|
||||
late final _suffixController =
|
||||
TextEditingController(text: initialValue?.suffix);
|
||||
late final _cityController = TextEditingController(text: initialValue?.city);
|
||||
|
||||
AddressModel get model => _model;
|
||||
|
||||
bool validate() {
|
||||
assert(_formKey.currentState != null,
|
||||
'Validation can only be used if a FormKey is attached to the current Form');
|
||||
return _formKey.currentState!.validate();
|
||||
}
|
||||
|
||||
void _update() async {
|
||||
AddressModel updatedModel = _model.copyWith(
|
||||
zipcode: _zipcodeController.text,
|
||||
street: _streetController.text,
|
||||
housenumber: _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 ?? '';
|
||||
}
|
||||
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();
|
||||
}
|
||||
}
|
33
lib/src/models/address_model.dart
Normal file
33
lib/src/models/address_model.dart
Normal file
|
@ -0,0 +1,33 @@
|
|||
import 'package:flutter/material.dart' show immutable;
|
||||
|
||||
@immutable
|
||||
class AddressModel {
|
||||
const AddressModel({
|
||||
this.zipcode,
|
||||
this.street,
|
||||
this.housenumber,
|
||||
this.suffix,
|
||||
this.city,
|
||||
});
|
||||
|
||||
final String? zipcode;
|
||||
final String? street;
|
||||
final String? housenumber;
|
||||
final String? suffix;
|
||||
final String? city;
|
||||
|
||||
AddressModel copyWith({
|
||||
String? zipcode,
|
||||
String? street,
|
||||
String? housenumber,
|
||||
String? suffix,
|
||||
String? city,
|
||||
}) =>
|
||||
AddressModel(
|
||||
zipcode: zipcode ?? this.zipcode,
|
||||
street: street ?? this.street,
|
||||
housenumber: housenumber ?? this.housenumber,
|
||||
suffix: suffix ?? this.suffix,
|
||||
city: city ?? this.city,
|
||||
);
|
||||
}
|
|
@ -1,7 +1,6 @@
|
|||
name: flutter_address_form
|
||||
description: A new Flutter package project.
|
||||
version: 0.0.1
|
||||
homepage:
|
||||
|
||||
environment:
|
||||
sdk: '>=2.18.2 <3.0.0'
|
||||
|
|
|
@ -1,12 +1,77 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import 'package:flutter_address_form/flutter_address_form.dart';
|
||||
|
||||
void main() {
|
||||
test('adds one to input values', () {
|
||||
final calculator = Calculator();
|
||||
expect(calculator.addOne(2), 3);
|
||||
expect(calculator.addOne(-7), -6);
|
||||
expect(calculator.addOne(0), 1);
|
||||
late RegExp zipcodeRegExp;
|
||||
|
||||
setUp(() {
|
||||
// Testing with Dutch ZipCode
|
||||
zipcodeRegExp = RegExp(r'^[1-9][0-9]{3}\s?[a-zA-Z]{2}$');
|
||||
});
|
||||
testWidgets('Render App with AddressForm Widget', (tester) async {
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Scaffold(
|
||||
appBar: AppBar(),
|
||||
body: AddressForm(
|
||||
onSubmit: (value) => value,
|
||||
controller: AddressController(
|
||||
zipCodeValidator: (text) {
|
||||
if (text != null) {
|
||||
if (text.isEmpty) {
|
||||
return 'Can\'t be empty';
|
||||
}
|
||||
if (!zipcodeRegExp.hasMatch(text)) {
|
||||
return 'Invalid zipcode';
|
||||
}
|
||||
}
|
||||
return null;
|
||||
},
|
||||
housenumberValidator: (text) {
|
||||
if (text != null) {
|
||||
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 != null) {
|
||||
if (text.isNotEmpty && RegExp(r'/^[a-z]*$/').hasMatch(text)) {
|
||||
return 'Invalid prefix';
|
||||
}
|
||||
}
|
||||
return null;
|
||||
},
|
||||
streetValidator: (text) {
|
||||
if (text != null) {
|
||||
if (text.isEmpty) {
|
||||
return 'Can\'t be empty';
|
||||
}
|
||||
}
|
||||
return null;
|
||||
},
|
||||
cityValidator: (text) {
|
||||
if (text != null) {
|
||||
if (text.isEmpty) {
|
||||
return 'Can\'t be empty';
|
||||
}
|
||||
}
|
||||
return null;
|
||||
},
|
||||
onAutoComplete: (address) {
|
||||
return address;
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pump();
|
||||
});
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue